doldrusidus

repository: doldrusidus

An open-ended, obscure simulation realized as a multiplayer game taking place in a small universe, where you can participate by assembling and emulating machine code for the uxn virtual machine architecture.

The game is separated into a core engine and a modding layer that can be scripted in C. You can find out more about the uxn virtual machine at the xxiivv wiki, and a good place to familiarize yourself with programming it is the compudanzas uxn tutorial. However, you can jump right in without these at first, because some example assemblies and roms are already included in the downloads.

Install instructions

The game requires a C compiler for mods, make sure to set the path to it in res/config.json if it differs from the default, which is cc. Also, to be able to create ROMs that machines on the server will emulate, you’ll need an assembler for the uxn architecture.

Supported CPU architectures:

  • x86_64 - 64-bit x86
  • i686 - 32-bit x86
  • aarch64 - 64-bit ARM
  • armhf - 32-bit ARM with the VFP3-D16 extension

For more information, see the included README.md and the help menus of the server and client programs by passing them the --help flag on the command line.

Binaries

Hosted on desertslug.itch.io/doldrusidus.

Note: the builds for x86_64 are targeting 64-bit machines, and the ones for i686 are for 32-bit machines. Use the release version by default, debug builds are meant for diagnostic purposes.

Development logs

2022-12-26 - Client graphical interface

At long last, development has arrived at the point where the next step seemed to be to start creating a graphical interface for the client. More concretely, the world is now rendered from a third person perspective, in a bitmapped/aliased style.

Currently, each entity type is shown with a distinct sprite, and their position above or below the camera’s plane with a continuous or stippled line. The entity’s unique ID is shown to the left of its sprite, and the distance to the camera to its right. Some basic movement options like jumping to an entity or following it were also implemented. For now, all of these controls are wired up to both the keyboard & mouse, and a generic gamepad.

A lot of these changes happened on the modded side of the code, so you can easily inspect or change a lot of what drives this functionality. For example, to use a different font, if you have a “.bdf” file for it, all you have to do is to use the --bdf2tga option on the command line to convert it to a spritesheet in .tga, and to load the font from there instead, changing the specified width and height of the glyph if necessary.

You can fill a world with entities now using the >generate <seed> command, which will generate the world corresponding to that seed each time, deterministically.

2023-02-08 - Interface improvements, proquints

The 0.4.1 version is a smaller update that expands on the graphical interface, and adds support for proquints (pronounceable quintuplets) in multiple places.

The interface now shows the seconds elapsed since the epoch at the top, and the camera’s origin and distance at the bottom, all in hexadecimal. When you hover over an entity using the cursor, a panel is displayed for them that shows their most important properties and those that are specific to them.

Notice that after the hexadecimal entity identifier, the same is also displayed in proquint form (e.g. bajon for 0x0169). To learn more about proquints, check out this proposal. Also, you can find the implementation I’ve derived from the reference in the project’s git repository. In a nutshell, proquints encode 16-bit words as five letters (alternating consonants and vowels).

Four-bits as a consonant:
    0 1 2 3 4 5 6 7 8 9 A B C D E F
    b d f g h j k l m n p r s t v z

Two-bits as a vowel:
    0 1 2 3
    a i o u

Whole 16-bit word, where "con" = consonant, "vo" = vowel.
    0 1 2 3 4 5 6 7 8 9 A B C D E F
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |con0   |vo1|con2   |vo3|con4   |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |-Hex0--|-Hex1--|-Hex2--|-Hex3--|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The server can tell you the proquint form of an IPv4 address if you specify it after the --ipv4-proquint option. The client now accepts addresses in proquint form after the -a option (e.g. lusab-babad for 127.0.0.1). Note that the configuration file still only takes addresses in dot-decimal notation, proquints are reserved for the command line for the time being.

2023-02-13 - Instant opcodes, more proquints, manatee

The v0.4.2 version brings uxn emulation up to spec regarding the call/lit opcodes, rounds out proquint support, introduces the default OS for starships, and fixes a few bugs encountered along the way.

The last three opcodes added to uxn (JCI/JMI/JSI) should now be emulated as they are in uxnemu. The JCI and JMI opcodes are emitted by the ? and ! runes, and they take the relative address to jump to as the next short in memory, making these jumps faster during emulation, and terser in the source code.

The delown server-side command now takes proquint identifiers, and the ipv4address starting configuration option now also accepts a double proquint (with a separator in between). The client-side focus-setting command > now accepts proquints, and the prompt shows the focused entity in both forms.

Lastly, the new opcodes were a good excuse to finally rewrite cmd.tal and controller.tal to merge them into manatee.tal, the first iteration of an interactive operating system for ships. For now, it is a state machine with a shell that can execute commands with some arguments.

The emulator now triggers the vector of the Screen device every tick, so ships can now step in sync with the simulation. For example, if the ship has to do nothing in an idle state, and a zero byte represents that state, the vector can take as few as three instructions to run.

@on-screen ( -> )

    ;state LDA [ ?on-screen/no-idle ] BRK
    &no-idle

BRK

Other notes:

  • delown now correctly removes even non-owned entities, and it removes from the owner otherwise (not the user running the command)
  • the reported OpenGL version is now logged during initialization
  • logs are flushed after shader compilation, now they reach disk even when an OpenGL function crashes
  • keyboard keys can now be mapped to dual controller axes, correctly updating both
  • another pixel-offsetting trick should help with pixel-perfect display of sprites & bitmap fonts

2023-02-20 - Oases

The v0.5.0 version sees the implementation of the first mechanism that resembles a game mechanic: oases that host nonsentient organic life (autotrophs and heterotrophs). Ships can now carry vat modules that will allow them to store samples of such life, and to spread them to barren worlds.

Oases run a sort of probabilistic cellular automata inspired by predator-prey simulations (heterotroph-autotroph here, respectively).

Each simulated step consists of two phases:

  • heterotroph phase, where autotrophs become heterotrophs (according to the heterotroph reproduction rate, if they have at least two heterotroph neighbors), and heterotrophs become dead cells (according to the heterotroph death rate),
  • autotroph phase, where autotrophs can reproduce into a random neighboring dead cell (according to the autotroph birth rate), or become a dead cell (according to the autotroph death rate).

Trophs may be injected into a simulation according to a specific ratio (which currently favors autotrophs).

Trophs may be sampled from a simulation. Only those that exceed a certain population threshold are sampled (to protect against extinction due to oversampling), and this threshold is applied to each cell type according to a specific ratio.

Other notes:

  • fixed the lack of indexing on memory components in ECS queries
  • separated sprite renderer setup from texture loading (so that it may support more use cases)
  • the maximum velocity and acceleration of ships is now determined based on their thrust-to-weight ratio
  • replaced ;label JSR2 calls with just label, which will emit the JSI opcode

The v0.5.1 version adds more guidance modes to the navigation module, alongside the smaller fixes it brings.

Ships now have a greater variety of movement methods to choose from. They can approach or orbit a coordinate at a specified distance, or impact a coordinate at speed.

HOST_WRITE_NAV_ABS : &x $2 &y $2 &z $2 &dist $2 ->

Sets navigation to approach the specified coordinates at a distance.

HOST_WRITE_NAV_REL : &x $1 &y $1 &z $1 ->

Sets thrusters to push towards the specified direction. The coordinates are integers in [0, 255] representing a floating point number in [-1, 1), with 127, `0x7f` mapping to zero.

HOST_WRITE_NAV_ORB : &x $2 &y $2 &z $2 &dist $2 ->

Sets navigation to orbit the specified coordinates at a distance.

HOST_WRITE_NAV_IMP : &x $2 &y $2 &z $2 ->

Sets navigation to impact the specified coordinates.

Ships can now query the position of entities, and use this information to, for example, follow other entities by continually updating their navigation target to their position.

HOST_WRITE_NAV_ENT_POS : &entity $2 -> &entity-exists $1 : &x $2 &y $2 &z $2

Attempts to query the position of an entity.

Other notes:

  • nav module targets are displayed for some movement modes
  • fixed the indexing on uxn components upon pushing a screen device events
  • disabled diagnostic messages for MsgSrvEntityCommand
  • expanded line colors with an alpha component
  • fixed an out of bounds loop in manatee that occurred upon parsing fewer parameters to a command than expected

2023-03-24 - Habitable worlds

The v0.5.2 version introduces habitable worlds that host larger autotroph/heterotroph simulations, which can be harvested for organic raw materials.

Habitable worlds differ from oases in that their stochastic parameters will not always allow stable ecosystems. Their base parameters vary randomly, and they are also affected by changing weather conditions. Some weather conditions may combine with base parameters in a way that will lead to collapse over a long enough time period.

These worlds will accumulate organic raw materials with a rate proportional to how large and diverse their populations are. For example, if heterotrophs go extinct and only autotrophs remain, no output materials will be generated. Output materials can be harvested, but this will harm populations in the process, so tending to such worlds will require a steady input of autotroph and heterotroph samples.

HOST_WRITE_VAT_HAR : &entity $2 ->

Harvests organic raw materials from a troph simulation that accumulates them. Populations are reset to zero in the process. The entity must be no farther than TROPHS_INTERACTION_UNITS.

Other notes:

  • jumping to entities now only happens upon the initial button press
  • fixed vertex counts while displaying nav module targets
  • the current octant of the camera is now displayed at the bottom
  • frame/tick Screen device events are now only pushed to waiting machines
  • fixed an inconsistency in entity synchronization that caused some entities to be left out

2023-03-31 - Monoliths & module growth

The v0.5.3 version adds monoliths that hold secret texts, which can be deciphered one character at a time.

Monoliths can be deciphered by donating machine instructions to them. These instructions can be generated by compute modules, and can be used for general-purpose computation, or to be donated to other entities.

Because monoliths output their next deciphered character to the last donator, it only makes sense for a single machine to decipher a monolith. However, deciphering can be sped up by chaining multiple machines together, each donating instructions to the one in front of them.

HOST_WRITE_COM_POW : &entity $2 -> &monolith-deciphered $1 : &char $1

Donates remaining instructions to an entity that can use them. Ships can use received instructions for computation or they can pass them on. Monoliths use these instructions in their deciphering process.

The secrets themselves are intended to come from a server configuration file so that server implementors can choose to omit these secrets from the files they distribute to their clients, to avoid spoiling them.

The capabilities of ships can now be increased and decreased through the set of modules they hold. This set may be increased until a limit on the total number of modules, and decreased until a limit on the minimum set of modules.

HOST_WRITE_SYN_GRO : &module-type $1 ->

Grows an additional module, consuming the materials it requires in the process. Needs at least one free module space.

HOST_WRITE_SYN_ABS : &module-type $1 ->

Absorbs an existing module, regaining some portion of the materials it required in the process. Needs at least one more module of that type than is required by minimum.

Other notes:

  • instruction budgets for ships are now determined from compute modules
  • fixed progressive deserialization clashing with world saving/loading by zeroing out memory in newly added components
  • fixed entities disappearing after a world is loaded, which was caused by not adding them to the sweep-and-prune data structure
  • commands on the host device now deduct instructions upon being invoked, respective to their complexity
  • navigation now tries to take maximum acceleration and velocity into account to avoid overshoots

2023-05-19 - Wormholes

The v0.5.4 version brings another option for traversing the world.

Wormholes are warps and folds in the curvature of spacetime, a pair of which connect two distant points in space. A wormhole draws in entities inside its sphere of influence, and warps them below a threshold distance to a target position which lies outside the influence of the wormhole on the other side.

Currently, wormholes are naturally occurring features, becoming fixed upon world generation. They should help break up the flatness of space, bringing a few distant locations closer together.

The drives of ships can now be throttled between zero and full thrust, which will affect the maximum velocity of movement modes. Useful for slowing down the leading ship in a chain so that the slowest ship can keep up with it.

HOST_WRITE_DRV_THR : &throttle $1 ->

Throttles drives between zero thrust and full thrust with 0x00 - 0xff.

Other notes:

  • the maximum velocity of ships is now greatly reduced, bringing the size of the world closer to its planned proportions

2023-06-02 - Signals & barren worlds

The v0.5.5 version adds a communication channel for ships, and inorganic materials that come from barren worlds.

Barren worlds are common bodies where the harsh environmental conditions are unsuitable for the growth of organic life. However, they contain vast amounts of inorganic raw materials, which makes them quite valuable in the eyes of interstellar entities.

The extraction modules of ships can accumulate and convert inorganic raw materials.

HOST_WRITE_EXT_BAR : &entity $2 ->

Extracts inorganic raw materials from barren worlds. The entity must be no farther than `BARREN_INTERACTION_UNITS`.

Ships are now capable of emitting a signal that can carry a few bytes of information (8, currently), as well as reading the signals of other ships. This is intended to be a flexible tool to allow ships to communicate with each other through user-defined protocols.

HOST_WRITE_NAV_SIG_SET : &signal $8 ->

Sets the signal of the navigation module.

HOST_WRITE_NAV_SIG_GET : &entity $2 -> &entity-exists $1 : &signal $8 

Attempts to read the signal of the specified entity's navigation module.

For example, the manatee OS now supports mimicry. When a ship is set to mimic another, it reads its signal, and finds the state of the ship in the first byte. Then, it jumps to the handler function of that state, while staying in the mimicry state itself. Other arguments of states can be placed after the state byte, such as the barren world to extract inorganic materials from in an extraction state.

Other notes:

  • two bytes of the Uxn component are now updated after emulation so that clients can now display the sizes of the working and return stacks
  • the client can now toggle between displaying entity identifiers as either hexadecimal quartets or proquints
  • fixed a small leak in srv_msg_recvproc_srventitycommand
  • wired up better camera controls in controller input mode
  • drive modules produce more thrust, and the maximum velocity and acceleration of ships is increased
  • relative ship motion was only using half thrust, now it’s using full thrust again

2023/06/13 - Hives, wasps & leviathans

The v0.6.0 version unleashes the first spacegoing forms of life.

In this cycle, a fair bit of time was spent refactoring ship modules to be usable with other entity types as well. Adding a module to an entity now requires adding it to the struct that holds its data members, and registering the byte offset of the module inside a table. This table is zero-initialized, so zero is considered a safe invalid value for a module because entities never have a module at a zero-byte offset (they have their type information there). I guess you could think of this as a crude form of a static entity component system.

This means that a lot of entity-related functionality on both the server and the client sides can now be expressed generically, without constraining the type of the entity. For example, the host device event handlers were all rewritten to work with modules, not ships. Also, clients can now use navigation and drive modules to integrate entity positions more accurately, reducing jitter from acceleration.

Hives are ancient, sprawling structures found at regular intervals, buzzing with calculated self-replication. They send wasps towards barren worlds to secure and exploit inorganic raw materials.

Wasps are non-sentient forms of machine life assembled in hives. A wasp flies out towards a barren world to guard and extract inorganic raw materials from it. Launches stingers at hostile entities that come into the range of its sensors, but will not deviate otherwise from its purpose.

Leviathans are immortal creatures of the void exhibiting deep-space gigantism. Suspected to be non-sentient, they are hostile to all other known forms of life. They wander the universe randomly, and they avoid deep gravity wells. If their hull is destroyed, they fall into a dormant state to regenerate it.

Stingers are projectile weapons launched by wasps that damage the targeted entity’s hull upon impact. They expire and self-destruct if they do not reach their target in time.

Also, now that there are some entities moving about and querying the world around them, I got curious about performance. While I’m uncomfortable sharing numbers that will not reflect your configuration (due to hardware) and will increase both in the future (as the world grows denser and more complex) and with the number of connected clients (as the server needs to spend time synchronizing with them), I still think there are good reasons to record these numbers here.

Running on an i3-10100F, compiling mods with gcc and -O1, starting a server, logging in, generating a world, and waiting for wasps to fly out, the time taken to tick the world climbed up to roughly 2 milliseconds in release mode, and 3 milliseconds in debug mode. For a server that ticks 10 times a second, this means about a 2-3% load (currently running on a single core).

I got curious about these numbers because I would like servers to be able to run on efficient, low power CPUs, such as ARM Cortex and Intel Atom processors. For example, running the current world on a Cortex-A72 (4-core, 1.8 Ghz), I could expect performance on a single core to be 4-6x worse, resulting in a 10-15% load, which makes me hopeful that there’s still room to grow.

Note that while performance seemed even, the measurements showed spikes every few ticks, because the cpu frequency scaling governor on my linux machine was set to schedutil. When measuring performance, make sure to set your governor to performance temporarily to make it avoid dropping cpu frequency at low loads, which skews measured times by about 3-5x in my experience.

Other notes:

  • the client now uses a color palette, removing many repetitive color conversions
  • the client now integrates position and point physics from navigation and drive modules
  • some host device event commands were renamed and moved to different indices
  • some utility functions were generalized to help with concise entity logic
  • troph simulaition updates are now staggered, spreading out their performance spikes
  • ships are now spawned with a slight random offset from the origin
  • entity addition and deletion queues were added for the server
  • controller keybind conflict for GL_GAMEPAD_BUTTON_DPAD_DOWN is now resolved

2023/06/22 - Wreckages & missiles

The v0.6.1 version enables ships to repair, restock, and defend themselves.

One comfort that was missing from hex-quartet-or-proquint entity identifiers was being able to specify proquints in ship commands. The default ship OS, manatee, is now able to parse shorts in both hexadecimal and proquint form. The uxntal code that does proquint parsing is available separately at proquint-parse.

$ hexdump proquint-parse.rom
0000000 4020 4100 4200 4443 4521 4746 4948 4a22
0000010 4b00 4d4c 4e23 0000 4f00 0000 00a0 a000
0000020 1a01 2635 39a4 00a0 2805 0020 9414 8006
0000030 0a60 8004 0b7b 201c 0300 0040 2104 ff40
0000040 24e3 a0b9 0500 2028 0500 2222 0040 a83f
0000050 0020 9431 6180 8019 0400 01a0 3800 0614
0000060 0f80 041c f080 061c 0020 2206 2222 0040
0000070 a01d 1a01 0534 053f 0080 3804 01a0 351a
0000080 4021 cbff 2222 01a0 341a 00a0 6c05 00a0
0000090 a000 0000 006c
0000095

Along with this, command argument parsing was reworked in manatee. While the previous way of returning values on the return stack, and jumping from parsers to an error handling label to pop parsed bytes was really clever, it got too clever and unstructured for my taste. Parsers now write their result to a location they receive as an argument, and the returned length of the parsed word can be used to determine if the parser failed, in which case execution can safely skip forward without leaving the stack unbalanced.

In general, this rework taught me that knowing when not to use the stack while writing uxntal is just as important as knowing how to use it. Writing values out to memory takes little overhead, and it seems to me that it’s worth it if it means the contents of the relevant portion of the stack can be kept as shallow as possible, to keep the code understandable.

Wreckages are the remains of destroyed ships. A wreckage repeats the signal of the originating entity, and contains a portion of the materials the originating entity used to grow beyond its minimum set of modules. These materials can be salvaged using a synth module.

HOST_WRITE_SYN_SAL : &entity $2 ->

Salvages raw materials from wreckages. The entity must be no farther than SYNTH_INTERACTION_UNITS.

Synthesis modules are also capable of repairing and restocking entities.

HOST_WRITE_SYN_REP : &entity $2 ->

If the synth module is idle, sets it to repair an entity's hull, and sets the specified entity as its target. If the entity is farther than SYNTH_INTERACTION_UNITS, the synth module will return to idling.

HOST_WRITE_SYN_RES : &entity $2 ->

If the synth module is idle, sets it to restock an entity's bay, and sets the specified entity as its target. If the entity is farther than SYNTH_INTERACTION_UNITS, the synth module will return to idling.

Missiles are projectile weapons launched by ships that damage the targeted entity’s hull upon impact. They expire and self-destruct if they do not reach their target in time. Missiles are stored in and launched from bay modules.

HOST_WRITE_BAY_LAU : &entity $2 ->

If the bay module is idle, sets it to launch a missile towards the specified entity. If no missile is available, the bay module will return to idling.

The host device now provides utility functions to generate random bytes and shorts.

HOST_WRITE_RNG : ->

Generates a random byte, and writes it to the UXN_PORT_HOST_RNG_LO port of the host device. Writes no other ports.

HOST_WRITE_RNG2 : ->

Generates a random short, and writes it to the UXN_PORT_HOST_RNG_HI - UXN_PORT_HOST_RNG_LO ports of the host device. Writes no other ports.

The code that needed a random number to alter control flow was the sustain state in manatee, in which it tries to extract inorganic materials from a barren world, while repairing and restocking any ships in range. To make sure that it both repairs and restocks entities (because the state machine of synth modules would ignore the second request, as it’s not idling), the ship decides which action to take based on a coin flip.

HOST_WRITE_RNG .Host/write DEO
.Host/rnglo DEI #80 LTH [ ?on-screen/sustain-rng-less ]

    ( write synth restock command to the host )
    HOST_WRITE_SYN_RES .Host/write DEO

&sustain-rng-less

( write synth repair command to the host )
HOST_WRITE_SYN_REP .Host/write DEO

Nav modules can query a radius around themselves to see if there is an entity inside it. They can also query the type of an entity.

HOST_WRITE_NAV_ENT_TYP : &entity $2 -> &entity-exists $1 : &type $1

Attempts to query the type of an entity.

HOST_WRITE_NAV_ENT_GET : &radius $2 -> &entity-exists $1 : &entity $2

Attempts to query an entity within a specified radius.

Note that the current configuration values for entities may be unbalanced, so gameplay may end up being too easy or too hard at this point.

Other notes:

  • extract modules now operate with cooldowns instead of accumulators, for easier programmability
  • the way module capacities are calculated was reworked for consistency and to reward specialization
  • some host device event commands were moved to different indices
  • fixed module stats not updating after modules were grown or absorbed
  • the zeroeth entity is now a star at the origin, to ward off leviathans from the current spawn point

2023/06/27 - Artifacts & ruins

The v0.6.2 version enables ships to grow more modules by gaining fascination.

Civilizations are biologically evolved, sentient species living on their home planets. They create and appreciate cultural artifacts. When receiving an artifact, they share their fascination with whoever delivered it. They find artifacts from more distant civilizations more fascinating. Ruins are the remains of fallen civilizations.

Ships now carry cargo modules, which can store a single artifact.

HOST_WRITE_CRG_GET : &entity $2 ->

Takes an artifact from a civilization and puts it into an empty cargo hold. The entity must be no farther than CIVIL_INTERACTION_UNITS.

HOST_WRITE_CRG_PUT : &entity $2 ->

Takes an artifact from the cargo hold and gives it to a civilization. The entity must be no farther than CIVIL_INTERACTION_UNITS.

Once fascination is gained from an artifact, it will be accumulated in growth modules. As fascination increases towards its upper bound, the growth module grants further module space progressively.

Other notes:

  • reset spawn location to the origin
  • some host device event commands were moved to different indices
  • updated the uxn-ram entity command, because it gradually grew out of sync with everything else

2023/07/21 - Rewrite

The v0.7.0 version is a complete rebuild and a partial rewrite of the engine, on both the core and modded sides.

Let’s begin with a bit of a background on why this change seemed necessary. As I was closing in on releasing the previous version, I thought that it would make sense to test whether things really worked in a networked environment. While I already knew that things worked in a purely technical sense, I soon realized that the protocol of port usage is unpractical in real-world scenarios.

More specifically, every time a client has authenticated, the server would tell the client which port it will start listening on for a connection from that client. This port could be any from the ephemeral port range of 1024-65535. That means whatever environment a server administrator is working with, they have to be able to open all of these ports, and many real-world scenarios don’t really allow for that. For me, the reason was my router only being able to add a maximum port range of 100 in a port forward rule, and managing ~640 rules through the buggy abomination that is its web interface strikes a few points too high on my pain scale.

The problem I ran into was that I couldn’t figure out how to create the usual star topology on a single port using the abstractions of nanomsg-next-gen, the networking library I used at the time. The documentation was too terse, the interface seemed like it was working against my use case (even though it was supposed to be generic), and after finding an open issue where even the creators acknowledge this topology with free pairwise communication is a bit difficult, I knew it was time to move on to greener pastures. It would have been possible to tack on some solution around pooling a specific port range, but at that point I was already carried away by all the benefits that a rewrite usually brings.

In the end, I rebuilt the engine from the ground up around a networking library called enet. It’s a completely different beast aimed specifically at games, being built around UDP transport with optional reliability, with connections, sequencing and fragmentation all handled by the library. Using it ended up being fairly simple and comfortable, and it was easy to build my own generic packet handling interface on top of it. The first improvement of this rewrite is that I can put more faith in the security of the packet processing functions.

Rewrites are also a good opportunity to shed dependencies, to replace them with leaner and/or custom-made ones specifically suited to your needs. I’ve replaced ncurses with termbox2, which ended up being a great single-header alternative to a dependency with a lot of historical cruft. Also, I got rid of Cello in favor of my own type-erased data structures, such as unordered vectors, hashmaps, and strings.

This is where perhaps the second greatest change comes in. Because my ECS was written on top of the data structures of Cello, I had to rewrite it to use those of my own. The current ECS pretends very convincingly that it works right now, but it was very humbling to find a long-standing bug in the implementation after all this time (deleted entities were not removed from the empty archetype). On a related note, the static ECS that spontaneously emerged around entity modules is no more. It’s all part of the dynamic ECS now. Same goes for the Memory component and the segmented serialization scheme. In the end, I just wrote serialization and deserialization functions for all components by hand, and it ended up being simpler and less error-prone than the previous serialization scheme.

The last sweeping change is that a lot of core functionality was moved out into the default mod implementation. This makes the core less rigid, allows greater freedom for modders, and it simplifies the modded interface down to just a few functions. This means the game world, the ECS, serialization, windowing, input handling and a few other things are mostly up to modders now.

To summarize, a lot of core functionality was rewritten (and some of it became safer for it), a lot of it was moved out to the modded side, multiple dependencies were replaced (the core should build from scratch faster), redundant mechanisms were merged into more general ones (such as the static ECS being moved into the true, dynamic ECS), and the uxn core was updated to the current one (which should be a bit faster, while still remaining compact), and servers should be easy to set up to run on a single port. The interface towards uxn emulation is much better formed now (thanks to its creators), so this means that it should be much easier to swap in new cores in the future.

Rewrites are a strangely powerful thing, even if they usually take painful exertions of willpower to execute on moderately-sized personal projects. In retrospect, the old implementation usually looks like a bunch of shoddy abstractions piled on top of each other, even if you genuinely tried to build it to your highest standards. You can only get things so right on the first time around (or, as it seems, the second time around).

2023/07/30 - Quickstart & first argument insertion

The v0.7.1 version brings a quickstart guide and reimplements a command-line convenience.

After some time, it has dawned on me that the game is non-obvious enough that it would benefit from a guide that quickly introduces its core concepts and the interface to it in a walk-through sort of way. Now that I wrote it at last, it looks like it really had a gap to fill. While most of that information exists in the documentation somewhere, this provides a much more accessible way for someone to familiarize themselves with the project.

During the rewrite, one convenience feature was left out. If you press Tab right after a command’s name or shorthand in the client terminal, it will now insert the most recent first argument used for that command. This is most useful for repeating commands with the same target entity or to send chat messages to the same user.

Lastly, to reduce visual flicker when large portions of the world are synchronized sparsely, the client now forgets entities that have not received an update in a while in two stages, instead of just deleting them after a few seconds. Entities will first fade to grey, they are only deleted if a second limit expires. These limits can be tuned in the client configuration header.

Other notes:

  • the velocity and acceleration of moving entities was doubled

2023/08/18 - Porpoise, chibicc & particles

The v0.7.2 version sees a rewrite of the default ship program and adds particle effects.

Recently, I found a fork of chibicc (a tiny C compiler) that retargets it to transpile C code to uxntal. After making sure that it could emit code against emulators whose devices don’t conform to varvara (the mainline emulator for uxn), I set out to rewrite the ship program manatee (written in uxntal) into another one called porpoise (written in C).

It may be strange to hear that it seemed like I just had to reproduce the previous code, because the philosophy of programming in concatenative languages such as Forth says that if you are writing the imperative structures as you would in C (essentially transpiling a mental image by hand), you are doing it wrong. My impression is that in this case, that advice boils down to not ignoring the stack in how you think about and how you implement your program (as the hypothetical C programmer who would store every value in memory). It really makes me curious how the people familiar with Forth would have approached the task that I have here (parsing text commands).

I’m not sure if it’s indicative of anything, but it seems that the size of the original version’s assembly was 3552 bytes, and the rewrite came out to be 5049 bytes (grain of salt necessary, as they may not be equivalent in function) while I was thinking about the task in a similar way when I wrote the original version. That roughly corresponds to the size increase I saw when I tried transpiling the mandelbrot demo.

If you can put up with the increase in the size of the rom and the extra complexity that a small subset-of-C compiler requires, I think there’s a good argument for writing spaceship programs in C instead of uxntal, or any other high(er) level language that isn’t difficult to transpile to uxntal. The main thing I learned during this rewrite is that even if I try to indent and comment uxntal code, my brain wastes many cycles in decoding the structure I laid out every time I have to read it. Being forced to think in lower-level mechanisms introduces a mental overhead for me that is hard to ignore. Purely subjectively, it also felt less fussy and error-prone to write C, where I had a much better idea about the correctness of the code just by reading it. One feels like throwing nuts and bolts together to see what works, and the other feels more like doing clockwork surgery. That said, I still find it really cool that it’s possible to write these programs with nothing but an uxntal assembler.

After all this, I wanted to stretch a little with some graphics programming, for the sake of visual variety. Particles seemed like a well-separable system that could add some texture to the previously flat background. Basically, I only had to generalize the existing ECS slightly, define a few components for particles, and then render them with the sprite renderer. After I got up to the point where I could render some cosmic wind gently blowing some faint particles around, I went on to add a few other effects, around stars, ships, oases, monoliths and wormholes.

Other notes:

  • fixed the background not being cleared to the background color
  • added mass to stellar bodies (currently only used for particles)
  • clients receive acceleration as it is now reset after synchronization

created

modified