By factoring element and other static-ish data out of Simulation and protecting basic graphical element properties (i.e. everything that contributes to graphics other than the Graphics callback) with an std::shared_mutex. This is taken exclusively (std::unique_lock) by the main thread when it changes these properties, and inclusively (std::shared_lock) by non-main-thread code that uses Renderer.
The idea is to have the following version information included:
- 1-component save version
- 2-component under the hood but the minor component shouldn't ever change again
- see currentVersionMajor in GameSave.cpp
- 1-component website API version
- again, currently 2-component because that's what the website code expects
- see apiVersion in requestmanager/Common.cpp
- 2-component display version, entirely cosmetic
- exposed as meson options display_version_major and display_version_minor
- see APP_VERSION in Config.template.h
- 1-component business logic version aka build number
- exposed as meson option build_num
- see APP_VERSION in Config.template.h
- variant id aka mod id, tightly coupled with the build number
- exposed as meson option mod_id
- see MOD_ID in Config.template.h
- display and business logic versions repeated for the upstream
- exposed as meson options upstream_version_major, upstream_version_minor, and upstream_build_num
- we'll have to update these alongside display_version_major, display_version_minor, and build_num, but mod owners can just merge our changes
- see UPSTREAM_VERSION in Config.template.h
- update channel, makes sense in the context of the variant (and yes, this would later enable mod snapshots)
- currently not exposed as a meson option but derived from meson options snapshot and mod_id
- see IDENT_RELTYPE in Config.template.h
- vcs tag aka git commit hash
- set by build.sh in ghactions workflows
- see VCS_TAG in VcsTag.tempalte.h
Rather importantly, the save and website API versions are now allowed to change independently of the display version.
These changes also allowed me to remove the ugly sed hacks in build.sh used to provision some manifest files; they are now provisioned by meson.
Also add version info for windows and android.
For this to work, loading code needed to stop trusting DEFAULT_PT_ identifiers, which it trusted because there have been some identifier changes between vanilla releases. I dug these up and listed them explicitly; they are now taken into account as needed when loading old enough saves.
Saving blockair/blockairh is nice because RecalculateBlockAirMaps uses the sim rng, which means the sim rng would get advanced in Simulation::Load. Also rename RecalculateBlockAirMaps to ApproximateBlockAirMaps because that's what it is, an approximation, and it's needed only if there are no block air maps in the save.
Simulation::frameCount keeps track of frames elapsed since the beginning of the simulation, zeroed at clear_sim. It overflows when it reaches the 64-bit limit, which means anything that depends on it should either handle this, or not fail catastrophically. sandcolour (the only thing that depends on it as of now) is a good example of the latter: sandcolour has a periodicity of 360 frames, which means that there is one sandcolour period that is cut short by the overflow. This is not "handled" (the period is cut short, which is detectable by users) but is not catastrophic either (it's not a big deal, and it won't ever happen unless someone hacks the save).
Also restrict saves with determinism data to 98.0.
Achieved by adding a new element property called CarriesTypeIn, whose bits signal to save loading code which properties of particles of the element class in question carry element IDs. The bits in this property are numbered the same way as sim.FIELD_* constants for consistency. One would signal from Lua that a custom element carries element IDs in its tmp like this:
elem.property(id, "CarriesTypeIn", 2 ^ sim.FIELD_TMP)
"Carrying an element ID in a property" is to be interpreted as follows: the property is treated as a combination of a PMAPBITS-bit (so, currently 9-bit) unsigned integer lower part holding an element ID and a 32-PMAPBITS-bit (so, currently 23-bit) signed integer upper part holding whatever makes sense for the element. CONV, for example, uses this signed integer in its ctype as the extra "v" parameter for particle creation.
Some operator[]s that know the size of the container they wrap like to assert(index >= 0 && index < size), which is bad for us because we sometimes use &container[size]. This is not undefined behaviour until that pointer is dereferenced, but certain operator[]s choose to ignore this fact and err on the side of caution. The solution is to use &container[0] + size instead of &container[size].
I took extra care to not mess up signedness in readOPS in ab600780d0, but apparently didn't do the same in readPSv.
Also fix a bound check that was broken since aac6b7258c. It's a good thing this was broken, because this allowed negative type values from broken signedness readPSv to get past and cause a crash later on, rather than just cause particles to disappear or something.
Namely:
- get rid of unsafe memory management;
- use vectors / Planes everywhere;
- return a vector from serialization functions;
- have read functions take a vector;
- improve constness;
- hide a few implementation details from GameSave.h;
- get rid of GameSave copy constructor;
- better member initialization;
- use the slightly more C++-looking BZ2 wrappers.
The BSON library still takes ownership of the data it parses, and GameSave
ownership is still a joke. Those will need to be fixed later.
... and everything built around them.
A GameSave would hold at least one but sometimes two representations of a save:
one serialized, and one "friendly", accessible for modification. Thus, a
GameSave would have three states:
- "Collapsed": only the serialized representation was present; this was the
initial state of GameSaves loaded from files;
- "Expanded With Data": both the serialized and the friendly representations
were present; this was the state of GameSaves loaded from files after a call
to Expand;
- "Expanded Without Data": only the friendly representation was present; this
was the initial state of GameSaves being prepared for being saved to files.
A GameSave would be able to go from Collapsed to Expanded With Data with a call
to Expand, and back with a call to Collapse. Of course, this latter transition
would discard any changes made to the friendly representation, for example with
Translate. A GameSave would however be unable to go from Expanded Without Data
to any other state; a call to Collapse in this state would have been a no-op.
There were two instances of Collapse being called, one in the GameSave
constructor taking the serialized representation, immediately after a call to
Expand, and another in SaveRenderer, which would Collapse a save "back down" if
it had originally been Collapsed. Now, consider that there reasons for
constructing a GameSave from the serialized representation are as follows:
- loading an online save at startup from the command line;
- loading a local save at startup from the command line;
- loading a local save when it is dropped into the window;
- loading a local save for placement of the most recently used stamp;
- loading a local save for stamp placement via Lua;
- loading an online save for preview generation while browsing;
- loading a local save in the stamp browser for thumbnail generation;
- loading a local save in the local save browser for thumbnail generation.
In some cases, the friendly representation is needed for thumbnail generation
by ThumbnailRendererTask. ThumbnailRendererTask operates on its own copy of the
GameSave, because it runs SaveRenderer on a thread different from the main one
and cannot be sure of the lifetime of the original GameSave. It destroys this
copy when it is done rendering, so the call SaveRenderer makes to Collapse is
pointless.
In all other cases, the friendly representation is needed immediately. In some
of these, SaveRenderer is used from the main thread, but since the friendly
representation of the GameSave will be needed for pasting anyway, the call
SaveRenderer makes to Collapse is pointless again.
So, Collapse goes away. This also means that it is pointless for GameSaves to
hold on to the serialized representation, since in all cases in which they have
access to it, the friendly representation is needed immediately, and with
Collapse gone, they will never need it again.
Also Disallow linking against non-C++ system Lua, unless configuring with -Dworkaround_noncpp_lua=true, add -Dworkaround_elusive_bzip2 and friends, and get rid of the -image_base hack for macos.
This made it possible to get rid of two GameSave constructors.
Also clean up Client::LoadSaveFile, Client::ReadFile, and Client::WriteFile in the process, and remove unused SaveRenderer::Render
Also restructure meson.build and the ghactions workflow a bit, and enable -ffunction-sections and -fdata-sections.
Note that starcatcher uploads have not been tested and most likely don't work.
Empty stamps are those whose block width or height is 0. While they are technically valid and certain parts of the game are prepared to handle them, others aren't, so it's safest to just adjust the definition of valid stamps to exclude empty ones.
Repurposing the comment I added because I'm lazy: pavg[1] used to be saved as a u16, which PressureInTmp3 elements then treated as an i16. tmp3 is now saved as a u32, or as a u16 if it's small enough. PressureInTmp3 elements will never use the upper 16 bits, and should still treat the lower 16 bits as an i16, so they need sign extension.
The code that was originally responsible for this somehow got lost in the tmp3/tmp4 migration.
Also add GameSave::PressureInTmp3 to check for elements with pressure memory and fix TUNG not sampling pressure on creation. This does not in itself fix#822 because tmp3 and tmp4 are still saved in 16 bits each, so full ctypes still don't fit in tmp3.