Create Extension Systems
The commented code can be found in
packages/contracts/src/systems/WriteDemoSystem.sol (opens in a new tab).
We want our expansion system to call a system on the Primodium world. If this is
successful, we return the buildingEntity.
This system is quite a bit more involved. We'll break it down by function.
Imports
import {
Asteroid,
Home,
Level,
Dimensions,
DimensionsData,
PositionData,
Spawned,
UsedTiles,
P_AsteroidData,
P_Asteroid,
P_Blueprint,
P_EnumToPrototype,
P_MaxLevel,
P_RequiredTile,
P_Terrain,
} from "primodium/index.sol";
import { EBuilding } from "primodium/common.sol";
import { Bounds, EResource } from "src/Types.sol";
import { BuildingKey, ExpansionKey } from "src/Keys.sol";Explanation
import {
Asteroid,
Home,
Level,
Dimensions,
DimensionsData,
PositionData,
Spawned,
UsedTiles,
P_AsteroidData,
P_Asteroid,
P_Blueprint,
P_EnumToPrototype,
P_MaxLevel,
P_RequiredTile,
P_Terrain,
} from "primodium/index.sol";Previously we imported Tables directly, one at a time. However, MUD collects
and exposes them in index.sol automatically, so we're going to use that now.
import { EBuilding } from "primodium/common.sol";Primodium stores lists of entity lookup keys in common.sol as enumerations.
Here we can find buildings, resources, units, etc.
import { Bounds, EResource } from "src/Types.sol";
import { BuildingKey, ExpansionKey } from "src/Keys.sol";We use structs to pass around collections of related data; these can be found
in Types.sol. Similarly, we have some internal keys we used look up data, and
those can be found in Keys.sol.
Top Level Function
function buildIronMine() public returns (bytes32 buildingEntity) {
StoreSwitch.setStoreAddress(_world());
bytes32 playerEntity = addressToEntity(_msgSender());
bool playerIsSpawned = Spawned.get(playerEntity);
require(playerIsSpawned, "[WriteDemoSystem] Player is not spawned");
bytes32 asteroidEntity = Home.get(playerEntity);
EBuilding building = EBuilding.IronMine;
PositionData memory position = getTilePosition(asteroidEntity, building);
ResourceId buildSystemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, PRIMODIUM_NAMESPACE, "BuildSystem");
buildingEntity = bytes32(
IPrimodiumWorld(_world()).callFrom(
_msgSender(),
buildSystemId,
abi.encodeWithSignature("build(uint8,(int32,int32,bytes32))", building, (position))
)
);
}Explanation
bool playerIsSpawned = Spawned.get(playerEntity);Spawned is a Primodium Table we can call to check if a player has started
playing. You can examine the imported contract, and see what types to expect in
return, and any other functionality it provides. Most of the time we will only
be using get() and set().
EBuilding building = EBuilding.IronMine;Enumerations let us use clear names instead of magic numbers to represent data.
It makes the code much more readable. Enumerations decode to uint8 in the abi.
PositionData memory position = getTilePosition(asteroidEntity, building);This function gets pretty complex. We will walk through it in detail later, but at the high level, the map on each asteroid is broken into a tile grid, and this function walks the map and looks for an appropriate tile for the building passed in.
ResourceId buildSystemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, PRIMODIUM_NAMESPACE, "BuildSystem");
buildingEntity = bytes32(
IPrimodiumWorld(_world()).callFrom(
_msgSender(),
buildSystemId,
abi.encodeWithSignature("build(uint8,(int32,int32,bytes32))", building, (position))
)
);This is how we call a System that is going to take action for the user. Find
the ResourceId for the system, and execute a callFrom using _msgSender() to
identify the user, with the signature of the of the function being called. The
target System needs to have received delegation permission from the user in
advance. We'll discuss delegation later.
The parameters of the function signature deconstruct the struct into its
internal native types, to match the abi. Note the extra () around position to
indicate that the struct is a tuple.
callFrom returns bytes memory, so we convert it to the bytes32 we expect
and return buildingEntity.
Enumerations
We need to select the building we want to build. Primodium stores lists of
entity lookup keys in common.sol. Here we can find buildings, resources,
units, etc. The enums are used to lookup prototypes and blueprints later, in
their specific Systems. We're going to build an EBuilding.IronMine for now.
Map Interactions
Each asteroid has a bounded set of tiles where buildings can be placed. We need
to find the bounds for this specific map (our Home Base in this case). These
bounds change as you increase your expansion level, so we get the level, and
save the DimensionData range. From there, we use an P_AsteroidData struct to
manage the data, calculate the bounds, and return them.
We step through each tile and check if it is the correct type. To check if it is
empty, we use a P_Blueprint and verify the building fits within the bounds. If
all of this is successful, we return the tile position.
Calling a Game System
We want our expansion system to call a system on the Primodium world. If all of
this is successful, we return the buildingEntity.
Unlike the ReadDemo, the WriteDemo acts on behalf of the user. Our system needs to have permission to do so. This is allowed through a Delegation that needs to occur before the system it called. You can find documentation and example code for delegations at:
- https://mud.dev/world/account-delegation (opens in a new tab)
WriteDemoSystem.t.sol:100(opens in a new tab)
As mentioned in the detail, we normally get the system ResourceId, and the use
abi.encodeWithSignature to call the function. However, there is currently a
bug (opens in a new tab) in MUD, and we need to
retrieve the internal function selector from the FunctionSelectors table, and
then use that with abi.encodeWithSelector. Example code is provided; we expect
the normal usage to return in future MUD versions.