Major Changes
-
#1482
07dd6f32
Thanks @alvrs! - Renamed all occurrences ofschema
where it is used as "value schema" tovalueSchema
to clearly distinguish it from "key schema".
The only breaking change for users is the change fromschema
tovalueSchema
inmud.config.ts
.// mud.config.ts export default mudConfig({ tables: { CounterTable: { keySchema: {}, - schema: { + valueSchema: { value: "uint32", }, }, } }
-
#1336
de151fec
Thanks @dk1a! - - AddFieldLayout
, which is abytes32
user-type similar toSchema
.Both
FieldLayout
andSchema
have the same kind of data in the first 4 bytes.- 2 bytes for total length of all static fields
- 1 byte for number of static size fields
- 1 byte for number of dynamic size fields
But whereas
Schema
hasSchemaType
enum in each of the other 28 bytes,FieldLayout
has static byte lengths in each of the other 28 bytes.-
Replace
Schema valueSchema
withFieldLayout fieldLayout
in Store and World contracts.FieldLayout
is more gas-efficient because it already has lengths, andSchema
has types which need to be converted to lengths. -
Add
getFieldLayout
toIStore
interface.There is no
FieldLayout
for keys, only for values, because key byte lengths aren't usually relevant on-chain. You can still usegetKeySchema
if you need key types. -
Add
fieldLayoutToHex
utility toprotocol-parser
package. -
Add
constants.sol
for constants shared betweenFieldLayout
,Schema
andPackedCounter
.
-
#1575
e5d208e4
Thanks @alvrs! - TheregisterRootFunctionSelector
function's signature was changed to accept astring functionSignature
parameter instead of abytes4 functionSelector
parameter.
This change enables theWorld
to store the function signatures of all registered functions in aFunctionSignatures
offchain table, which will allow for the automatic generation of interfaces for a givenWorld
address in the future.IBaseWorld { function registerRootFunctionSelector( ResourceId systemId, - bytes4 worldFunctionSelector, + string memory worldFunctionSignature, bytes4 systemFunctionSelector ) external returns (bytes4 worldFunctionSelector); }
-
#1574
31ffc9d5
Thanks @alvrs! - TheregisterFunctionSelector
function now accepts a singlefunctionSignature
string paramemer instead of separating function name and function arguments into separate parameters.IBaseWorld { function registerFunctionSelector( ResourceId systemId, - string memory systemFunctionName, - string memory systemFunctionArguments + string memory systemFunctionSignature ) external returns (bytes4 worldFunctionSelector); }
This is a breaking change if you were manually registering function selectors, e.g. in a
PostDeploy.s.sol
script or a module.
To upgrade, simply replace the separatesystemFunctionName
andsystemFunctionArguments
parameters with a singlesystemFunctionSignature
parameter.world.registerFunctionSelector( systemId, - systemFunctionName, - systemFunctionArguments, + string(abi.encodePacked(systemFunctionName, systemFunctionArguments)) );
-
#1318
ac508bf1
Thanks @holic! - Renamed the default filename of generated user types fromTypes.sol
tocommon.sol
and the default filename of the generated table index file fromTables.sol
toindex.sol
.Both can be overridden via the MUD config:
export default mudConfig({ /** Filename where common user types will be generated and imported from. */ userTypesFilename: "common.sol", /** Filename where codegen index will be generated. */ codegenIndexFilename: "index.sol", });
Note:
userTypesFilename
was renamed fromuserTypesPath
and.sol
is not appended automatically anymore but needs to be part of the provided filename.To update your existing project, update all imports from
Tables.sol
toindex.sol
and all imports fromTypes.sol
tocommon.sol
, or override the defaults in your MUD config to the previous values.- import { Counter } from "../src/codegen/Tables.sol"; + import { Counter } from "../src/codegen/index.sol"; - import { ExampleEnum } from "../src/codegen/Types.sol"; + import { ExampleEnum } from "../src/codegen/common.sol";
-
#1544
5e723b90
Thanks @alvrs! - -ResourceSelector
is replaced withResourceId
,ResourceIdLib
,ResourceIdInstance
,WorldResourceIdLib
andWorldResourceIdInstance
.Previously a "resource selector" was a
bytes32
value with the first 16 bytes reserved for the resource's namespace, and the last 16 bytes reserved for the resource's name.
Now a "resource ID" is abytes32
value with the first 2 bytes reserved for the resource type, the next 14 bytes reserved for the resource's namespace, and the last 16 bytes reserved for the resource's name.Previously
ResouceSelector
was a library and the resource selector type was a plainbytes32
.
NowResourceId
is a user type, and the functionality is implemented in theResourceIdInstance
(for type) andWorldResourceIdInstance
(for namespace and name) libraries.
We split the logic into two libraries, becauseStore
now also usesResourceId
and needs to be aware of resource types, but not of namespaces/names.- import { ResourceSelector } from "@latticexyz/world/src/ResourceSelector.sol"; + import { ResourceId, ResourceIdInstance } from "@latticexyz/store/src/ResourceId.sol"; + import { WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; + import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; - bytes32 systemId = ResourceSelector.from("namespace", "name"); + ResourceId systemId = WorldResourceIdLib.encode(RESOURCE_SYSTEM, "namespace", "name"); - using ResourceSelector for bytes32; + using WorldResourceIdInstance for ResourceId; + using ResourceIdInstance for ResourceId; systemId.getName(); systemId.getNamespace(); + systemId.getType();
-
All
Store
andWorld
methods now use theResourceId
type fortableId
,systemId
,moduleId
andnamespaceId
.
All mentions ofresourceSelector
were renamed toresourceId
or the more specific type (e.g.tableId
,systemId
)import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; IStore { function setRecord( - bytes32 tableId, + ResourceId tableId, bytes32[] calldata keyTuple, bytes calldata staticData, PackedCounter encodedLengths, bytes calldata dynamicData, FieldLayout fieldLayout ) external; // Same for all other methods }
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; IBaseWorld { function callFrom( address delegator, - bytes32 resourceSelector, + ResourceId systemId, bytes memory callData ) external payable returns (bytes memory); // Same for all other methods }
-
Minor Changes
Patch Changes
-
#1490
aea67c58
Thanks @alvrs! - Include bytecode forWorld
andStore
in npm packages. -
#1592
c07fa021
Thanks @alvrs! - Tables and interfaces in theworld
package are now generated to thecodegen
folder.
This is only a breaking change if you imported tables or codegenerated interfaces from@latticexyz/world
directly.
If you're using the MUD CLI, the changed import paths are already integrated and no further changes are necessary.- import { IBaseWorld } from "@latticexyz/world/src/interfaces/IBaseWorld.sol"; + import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol";
-
#1508
211be2a1
Thanks @Boffee! - TheFieldLayout
in table libraries is now generated at compile time instead of dynamically in a table library function.
This significantly reduces gas cost in all table library functions. -
#1503
bd9cc8ec
Thanks @holic! - Refactordeploy
command to break up logic into modules -
#1558
bfcb293d
Thanks @alvrs! - What used to be known asephemeral
table is now calledoffchain
table.
The previousephemeral
tables only supported anemitEphemeral
method, which emitted aStoreSetEphemeralRecord
event.Now
offchain
tables support all regular table methods, except partial operations on dynamic fields (push
,pop
,update
).
Unlike regular tables they don't store data on-chain but emit the same events as regular tables (StoreSetRecord
,StoreSpliceStaticData
,StoreDeleteRecord
), so their data can be indexed by offchain indexers/clients.- EphemeralTable.emitEphemeral(value); + OffchainTable.set(value);
-
#1521
55ab88a6
Thanks @alvrs! -StoreCore
andIStore
now expose specific functions forgetStaticField
andgetDynamicField
in addition to the generalgetField
.
Using the specific functions reduces gas overhead because more optimized logic can be executed.interface IStore { /** * Get a single static field from the given tableId and key tuple, with the given value field layout. * Note: the field value is left-aligned in the returned bytes32, the rest of the word is not zeroed out. * Consumers are expected to truncate the returned value as needed. */ function getStaticField( bytes32 tableId, bytes32[] calldata keyTuple, uint8 fieldIndex, FieldLayout fieldLayout ) external view returns (bytes32); /** * Get a single dynamic field from the given tableId and key tuple at the given dynamic field index. * (Dynamic field index = field index - number of static fields) */ function getDynamicField( bytes32 tableId, bytes32[] memory keyTuple, uint8 dynamicFieldIndex ) external view returns (bytes memory); }
-
#1483
83583a50
Thanks @holic! -deploy
anddev-contracts
CLI commands now useforge build --skip test script
before deploying and runmud abi-ts
to generate strong types for ABIs. -
#1587
24a6cd53
Thanks @alvrs! - Changed theuserTypes
property to accept{ filePath: string, internalType: SchemaAbiType }
to enable strong type inference from the config. -
#1513
708b49c5
Thanks @Boffee! - Generated table libraries now have a set of functions prefixed with_
that always use their own storage for read/write.
This saves gas for use cases where the functionality to dynamically determine whichStore
to use for read/write is not needed, e.g. root systems in aWorld
, or when usingStore
withoutWorld
.We decided to continue to always generate a set of functions that dynamically decide which
Store
to use, so that the generated table libraries can still be imported by non-root systems.library Counter { // Dynamically determine which store to write to based on the context function set(uint32 value) internal; // Always write to own storage function _set(uint32 value) internal; // ... equivalent functions for all other Store methods }
-
#1472
c049c23f
Thanks @alvrs! - - TheWorld
contract now has aninitialize
function, which can be called once by the creator of the World to install the core module.
This change allows the registration of all core tables to happen in theCoreModule
, so no table metadata has to be included in theWorld
's bytecode.interface IBaseWorld { function initialize(IModule coreModule) public; }
-
The
World
contract now stores the original creator of theWorld
in an immutable state variable.
It is used internally to only allow the original creator to initialize theWorld
in a separate transaction.interface IBaseWorld { function creator() external view returns (address); }
-
The deploy script is updated to use the
World
'sinitialize
function to install theCoreModule
instead ofregisterRootModule
as before.
-
-
#1591
251170e1
Thanks @alvrs! - All optional modules have been moved from@latticexyz/world
to@latticexyz/world-modules
.
If you're using the MUD CLI, the import is already updated and no changes are necessary. -
#1598
c4f49240
Thanks @dk1a! - Table libraries now correctly handle uninitialized fixed length arrays. -
#1581
cea754dd
Thanks @alvrs! - - The externalsetRecord
anddeleteRecord
methods ofIStore
no longer accept aFieldLayout
as input, but load it from storage instead.
This is to prevent invalidFieldLayout
values being passed, which could cause the onchain state to diverge from the indexer state.
However, the internalStoreCore
library still exposes asetRecord
anddeleteRecord
method that allows aFieldLayout
to be passed.
This is becauseStoreCore
can only be used internally, so theFieldLayout
value can be trusted and we can save the gas for accessing storage.interface IStore { function setRecord( ResourceId tableId, bytes32[] calldata keyTuple, bytes calldata staticData, PackedCounter encodedLengths, bytes calldata dynamicData, - FieldLayout fieldLayout ) external; function deleteRecord( ResourceId tableId, bytes32[] memory keyTuple, - FieldLayout fieldLayout ) external; }
-
The
spliceStaticData
method andStore_SpliceStaticData
event ofIStore
andStoreCore
no longer includedeleteCount
in their signature.
This is because when splicing static data, the data afterstart
is always overwritten withdata
instead of being shifted, sodeleteCount
is always the length of the data to be written.event Store_SpliceStaticData( ResourceId indexed tableId, bytes32[] keyTuple, uint48 start, - uint40 deleteCount, bytes data ); interface IStore { function spliceStaticData( ResourceId tableId, bytes32[] calldata keyTuple, uint48 start, - uint40 deleteCount, bytes calldata data ) external; }
-
The
updateInField
method has been removed fromIStore
, as it's almost identical to the more generalspliceDynamicData
.
If you're manually callingupdateInField
, here is how to upgrade tospliceDynamicData
:- store.updateInField(tableId, keyTuple, fieldIndex, startByteIndex, dataToSet, fieldLayout); + uint8 dynamicFieldIndex = fieldIndex - fieldLayout.numStaticFields(); + store.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, uint40(startByteIndex), uint40(dataToSet.length), dataToSet);
-
All other methods that are only valid for dynamic fields (
pushToField
,popFromField
,getFieldSlice
)
have been renamed to make this more explicit (pushToDynamicField
,popFromDynamicField
,getDynamicFieldSlice
).Their
fieldIndex
parameter has been replaced by adynamicFieldIndex
parameter, which is the index relative to the first dynamic field (i.e.dynamicFieldIndex
=fieldIndex
-numStaticFields
).
TheFieldLayout
parameter has been removed, as it was only used to calculate thedynamicFieldIndex
in the method.interface IStore { - function pushToField( + function pushToDynamicField( ResourceId tableId, bytes32[] calldata keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, bytes calldata dataToPush, - FieldLayout fieldLayout ) external; - function popFromField( + function popFromDynamicField( ResourceId tableId, bytes32[] calldata keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, uint256 byteLengthToPop, - FieldLayout fieldLayout ) external; - function getFieldSlice( + function getDynamicFieldSlice( ResourceId tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, - FieldLayout fieldLayout, uint256 start, uint256 end ) external view returns (bytes memory data); }
-
IStore
has a newgetDynamicFieldLength
length method, which returns the byte length of the given dynamic field and doesn't require theFieldLayout
.IStore { + function getDynamicFieldLength( + ResourceId tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex + ) external view returns (uint256); }
-
IStore
now has additional overloads forgetRecord
,getField
,getFieldLength
andsetField
that don't require aFieldLength
to be passed, but instead load it from storage. -
IStore
now exposessetStaticField
andsetDynamicField
to save gas by avoiding the dynamic inference of whether the field is static or dynamic. -
The
getDynamicFieldSlice
method no longer accepts reading outside the bounds of the dynamic field.
This is to avoid returning invalid data, as the data of a dynamic field is not deleted when the record is deleted, but only its length is set to zero.
-
-
Updated dependencies [
77dce993
,748f4588
,aea67c58
,07dd6f32
,c07fa021
,90e4161b
,65c9546c
,331dbfdc
,f9f9609e
,331dbfdc
,759514d8
,d5094a24
,0b8ce3f2
,de151fec
,ae340b2b
,e5d208e4
,211be2a1
,0f3e2e02
,1f80a0b5
,d0878928
,4c7fd3eb
,a0341daf
,83583a50
,5e723b90
,6573e38e
,44a5432a
,6e66c5b7
,65c9546c
,f8a01a04
,44a5432a
,672d05ca
,f1cd43bf
,31ffc9d5
,5e723b90
,63831a26
,331dbfdc
,92de5998
,5741d53d
,22ee4470
,be313068
,ac508bf1
,9ff4dd95
,bfcb293d
,1890f1a0
,9b43029c
,55ab88a6
,af639a26
,5e723b90
,99ab9cd6
,c049c23f
,80dd6992
,24a6cd53
,708b49c5
,22ba7b67
,c049c23f
,251170e1
,c4f49240
,cea754dd
,95c59b20
]:- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]
- @latticexyz/[email protected]