Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: Build for drops #5

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 181 additions & 6 deletions packages/hardhat/contracts/Game.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,13 +13,30 @@ import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol";
import { OptionalSystemHooks } from "@latticexyz/world/src/codegen/tables/OptionalSystemHooks.sol";

import { IWorld } from "@biomesaw/world/src/codegen/world/IWorld.sol";
import { IDropSystem } from "@biomesaw/world/src/codegen/world/IDropSystem.sol";
import { VoxelCoord } from "@biomesaw/utils/src/Types.sol";
import { Build, buildExistsInWorld } from "../utils/BuildUtils.sol";
import { ObjectTypeMetadata } from "@biomesaw/world/src/codegen/tables/ObjectTypeMetadata.sol";
import { AirObjectID } from "@biomesaw/world/src/ObjectTypeIds.sol";
import { getObjectType, getEntityAtCoord, getPosition, getEntityFromPlayer, getObjectTypeAtCoord } from "../utils/EntityUtils.sol";
import { voxelCoordsAreEqual } from "@biomesaw/utils/src/VoxelCoordUtils.sol";
import { decodeCallData } from "../utils/HookUtils.sol";

import { NamedBuild, getEmptyBlockOnGround } from "../utils/GameUtils.sol";

contract Game is ICustomUnregisterDelegation, IOptionalSystemHook {
address public immutable biomeWorldAddress;

address public delegatorAddress;

address[] public allowedItemDrops;
mapping(bytes32 => address) public coordHashToBuilder;

Build private build;

ResourceId BuildSystemId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "BuildSystem" });
ResourceId DropSystemId = WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "DropSystem" });

event GameNotif(address player, string message);

constructor(address _biomeWorldAddress, address _delegatorAddress) {
Expand Down Expand Up @@ -46,7 +64,7 @@ contract Game is ICustomUnregisterDelegation, IOptionalSystemHook {
}

function canUnregister(address delegator) external override onlyBiomeWorld returns (bool) {
return true;
return allowedItemDrops.length == 0;
}

function onRegisterHook(
Expand All @@ -69,17 +87,174 @@ contract Game is ICustomUnregisterDelegation, IOptionalSystemHook {
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)));
}

function onAfterCallSystem(
address msgSender,
ResourceId systemId,
bytes memory callData
) external override onlyBiomeWorld {}
) external override onlyBiomeWorld {
if (ResourceId.unwrap(systemId) == ResourceId.unwrap(BuildSystemId)) {
(, bytes memory callDataArgs) = decodeCallData(callData);
(, VoxelCoord memory coord) = abi.decode(callDataArgs, (uint8, VoxelCoord));
coordHashToBuilder[getCoordHash(coord)] = msgSender;
}
}

function basicGetter() external view returns (uint256) {
return 42;
function setBuild(uint8[] memory objectTypeIds, VoxelCoord[] memory relativePositions) external {
require(msg.sender == delegatorAddress, "Only delegator can add build");
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 positions"
);
require(
voxelCoordsAreEqual(relativePositions[0], VoxelCoord({ x: 0, y: 0, z: 0 })),
"AddBuild: First relative position must be (0, 0, 0)"
);
require(build.objectTypeIds.length == 0, "Logo build already set");

for (uint i = 0; i < objectTypeIds.length; ++i) {
build.objectTypeIds.push(objectTypeIds[i]);
}
for (uint i = 0; i < relativePositions.length; ++i) {
build.relativePositions.push(
VoxelCoord({ x: relativePositions[i].x, y: relativePositions[i].y, z: relativePositions[i].z })
);
}
}

function getRegisteredPlayers() external view returns (address[] memory) {
return new address[](0);
function matchBuild(VoxelCoord memory baseWorldCoord) external {
require(build.objectTypeIds.length > 0, "Logo build not set");

address msgSender = msg.sender;

// Go through each relative position, aplpy it to the base world coord, and check if the object type id matches
for (uint256 i = 0; i < build.objectTypeIds.length; i++) {
VoxelCoord memory absolutePosition = VoxelCoord({
x: baseWorldCoord.x + build.relativePositions[i].x,
y: baseWorldCoord.y + build.relativePositions[i].y,
z: baseWorldCoord.z + build.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 != build.objectTypeIds[i]) {
revert("Build does not match");
}
}

// Add user to allowed item drops, if not already added
bool isAllowed = false;
for (uint i = 0; i < allowedItemDrops.length; i++) {
if (allowedItemDrops[i] == msgSender) {
isAllowed = true;
break;
}
}
require(!isAllowed, "Already allowed to drop items");
allowedItemDrops.push(msgSender);

emit GameNotif(delegatorAddress, "A new player has been added to allowed item drops");
}

function dropItem(bytes32 toolEntityId) external {
address msgSender = msg.sender;
bool isAllowed = false;
for (uint i = 0; i < allowedItemDrops.length; i++) {
if (allowedItemDrops[i] == msgSender) {
allowedItemDrops[i] = allowedItemDrops[allowedItemDrops.length - 1];
allowedItemDrops.pop();
isAllowed = true;
break;
}
}
require(isAllowed, "Not allowed to drop items");

bytes32 playerEntityId = getEntityFromPlayer(delegatorAddress);
require(playerEntityId != bytes32(0), "Player entity not found");
VoxelCoord memory playerPosition = getPosition(playerEntityId);
VoxelCoord memory dropCoord = getEmptyBlockOnGround(biomeWorldAddress, playerPosition);

bytes memory dropCallData = abi.encodeCall(IDropSystem.dropTool, (toolEntityId, dropCoord));

IWorld(biomeWorldAddress).callFrom(delegatorAddress, DropSystemId, dropCallData);

emit GameNotif(delegatorAddress, "Item dropped");
}

// Getters
// ------------------------------------------------------------------------
function getBuild() external view returns (Build memory) {
return build;
}

function getAllowedItemDrops() external view returns (address[] memory) {
return allowedItemDrops;
}

function getBuilds() external view returns (NamedBuild[] memory) {
NamedBuild[] memory builds = new NamedBuild[](1);
builds[0] = NamedBuild({ name: "Logo", build: build });
return builds;
}

function getDisplayName() external view returns (string memory) {
return "Build For Drops";
}

function getStatus() external view returns (string memory) {
if (msg.sender == delegatorAddress) {
if (build.objectTypeIds.length == 0) {
return "Build not set yet. Please set the build.";
}

if (allowedItemDrops.length == 0) {
return "No players have been submitted matching builds. Please wait.";
}

return string.concat("Build set. ", Strings.toString(allowedItemDrops.length), " players allowed to drop items.");
} else {
if (build.objectTypeIds.length == 0) {
return "Build not set yet. Please wait.";
}

bool isAllowed = false;
for (uint i = 0; i < allowedItemDrops.length; i++) {
if (allowedItemDrops[i] == msg.sender) {
isAllowed = true;
break;
}
}
if (isAllowed) {
return "Allowed to drop items!";
} else {
return "Not allowed to drop items. Build and submit to be allowed.";
}
}
}

function getUnregisterMessage() external view returns (string memory) {
if (msg.sender == delegatorAddress && allowedItemDrops.length > 0) {
return
string.concat(
"You cannot unregister until all ",
Strings.toString(allowedItemDrops.length),
" players have used their allowed item drops."
);
}

return "";
}
}
5 changes: 5 additions & 0 deletions packages/nextjs/.firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "biomes-build-for-drops"
}
}
6 changes: 4 additions & 2 deletions packages/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading