This file holds the versioning rule and the changes of each release, newest first. Each release says who has to rebuild and how to migrate.
Two numbers describe a release, and they answer different questions.
The package version (2.1.0, in configure.ac and package.json) follows semantic versioning for the
source interface: a major number changes when programs have to be changed or rebuilt because the public
headers or the layout of the classes changed, a minor number when something is added or the build and
packaging change without touching the interface, and the last number for fixes. Release 2.1.0 is a minor
release because the source interface and the class layout are the same as in 2.0.0. Consumers need a
rebuild only because the shared library now carries a compatibility version that records what has been true
since 2.0.0.
The shared-library version is the libtool triple current:revision:age in src/Makefile.am
(-version-info 2:0:0). It decides the file name: on Linux the soname is libecs-cpp.so.N with
N = current - age, and on Windows the library is libecs-cpp-N.dll. The rule for changing it:
- The soname major starts at the major number of the package, which is why it is
2and not0. Earlier releases shipped0while the layout of the classes changed and names were removed, so0could not tell them apart. - After that, change the triple by the usual libtool rule. Code changes only: raise
revision. Interface added: raisecurrent, resetrevisionto 0 and raiseage. Interface removed or changed incompatibly, or the layout of an exported class changed: raisecurrent, and resetrevisionandageto 0. Only the last case changesN, so a program keeps running with any newer library of the sameNand the loader refuses a library of a differentN. tests/check-install.shnames the expected files, so a change to the triple and to that check go in the same commit.
Packaging and build release. The source interface and the class layout are the same as in 2.0.0, so this is a minor release (see Versioning). What changes is what is installed, how the library is built and tested, and the name of the shared library.
Rebuild libthe-seed, the-seed's native addon and every plugin that links libecs-cpp after installing
2.1.0. They were linked against libecs-cpp.so.0; the new library is libecs-cpp.so.2 (Windows:
libecs-cpp-2.dll), so a program built against 2.0.0 is refused by the loader instead of running against
a library it was not built for. Projects keep requiring ecs-cpp >= 2.0.0, which 2.1.0 satisfies.
Changed:
- No program is installed. The sample
src/example.cppis still built in the build tree and run by the tests, butmake installno longer putsexampleinbin. On Windows the only file inbinis the DLL. --enable-werror=yesturns compiler warnings into errors in the library, the sample and the tests. It is off by default, so a newer compiler cannot break a build that only installs the library.--enable-tests=auto|yes|nocontrols the tests. Catch2 is optional: the default,auto, builds the tests when Catch2 is found, andconfigureno longer fails without it.yesinsists on it.- The shared library is
libecs-cpp.so.2(withlibecs-cpp.so.2.0.0and thelibecs-cpp.solink) and on Windowslibecs-cpp-2.dll. A new install does not remove the files of an older one, solibecs-cpp.so.0andlibecs-cpp.so.0.0.0can be left in the library directory; they are harmless and can be deleted once nothing needs them. The rule for future changes is in Versioning. - Every public header can be included on its own, and
Container.hppandSystem.hppno longer include<iostream>. A program that relied on getting<iostream>through the library headers must include it itself. - Out-of-tree builds work, and
make distcheckpasses. - The installed pkg-config file is checked by building and running a small consumer against an installed
copy, with
pkg-configflags only. - The reference documentation is published as a site (GitHub Pages) by pushes to
main, anddocs/is no longer stored in the repository. Build it locally withmake doxygen. The repository owner must switch Settings, Pages, Build and deployment, Source to "GitHub Actions" once; until then the publishing job reports an error. - There is one readme,
README.md; the plain-textREADMEis removed, and a check keeps the readme's statements about files and options true. npm testrunsmake check(it was a placeholder that failed).
Migration:
| Before | After |
|---|---|
A packaging script or a user runs the installed example (PREFIX/bin/example) |
It is not installed. Build the tree and run the sample from the src directory of the build, or compile src/example.cpp against the installed library. |
Reading the reference documentation from the committed docs/ directory |
docs/ is gone. Open the published site, or run make doxygen and read doxygen/html. |
Warnings always stopped the build (-Werror by default) |
Warnings are shown and the build continues. Add --enable-werror=yes to get the old behaviour, as the continuous integration does. |
configure failed when Catch2 was missing |
The tests are skipped with a message. Use --enable-tests=yes to require them, or --enable-tests=no to skip them without looking. |
Scripts that name libecs-cpp.so.0 or libecs-cpp.so.0.0.0 |
The files are libecs-cpp.so.2 and libecs-cpp.so.2.0.0 (Windows: libecs-cpp-2.dll). Update the names; delete leftover old files when nothing needs them. |
| libthe-seed and the-seed's native addon built against 2.0.0 | Rebuild and reinstall libthe-seed after libecs-cpp, then rebuild the-seed's addon. Plugins built against 2.0.0 are rebuilt with the-seed build. |
A program that got <iostream> through Container.hpp or System.hpp |
Add #include <iostream> to that program. |
Consistent logging, a system identifier that cannot change and a smaller public surface. This is a major
release: the layout of the base classes changes, and System::Handle can no longer be assigned.
Rebuild every system plugin and every component plugin (the-seed build). Removing
Component::Handle shrinks the Component base class, and System::Handle is now const. Plugins
compile these base classes into themselves, so a plugin built against 1.8.0 and loaded by 2.0.0 is not
supported: its behaviour is undefined. Loading was tried with minimal plugins. A component plugin built
against 1.8.0 does not load, because the constructor it calls is no longer exported by the library. A
system plugin that reads a component's Type and EntityHandle reads them from the old, shifted
positions and the program crashed. A system plugin that does nothing with components happened to run, but
that is luck of its layout and not something to rely on. Projects require ecs-cpp >= 2.0.0; libthe-seed
0.3.3 and the project templates of the-seed 1.8.0 do, and configure refuses a 1.8.0 install.
The 2.0.0 release shipped the shared library as libecs-cpp.so.0, the same file name as 1.8.0. No libtool
version-info was set for it, so the loader could not tell 1.8.0 and 2.0.0 apart by name. The rebuild rule
above and the ecs-cpp >= 2.0.0 requirement are what kept the two apart. Release 2.1.0 gives the library a
compatibility version of its own (see Versioning).
Changed:
Component::Handleis removed. A component has no identifier of its own; address it by its entity and type. The twoComponentconstructors are defined in the header and the library no longer builds aComponent.cpp.System::Handleis aconst std::string. It is set only by the base constructorSystem(handle)(or generated bySystem()), and cannot be assigned afterwards.System::Log(message, level)takeslevel = "info", the same default asContainer::Log.- Lines a system logs before it is registered are delivered to the container's destination at registration,
in order, before the container starts the system. Before, they waited inside the system until its next
Log()call after registration, and a system that never logged again never delivered them. - The default destination colours a stream only when it is an interactive terminal, and not when
NO_COLORis set to a non-empty value. Redirected output is plain text. Before, every line carried escape sequences. - A warning from the library itself (
componentsClear()with no container) goes through the log destination instead of straight to standard output. - Exported names: the global
loggerFunctionis no longer exported. OnlyECSand the publicecs::names are, andtests/check-exports.shkeeps it that way. - The private
Container::system_orderis nowsystemOrder.
Migration:
| Before | After |
|---|---|
Foo() { this->Handle = "name"; } in a system |
Foo() : ecs::System("name") {} |
system->Handle = "name"; after construction |
Not possible. Pass the name to the constructor. |
Reading component->Handle |
No replacement. Address a component by EntityHandle and Type. |
this->Log("text", "info") |
Unchanged. this->Log("text") now means info. |
| A log destination that stripped colour codes | Remove the stripping. A replacement destination receives plain text. |
| Relying on colour in a redirected file | Gone. Colour appears on a terminal only; NO_COLOR turns it off there too. |
| Lines logged in a constructor arriving late | They now reach the destination at registration. |
| System and component plugins built against 1.8.0 | Rebuild all of them with the-seed build. |
PKG_CHECK_MODULES([LIBECS], [ecs-cpp >= 1.8.0]) |
ecs-cpp >= 2.0.0. The 2.0.0 release shipped the shared library as libecs-cpp.so.0; 2.1.0 ships libecs-cpp.so.2. |
Tests: test_Logging (default severity, held lines, colour decisions, the library warning and
construction-time identifiers), test_Resources and test_Export are new, as are the script tests
run-logging.sh, check-exports.sh, check-exports-selftest.sh, check-style.sh and
check-style-selftest.sh.
Clear timer lengths and intervals, trustworthy elapsed time and a clock that tests can control. This is a
minor release although System and Timing change layout, following the precedent of 1.4.0 to 1.7.0;
the migration of each change is below, and the rules are in Time.
Rebuild every system plugin (the-seed build). System::Timing is a public member and its code is
compiled into each system plugin, and System gained members, so a system plugin built against 1.7.0 and
loaded by 1.8.0 is not safe. Loading one was tried: the plugin allocates the old, smaller object, the
1.8.0 constructor writes past its end, and the program corrupts memory and then crashes. A plugin that
only defines components (create_component) contains none of this code and keeps working without a
rebuild; that was checked by loading one built against 1.7.0 into 1.8.0. Container::Start(unsigned int)
and System::DeltaTimeGet() are still exported, so code rebuilt in place still links. libthe-seed 0.3.2
requires ecs-cpp >= 1.8.0.
New:
ecs::Clock,ecs::SteadyClock(the default) andecs::ManualClock, in<libecs-cpp/Clock.hpp>, withSystem::ClockSet()andContainer::ClockSet().System::ElapsedGet()(microseconds) andSystem::ElapsedSecondsGet()(adoublein seconds).Timingtakesstd::chrono::microseconds:SetInterval(),GetInterval(),ShouldUpdate(now),Restart(now); the constantsecs::MAX_INTERVALandecs::DEFAULT_INTERVAL.Timertakes astd::chronoduration, andContainer::Start(std::chrono::microseconds).- Out-of-range values are rejected with
std::runtime_errorinstead of wrapping around.
Fixed:
- A timer length of more than about 71 minutes used to wrap around in a 32-bit count of microseconds: a
two-hour timer fired after about 48 minutes. Lengths up to
MAX_INTERVALare now exact. - Elapsed time no longer rounds a sub-millisecond gap down to zero or drops the remainder of each step, and no longer depends on how often it is read.
Tests: test_Elapsed (elapsed time, the first update, stalls and the container clock, all on a controlled
clock) and test_Compatibility (every deprecated name still gives its previous result) are new.
Migration:
| Old | New |
|---|---|
Timing.SetFrequency(us) / GetFrequency() |
Timing.SetInterval(std::chrono::microseconds(us)) / GetInterval() |
Timing(us) |
Timing(std::chrono::microseconds(us)) |
Timer("n", cb, 30) (seconds; a floating-point count no longer compiles) |
Timer("n", cb, std::chrono::seconds(30)) |
uint32_t ms = DeltaTimeGet(); |
ElapsedGet() (microseconds) or ElapsedSecondsGet() (seconds, double) |
the protected member lastTime |
nothing: it is no longer maintained, use ElapsedGet() |
Start(us) |
unchanged and not deprecated; Start(std::chrono::microseconds(us)) shows the unit |
The deprecated names compile with a notice and keep their previous units and meaning, apart from the
behaviours below. A floating-point timer length (Timer("n", cb, 0.5)) no longer compiles: pass a duration.
Three behaviours to know about:
DeltaTimeGet()used to return the time since the previous call. It now returns the time of the current update, measured once. A system that read it only every Nth update used to see N updates of time and now sees one, and a second call in the same update no longer returns about zero. Accumulate in the system, or switch toElapsedGet()and sum the values you need.DeltaTimeGet()now carries the part of a millisecond that it cannot show into the next update, so the running total stays within a millisecond of the real one. A 33 333 microsecond step reads mostly 33 with a 34 about every third update, where it used to read 33 every time.DeltaTimeGet()andTiming::GetFrequency()hold 32 bits and saturate at 4 294 967 295 (about 49.7 days of milliseconds, and about 71 minutes of microseconds) instead of wrapping.ElapsedGet()andGetInterval()are never limited.
Component lookups that do not change the container, a sole-ownership attach and shared read-only resources.
This is a minor release although several changes are source-incompatible, following the precedent of 1.4.0
to 1.6.0; the migration of each is below. Plugins that call a changed function must be rebuilt
(the-seed build); plugins that only define a component class and create_component keep working without
a rebuild, because the changed functions are not part of what they call. This was checked by loading both
kinds of plugin, built against 1.6.0, into 1.7.0: the factory-only plugin loaded and worked, and the plugin
that called Entity::Component(new ...) failed with an undefined symbol.
New:
Container::ComponentGet<T>(entity, type)andComponentHas(entity, type), and the same two onEntitywithout the entity argument. See Entities, components and resources.src/example.cppno longer reads through the table: it looks velocities up withComponentGetand skips an entity that has none.
Source-incompatible changes and their migration:
| Old | New |
|---|---|
e->Component(new T(cfg)) |
e->Component(std::make_unique<T>(cfg)) |
e->Component(loader.Create(...).release()) |
e->Component(loader.Create(...)), passing the std::unique_ptr itself |
container->Component(shared) |
container->Component(std::move(unique)) |
(*Components)["T"][entity] to read a component |
Container->ComponentGet<T>(entity, "T"), then test the result for empty |
auto r = container->ResourceGet(name); (a copy of the bytes, throws for an unknown name) |
auto r = container->ResourceGet(name); is a std::shared_ptr<const ecs::Resource>: test it for empty and read through ->; an unknown name no longer throws |
Entity::Component()andContainer::Component()take astd::unique_ptr<ecs::Component>by value. The overloads taking a raw pointer and astd::shared_ptrare removed, so those forms stop compiling. The caller's handle is empty after the call, and a rejected component is released exactly once. The error texts and the rule that a second component of a type replaces the first are unchanged.Container::ResourceGet()isconstand returnsstd::shared_ptr<const ecs::Resource>without copying the bytes; an unknown name gives an empty pointer instead of an exception. A caller that modified the returned copy must now add a changed resource withResourceAdd().Container::ResourceAdd()moves its argument when it is given a temporary or astd::move()d value.
No data member or virtual function was added to or removed from a public class.
New Container::Stop(). No existing signature changed and System is unchanged. Container gained
private members at its end, so code that embeds a Container by value must be rebuilt; no plugin
needs rebuilding. The behaviours below changed. See Running a container for the rules.
-
System::Shutdown()now runs. Before, it was never called. A system that overrides it will start running that code. It is called once for each started system, on these paths and threads:Path Thread SystemDestroy()or replacementThe container thread, when the system is released Container::Stop()orManager::Shutdown()on a container with its own threadThat container's thread, before it ends Destruction of a container with its own thread, or of its manager That container's thread, before it ends Container::Stop()on a container driven byUpdate()The thread that calls Stop()Destruction of a container driven by Update(), or of its managerThe thread that destroys it Migration: check that every existing
Shutdown()override is safe to run, that it uses only what is still valid then (the container's tables and log destination are), and that it does not block for long. Release resources there that were released by process exit before. -
Systems registered after a container started, and systems in a container driven by
Update(), are now started withInitialize()before their first update. Before, they were never started unless the code calledSystemsInitialize(). Migration: remove code that relies onInitialize()being skipped for such a system. Code that callsSystemsInitialize()itself keeps working and does not start a system twice. -
Start()on a container that is started is a no-op, and a stopped container cannot be restarted. Before, a second call started a second thread. Migration: make a new container to run again. -
Manager::Shutdown()blocks until the containers with their own threads have stopped, unless it is called from a container thread, where it only requests. Before, it returned at once. Migration: do not call it while holding a lock that a system also takes; pollingIsRunning()is unaffected. -
A container thread that fails, in start-up or in an update, now stops every other container of its manager, because it calls
Manager::Shutdown(). Migration: if the other containers must outlive a failure, catch the error in the system. -
Update()after a container has been stopped does nothing, andDefer()after a stop is dropped. Migration: stop a container only when it is finished. -
The program template and
src/example.cppcallECS->Shutdown()after their loops. Migration: add the call to an existing program that relies on process exit to end its container threads, beforemainreturns.
No plugin rebuild is required.
Public signatures and object layouts are unchanged, so nothing needs to be rebuilt. The behaviours below changed. See Errors for the rules; the error tables are with systems, components and messages.
- A malformed message to
Manager::MessageSubmit()orContainer::MessageSubmit()throwsstd::runtime_errorinstead of aborting the process. A field of the wrong type, which used to thrownlohmann::json::type_error, now throwsstd::runtime_error. Code that catchestype_erroraround a send should catchstd::runtime_error. - An empty container name or system name in a message is rejected. A container created with an empty handle can no longer be addressed by message; give it a non-empty handle.
- Registering a system under an identifier that is still in use replaces the earlier system: one system
remains, at the old position in the update order, started and updated once. Before, the new system was
also listed a second time, so it was started and updated twice in every pass. Code that registered a second system under a used identifier on purpose should remove the
first with
SystemDestroy()if it needs the new one at the end of the order. - A null system, a system with an empty
Handleand a system that is already registered are rejected. A rejectedSystem()call destroys the system that was passed in (an already registered object is not deleted). - A component with an empty
Type, an emptyEntityHandleor an unknown entity is rejected. A plugin component that never setsType, or a factory that returns null, now fails when it is loaded. SetTypein the component's constructor. - A rejected
Entity::Component()call deletes the component that was passed in. Do not use the pointer afterwards. - A second component of a type on the same entity replaces the first; before, the outcome was not defined.
- Creating an
Entitywith a null container throws.
- New
Container::Defer()hands a change to a running container's own thread. See Deferred changes. - The layout of
System,ContainerandManagerchanged, so plugins and every consumer built against 1.3.0 must be rebuilt against the new headers.the-seed builddoes this. - Systems are addressable by routed messages only if they are registered through
Container::System(). Code that inserts intoContainer::Systemsdirectly must register withContainer::System(std::make_unique<...>(...)). System::MessagesWaiting()now also counts messages that were delivered but not yet moved to the queue (it moves them first), and is for the container thread only. Code that called it from another thread to see a backlog should count on the container thread, or have the system publish its own counter.- The error for a message to an unknown system names the container it was sent to, rather than reading the container from the message. The exception type is unchanged.
- A message sent during a pass is first handled at the destination's next update. Do not rely on same-call or same-pass handling.
- Changing a running container from outside threads was always a data race. Wrap such a change in
Container::Defer(). ~Managerwaits for sends in progress and destroys its containers from its body, which removes a race for embedding code that destroys aManagerwhile its container threads message each other.
- The object layout of
ecs::Uuidchanged (16 bytes, alignment 8), so plugins must be rebuilt against the new headers.the-seed builddoes this. uuid_v4.handendianness.hare no longer installed, and installing removes any copies left in the prefix by an earlier version.--disable-builtin-uuidis retired. The library always uses its own identifier generator and no longer needs libuuid.- Parsing identifier text is correct and strict, and throws
std::runtime_erroron malformed text. ecs::Uuid::Get()isconst.ecs-cpp.pccarries-std=c++20 -pthread.- The library no longer needs processor extensions and builds on 64-bit ARM.
The rules in Adding and removing systems during a pass
and Changing timers from a callback are now guaranteed. The object layout of
ecs::Container, ecs::System and ecs::Timer changed (private members only, no public signature
changed), so plugins must be rebuilt against the new headers. the-seed build does this.