Prefabs in DST - nsimplex/wicker GitHub Wiki

How to write DST compatible prefabs

The following sections explain how to write prefabs compatible with both Don't Starve Together and singleplayer Don't Starve, with respect to setting up networking. The first section explains how Don't Starve Together handles prefab configuration (i.e. construction) in its own prefab files, and the reasoning behind that. It is important to understand this first section, but directly implementing what it covers in mod code will render it incompatible with singleplayer. For the recommended way of handling networking setup under wicker (compatible with both single and multiplayer), check the dedicated section.

How Don't Starve Together configures prefabs

In DST, almost all entities are networked. A networked entity exists in the host and in all clients; in the host, it will be a "complete entity", with all of its components, brain, stategraph, etc.; in the clients, it is only a "stub", with only basic initialization performed, with no brain, no stategraph and usually with no components. The basic initialization done in the clients is restricted to setting up engine level components related to the visual properties of the entity (adding a Transform, AnimState, DynamicShadow, etc.) and configuring the entity's physics; while the physics of an entity is not properly speaking a part of its visual state, this is done as a technical compromise, since relying on the server for all physical computations would add major latency and bandwidth overhead (so, instead, physics is handled independently on each client, and the position of each entity is periodically synced with the server to keep world consistency). Tags added on the host are transmitted to the clients, being the most basic form of network communication (and usually the least efficient one, since transmitting a string takes one byte per character). Adding tags in the part of the constructor which runs in all clients saves bandwidth, since in that case they are not transmitted through the network (being simply added locally everywhere), but from the point of view of functionality it does not matter if they are added afterwards. The world entity is one of the only examples of a non-networked entity, with the host and each client having a distinct world entity, and with world state being synced via custom logic in the world components (the world entity is also one of the only examples of entities which get components added even on clients).

Other than the world entity, non-networked entities tend to be FX entities which can exist locally without having their state shared/synced; an example of a non-networked entity is the "mist" entity, which simply applies a visual effect to a static portion of the map, and hence can be spawned independently on the host and each client; on the other hand, FX entities such as "lightning_rod_fx" are and must be networked, since they are spawned dynamically in response to certain conditions (which are only raised/checked on the host), and once spawned must sync up across the clients with respect to their state (which animations they play, when they despawn, etc.). Attempting to spawn a "regular" entity without networking (such as by manually deleting the network initialization part of its prefab constructor) will usually lead to a crash, because since on clients almost all clients are "stubs", the prefab logic is likely to not work without further adjustments (and the crash is likely to be of the "attempt to index a nil value" kind).

Setting up networking in an entity is done in DST by putting the following code inside its prefab constructor (its "fn"), placed after the part of the constructor which may run on clients (setting up Transform, AnimState, Physics, etc., as discussed above) and before the rest of the constructor, which should run only on the host.

inst.entity:AddNetwork()

if not TheWorld.ismastersim then
	return inst
end

inst.entity:SetPristine()

The member TheWorld.ismastersim is set to true on the host and false on the clients (under wicker, the recommended way of checking that is by calling IsHost()). As the code above shows, the constructor ends prematurely on clients, returning the inst before the rest of it could run. The call inst.entity:SetPristine() tells the engine that whatever follows will be host-specific stuff. The SetPristine call is only done in the "master" entity (i.e., the entity existing on the server side), which must export its changes of state to its "slave" counterpart entities existing on the clients. The code snippet above shall henceforth be referred to as "vanilla network setup code".

How to configure prefabs in a wicker based mod

To setup networking for a prefab within a wicker-based mod, simply add the following line within the prefab constructor in the same place where the vanilla network setup code would be inserted:

SetupNetwork(inst)

In singleplayer, SetupNetwork(inst) does nothing. In multiplayer, it is equivalent to the vanilla network setup code. To understand further how SetupNetwork does its job (in particular, how it's able to end prematurely the execution of the prefab constructor when we are not the host), read the following subsection.

If it is necessary to call MakeSnowCovered(inst) over an entity, it must be done before calling SetupNetwork(inst). This is the only exception to the general guidelines presented above.

How SetupNetwork() works.

Lua implements what is known as (asymmetrical) collaborative multithreading via its coroutines, which are also called "threads", though they do not run in parallel (only one thread may be running at any given time). Lua's threads are essentially a mechanism for piecewise execution of functions, allowing them to be paused before returning. The main execution state of a Lua program is a thread, called the main thread. New threads can be created by specifying a function which the thread should run (local co = coroutine.create(fn)); once this function returns, the thread ends. A thread is like any other Lua value, and may be stored in variables and passed as argument to functions; it is also garbage collected, so a thread which is no longer being used will be cleanly removed by Lua; this means one can safely forget about a suspended thread (i.e., a thread which never finished running). When a thread is created, it doesn't immediately start running, remaining paused (it remains in its suspended state). The current running thread can resume a suspended thread, stored in a variable co, by calling coroutine.resume(co); by doing that, the now formerly current thread becomes suspended and the previously suspended thread is resumed, becoming the new running thread. A resumed thread may at any point call coroutine.yield(), which suspends it and passes execution to the thread that resumed it. For more information, check the Lua 5.1 manual.

SetupNetwork() is implemented through the use of coroutines. When (and only when) in DST, wicker wraps all the prefab constructors of the mod using it with a function that starts a thread, whose associated function is the original prefab constructor, and resumes it. SetupNetwork(), which thus is called within a thread dedicated exclusively to the construction of a single entity, has (if in DST) an implementation equivalent to

function SetupNetwork(inst)
	inst.entity:AddNetwork()

	if not IsHost() then
		coroutine.yield(inst)
	end

	inst.entity:SetPristine()
end

Therefore, by use of coroutine.yield, it is able to suspend the execution of the prefab constructor (after adding the Network engine-level component) when the mod is not running on the host. In this scenario, the suspended thread will never be resumed, being eventually garbage collected. Thus, usage of SetupNetwork() instead of the vanilla network setup code allows for (a) preserving singleplayer compatibility, (b) more concise code, reducing the likelihood of bugs in the network setup, and (c) a centralized implementation of network setup, allowing any adjustments required by changes in the base game code to instantly propagate to all mod prefabs. Hence, using SetupNetwork() is heavily encouraged. The wrapping of prefab construction in coroutines also allows for other facilities implemented, such as the preservation of usage of MakeSnowCovered(inst) exactly as it was in multiplayer (provided it is called before SetupNetwork(inst)).

⚠️ **GitHub.com Fallback** ⚠️