diff --git a/packages/world/mud.config.ts b/packages/world/mud.config.ts index dce3430995..72b51effd7 100644 --- a/packages/world/mud.config.ts +++ b/packages/world/mud.config.ts @@ -84,6 +84,16 @@ export default mudConfig({ }, valueSchema: "bytes21[]", }, + OptionalSystemHooks: { + keySchema: { + player: "address", + systemId: "ResourceId", + callDataHash: "bytes32", + }, + valueSchema: { + hooks: "bytes21[]", + }, + }, FunctionSelectors: { keySchema: { worldFunctionSelector: "bytes4", diff --git a/packages/world/src/IOptionalSystemHook.sol b/packages/world/src/IOptionalSystemHook.sol new file mode 100644 index 0000000000..5efdcf9912 --- /dev/null +++ b/packages/world/src/IOptionalSystemHook.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ISystemHook } from "./ISystemHook.sol"; + +/** + * @title IOptionalSystemHook + * @dev Interface defining optional hooks for external functionality. + * Provides pre and post hooks that can be triggered before and after a system call respectively. + * This interface adheres to the ERC-165 standard for determining interface support. + */ +interface IOptionalSystemHook is ISystemHook { + /** + * @notice Executes when a system hook is registered by the user. + * @dev Provides the ability to add custom logic or checks when a system hook is registered. + */ + function onRegisterHook() external; + + /** + * @notice Executes when a system hook is unregistered by the user. + * @dev Provides the ability to add custom logic or checks when a system hook is unregistered. + */ + function onUnregisterHook() external; +} diff --git a/packages/world/src/SystemCall.sol b/packages/world/src/SystemCall.sol index a60ffe579e..47e89e5a10 100644 --- a/packages/world/src/SystemCall.sol +++ b/packages/world/src/SystemCall.sol @@ -12,9 +12,11 @@ import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM } from "./systemHookTypes.sol"; import { IWorldErrors } from "./IWorldErrors.sol"; import { ISystemHook } from "./ISystemHook.sol"; +import { IOptionalSystemHook } from "./IOptionalSystemHook.sol"; import { Systems } from "./codegen/tables/Systems.sol"; import { SystemHooks } from "./codegen/tables/SystemHooks.sol"; +import { OptionalSystemHooks } from "./codegen/tables/OptionalSystemHooks.sol"; import { Balances } from "./codegen/tables/Balances.sol"; /** @@ -93,6 +95,10 @@ library SystemCall { // Get system hooks bytes21[] memory hooks = SystemHooks._get(systemId); + // Get optional hooks specified by the caller + bytes21[] memory optionalSystemHooks = OptionalSystemHooks._get(caller, systemId, bytes32(0)); + bytes21[] memory optionalSystemHooksWithCallData = OptionalSystemHooks._get(caller, systemId, keccak256(callData)); + // Call onBeforeCallSystem hooks (before calling the system) for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); @@ -101,6 +107,20 @@ library SystemCall { } } + // Call optional onBeforeCallSystem hooks (before calling the system) + for (uint256 i; i < optionalSystemHooks.length; i++) { + Hook hook = Hook.wrap(optionalSystemHooks[i]); + if (hook.isEnabled(BEFORE_CALL_SYSTEM)) { + IOptionalSystemHook(hook.getAddress()).onBeforeCallSystem(caller, systemId, callData); + } + } + for (uint256 i; i < optionalSystemHooksWithCallData.length; i++) { + Hook hook = Hook.wrap(optionalSystemHooksWithCallData[i]); + if (hook.isEnabled(BEFORE_CALL_SYSTEM)) { + IOptionalSystemHook(hook.getAddress()).onBeforeCallSystem(caller, systemId, callData); + } + } + // Call the system and forward any return data (success, data) = call({ caller: caller, value: value, systemId: systemId, callData: callData }); @@ -111,6 +131,20 @@ library SystemCall { ISystemHook(hook.getAddress()).onAfterCallSystem(caller, systemId, callData); } } + + // Call optional onAfterCallSystem hooks (after calling the system) + for (uint256 i; i < optionalSystemHooks.length; i++) { + Hook hook = Hook.wrap(optionalSystemHooks[i]); + if (hook.isEnabled(AFTER_CALL_SYSTEM)) { + IOptionalSystemHook(hook.getAddress()).onAfterCallSystem(caller, systemId, callData); + } + } + for (uint256 i; i < optionalSystemHooksWithCallData.length; i++) { + Hook hook = Hook.wrap(optionalSystemHooksWithCallData[i]); + if (hook.isEnabled(AFTER_CALL_SYSTEM)) { + IOptionalSystemHook(hook.getAddress()).onAfterCallSystem(caller, systemId, callData); + } + } } /** diff --git a/packages/world/src/codegen/index.sol b/packages/world/src/codegen/index.sol index b00d58a490..1b9586c5b7 100644 --- a/packages/world/src/codegen/index.sol +++ b/packages/world/src/codegen/index.sol @@ -12,6 +12,7 @@ import { Balances } from "./tables/Balances.sol"; import { Systems } from "./tables/Systems.sol"; import { SystemRegistry } from "./tables/SystemRegistry.sol"; import { SystemHooks } from "./tables/SystemHooks.sol"; +import { OptionalSystemHooks } from "./tables/OptionalSystemHooks.sol"; import { FunctionSelectors } from "./tables/FunctionSelectors.sol"; import { FunctionSignatures } from "./tables/FunctionSignatures.sol"; import { InitModuleAddress } from "./tables/InitModuleAddress.sol"; diff --git a/packages/world/src/codegen/interfaces/IBaseWorld.sol b/packages/world/src/codegen/interfaces/IBaseWorld.sol index a30241297a..37fc82ada5 100644 --- a/packages/world/src/codegen/interfaces/IBaseWorld.sol +++ b/packages/world/src/codegen/interfaces/IBaseWorld.sol @@ -9,6 +9,7 @@ import { IWorldKernel } from "../../IWorldKernel.sol"; import { IAccessManagementSystem } from "./IAccessManagementSystem.sol"; import { IBalanceTransferSystem } from "./IBalanceTransferSystem.sol"; import { IBatchCallSystem } from "./IBatchCallSystem.sol"; +import { IExtendedWorldRegistrationSystem } from "./IExtendedWorldRegistrationSystem.sol"; import { IModuleInstallationSystem } from "./IModuleInstallationSystem.sol"; import { IWorldRegistrationSystem } from "./IWorldRegistrationSystem.sol"; import { IRegistrationSystem } from "./IRegistrationSystem.sol"; @@ -26,6 +27,7 @@ interface IBaseWorld is IAccessManagementSystem, IBalanceTransferSystem, IBatchCallSystem, + IExtendedWorldRegistrationSystem, IModuleInstallationSystem, IWorldRegistrationSystem, IRegistrationSystem diff --git a/packages/world/src/codegen/interfaces/IExtendedWorldRegistrationSystem.sol b/packages/world/src/codegen/interfaces/IExtendedWorldRegistrationSystem.sol new file mode 100644 index 0000000000..04d8ea133f --- /dev/null +++ b/packages/world/src/codegen/interfaces/IExtendedWorldRegistrationSystem.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { ISystemHook } from "./../../ISystemHook.sol"; + +/** + * @title IExtendedWorldRegistrationSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IExtendedWorldRegistrationSystem { + function registerOptionalSystemHook( + ResourceId systemId, + ISystemHook hookAddress, + uint8 enabledHooksBitmap, + bytes32 callDataHash + ) external; + + function unregisterOptionalSystemHook(ResourceId systemId, ISystemHook hookAddress, bytes32 callDataHash) external; +} diff --git a/packages/world/src/codegen/tables/OptionalSystemHooks.sol b/packages/world/src/codegen/tables/OptionalSystemHooks.sol new file mode 100644 index 0000000000..8c77954264 --- /dev/null +++ b/packages/world/src/codegen/tables/OptionalSystemHooks.sol @@ -0,0 +1,580 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +// Import user types +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library OptionalSystemHooks { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "world", name: "OptionalSystemHo", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x7462776f726c640000000000000000004f7074696f6e616c53797374656d486f); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0000000100000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (address, bytes32, bytes32) + Schema constant _keySchema = Schema.wrap(0x00540300615f5f00000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (bytes21[]) + Schema constant _valueSchema = Schema.wrap(0x00000001b6000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](3); + keyNames[0] = "player"; + keyNames[1] = "systemId"; + keyNames[2] = "callDataHash"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "hooks"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get hooks. + */ + function getHooks( + address player, + ResourceId systemId, + bytes32 callDataHash + ) internal view returns (bytes21[] memory hooks) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** + * @notice Get hooks. + */ + function _getHooks( + address player, + ResourceId systemId, + bytes32 callDataHash + ) internal view returns (bytes21[] memory hooks) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** + * @notice Get hooks. + */ + function get( + address player, + ResourceId systemId, + bytes32 callDataHash + ) internal view returns (bytes21[] memory hooks) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** + * @notice Get hooks. + */ + function _get( + address player, + ResourceId systemId, + bytes32 callDataHash + ) internal view returns (bytes21[] memory hooks) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (SliceLib.getSubslice(_blob, 0, _blob.length).decodeArray_bytes21()); + } + + /** + * @notice Set hooks. + */ + function setHooks(address player, ResourceId systemId, bytes32 callDataHash, bytes21[] memory hooks) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, EncodeArray.encode((hooks))); + } + + /** + * @notice Set hooks. + */ + function _setHooks(address player, ResourceId systemId, bytes32 callDataHash, bytes21[] memory hooks) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, EncodeArray.encode((hooks))); + } + + /** + * @notice Set hooks. + */ + function set(address player, ResourceId systemId, bytes32 callDataHash, bytes21[] memory hooks) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, EncodeArray.encode((hooks))); + } + + /** + * @notice Set hooks. + */ + function _set(address player, ResourceId systemId, bytes32 callDataHash, bytes21[] memory hooks) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, EncodeArray.encode((hooks))); + } + + /** + * @notice Get the length of hooks. + */ + function lengthHooks(address player, ResourceId systemId, bytes32 callDataHash) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 21; + } + } + + /** + * @notice Get the length of hooks. + */ + function _lengthHooks(address player, ResourceId systemId, bytes32 callDataHash) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 21; + } + } + + /** + * @notice Get the length of hooks. + */ + function length(address player, ResourceId systemId, bytes32 callDataHash) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 21; + } + } + + /** + * @notice Get the length of hooks. + */ + function _length(address player, ResourceId systemId, bytes32 callDataHash) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 21; + } + } + + /** + * @notice Get an item of hooks. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemHooks( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index + ) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 21, (_index + 1) * 21); + return (bytes21(_blob)); + } + } + + /** + * @notice Get an item of hooks. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemHooks( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index + ) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 21, (_index + 1) * 21); + return (bytes21(_blob)); + } + } + + /** + * @notice Get an item of hooks. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItem( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index + ) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 21, (_index + 1) * 21); + return (bytes21(_blob)); + } + } + + /** + * @notice Get an item of hooks. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItem( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index + ) internal view returns (bytes21) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 21, (_index + 1) * 21); + return (bytes21(_blob)); + } + } + + /** + * @notice Push an element to hooks. + */ + function pushHooks(address player, ResourceId systemId, bytes32 callDataHash, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, abi.encodePacked((_element))); + } + + /** + * @notice Push an element to hooks. + */ + function _pushHooks(address player, ResourceId systemId, bytes32 callDataHash, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, abi.encodePacked((_element))); + } + + /** + * @notice Push an element to hooks. + */ + function push(address player, ResourceId systemId, bytes32 callDataHash, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, abi.encodePacked((_element))); + } + + /** + * @notice Push an element to hooks. + */ + function _push(address player, ResourceId systemId, bytes32 callDataHash, bytes21 _element) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, abi.encodePacked((_element))); + } + + /** + * @notice Pop an element from hooks. + */ + function popHooks(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 21); + } + + /** + * @notice Pop an element from hooks. + */ + function _popHooks(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 21); + } + + /** + * @notice Pop an element from hooks. + */ + function pop(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 21); + } + + /** + * @notice Pop an element from hooks. + */ + function _pop(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 21); + } + + /** + * @notice Update an element of hooks at `_index`. + */ + function updateHooks( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index, + bytes21 _element + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _encoded = abi.encodePacked((_element)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 21), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update an element of hooks at `_index`. + */ + function _updateHooks( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index, + bytes21 _element + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _encoded = abi.encodePacked((_element)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 21), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update an element of hooks at `_index`. + */ + function update( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index, + bytes21 _element + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _encoded = abi.encodePacked((_element)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 21), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update an element of hooks at `_index`. + */ + function _update( + address player, + ResourceId systemId, + bytes32 callDataHash, + uint256 _index, + bytes21 _element + ) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + unchecked { + bytes memory _encoded = abi.encodePacked((_element)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 21), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(address player, ResourceId systemId, bytes32 callDataHash) internal { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack dynamic data lengths using this table's schema. + * @return _encodedLengths The lengths of the dynamic fields (packed into a single bytes32 value). + */ + function encodeLengths(bytes21[] memory hooks) internal pure returns (PackedCounter _encodedLengths) { + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(hooks.length * 21); + } + } + + /** + * @notice Tightly pack dynamic (variable length) data using this table's schema. + * @return The dynamic data, encoded into a sequence of bytes. + */ + function encodeDynamic(bytes21[] memory hooks) internal pure returns (bytes memory) { + return abi.encodePacked(EncodeArray.encode((hooks))); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(bytes21[] memory hooks) internal pure returns (bytes memory, PackedCounter, bytes memory) { + bytes memory _staticData; + PackedCounter _encodedLengths = encodeLengths(hooks); + bytes memory _dynamicData = encodeDynamic(hooks); + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple( + address player, + ResourceId systemId, + bytes32 callDataHash + ) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](3); + _keyTuple[0] = bytes32(uint256(uint160(player))); + _keyTuple[1] = ResourceId.unwrap(systemId); + _keyTuple[2] = callDataHash; + + return _keyTuple; + } +} diff --git a/packages/world/src/modules/init/InitModule.sol b/packages/world/src/modules/init/InitModule.sol index f2085d0c34..e581afcee5 100644 --- a/packages/world/src/modules/init/InitModule.sol +++ b/packages/world/src/modules/init/InitModule.sol @@ -28,6 +28,7 @@ import { Systems } from "../../codegen/tables/Systems.sol"; import { FunctionSelectors } from "../../codegen/tables/FunctionSelectors.sol"; import { FunctionSignatures } from "../../codegen/tables/FunctionSignatures.sol"; import { SystemHooks } from "../../codegen/tables/SystemHooks.sol"; +import { OptionalSystemHooks } from "../../codegen/tables/OptionalSystemHooks.sol"; import { SystemRegistry } from "../../codegen/tables/SystemRegistry.sol"; import { InitModuleAddress } from "../../codegen/tables/InitModuleAddress.sol"; import { Balances } from "../../codegen/tables/Balances.sol"; @@ -92,6 +93,7 @@ contract InitModule is Module { FunctionSelectors.register(); FunctionSignatures.register(); SystemHooks.register(); + OptionalSystemHooks.register(); SystemRegistry.register(); InitModuleAddress.register(); diff --git a/packages/world/src/modules/init/implementations/ExtendedWorldRegistrationSystem.sol b/packages/world/src/modules/init/implementations/ExtendedWorldRegistrationSystem.sol new file mode 100644 index 0000000000..6240958dd9 --- /dev/null +++ b/packages/world/src/modules/init/implementations/ExtendedWorldRegistrationSystem.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { Hook, HookLib } from "@latticexyz/store/src/Hook.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; + +import { System } from "../../../System.sol"; +import { WorldContextConsumer, IWorldContextConsumer } from "../../../WorldContext.sol"; +import { WorldResourceIdLib, WorldResourceIdInstance } from "../../../WorldResourceId.sol"; +import { SystemCall } from "../../../SystemCall.sol"; +import { ROOT_NAMESPACE_ID, ROOT_NAME } from "../../../constants.sol"; +import { RESOURCE_NAMESPACE, RESOURCE_SYSTEM } from "../../../worldResourceTypes.sol"; +import { AccessControl } from "../../../AccessControl.sol"; +import { Delegation } from "../../../Delegation.sol"; +import { requireInterface } from "../../../requireInterface.sol"; +import { NamespaceOwner } from "../../../codegen/tables/NamespaceOwner.sol"; +import { ResourceAccess } from "../../../codegen/tables/ResourceAccess.sol"; +import { UserDelegationControl } from "../../../codegen/tables/UserDelegationControl.sol"; +import { NamespaceDelegationControl } from "../../../codegen/tables/NamespaceDelegationControl.sol"; +import { ISystemHook } from "../../../ISystemHook.sol"; +import { IOptionalSystemHook } from "../../../IOptionalSystemHook.sol"; +import { IWorldErrors } from "../../../IWorldErrors.sol"; +import { IDelegationControl } from "../../../IDelegationControl.sol"; +import { ICustomUnregisterDelegation } from "../../../ICustomUnregisterDelegation.sol"; +import { ERC165Checker } from "../../../ERC165Checker.sol"; + +import { SystemHooks } from "../../../codegen/tables/SystemHooks.sol"; +import { OptionalSystemHooks } from "../../../codegen/tables/OptionalSystemHooks.sol"; +import { SystemRegistry } from "../../../codegen/tables/SystemRegistry.sol"; +import { Systems } from "../../../codegen/tables/Systems.sol"; +import { FunctionSelectors } from "../../../codegen/tables/FunctionSelectors.sol"; +import { FunctionSignatures } from "../../../codegen/tables/FunctionSignatures.sol"; +import { requireNamespace } from "../../../requireNamespace.sol"; +import { requireValidNamespace } from "../../../requireValidNamespace.sol"; + +import { LimitedCallContext } from "../LimitedCallContext.sol"; + +/** + * @title ExtendedWorldRegistrationSystem + * @dev This contract provides extended functions related to registering resources other than tables in the World. + */ +contract ExtendedWorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { + using WorldResourceIdInstance for ResourceId; + + /** + * @notice Registers a new optional system hook for the user + * @dev Adds a new hook for the system at the provided user, system, and call data hash (optional) + * @param systemId The ID of the system + * @param hookAddress The address of the hook being registered + * @param enabledHooksBitmap Bitmap indicating which hooks are enabled + * @param callDataHash The hash of the call data for the system hook + */ + function registerOptionalSystemHook( + ResourceId systemId, + ISystemHook hookAddress, + uint8 enabledHooksBitmap, + bytes32 callDataHash + ) public onlyDelegatecall { + // Require the provided system ID to have type RESOURCE_SYSTEM + if (systemId.getType() != RESOURCE_SYSTEM) { + revert World_InvalidResourceType(RESOURCE_SYSTEM, systemId, systemId.toString()); + } + + // Require the provided address to implement the ISystemHook interface + requireInterface(address(hookAddress), type(IOptionalSystemHook).interfaceId); + + // Require the system to exist + AccessControl.requireExistence(systemId); + + // Require the system's namespace to exist + AccessControl.requireExistence(systemId.getNamespaceId()); + + IOptionalSystemHook(address(hookAddress)).onRegisterHook(); + + // Register the hook + OptionalSystemHooks.push( + _msgSender(), + systemId, + callDataHash, + Hook.unwrap(HookLib.encode(address(hookAddress), enabledHooksBitmap)) + ); + } + + /** + * @notice Unregisters an optional system hook + * @dev Removes a hook for the system at the provided user, system, and call data hash (optional) + * @param systemId The ID of the system + * @param hookAddress The address of the hook being unregistered + */ + function unregisterOptionalSystemHook( + ResourceId systemId, + ISystemHook hookAddress, + bytes32 callDataHash + ) public virtual onlyDelegatecall { + // Remove the hook from the list of hooks for this system in the optional system hooks table + bytes21[] memory currentHooks = OptionalSystemHooks._get(_msgSender(), systemId, callDataHash); + + // Initialize the new hooks array with the same length because we don't know if the hook is registered yet + bytes21[] memory newHooks = new bytes21[](currentHooks.length); + + // Filter the array of current hooks + uint256 newHooksIndex; + unchecked { + for (uint256 currentHooksIndex; currentHooksIndex < currentHooks.length; currentHooksIndex++) { + if (Hook.wrap(currentHooks[currentHooksIndex]).getAddress() != address(hookAddress)) { + newHooks[newHooksIndex] = currentHooks[currentHooksIndex]; + newHooksIndex++; + } else { + IOptionalSystemHook(address(hookAddress)).onUnregisterHook(); + } + } + } + + // Set the new hooks table length in place + // (Note: this does not update the free memory pointer) + assembly { + mstore(newHooks, newHooksIndex) + } + + // Set the new hooks table + OptionalSystemHooks._set(_msgSender(), systemId, callDataHash, newHooks); + } +}