From bbd6e1f4be18dcae94addc65849136ad01d1ba2a Mon Sep 17 00:00:00 2001 From: Andy Cernera Date: Fri, 16 Sep 2022 12:56:25 -0400 Subject: [PATCH] feat(cli): forge bulk upload ecs state script (#142) * feat: wip forge bulkupload script * feat: wip 2 forge bulkupload script * feat: wip 3 forge bulkupload script * feat: wip 4 forge bulkupload script * feat: support prototype dev system in ecs map * fix: use entity index to copy prototype components * feat(std-contracts): bulk upload script using BulkSetState system * feat(solecs): add getIndex function Set * feat(cli): only send needed entities in BulkSetState transactions * fix(cli): remove test ecs map * fix(std-client): never error in getGameConfig * feat(cli): add mud bulkupload script * fix(std-client): explicit null check Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> Co-authored-by: alvrs Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> --- packages/cli/.gitignore | 5 +- packages/cli/.npmignore | 1 + packages/cli/foundry.toml | 9 + packages/cli/git-install.sh | 5 + packages/cli/package.json | 12 +- packages/cli/remappings.txt | 5 + packages/cli/src/commands/bulkupload.ts | 44 +++ packages/cli/src/contracts/BulkUpload.sol | 183 ++++++++++ packages/cli/src/contracts/Cheats.sol | 320 ++++++++++++++++++ packages/network/src/workers/constants.ts | 2 +- packages/solecs/src/Set.sol | 6 + packages/std-client/src/utils.ts | 4 +- .../src/systems/BulkSetStateSystem.sol | 45 +++ .../src/systems/ComponentDevSystem.sol | 35 ++ yarn.lock | 8 + 15 files changed, 677 insertions(+), 7 deletions(-) create mode 100644 packages/cli/foundry.toml create mode 100755 packages/cli/git-install.sh create mode 100644 packages/cli/remappings.txt create mode 100644 packages/cli/src/commands/bulkupload.ts create mode 100644 packages/cli/src/contracts/BulkUpload.sol create mode 100644 packages/cli/src/contracts/Cheats.sol create mode 100644 packages/std-contracts/src/systems/BulkSetStateSystem.sol create mode 100644 packages/std-contracts/src/systems/ComponentDevSystem.sol diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index 53c37a1660..ddd64d2c51 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -1 +1,4 @@ -dist \ No newline at end of file +dist +out +broadcast +cache \ No newline at end of file diff --git a/packages/cli/.npmignore b/packages/cli/.npmignore index 4ec786cbee..eb5910442f 100644 --- a/packages/cli/.npmignore +++ b/packages/cli/.npmignore @@ -1,5 +1,6 @@ * !dist/** +!src/** !package.json !README.md \ No newline at end of file diff --git a/packages/cli/foundry.toml b/packages/cli/foundry.toml new file mode 100644 index 0000000000..a8c44730e6 --- /dev/null +++ b/packages/cli/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 1000000 +verbosity = 1 +libs = ["../../node_modules", "../solecs", "../std-contracts"] +src = "src" +out = "out" \ No newline at end of file diff --git a/packages/cli/git-install.sh b/packages/cli/git-install.sh new file mode 100755 index 0000000000..b86de4f8c9 --- /dev/null +++ b/packages/cli/git-install.sh @@ -0,0 +1,5 @@ +#! usr/bin/bash +giturl=https://github.com/$1.git +head=$(git ls-remote $giturl HEAD | head -n1 | awk '{print $1;}') +yarn add $giturl#$head +echo "Installed $giturl#$head" \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index e0040463cb..eac90f9bf2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,11 +13,12 @@ "directory": "packages/cli" }, "scripts": { - "prepare": "yarn build", - "globalinstall": "yarn build && npm link", + "prepare": "yarn build && chmod u+x git-install.sh", + "globalinstall": "yarn prepare && npm link", "build": "rimraf dist && tsc -p . && chmod u+x dist/index.js", "link": "yarn link", - "test": "echo 'todo: add tests'" + "test": "echo 'todo: add tests'", + "git:install": "bash git-install.sh" }, "devDependencies": { "@types/clear": "^0.1.2", @@ -36,13 +37,17 @@ "dependencies": { "@latticexyz/solecs": "^0.11.1", "@latticexyz/utils": "^0.11.1", + "@latticexyz/std-contracts": "^0.11.1", + "chalk": "^5.0.1", "clear": "^0.1.0", "commander": "^9.2.0", + "ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5", "esm": "^3.2.25", "ethers": "^5.6.7", "execa": "^6.1.0", "figlet": "^1.5.2", + "forge-std": "https://github.com/foundry-rs/forge-std.git#6b4ca42943f093642bac31783b08aa52a5a6ff64", "glob": "^8.0.3", "inquirer": "^8.2.4", "inquirer-prompt-suggest": "^0.1.0", @@ -51,6 +56,7 @@ "node-fetch": "^3.2.6", "openurl": "^1.1.1", "path": "^0.12.7", + "solmate": "https://github.com/Rari-Capital/solmate.git#9cf1428245074e39090dceacb0c28b1f684f584c", "typechain": "^8.1.0", "uuid": "^8.3.2", "yargs": "^17.5.1" diff --git a/packages/cli/remappings.txt b/packages/cli/remappings.txt new file mode 100644 index 0000000000..8a59ec84ae --- /dev/null +++ b/packages/cli/remappings.txt @@ -0,0 +1,5 @@ +ds-test/=../../node_modules/ds-test/src/ +forge-std/=../../node_modules/forge-std/src/ +solmate/=../../node_modules/@rari-capital/solmate/src +std-contracts/=../../node_modules/@latticexyz/std-contracts/src/ +solecs/=../../node_modules/@latticexyz/solecs/src/ \ No newline at end of file diff --git a/packages/cli/src/commands/bulkupload.ts b/packages/cli/src/commands/bulkupload.ts new file mode 100644 index 0000000000..1bbca6d4ee --- /dev/null +++ b/packages/cli/src/commands/bulkupload.ts @@ -0,0 +1,44 @@ +import { Arguments, CommandBuilder } from "yargs"; + +const importExeca = eval('import("execa")') as Promise; + +type Options = { + statePath: string; + worldAddress: string; + rpc: string; +}; + +export const command = "bulkupload"; +export const desc = "Uploads the provided ECS state to the provided World"; + +export const builder: CommandBuilder = (yargs) => + yargs.options({ + statePath: { type: "string", demandOption: true, desc: "Path to the ECS state to upload" }, + worldAddress: { type: "string", demandOption: true, desc: "Contract address of the World to upload to" }, + rpc: { type: "string", demandOption: true, desc: "JSON RPC endpoint" }, + }); + +export const handler = async (argv: Arguments): Promise => { + const { execa } = await importExeca; + const { statePath, worldAddress, rpc } = argv; + console.log("Uploading state at ", statePath, "to", worldAddress, "on", rpc); + const url = __dirname + "/../../src/contracts/BulkUpload.sol"; + console.log("Using BulkUpload script from", url); + + try { + await execa("forge", [ + "script", + "--sig", + '"run(string, address)"', + "--rpc-url", + rpc, + `${url}:BulkUpload`, + statePath, + worldAddress, + ]); + } catch (e) { + console.error(e); + } + + process.exit(0); +}; diff --git a/packages/cli/src/contracts/BulkUpload.sol b/packages/cli/src/contracts/BulkUpload.sol new file mode 100644 index 0000000000..c706c42911 --- /dev/null +++ b/packages/cli/src/contracts/BulkUpload.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.0; + +import "ds-test/test.sol"; +import {console} from "forge-std/console.sol"; +import {Cheats} from "./Cheats.sol"; +import {BulkSetStateSystem, ID as BulkSetStateSystemID, ECSEvent} from "std-contracts/systems/BulkSetStateSystem.sol"; +import {World} from "solecs/World.sol"; +import {System} from "solecs/System.sol"; +import {getAddressById} from "solecs/utils.sol"; +import {Set} from "solecs/Set.sol"; + +struct ParsedState { + string[] componentIds; + string[] entities; + ParsedECSEvent[] state; +} + +struct ParsedECSEvent { + uint8 component; + uint32 entity; + string value; +} + +struct State { + uint256[] componentIds; + uint256[] entities; + ECSEvent[] state; +} + +/** + * Usage: + * forge script --sig "run(string, address)" --rpc-url http://localhost:8545 src/contracts/BulkUpload.sol:BulkUpload path/to/ecs-map-test.json + */ +contract BulkUpload is DSTest { + Cheats internal immutable vm = Cheats(HEVM_ADDRESS); + + function run( + string memory path, + address worldAddress, + uint256 eventsPerTx + ) public { + vm.startBroadcast(); + + // Read JSON + console.log(path); + string memory json = vm.readFile(path); + + console.log(worldAddress); + + // Parse JSON + ParsedState memory parsedState = abi.decode(vm.parseJson(json), (ParsedState)); + + // Convert component ids + uint256[] memory componentIds = new uint256[](parsedState.componentIds.length); + for (uint256 i; i < componentIds.length; i++) { + componentIds[i] = hexToUint256(parsedState.componentIds[i]); + } + + // Convert entitiy ids + uint256[] memory entities = new uint256[](parsedState.entities.length); + for (uint256 i; i < entities.length; i++) { + entities[i] = hexToUint256(parsedState.entities[i]); + } + World world = World(worldAddress); + System bulkSetStateSystem = System(getAddressById(world.systems(), BulkSetStateSystemID)); + + // Convert state + ECSEvent[] memory allEvents = new ECSEvent[](parsedState.state.length); + for (uint256 i; i < parsedState.state.length; i++) { + ParsedECSEvent memory p = parsedState.state[i]; + + // Convert value hex string to bytes + bytes memory value = hexToBytes(substring(p.value, 2, bytes(p.value).length)); + allEvents[i] = ECSEvent(p.component, p.entity, value); + } + + uint256 numTxs = allEvents.length / eventsPerTx; + + for (uint256 i = 0; i < numTxs; i++) { + ECSEvent[] memory events = new ECSEvent[](eventsPerTx); + for (uint256 j = 0; j < eventsPerTx; j++) { + events[j] = allEvents[i * eventsPerTx + j]; + } + + (uint256[] memory newEntities, ECSEvent[] memory newEvents) = transformEventsToOnlyUseNeededEntities( + entities, + events + ); + + bulkSetStateSystem.execute(abi.encode(componentIds, newEntities, newEvents)); + } + + // overflow tx + uint256 overflowEventCount = allEvents.length - numTxs * eventsPerTx; + + ECSEvent[] memory overflowEvents = new ECSEvent[](overflowEventCount); + for (uint256 j = 0; j < overflowEventCount; j++) { + overflowEvents[j] = allEvents[numTxs * eventsPerTx + j]; + } + + ( + uint256[] memory newOverflowEntities, + ECSEvent[] memory newOverflowEvents + ) = transformEventsToOnlyUseNeededEntities(entities, overflowEvents); + + bulkSetStateSystem.execute(abi.encode(componentIds, newOverflowEntities, newOverflowEvents)); + + vm.stopBroadcast(); + } +} + +function transformEventsToOnlyUseNeededEntities(uint256[] memory entities, ECSEvent[] memory events) + returns (uint256[] memory, ECSEvent[] memory) +{ + Set uniqueEntityIndices = new Set(); + + // Find unique entity indices + for (uint256 i = 0; i < events.length; i++) { + ECSEvent memory e = events[i]; + + uniqueEntityIndices.add(e.entity); + } + + // Grab the Entity IDs from the big entities array and put them into our new array + uint256[] memory relevantEntities = new uint256[](uniqueEntityIndices.size()); + for (uint256 i = 0; i < uniqueEntityIndices.size(); i++) { + relevantEntities[i] = entities[uniqueEntityIndices.getItems()[i]]; + } + + // Re-assign event entity indices to point to our new array + for (uint256 i = 0; i < events.length; i++) { + (, uint256 index) = uniqueEntityIndices.getIndex(events[i].entity); + events[i].entity = uint32(index); + } + + return (relevantEntities, events); +} + +function hexToUint8(bytes1 b) pure returns (uint8 res) { + if (b >= "0" && b <= "9") { + return uint8(b) - uint8(bytes1("0")); + } else if (b >= "A" && b <= "F") { + return 10 + uint8(b) - uint8(bytes1("A")); + } else if (b >= "a" && b <= "f") { + return 10 + uint8(b) - uint8(bytes1("a")); + } + return uint8(b); // or return error ... +} + +function hexToUint256(string memory str) pure returns (uint256 value) { + bytes memory b = bytes(str); + uint256 number = 0; + for (uint256 i = 0; i < b.length; i++) { + number = number << 4; // or number = number * 16 + number |= hexToUint8(b[i]); // or number += numberFromAscII(b[i]); + } + return number; +} + +// Convert an hexadecimal string to raw bytes +function hexToBytes(string memory s) pure returns (bytes memory) { + bytes memory ss = bytes(s); + require(ss.length % 2 == 0); // length must be even + bytes memory r = new bytes(ss.length / 2); + for (uint256 i = 0; i < ss.length / 2; ++i) { + r[i] = bytes1(hexToUint8(ss[2 * i]) * 16 + hexToUint8(ss[2 * i + 1])); + } + return r; +} + +function substring( + string memory str, + uint256 start, + uint256 end +) pure returns (string memory) { + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(end - start); + for (uint256 i = start; i < end; i++) { + result[i - start] = strBytes[i]; + } + return string(result); +} diff --git a/packages/cli/src/contracts/Cheats.sol b/packages/cli/src/contracts/Cheats.sol new file mode 100644 index 0000000000..fed0d71532 --- /dev/null +++ b/packages/cli/src/contracts/Cheats.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.0; + +interface Cheats { + // This allows us to getRecordedLogs() + struct Log { + bytes32[] topics; + bytes data; + } + + // Set block.timestamp (newTimestamp) + function warp(uint256) external; + + // Set block.height (newHeight) + function roll(uint256) external; + + // Set block.basefee (newBasefee) + function fee(uint256) external; + + // Set block.coinbase (who) + function coinbase(address) external; + + // Loads a storage slot from an address (who, slot) + function load(address, bytes32) external returns (bytes32); + + // Stores a value to an address' storage slot, (who, slot, value) + function store( + address, + bytes32, + bytes32 + ) external; + + // Signs data, (privateKey, digest) => (v, r, s) + function sign(uint256, bytes32) + external + returns ( + uint8, + bytes32, + bytes32 + ); + + // Gets address for a given private key, (privateKey) => (address) + function addr(uint256) external returns (address); + + // Derive a private key from a provided mnenomic string (or mnenomic file path) at the derivation path m/44'/60'/0'/0/{index} + function deriveKey(string calldata, uint32) external returns (uint256); + + // Derive a private key from a provided mnenomic string (or mnenomic file path) at the derivation path {path}{index} + function deriveKey( + string calldata, + string calldata, + uint32 + ) external returns (uint256); + + // Performs a foreign function call via terminal, (stringInputs) => (result) + function ffi(string[] calldata) external returns (bytes memory); + + // Set environment variables, (name, value) + function setEnv(string calldata, string calldata) external; + + // Read environment variables, (name) => (value) + function envBool(string calldata) external returns (bool); + + function envUint(string calldata) external returns (uint256); + + function envInt(string calldata) external returns (int256); + + function envAddress(string calldata) external returns (address); + + function envBytes32(string calldata) external returns (bytes32); + + function envString(string calldata) external returns (string memory); + + function envBytes(string calldata) external returns (bytes memory); + + // Read environment variables as arrays, (name, delim) => (value[]) + function envBool(string calldata, string calldata) external returns (bool[] memory); + + function envUint(string calldata, string calldata) external returns (uint256[] memory); + + function envInt(string calldata, string calldata) external returns (int256[] memory); + + function envAddress(string calldata, string calldata) external returns (address[] memory); + + function envBytes32(string calldata, string calldata) external returns (bytes32[] memory); + + function envString(string calldata, string calldata) external returns (string[] memory); + + function envBytes(string calldata, string calldata) external returns (bytes[] memory); + + // Sets the *next* call's msg.sender to be the input address + function prank(address) external; + + // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called + function startPrank(address) external; + + // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input + function prank(address, address) external; + + // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input + function startPrank(address, address) external; + + // Resets subsequent calls' msg.sender to be `address(this)` + function stopPrank() external; + + // Sets an address' balance, (who, newBalance) + function deal(address, uint256) external; + + // Sets an address' code, (who, newCode) + function etch(address, bytes calldata) external; + + // Expects an error on next call + function expectRevert() external; + + function expectRevert(bytes calldata) external; + + function expectRevert(bytes4) external; + + // Record all storage reads and writes + function record() external; + + // Gets all accessed reads and write slot from a recording session, for a given address + function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); + + // Record all the transaction logs + function recordLogs() external; + + // Gets all the recorded logs + function getRecordedLogs() external returns (Log[] memory); + + // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). + // Call this function, then emit an event, then call a function. Internally after the call, we check if + // logs were emitted in the expected order with the expected topics and data (as specified by the booleans). + // Second form also checks supplied address against emitting contract. + function expectEmit( + bool, + bool, + bool, + bool + ) external; + + function expectEmit( + bool, + bool, + bool, + bool, + address + ) external; + + // Mocks a call to an address, returning specified data. + // Calldata can either be strict or a partial match, e.g. if you only + // pass a Solidity selector to the expected calldata, then the entire Solidity + // function will be mocked. + function mockCall( + address, + bytes calldata, + bytes calldata + ) external; + + // Mocks a call to an address with a specific msg.value, returning specified data. + // Calldata match takes precedence over msg.value in case of ambiguity. + function mockCall( + address, + uint256, + bytes calldata, + bytes calldata + ) external; + + // Clears all mocked calls + function clearMockedCalls() external; + + // Expect a call to an address with the specified calldata. + // Calldata can either be strict or a partial match + function expectCall(address, bytes calldata) external; + + // Expect a call to an address with the specified msg.value and calldata + function expectCall( + address, + uint256, + bytes calldata + ) external; + + // Gets the code from an artifact file. Takes in the relative path to the json file + function getCode(string calldata) external returns (bytes memory); + + // Labels an address in call traces + function label(address, string calldata) external; + + // If the condition is false, discard this run's fuzz inputs and generate new ones + function assume(bool) external; + + // Set nonce for an account + function setNonce(address, uint64) external; + + // Get nonce for an account + function getNonce(address) external returns (uint64); + + // Set block.chainid (newChainId) + function chainId(uint256) external; + + // Using the address that calls the test contract, has the next call (at this call depth only) create a transaction that can later be signed and sent onchain + function broadcast() external; + + // Has the next call (at this call depth only) create a transaction with the address provided as the sender that can later be signed and sent onchain + function broadcast(address) external; + + // Using the address that calls the test contract, has the all subsequent calls (at this call depth only) create transactions that can later be signed and sent onchain + function startBroadcast() external; + + // Has the all subsequent calls (at this call depth only) create transactions that can later be signed and sent onchain + function startBroadcast(address) external; + + // Stops collecting onchain transactions + function stopBroadcast() external; + + // Reads the entire content of file to string. Path is relative to the project root. (path) => (data) + function readFile(string calldata) external returns (string memory); + + // Reads next line of file to string, (path) => (line) + function readLine(string calldata) external returns (string memory); + + // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it does. + // Path is relative to the project root. (path, data) => () + function writeFile(string calldata, string calldata) external; + + // Writes line to file, creating a file if it does not exist. + // Path is relative to the project root. (path, data) => () + function writeLine(string calldata, string calldata) external; + + // Closes file for reading, resetting the offset and allowing to read it from beginning with readLine. + // Path is relative to the project root. (path) => () + function closeFile(string calldata) external; + + // Removes file. This cheatcode will revert in the following situations, but is not limited to just these cases: + // - Path points to a directory. + // - The file doesn't exist. + // - The user lacks permissions to remove the file. + // Path is relative to the project root. (path) => () + function removeFile(string calldata) external; + + function toString(address) external returns (string memory); + + function toString(bytes calldata) external returns (string memory); + + function toString(bytes32) external returns (string memory); + + function toString(bool) external returns (string memory); + + function toString(uint256) external returns (string memory); + + function toString(int256) external returns (string memory); + + // Snapshot the current state of the evm. + // Returns the id of the snapshot that was created. + // To revert a snapshot use `revertTo` + function snapshot() external returns (uint256); + + // Revert the state of the evm to a previous snapshot + // Takes the snapshot id to revert to. + // This deletes the snapshot and all snapshots taken after the given snapshot id. + function revertTo(uint256) external returns (bool); + + // Creates a new fork with the given endpoint and block and returns the identifier of the fork + function createFork(string calldata, uint256) external returns (uint256); + + // Creates a new fork with the given endpoint and the _latest_ block and returns the identifier of the fork + function createFork(string calldata) external returns (uint256); + + // Creates _and_ also selects a new fork with the given endpoint and block and returns the identifier of the fork + function createSelectFork(string calldata, uint256) external returns (uint256); + + // Creates _and_ also selects a new fork with the given endpoint and the latest block and returns the identifier of the fork + function createSelectFork(string calldata) external returns (uint256); + + // Takes a fork identifier created by `createFork` and sets the corresponding forked state as active. + function selectFork(uint256) external; + + // Returns the currently active fork + // Reverts if no fork is currently active + function activeFork() external returns (uint256); + + // Marks that the account(s) should use persistent storage across fork swaps. + // Meaning, changes made to the state of this account will be kept when switching forks + function makePersistent(address) external; + + function makePersistent(address, address) external; + + function makePersistent( + address, + address, + address + ) external; + + function makePersistent(address[] calldata) external; + + // Revokes persistent status from the address, previously added via `makePersistent` + function revokePersistent(address) external; + + function revokePersistent(address[] calldata) external; + + // Returns true if the account is marked as persistent + function isPersistent(address) external returns (bool); + + // Updates the currently active fork to given block number + // This is similar to `roll` but for the currently active fork + function rollFork(uint256) external; + + // Updates the given fork to given block number + function rollFork(uint256 forkId, uint256 blockNumber) external; + + /// Returns the RPC url for the given alias + function rpcUrl(string calldata) external returns (string memory); + + /// Returns all rpc urls and their aliases `[alias, url][]` + function rpcUrls() external returns (string[2][] memory); + + function parseJson(string calldata, string calldata) external returns (bytes memory); + + function parseJson(string calldata) external returns (bytes memory); +} diff --git a/packages/network/src/workers/constants.ts b/packages/network/src/workers/constants.ts index 78c17489d9..d6469ec3b4 100644 --- a/packages/network/src/workers/constants.ts +++ b/packages/network/src/workers/constants.ts @@ -6,4 +6,4 @@ export enum SyncState { LIVE, } -export const GodID = "0x060D" as EntityID; +export const GodID = "0x060d" as EntityID; diff --git a/packages/solecs/src/Set.sol b/packages/solecs/src/Set.sol index 6a7725f7d7..15074b95eb 100644 --- a/packages/solecs/src/Set.sol +++ b/packages/solecs/src/Set.sol @@ -31,6 +31,12 @@ contract Set { items.pop(); } + function getIndex(uint256 item) public view returns (bool, uint256) { + if (!has(item)) return (false, 0); + + return (true, itemToIndex[item]); + } + function has(uint256 item) public view returns (bool) { if (items.length == 0) return false; if (itemToIndex[item] == 0) return items[0] == item; diff --git a/packages/std-client/src/utils.ts b/packages/std-client/src/utils.ts index 9837b5bba9..d686df828c 100644 --- a/packages/std-client/src/utils.ts +++ b/packages/std-client/src/utils.ts @@ -56,9 +56,9 @@ export function getGameConfig( gameConfigComponent: Component<{ startTime: Type.String; turnLength: Type.String; actionCooldownLength: Type.String }> ) { const godEntityIndex = world.entityToIndex.get(GodID); - if (!godEntityIndex) return null; + if (godEntityIndex == null) return; - return getComponentValueStrict(gameConfigComponent, godEntityIndex); + return getComponentValue(gameConfigComponent, godEntityIndex); } export function isUntraversable( diff --git a/packages/std-contracts/src/systems/BulkSetStateSystem.sol b/packages/std-contracts/src/systems/BulkSetStateSystem.sol new file mode 100644 index 0000000000..30542a3ed3 --- /dev/null +++ b/packages/std-contracts/src/systems/BulkSetStateSystem.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.0; +import "solecs/System.sol"; +import { IWorld } from "solecs/interfaces/IWorld.sol"; +import { IUint256Component } from "solecs/interfaces/IUint256Component.sol"; +import { IComponent } from "solecs/interfaces/IComponent.sol"; +import { getAddressById, getSystemAddressById } from "solecs/utils.sol"; + +import { ComponentDevSystem, ID as ComponentDevSystemID } from "./ComponentDevSystem.sol"; + +uint256 constant ID = uint256(keccak256("system.BulkSetState")); + +struct ECSEvent { + uint8 component; + uint32 entity; + bytes value; +} + +contract BulkSetStateSystem is System { + constructor(IWorld _world, address _components) System(_world, _components) {} + + function requirement(bytes memory) public view returns (bytes memory) { + // NOTE: Make sure to not include this system in a production deployment, as anyone can cahnge all component values + } + + function execute(bytes memory arguments) public returns (bytes memory) { + (uint256[] memory componentIds, uint256[] memory entities, ECSEvent[] memory state) = abi.decode( + arguments, + (uint256[], uint256[], ECSEvent[]) + ); + + for (uint256 i; i < state.length; i++) { + IComponent c = IComponent(getAddressById(components, componentIds[state[i].component])); + c.set(entities[state[i].entity], state[i].value); + } + } + + function executeTyped( + uint256[] memory componentIds, + uint256[] memory entities, + ECSEvent[] memory state + ) external returns (bytes memory) { + return execute(abi.encode(componentIds, entities, state)); + } +} diff --git a/packages/std-contracts/src/systems/ComponentDevSystem.sol b/packages/std-contracts/src/systems/ComponentDevSystem.sol new file mode 100644 index 0000000000..85769f5ba1 --- /dev/null +++ b/packages/std-contracts/src/systems/ComponentDevSystem.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity >=0.8.0; +import "solecs/System.sol"; +import { IWorld } from "solecs/interfaces/IWorld.sol"; +import { IUint256Component } from "solecs/interfaces/IUint256Component.sol"; +import { IComponent } from "solecs/interfaces/IComponent.sol"; +import { getAddressById } from "solecs/utils.sol"; + +uint256 constant ID = uint256(keccak256("system.ComponentDev")); + +contract ComponentDevSystem is System { + constructor(IWorld _world, address _components) System(_world, _components) {} + + function requirement(bytes memory) public view returns (bytes memory) { + // NOTE: Make sure to not include this system in a production deployment, as anyone can change all component values + } + + function execute(bytes memory arguments) public returns (bytes memory) { + (uint256 componentId, uint256 entity, bytes memory value) = abi.decode(arguments, (uint256, uint256, bytes)); + IComponent c = IComponent(getAddressById(components, componentId)); + if (value.length == 0) { + c.remove(entity); + } else { + c.set(entity, value); + } + } + + function executeTyped( + uint256 componentId, + uint256 entity, + bytes memory value // If value has length 0, the component is removed + ) public returns (bytes memory) { + return execute(abi.encode(componentId, entity, value)); + } +} diff --git a/yarn.lock b/yarn.lock index 43c904fab6..8a72357fe2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8923,6 +8923,10 @@ forever-agent@~0.6.1: version "0.0.0" resolved "https://github.com/foundry-rs/forge-std.git#37a3fe48c3a4d8239cda93445f0b5e76b1507436" +"forge-std@https://github.com/foundry-rs/forge-std.git#6b4ca42943f093642bac31783b08aa52a5a6ff64": + version "0.0.0" + resolved "https://github.com/foundry-rs/forge-std.git#6b4ca42943f093642bac31783b08aa52a5a6ff64" + "forge-std@https://github.com/foundry-rs/forge-std.git#82e6f5376c15341629ca23098e0b32d303f44f02": version "0.0.0" resolved "https://github.com/foundry-rs/forge-std.git#82e6f5376c15341629ca23098e0b32d303f44f02" @@ -15679,6 +15683,10 @@ solidity-docgen@^0.6.0-beta.22: handlebars "^4.7.7" solidity-ast "^0.4.31" +"solmate@https://github.com/Rari-Capital/solmate.git#9cf1428245074e39090dceacb0c28b1f684f584c": + version "6.5.0" + resolved "https://github.com/Rari-Capital/solmate.git#9cf1428245074e39090dceacb0c28b1f684f584c" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"