From d6a49dbfb3755868368ae16350697dab224702d2 Mon Sep 17 00:00:00 2001 From: Dhvani Patel Date: Mon, 18 Mar 2024 16:21:46 +0000 Subject: [PATCH] feat: add optional system hooks --- packages/world/mud.config.ts | 10 + packages/world/src/IOptionalSystemHook.sol | 24 + packages/world/src/SystemCall.sol | 35 ++ packages/world/src/codegen/index.sol | 1 + .../interfaces/IWorldRegistrationSystem.sol | 9 + .../codegen/tables/OptionalSystemHooks.sol | 580 ++++++++++++++++++ .../WorldRegistrationSystem.sol | 81 +++ 7 files changed, 740 insertions(+) create mode 100644 packages/world/src/IOptionalSystemHook.sol create mode 100644 packages/world/src/codegen/tables/OptionalSystemHooks.sol 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..4f17f5ea20 100644 --- a/packages/world/src/SystemCall.sol +++ b/packages/world/src/SystemCall.sol @@ -15,6 +15,7 @@ import { ISystemHook } from "./ISystemHook.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 +94,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 +106,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)) { + ISystemHook(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)) { + ISystemHook(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 +130,22 @@ 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)) { + ISystemHook(hook.getAddress()).onAfterCallSystem(caller, systemId, callData); + } + } + + // Call optional onAfterCallSystem hooks (after calling the system) + for (uint256 i; i < optionalSystemHooksWithCallData.length; i++) { + Hook hook = Hook.wrap(optionalSystemHooksWithCallData[i]); + if (hook.isEnabled(AFTER_CALL_SYSTEM)) { + ISystemHook(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/IWorldRegistrationSystem.sol b/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol index 7db5058421..8bfa7cf7a2 100644 --- a/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol +++ b/packages/world/src/codegen/interfaces/IWorldRegistrationSystem.sol @@ -17,8 +17,17 @@ interface IWorldRegistrationSystem { function registerSystemHook(ResourceId systemId, ISystemHook hookAddress, uint8 enabledHooksBitmap) external; + function registerOptionalSystemHook( + ResourceId systemId, + ISystemHook hookAddress, + uint8 enabledHooksBitmap, + bytes32 callDataHash + ) external; + function unregisterSystemHook(ResourceId systemId, ISystemHook hookAddress) external; + function unregisterOptionalSystemHook(ResourceId systemId, ISystemHook hookAddress, bytes32 callDataHash) external; + function registerSystem(ResourceId systemId, System system, bool publicAccess) external; function registerFunctionSelector( 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/implementations/WorldRegistrationSystem.sol b/packages/world/src/modules/init/implementations/WorldRegistrationSystem.sol index 195e593c03..f20a42b125 100644 --- a/packages/world/src/modules/init/implementations/WorldRegistrationSystem.sol +++ b/packages/world/src/modules/init/implementations/WorldRegistrationSystem.sol @@ -19,12 +19,14 @@ 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"; @@ -101,6 +103,45 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { SystemHooks.push(systemId, Hook.unwrap(HookLib.encode(address(hookAddress), enabledHooksBitmap))); } + /** + * @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 a system hook * @dev Removes a hook for the system at the provided system ID @@ -115,6 +156,46 @@ contract WorldRegistrationSystem is System, IWorldErrors, LimitedCallContext { HookLib.filterListByAddress(SystemHooks._tableId, systemId, address(hookAddress)); } + /** + * @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); + } + /** * @notice Registers a system * @dev Registers or upgrades a system at the given ID