diff --git a/packages/hardhat/contracts/Experience.sol b/packages/hardhat/contracts/Experience.sol index 0f5dffb8..553d4547 100644 --- a/packages/hardhat/contracts/Experience.sol +++ b/packages/hardhat/contracts/Experience.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.24; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; import { Hook } from "@latticexyz/store/src/Hook.sol"; @@ -28,19 +29,24 @@ import { NamedArea, NamedBuild, NamedBuildWithPos, weiToString, getEmptyBlockOnG contract Experience is ICustomUnregisterDelegation, IOptionalSystemHook { address public immutable biomeWorldAddress; - address public delegatorAddress; - // Event to show a notification in the Biomes World event GameNotif(address player, string message); - constructor(address _biomeWorldAddress, address _delegatorAddress) { + address public guardAddress; + VoxelCoord public vaultChestCoord; + + // Track who put what in the vault chest + mapping(bytes32 => address) public vaultChestToolOwners; + mapping(address => mapping(uint8 => uint16)) public vaultChestObjectCounts; + + constructor(address _biomeWorldAddress, address _guardAddress) { biomeWorldAddress = _biomeWorldAddress; // Set the store address, so that when reading from MUD tables in the // Biomes world, we don't need to pass the store address every time. StoreSwitch.setStoreAddress(_biomeWorldAddress); - delegatorAddress = _delegatorAddress; + guardAddress = _guardAddress; } // Use this modifier to restrict access to the Biomes World contract only @@ -50,6 +56,11 @@ contract Experience is ICustomUnregisterDelegation, IOptionalSystemHook { _; // Continue execution } + function setVaultChestCoord(VoxelCoord memory _vaultChestCoord) external { + require(msg.sender == guardAddress, "Only the guard can set the vault chest coord"); + vaultChestCoord = _vaultChestCoord; + } + function supportsInterface(bytes4 interfaceId) external view override returns (bool) { return interfaceId == type(ICustomUnregisterDelegation).interfaceId || @@ -58,9 +69,205 @@ contract Experience is ICustomUnregisterDelegation, IOptionalSystemHook { } function canUnregister(address delegator) external override onlyBiomeWorld returns (bool) { + bytes32 vaultChestEntityId = getEntityAtCoord(vaultChestCoord); + if (vaultChestEntityId != bytes32(0) && getNumSlotsUsed(vaultChestEntityId) > 0) { + return false; + } + return true; } + // Deposit into vault chest + function onAfterCallSystem( + address msgSender, + ResourceId systemId, + bytes memory callData + ) external override onlyBiomeWorld { + if (isSystemId(systemId, "TransferSystem")) { + if (msgSender == guardAddress) { + return; + } + + bytes32 vaultChestEntityId = getEntityAtCoord(vaultChestCoord); + + ( + bytes32 srcEntityId, + bytes32 dstEntityId, + uint8 transferObjectTypeId, + uint16 numToTransfer, + bytes32 toolEntityId + ) = getTransferArgs(callData); + // Check if dstEntityId is a chest that is beside the guard + require(srcEntityId != vaultChestEntityId, "You can't transfer from the vault chest"); + require(dstEntityId != vaultChestEntityId, "You can't transfer to the vault chest"); + uint8 dstObjectType = getObjectType(dstEntityId); + if (dstObjectType != ChestObjectID) { + return; + } + bytes32 guardEntityId = getEntityFromPlayer(guardAddress); + if (guardEntityId == bytes32(0)) { + return; + } + VoxelCoord memory guardCoord = getPosition(guardEntityId); + VoxelCoord memory dstCoord = getPosition(dstEntityId); + if (inSurroundingCube(dstCoord, 1, guardCoord)) { + if (vaultChestEntityId == bytes32(0)) { + emit GameNotif(msgSender, "The vault chest is missing"); + return; + } + + // Note: we don't check if the inventory of the guard or chest is full here + // as the tansfer call will fail if the inventory is full + + // Transfer the items to the guard + callTransfer( + biomeWorldAddress, + guardAddress, + dstEntityId, + guardEntityId, + transferObjectTypeId, + numToTransfer, + toolEntityId + ); + + // Then, transfer the items to the vault chest + callTransfer( + biomeWorldAddress, + guardAddress, + guardEntityId, + vaultChestEntityId, + transferObjectTypeId, + numToTransfer, + toolEntityId + ); + + // Update the vault chest tool owners and object counts + if (toolEntityId != bytes32(0)) { + vaultChestToolOwners[toolEntityId] = msgSender; + } + + vaultChestObjectCounts[msgSender][transferObjectTypeId] += numToTransfer; + + emit GameNotif(msgSender, "Items transferred to the vault chest"); + } + } + } + + function withdraw(uint8 objectTypeId, uint16 numToWithdraw, bytes32 withdrawChestEntityId) external { + require(withdrawChestEntityId != bytes32(0), "The withdrawl chest is missing"); + bytes32 vaultChestEntityId = getEntityAtCoord(vaultChestCoord); + require(withdrawChestEntityId != bytes32(0), "The vault chest is missing"); + bytes32 guardEntityId = getEntityFromPlayer(guardAddress); + require(guardEntityId != bytes32(0), "The guard is missing"); + VoxelCoord memory guardCoord = getPosition(guardEntityId); + VoxelCoord memory withdrawChestCoord = getPosition(withdrawChestEntityId); + require(inSurroundingCube(withdrawChestCoord, 1, guardCoord), "The withdrawl chest is not beside the guard"); + require(!isTool(objectTypeId), "Use the withdrawTool function to withdraw tools"); + + // Check if the player owns the items in the vault chest + address player = msg.sender; + require( + vaultChestObjectCounts[player][objectTypeId] >= numToWithdraw, + "You don't have enough items in the vault chest" + ); + + // Transfer the items to the guard + callTransfer( + biomeWorldAddress, + guardAddress, + vaultChestEntityId, + guardEntityId, + objectTypeId, + numToWithdraw, + bytes32(0) + ); + + // Then, transfer the items to the withdrawl chest + callTransfer( + biomeWorldAddress, + guardAddress, + guardEntityId, + withdrawChestEntityId, + objectTypeId, + numToWithdraw, + bytes32(0) + ); + + // Update the vault chest object counts + vaultChestObjectCounts[player][objectTypeId] -= numToWithdraw; + } + + function withdrawTool(bytes32 toolEntityId, bytes32 withdrawChestEntityId) external { + require(withdrawChestEntityId != bytes32(0), "The withdrawl chest is missing"); + bytes32 vaultChestEntityId = getEntityAtCoord(vaultChestCoord); + require(withdrawChestEntityId != bytes32(0), "The vault chest is missing"); + bytes32 guardEntityId = getEntityFromPlayer(guardAddress); + require(guardEntityId != bytes32(0), "The guard is missing"); + VoxelCoord memory guardCoord = getPosition(guardEntityId); + VoxelCoord memory withdrawChestCoord = getPosition(withdrawChestEntityId); + require(inSurroundingCube(withdrawChestCoord, 1, guardCoord), "The withdrawl chest is not beside the guard"); + + uint8 objectTypeId = getObjectType(toolEntityId); + require(objectTypeId != uint8(0), "The tool is missing"); + require(isTool(objectTypeId), "The entity is not a tool"); + uint16 numToWithdraw = 1; + + // Check if the player owns the items in the vault chest + address player = msg.sender; + require( + vaultChestObjectCounts[player][objectTypeId] >= numToWithdraw, + "You don't have enough items in the vault chest" + ); + require(vaultChestToolOwners[toolEntityId] == player, "You don't own the tool"); + + // Transfer the items to the guard + callTransfer( + biomeWorldAddress, + guardAddress, + vaultChestEntityId, + guardEntityId, + objectTypeId, + numToWithdraw, + toolEntityId + ); + + // Then, transfer the items to the withdrawl chest + callTransfer( + biomeWorldAddress, + guardAddress, + guardEntityId, + withdrawChestEntityId, + objectTypeId, + numToWithdraw, + toolEntityId + ); + + // Update the vault chest object counts + vaultChestObjectCounts[player][objectTypeId] -= numToWithdraw; + vaultChestToolOwners[toolEntityId] = address(0); + } + + function getDisplayName() external view returns (string memory) { + return "Vault Guard Service"; + } + + function getNumItemsInVaultChest() public view returns (uint256) { + uint256 numItemsInVaultChest = 0; + for (uint16 i = 0; i < 256; i++) { + numItemsInVaultChest += vaultChestObjectCounts[msg.sender][uint8(i)]; + } + return numItemsInVaultChest; + } + + function getStatus() external view returns (string memory) { + bytes32 guardEntityId = getEntityFromPlayer(guardAddress); + if (guardEntityId == bytes32(0)) { + return "ALERT: Guard is dead!"; + } + + return string.concat("You have ", Strings.toString(getNumItemsInVaultChest()), " items in the vault chest"); + } + function onRegisterHook( address msgSender, ResourceId systemId, @@ -80,26 +287,4 @@ contract Experience is ICustomUnregisterDelegation, IOptionalSystemHook { ResourceId systemId, bytes memory callData ) external override onlyBiomeWorld {} - - function onAfterCallSystem( - address msgSender, - ResourceId systemId, - bytes memory callData - ) external override onlyBiomeWorld {} - - function basicGetter() external view returns (uint256) { - return 42; - } - - function getRegisteredPlayers() external view returns (address[] memory) { - return new address[](0); - } - - function getDisplayName() external view returns (string memory) { - return "Experience"; - } - - function getStatus() external view returns (string memory) { - return "You are in the Experience"; - } } diff --git a/packages/hardhat/deploy/00_deploy_experience.ts b/packages/hardhat/deploy/00_deploy_experience.ts index 3a61821b..4b522b9a 100644 --- a/packages/hardhat/deploy/00_deploy_experience.ts +++ b/packages/hardhat/deploy/00_deploy_experience.ts @@ -49,7 +49,7 @@ const deployExperienceContract: DeployFunction = async function (hre: HardhatRun await deploy("Experience", { from: deployer, // Contract constructor arguments - args: [useBiomesWorldAddress, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"], + args: [useBiomesWorldAddress, "0xE0ae70caBb529336e25FA7a1f036b77ad0089d2a"], log: true, // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by // automatically mining the contract deployment transaction. There is no effect on live networks. diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 81ebbd1b..5f9685ef 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -15,7 +15,8 @@ const Home: NextPage = () => { const setStage = useGlobalState(({ setStage }) => setStage); const isBiomesRegistered = useGlobalState(({ isBiomesRegistered }) => isBiomesRegistered); - const isExperienceRegistered = useGlobalState(({ isExperienceRegistered }) => isExperienceRegistered); + // const isExperienceRegistered = useGlobalState(({ isExperienceRegistered }) => isExperienceRegistered); + const isExperienceRegistered = true; useEffect(() => { if (connectedAddress) { diff --git a/packages/nextjs/components/Experience.tsx b/packages/nextjs/components/Experience.tsx index e3fdc1eb..372b2a80 100644 --- a/packages/nextjs/components/Experience.tsx +++ b/packages/nextjs/components/Experience.tsx @@ -1,7 +1,8 @@ import { useReducer } from "react"; import { Abi, AbiFunction } from "abitype"; +import { TransactionReceipt } from "viem"; import { useAccount } from "wagmi"; -import { DisplayVariable, displayTxResult } from "~~/app/debug/_components/contract"; +import { DisplayVariable, WriteOnlyFunctionForm, displayTxResult } from "~~/app/debug/_components/contract"; import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; import { GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; @@ -18,6 +19,26 @@ export const Experience: React.FC = ({}) => { return
Loading...
; } + const writeFunctions = ((deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]) + .filter(fn => { + const isWriteableFunction = + fn.stateMutability !== "view" && + fn.stateMutability !== "pure" && + fn.name !== "onAfterCallSystem" && + fn.name !== "onBeforeCallSystem" && + fn.name !== "onRegisterHook" && + fn.name !== "onUnregisterHook" && + fn.name !== "canUnregister"; + return isWriteableFunction; + }) + .map(fn => { + return { + fn, + inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], + }; + }) + .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + const viewFunctions = ((deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[]) .filter(fn => { const isQueryableWithNoParams = @@ -32,7 +53,13 @@ export const Experience: React.FC = ({}) => { }) .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); - const basicGetterFn = viewFunctions.find(({ fn }) => fn.name === "basicGetter"); + const withdraw = writeFunctions.find(fn => fn.fn.name === "withdraw"); + const withdrawTool = writeFunctions.find(fn => fn.fn.name === "withdrawTool"); + const getNumItemsInVaultChest = viewFunctions.find(({ fn }) => fn.name === "getNumItemsInVaultChest"); + + if (withdraw === undefined || withdrawTool === undefined || getNumItemsInVaultChest === undefined) { + return
Missing required functions
; + } return (
@@ -62,26 +89,57 @@ export const Experience: React.FC = ({}) => {
-

Play Experience

+

Vault Guard Service

- Your Main Experience Page + Transfer your items for safe-guarding by the chest

-
+
+
+ { + return; + }} + onBlockConfirmation={(txnReceipt: TransactionReceipt) => { + console.log("txnReceipt", txnReceipt); + }} + contractAddress={deployedContractData.address} + inheritedFrom={withdraw?.inheritedFrom} + /> +
+
+ { + return; + }} + onBlockConfirmation={(txnReceipt: TransactionReceipt) => { + console.log("txnReceipt", txnReceipt); + }} + contractAddress={deployedContractData.address} + inheritedFrom={withdrawTool?.inheritedFrom} + /> +
+
- {basicGetterFn && ( + {getNumItemsInVaultChest && ( {({ result, RefreshButton }) => { @@ -91,7 +149,7 @@ export const Experience: React.FC = ({}) => { style={{ backgroundColor: "#42a232" }} >
- YOUR GETTER {RefreshButton} + Num Items In Vault {RefreshButton}
{displayTxResult(result)}
diff --git a/packages/nextjs/components/Landing.tsx b/packages/nextjs/components/Landing.tsx index 915b41ca..8c7e6cec 100644 --- a/packages/nextjs/components/Landing.tsx +++ b/packages/nextjs/components/Landing.tsx @@ -45,9 +45,9 @@ export const Landing: React.FC = ({}) => {
-

Your Experience Title

+

Vault Guard Service

- Your experience description + Transfer your items for safe-guarding by the chest

{ const { address: connectedAddress } = useAccount(); @@ -33,7 +33,7 @@ export const RegisterBiomes: React.FC = ({}) => { const delegatorAddress = await publicClient.readContract({ address: deployedContractData?.address, abi: deployedContractData?.abi, - functionName: "delegatorAddress", + functionName: "guardAddress", args: [], }); if (delegatorAddress === undefined || delegatorAddress === null || typeof delegatorAddress !== "string") { @@ -52,7 +52,7 @@ export const RegisterBiomes: React.FC = ({}) => { useEffect(() => { if (deployedContractData) { - const hasDelegatorAddress = deployedContractData?.abi.some(abi => abi.name === "delegatorAddress"); + const hasDelegatorAddress = deployedContractData?.abi.some(abi => abi.name === "guardAddress"); if (hasDelegatorAddress) { checkDelegatorAddress(); } else { @@ -87,7 +87,7 @@ export const RegisterBiomes: React.FC = ({}) => {

HOOKS

{ />
- {deployedContractData.abi.some(abi => abi.name === "delegatorAddress") && isDelegatorAddress && ( + {deployedContractData.abi.some(abi => abi.name === "guardAddress") && isDelegatorAddress && (

DELEGATIONS

diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 42dc0d27..38b3545a 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -6,7 +6,7 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; const deployedContracts = { 690: { - Experience: { + Game: { address: "0x09F61e35b34EB7855fb234dc81109d774Bb16973", abi: [ { @@ -248,7 +248,7 @@ const deployedContracts = { }, }, 17069: { - Experience: { + Game: { address: "0xaFFFd91f427b81e0e56be9A4b6369f8DE6f24994", abi: [ { @@ -491,7 +491,7 @@ const deployedContracts = { }, 31337: { Experience: { - address: "0x71089Ba41e478702e1904692385Be3972B2cBf9e", + address: "0x63fea6E447F120B8Faf85B53cdaD8348e645D80E", abi: [ { inputs: [ @@ -502,13 +502,34 @@ const deployedContracts = { }, { internalType: "address", - name: "_delegatorAddress", + name: "_guardAddress", type: "address", }, ], stateMutability: "nonpayable", type: "constructor", }, + { + inputs: [ + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "uint256", + name: "start", + type: "uint256", + }, + { + internalType: "uint256", + name: "end", + type: "uint256", + }, + ], + name: "Slice_OutOfBounds", + type: "error", + }, { anonymous: false, inputs: [ @@ -528,19 +549,6 @@ const deployedContracts = { name: "GameNotif", type: "event", }, - { - inputs: [], - name: "basicGetter", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - stateMutability: "view", - type: "function", - }, { inputs: [], name: "biomeWorldAddress", @@ -575,12 +583,12 @@ const deployedContracts = { }, { inputs: [], - name: "delegatorAddress", + name: "getDisplayName", outputs: [ { - internalType: "address", + internalType: "string", name: "", - type: "address", + type: "string", }, ], stateMutability: "view", @@ -588,12 +596,12 @@ const deployedContracts = { }, { inputs: [], - name: "getDisplayName", + name: "getNumItemsInVaultChest", outputs: [ { - internalType: "string", + internalType: "uint256", name: "", - type: "string", + type: "uint256", }, ], stateMutability: "view", @@ -601,12 +609,12 @@ const deployedContracts = { }, { inputs: [], - name: "getRegisteredPlayers", + name: "getStatus", outputs: [ { - internalType: "address[]", + internalType: "string", name: "", - type: "address[]", + type: "string", }, ], stateMutability: "view", @@ -614,12 +622,12 @@ const deployedContracts = { }, { inputs: [], - name: "getStatus", + name: "guardAddress", outputs: [ { - internalType: "string", + internalType: "address", name: "", - type: "string", + type: "address", }, ], stateMutability: "view", @@ -727,6 +735,36 @@ const deployedContracts = { stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + components: [ + { + internalType: "int16", + name: "x", + type: "int16", + }, + { + internalType: "int16", + name: "y", + type: "int16", + }, + { + internalType: "int16", + name: "z", + type: "int16", + }, + ], + internalType: "struct VoxelCoord", + name: "_vaultChestCoord", + type: "tuple", + }, + ], + name: "setVaultChestCoord", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, { inputs: [ { @@ -746,6 +784,113 @@ const deployedContracts = { stateMutability: "view", type: "function", }, + { + inputs: [], + name: "vaultChestCoord", + outputs: [ + { + internalType: "int16", + name: "x", + type: "int16", + }, + { + internalType: "int16", + name: "y", + type: "int16", + }, + { + internalType: "int16", + name: "z", + type: "int16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint8", + name: "", + type: "uint8", + }, + ], + name: "vaultChestObjectCounts", + outputs: [ + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "vaultChestToolOwners", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint8", + name: "objectTypeId", + type: "uint8", + }, + { + internalType: "uint16", + name: "numToWithdraw", + type: "uint16", + }, + { + internalType: "bytes32", + name: "withdrawChestEntityId", + type: "bytes32", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "toolEntityId", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "withdrawChestEntityId", + type: "bytes32", + }, + ], + name: "withdrawTool", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, ], inheritedFunctions: { canUnregister: "@latticexyz/world/src/ICustomUnregisterDelegation.sol",