From 2cbc9dd0c7768ad609ee1e37329b4f8711705446 Mon Sep 17 00:00:00 2001 From: Dhvani Patel Date: Fri, 3 May 2024 13:21:51 +0100 Subject: [PATCH 01/10] build a nomics contract and client --- packages/hardhat/contracts/Game.sol | 301 ++++++- packages/hardhat/deploy/00_deploy_game.ts | 2 +- packages/nextjs/app/page.tsx | 6 +- .../nextjs/components/CreateBuildModal.tsx | 296 +++++++ packages/nextjs/components/Game.tsx | 385 +++++++-- packages/nextjs/components/Landing.tsx | 14 +- packages/nextjs/components/RegisterBiomes.tsx | 7 +- .../nextjs/components/SubmitBuildModal.tsx | 273 +++++++ packages/nextjs/components/ViewBuildModal.tsx | 113 +++ .../nextjs/contracts/deployedContracts.ts | 768 +++++++++--------- packages/nextjs/public/moonumentslanding.webp | Bin 0 -> 556440 bytes 11 files changed, 1669 insertions(+), 496 deletions(-) create mode 100644 packages/nextjs/components/CreateBuildModal.tsx create mode 100644 packages/nextjs/components/SubmitBuildModal.tsx create mode 100644 packages/nextjs/components/ViewBuildModal.tsx create mode 100644 packages/nextjs/public/moonumentslanding.webp diff --git a/packages/hardhat/contracts/Game.sol b/packages/hardhat/contracts/Game.sol index ba911094..89cb2f3a 100644 --- a/packages/hardhat/contracts/Game.sol +++ b/packages/hardhat/contracts/Game.sol @@ -10,43 +10,240 @@ import { IOptionalSystemHook } from "@latticexyz/world/src/IOptionalSystemHook.s import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM, ALL } from "@latticexyz/world/src/systemHookTypes.sol"; import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; import { OptionalSystemHooks } from "@latticexyz/world/src/codegen/tables/OptionalSystemHooks.sol"; - +import { Build, buildExistsInWorld } from "../utils/BuildUtils.sol"; import { IWorld } from "@biomesaw/world/src/codegen/world/IWorld.sol"; import { VoxelCoord } from "@biomesaw/utils/src/Types.sol"; +import { getObjectType, getEntityAtCoord, getPosition, getEntityFromPlayer, getObjectTypeAtCoord } from "../utils/EntityUtils.sol"; +import { voxelCoordsAreEqual } from "@biomesaw/utils/src/VoxelCoordUtils.sol"; + +struct NamePair { + uint256 id; + string name; +} + +struct SubmissionPricePair { + uint256 id; + uint256 price; +} + +struct BuilderList { + uint256 id; + address[] builderAddresses; +} + +struct BlueprintPair { + uint256 id; + Build blueprint; +} -contract Game is ICustomUnregisterDelegation, IOptionalSystemHook { +struct LocationPair { + uint256 id; + VoxelCoord[] location; +} + +struct ListEntry { + uint256 id; + string name; + uint256 price; + address[] builders; + Build blueprint; + VoxelCoord[] locations; +} + +contract Game is IOptionalSystemHook { address public immutable biomeWorldAddress; + mapping(bytes32 => address) public coordHashToBuilder; - address public delegatorAddress; + uint256 public buildCount; + mapping(uint256 => string) names; + mapping(uint256 => Build) blueprints; + mapping(uint256 => uint256) submissionPrices; + mapping(uint256 => address[]) builders; + mapping(uint256 => VoxelCoord[]) locations; + mapping(address => uint256) earned; event GameNotif(address player, string message); - constructor(address _biomeWorldAddress, address _delegatorAddress) { - biomeWorldAddress = _biomeWorldAddress; + ResourceId BuildSystemId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "BuildSystem" }); - // 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. + constructor(address _biomeWorldAddress) { + biomeWorldAddress = _biomeWorldAddress; StoreSwitch.setStoreAddress(_biomeWorldAddress); + } + + function create( + uint8[] memory objectTypeIds, + VoxelCoord[] memory relativePositions, + uint256 submissionPrice, + string memory name + ) public { + require(objectTypeIds.length > 0, "AddBuild: Must specify at least one object type ID."); + require( + objectTypeIds.length == relativePositions.length, + "AddBuild: Number of object type IDs must match number of relative position." + ); + require( + voxelCoordsAreEqual(relativePositions[0], VoxelCoord({ x: 0, y: 0, z: 0 })), + "AddBuild: First relative position must be (0, 0, 0)." + ); + require(bytes(name).length > 0, "AddBuild: Must specify a name."); + require(submissionPrice > 0, "AddBuild: Must specify a submission price."); + + buildCount++; + + Build storage newBuild = blueprints[buildCount]; + for (uint i = 0; i < objectTypeIds.length; ++i) { + newBuild.objectTypeIds.push(objectTypeIds[i]); + } + for (uint i = 0; i < relativePositions.length; ++i) { + newBuild.relativePositions.push( + VoxelCoord({ x: relativePositions[i].x, y: relativePositions[i].y, z: relativePositions[i].z }) + ); + } + + submissionPrices[buildCount] = submissionPrice; + names[buildCount] = name; + } + + function submitBuilding(uint256 buildingId, VoxelCoord memory baseWorldCoord) external payable { + require(buildingId <= buildCount, "Invalid building ID"); + Build memory blueprint = blueprints[buildingId]; + uint256 submissionPrice = submissionPrices[buildingId]; + VoxelCoord[] memory existingBuildLocations = locations[buildingId]; + address[] memory buildersAtId = builders[buildingId]; + + address msgSender = msg.sender; + require(msg.value == submissionPrice, "Incorrect submission price."); + + for (uint i = 0; i < existingBuildLocations.length; ++i) { + if (voxelCoordsAreEqual(existingBuildLocations[i], baseWorldCoord)) { + revert("Location already exists"); + } + } + + // Go through each relative position, apply it to the base world coord, and check if the object type id matches + for (uint256 i = 0; i < blueprint.objectTypeIds.length; i++) { + VoxelCoord memory absolutePosition = VoxelCoord({ + x: baseWorldCoord.x + blueprint.relativePositions[i].x, + y: baseWorldCoord.y + blueprint.relativePositions[i].y, + z: baseWorldCoord.z + blueprint.relativePositions[i].z + }); + bytes32 entityId = getEntityAtCoord(absolutePosition); + + uint8 objectTypeId; + if (entityId == bytes32(0)) { + // then it's the terrain + objectTypeId = IWorld(biomeWorldAddress).getTerrainBlock(absolutePosition); + } else { + objectTypeId = getObjectType(entityId); + + address builder = coordHashToBuilder[getCoordHash(absolutePosition)]; + require(builder == msgSender, "Builder does not match"); + } + if (objectTypeId != blueprint.objectTypeIds[i]) { + revert("Build does not match."); + } + } + + uint256 count = buildersAtId.length; + + builders[buildingId].push(msgSender); + locations[buildingId].push(baseWorldCoord); + + if (count > 0) { + uint256 splitAmount = msg.value / count; + uint256 totalDistributed = splitAmount * count; + uint256 remainder = msg.value - totalDistributed; + + for (uint256 i = 0; i < count; i++) { + earned[buildersAtId[i]] += splitAmount; + (bool sent, ) = buildersAtId[i].call{ value: splitAmount }(""); + require(sent, "Failed to send submission price to builder"); + } + + if (remainder > 0) { + (bool sent, ) = msgSender.call{ value: remainder }(""); + require(sent, "Failed to refund remainder"); + } + } else { + earned[msgSender] += msg.value; + (bool sent, ) = msgSender.call{ value: msg.value }(""); + require(sent, "Failed to send submission price back to initial builder"); + } + } - delegatorAddress = _delegatorAddress; + function deleteBuilding(uint256 buildingId, uint256 n) internal { + require(n < builders[buildingId].length, "Invalid index"); + require(n < locations[buildingId].length, "Invalid index"); + + uint256 lastBuilderIndex = builders[buildingId].length - 1; + uint256 lastLocationIndex = locations[buildingId].length - 1; + + // Move the last element to the index `n` and then remove the last element for builders + builders[buildingId][n] = builders[buildingId][lastBuilderIndex]; + builders[buildingId].pop(); + + // Move the last element to the index `n` and then remove the last element for locations + locations[buildingId][n] = locations[buildingId][lastLocationIndex]; + locations[buildingId].pop(); + } + + function challengeBuilding(uint256 buildingId, uint256 n) public { + require(buildingId <= buildCount, "Invalid building ID"); + Build memory blueprint = blueprints[buildingId]; + require(n < locations[buildingId].length, "Invalid index"); + VoxelCoord memory baseWorldCoord = locations[buildingId][n]; + + bool doesMatch = true; + + // Go through each relative position, apply it to the base world coord, and check if the object type id matches + for (uint256 i = 0; i < blueprint.objectTypeIds.length; i++) { + VoxelCoord memory absolutePosition = VoxelCoord({ + x: baseWorldCoord.x + blueprint.relativePositions[i].x, + y: baseWorldCoord.y + blueprint.relativePositions[i].y, + z: baseWorldCoord.z + blueprint.relativePositions[i].z + }); + bytes32 entityId = getEntityAtCoord(absolutePosition); + + uint8 objectTypeId; + if (entityId == bytes32(0)) { + // then it's the terrain + objectTypeId = IWorld(biomeWorldAddress).getTerrainBlock(absolutePosition); + } else { + objectTypeId = getObjectType(entityId); + } + if (objectTypeId != blueprint.objectTypeIds[i]) { + doesMatch = false; + break; + } + } + + if (!doesMatch) { + deleteBuilding(buildingId, n); + } } - // Use this modifier to restrict access to the Biomes World contract only - // eg. for hooks that are only allowed to be called by the Biomes World contract modifier onlyBiomeWorld() { require(msg.sender == biomeWorldAddress, "Caller is not the Biomes World contract"); _; // Continue execution } - function supportsInterface(bytes4 interfaceId) external view override returns (bool) { - return - interfaceId == type(ICustomUnregisterDelegation).interfaceId || - interfaceId == type(IOptionalSystemHook).interfaceId || - interfaceId == type(IERC165).interfaceId; + function onAfterCallSystem( + address msgSender, + ResourceId systemId, + bytes memory callData + ) external override onlyBiomeWorld { + if (ResourceId.unwrap(systemId) == ResourceId.unwrap(BuildSystemId)) { + Slice callDataArgs = SliceLib.getSubslice(callData, 4); + (, VoxelCoord memory coord) = abi.decode(callDataArgs.toBytes(), (uint8, VoxelCoord)); + coordHashToBuilder[getCoordHash(coord)] = msgSender; + } } - function canUnregister(address delegator) external override onlyBiomeWorld returns (bool) { - return true; + //EXTRA STUFF: + + function supportsInterface(bytes4 interfaceId) external view override returns (bool) { + return interfaceId == type(IOptionalSystemHook).interfaceId || interfaceId == type(IERC165).interfaceId; } function onRegisterHook( @@ -69,17 +266,69 @@ contract Game is ICustomUnregisterDelegation, IOptionalSystemHook { bytes memory callData ) external override onlyBiomeWorld {} - function onAfterCallSystem( - address msgSender, - ResourceId systemId, - bytes memory callData - ) external override onlyBiomeWorld {} + function getCoordHash(VoxelCoord memory coord) internal pure returns (bytes32) { + return bytes32(keccak256(abi.encode(coord.x, coord.y, coord.z))); + } + + //GETTERS: + + function getAllNames() public view returns (NamePair[] memory) { + NamePair[] memory pairs = new NamePair[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + pairs[i - 1] = NamePair(i, names[i]); + } + return pairs; + } + + function getAllSubmissionPrices() public view returns (SubmissionPricePair[] memory) { + SubmissionPricePair[] memory pairs = new SubmissionPricePair[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + pairs[i - 1] = SubmissionPricePair(i, submissionPrices[i]); + } + return pairs; + } + + function getAllBuilders() public view returns (BuilderList[] memory) { + BuilderList[] memory buildersList = new BuilderList[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + buildersList[i - 1] = BuilderList(i, builders[i]); + } + return buildersList; + } + + function getAllLocations() public view returns (LocationPair[] memory) { + LocationPair[] memory locationPair = new LocationPair[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + locationPair[i - 1] = LocationPair(i, locations[i]); + } + return locationPair; + } + + function getAllBlueprints() public view returns (BlueprintPair[] memory) { + BlueprintPair[] memory blueprintPairs = new BlueprintPair[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + blueprintPairs[i - 1] = BlueprintPair(i, blueprints[i]); + } + return blueprintPairs; + } - function basicGetter() external view returns (uint256) { - return 42; + function getList() public view returns (ListEntry[] memory) { + ListEntry[] memory entries = new ListEntry[](buildCount); + for (uint256 i = 1; i <= buildCount; i++) { + entries[i - 1] = ListEntry({ + id: i, + name: names[i], + price: submissionPrices[i], + builders: builders[i], + blueprint: blueprints[i], + locations: locations[i] + }); + } + return entries; } - function getRegisteredPlayers() external view returns (address[] memory) { - return new address[](0); + // Getter for retrieving the balance of a specific address + function getEarned() public view returns (uint256) { + return earned[msg.sender]; } } diff --git a/packages/hardhat/deploy/00_deploy_game.ts b/packages/hardhat/deploy/00_deploy_game.ts index 322eef8e..51412fb7 100644 --- a/packages/hardhat/deploy/00_deploy_game.ts +++ b/packages/hardhat/deploy/00_deploy_game.ts @@ -49,7 +49,7 @@ const deployGameContract: DeployFunction = async function (hre: HardhatRuntimeEn await deploy("Game", { from: deployer, // Contract constructor arguments - args: [useBiomesWorldAddress, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"], + args: [useBiomesWorldAddress], 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 d24f87f5..a227533e 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -16,8 +16,10 @@ const Home: NextPage = () => { const setStage = useGlobalState(({ setStage }) => setStage); const isBiomesRegistered = useGlobalState(({ isBiomesRegistered }) => isBiomesRegistered); - const isGameRegistered = useGlobalState(({ isGameRegistered }) => isGameRegistered); - const isBiomesClientSetup = useGlobalState(({ isBiomesClientSetup }) => isBiomesClientSetup); + // const isGameRegistered = useGlobalState(({ isGameRegistered }) => isGameRegistered); + const isGameRegistered = true; + // const isBiomesClientSetup = useGlobalState(({ isBiomesClientSetup }) => isBiomesClientSetup); + const isBiomesClientSetup = true; useEffect(() => { if (connectedAddress) { diff --git a/packages/nextjs/components/CreateBuildModal.tsx b/packages/nextjs/components/CreateBuildModal.tsx new file mode 100644 index 00000000..f389dfbd --- /dev/null +++ b/packages/nextjs/components/CreateBuildModal.tsx @@ -0,0 +1,296 @@ +import { useEffect, useState } from "react"; +import { EtherInput, InputBase } from "./scaffold-eth"; +import { VoxelCoord } from "@latticexyz/utils"; +import { parseEther, parseGwei } from "viem"; +import { useAccount, useWriteContract } from "wagmi"; +import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; + +type CreateBuildModalProps = { + closeModal: () => void; +}; + +export enum CreateBuildModalStep { + ShowExtensionsToggle, + EnterBuildMode, + SelectBlocks, + SaveBuild, + SubmitBuild, +} + +export const CreateBuildModal: React.FC = ({ closeModal }) => { + const { address: connectedAddress } = useAccount(); + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo("Game"); + const [buildName, setBuildName] = useState(""); + const [buildPrice, setBuildPrice] = useState(""); + const [submitBuildPrice, setSubmitBuildPrice] = useState(0n); + const writeTxn = useTransactor(); + const [textInput, setTextInput] = useState(""); + const [step, setStep] = useState(CreateBuildModalStep.ShowExtensionsToggle); + const [objectTypeIds, setObjectTypeIds] = useState([]); + const { chain } = useAccount(); + const [relativePositions, setRelativePositions] = useState([]); + const [invalidJson, setInvalidJson] = useState(false); + const { writeContractAsync } = useWriteContract(); + const { targetNetwork } = useTargetNetwork(); + const writeDisabled = !chain || chain?.id !== targetNetwork.id; + const [isLoading, setIsLoading] = useState(false); + + const handleTextInputChange = event => { + setTextInput(event.target.value); + }; + + useEffect(() => { + if (textInput.length === 0) { + setInvalidJson(true); + return; + } + try { + const parsedJson = JSON.parse(textInput); + // validate JSON + if (!parsedJson.objectTypeIds || !parsedJson.relativePositions) { + throw new Error("Invalid JSON"); + } + setObjectTypeIds(parsedJson.objectTypeIds); + setRelativePositions(parsedJson.relativePositions); + setInvalidJson(false); + } catch (e) { + setInvalidJson(true); + } + }, [textInput]); + + useEffect(() => { + try { + if (buildPrice.length === 0) { + setSubmitBuildPrice(0n); + } + const weiValue = parseEther(buildPrice); + setSubmitBuildPrice(weiValue); + } catch (e) { + setSubmitBuildPrice(0n); + } + }, [buildPrice]); + + // const [displayedTxResult, setDisplayedTxResult] = useState(); + // const { data: txResult } = useWaitForTransaction({ + // hash: result?.hash, + // }); + // useEffect(() => { + // setDisplayedTxResult(txResult); + // }, [txResult]); + + const resetState = () => { + // reset state + setStep(CreateBuildModalStep.ShowExtensionsToggle); + setBuildName(""); + setBuildPrice(""); + setSubmitBuildPrice(0n); + setTextInput(""); + setObjectTypeIds([]); + setRelativePositions([]); + setInvalidJson(false); + }; + + const handleWrite = async () => { + try { + if (deployedContractData === undefined) { + return; + } + closeModal(); + resetState(); + setIsLoading(true); + const makeWriteWithParams = () => + writeContractAsync({ + address: deployedContractData.address, + functionName: "create", + maxFeePerGas: parseGwei("0.01"), + maxPriorityFeePerGas: parseGwei("0.001"), + abi: deployedContractData?.abi, + args: [objectTypeIds, relativePositions, submitBuildPrice, buildName], + }); + await writeTxn(makeWriteWithParams); + setIsLoading(false); + } catch (e: any) { + console.error("⚡️ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e); + } + }; + + if (connectedAddress === undefined) { + return
Connect your wallet to continue
; + } + + if (deployedContractData === undefined || deployedContractLoading) { + return
Loading...
; + } + + const renderStep = () => { + switch (step) { + case CreateBuildModalStep.ShowExtensionsToggle: + return ( +
+

+ 1. Toggle To "Show" in Biomes Client +

+
+ +
+ +
+ ); + case CreateBuildModalStep.EnterBuildMode: + return ( +
+

+ 2. Click "Create" in Builds Section +

+
+ +
+ +
+ ); + case CreateBuildModalStep.SelectBlocks: + return ( +
+

+ 3. Select blocks to use and create a build! This build is offchain. +

+
+ +
+ +
+ ); + case CreateBuildModalStep.SaveBuild: + return ( +
+

+ 4. Save Your Build, Give it a Name, and Copy the JSON +

+
+ + + + {/* + + */} +
+ +
+ ); + case CreateBuildModalStep.SubmitBuild: + return ( +
+

+ 5. Enter Name, Paste Build JSON, and Set Submission Price +

+ setBuildName(newValue)} + placeholder={"Build Name"} + error={undefined} + disabled={isLoading} + /> +