Skip to content
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
2 changes: 2 additions & 0 deletions packages/client/src/svelte/modules/state/base/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ declare global {
machinesInPod?: string[]
tanksInPod?: string[]
fixedEntities?: FixedEntities
lastFulfilled?: bigint
currentOrder?: string
tokenBalances?: bigint
recipe?: {
Expand Down Expand Up @@ -100,6 +101,7 @@ declare global {
spawnIndex: number
name?: string
carriedBy: string
lastFulfilled: bigint
currentOrder: string
incomingConnections: string[]
outgoingConnections: string[]
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export default defineWorld({
inlets: "bytes32[]"
}
},
LastFulfilled: "uint256", // Used to track time since last order fulfillment
CurrentOrder: "bytes32", // ID of the order the player has accepted
Recipe: {
key: ["machineType", "input"],
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/codegen/index.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ import { TankConnection } from "./tables/TankConnection.sol";
import { MachinesInPod } from "./tables/MachinesInPod.sol";
import { TanksInPod } from "./tables/TanksInPod.sol";
import { FixedEntities, FixedEntitiesData } from "./tables/FixedEntities.sol";
import { LastFulfilled } from "./tables/LastFulfilled.sol";
import { CurrentOrder } from "./tables/CurrentOrder.sol";
import { Recipe } from "./tables/Recipe.sol";
199 changes: 199 additions & 0 deletions packages/contracts/src/codegen/tables/LastFulfilled.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// 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 { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol";
import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";

library LastFulfilled {
// Hex below is the result of `WorldResourceIdLib.encode({ namespace: "", name: "LastFulfilled", typeId: RESOURCE_TABLE });`
ResourceId constant _tableId = ResourceId.wrap(0x746200000000000000000000000000004c61737446756c66696c6c6564000000);

FieldLayout constant _fieldLayout =
FieldLayout.wrap(0x0020010020000000000000000000000000000000000000000000000000000000);

// Hex-encoded key schema of (bytes32)
Schema constant _keySchema = Schema.wrap(0x002001005f000000000000000000000000000000000000000000000000000000);
// Hex-encoded value schema of (uint256)
Schema constant _valueSchema = Schema.wrap(0x002001001f000000000000000000000000000000000000000000000000000000);

/**
* @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[](1);
keyNames[0] = "id";
}

/**
* @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] = "value";
}

/**
* @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 value.
*/
function getValue(bytes32 id) internal view returns (uint256 value) {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
return (uint256(bytes32(_blob)));
}

/**
* @notice Get value.
*/
function _getValue(bytes32 id) internal view returns (uint256 value) {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
return (uint256(bytes32(_blob)));
}

/**
* @notice Get value.
*/
function get(bytes32 id) internal view returns (uint256 value) {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

bytes32 _blob = StoreSwitch.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
return (uint256(bytes32(_blob)));
}

/**
* @notice Get value.
*/
function _get(bytes32 id) internal view returns (uint256 value) {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

bytes32 _blob = StoreCore.getStaticField(_tableId, _keyTuple, 0, _fieldLayout);
return (uint256(bytes32(_blob)));
}

/**
* @notice Set value.
*/
function setValue(bytes32 id, uint256 value) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

/**
* @notice Set value.
*/
function _setValue(bytes32 id, uint256 value) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

/**
* @notice Set value.
*/
function set(bytes32 id, uint256 value) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreSwitch.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

/**
* @notice Set value.
*/
function _set(bytes32 id, uint256 value) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreCore.setStaticField(_tableId, _keyTuple, 0, abi.encodePacked((value)), _fieldLayout);
}

/**
* @notice Delete all data for given keys.
*/
function deleteRecord(bytes32 id) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreSwitch.deleteRecord(_tableId, _keyTuple);
}

/**
* @notice Delete all data for given keys.
*/
function _deleteRecord(bytes32 id) internal {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout);
}

/**
* @notice Tightly pack static (fixed length) data using this table's schema.
* @return The static data, encoded into a sequence of bytes.
*/
function encodeStatic(uint256 value) internal pure returns (bytes memory) {
return abi.encodePacked(value);
}

/**
* @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(uint256 value) internal pure returns (bytes memory, EncodedLengths, bytes memory) {
bytes memory _staticData = encodeStatic(value);

EncodedLengths _encodedLengths;
bytes memory _dynamicData;

return (_staticData, _encodedLengths, _dynamicData);
}

/**
* @notice Encode keys as a bytes32 array using this table's field layout.
*/
function encodeKeyTuple(bytes32 id) internal pure returns (bytes32[] memory) {
bytes32[] memory _keyTuple = new bytes32[](1);
_keyTuple[0] = id;

return _keyTuple;
}
}
2 changes: 2 additions & 0 deletions packages/contracts/src/constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ uint32 constant NUMBER_OF_TANKS = 3;
/// @dev The base rate of material pushed trough the network per block
uint256 constant FLOW_RATE = 10 * ONE_UNIT;
uint256 constant TANK_CAPACITY = 500 * ONE_UNIT;

uint256 constant ORDER_TIMELOCK = 10 minutes;
15 changes: 15 additions & 0 deletions packages/contracts/src/libraries/LibOrderTimelock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { LastFulfilled } from "../codegen/index.sol";
import { ORDER_TIMELOCK } from "../constants.sol";

library LibOrderTimelock {
function checkLock(bytes32 _playerEntity) internal view {
uint256 lastFulfilled = LastFulfilled.get(_playerEntity);
require(lastFulfilled == 0 || block.timestamp >= lastFulfilled + ORDER_TIMELOCK, "order fulfillment timelocked");
}

function updateLock(bytes32 _playerEntity) internal {
LastFulfilled.set(_playerEntity, block.timestamp);
}
}
1 change: 1 addition & 0 deletions packages/contracts/src/libraries/Libraries.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LibNetwork } from "./LibNetwork.sol";
import { LibMachine } from "./LibMachine.sol";
import { LibTank } from "./LibTank.sol";
import { LibOrder } from "./LibOrder.sol";
import { LibOrderTimelock } from "./LibOrderTimelock.sol";
import { LibWipe } from "./LibWipe.sol";
import { LibOffer } from "./LibOffer.sol";
import { LibMaterial, MaterialId } from "./LibMaterial.sol";
Expand Down
6 changes: 5 additions & 1 deletion packages/contracts/src/systems/tank/TankSystem.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { EntityType, CarriedBy, ContainedMaterial, MachineType, Amount, TankConnection, CurrentOrder, Order, OrderData, CompletedOrders, CompletedPlayers, Tutorial, TutorialLevel, NonTransferableBalance, TanksInPod, ProducedMaterials, GameConfig } from "../../codegen/index.sol";
import { ENTITY_TYPE, MACHINE_TYPE } from "../../codegen/common.sol";
import { LibUtils, LibNetwork, LibMaterial, PublicMaterials } from "../../libraries/Libraries.sol";
import { LibUtils, LibOrderTimelock, LibNetwork, LibMaterial, PublicMaterials } from "../../libraries/Libraries.sol";
import { NUMBER_OF_TUTORIAL_LEVELS, TANK_CAPACITY } from "../../constants.sol";

contract TankSystem is System {
Expand Down Expand Up @@ -166,6 +166,10 @@ contract TankSystem is System {
IN MAIN GAME
//////////////////////////////////////////////////////////////*/

// Prevent batching order shipments
LibOrderTimelock.checkLock(playerEntity);
LibOrderTimelock.updateLock(playerEntity);

// Reward player in real tokens
PublicMaterials.BUGS.mint(_msgSender(), currentOrder.reward);

Expand Down
47 changes: 46 additions & 1 deletion packages/contracts/test/systems/orders/OrderSystem.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BaseTest } from "../../BaseTest.sol";
import "../../../src/codegen/index.sol";
import "../../../src/libraries/Libraries.sol";
import { MACHINE_TYPE, PORT_INDEX } from "../../../src/codegen/common.sol";
import { FLOW_RATE, ONE_MINUTE, ONE_HOUR, ONE_UNIT } from "../../../src/constants.sol";
import { FLOW_RATE, ONE_MINUTE, ONE_HOUR, ONE_UNIT, ORDER_TIMELOCK } from "../../../src/constants.sol";

import { ResourceId } from "@latticexyz/store/src/ResourceId.sol";
import { WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol";
Expand Down Expand Up @@ -334,6 +334,51 @@ contract OrderSystemTest is BaseTest {
vm.stopPrank();
}

function testShipAfterTimelock() public {
prankAdmin();
bytes32 order1Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10);
bytes32 order2Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10);
world.devGraduate(alice);
world.devFillTank(tanksInPod[0], 200, PublicMaterials.BUGS);
vm.stopPrank();

vm.startPrank(alice);

world.acceptOrder(order1Entity);

world.shipTank(tanksInPod[0]);

world.acceptOrder(order2Entity);

vm.warp(block.timestamp + ORDER_TIMELOCK);

world.shipTank(tanksInPod[0]);

vm.stopPrank();
}

function testRevertOrderTimelocked() public {
prankAdmin();
bytes32 order1Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10);
bytes32 order2Entity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 10);
world.devGraduate(alice);
world.devFillTank(tanksInPod[0], 200, PublicMaterials.BUGS);
vm.stopPrank();

vm.startPrank(alice);

world.acceptOrder(order1Entity);

world.shipTank(tanksInPod[0]);

world.acceptOrder(order2Entity);

vm.expectRevert("order fulfillment timelocked");
world.shipTank(tanksInPod[0]);

vm.stopPrank();
}

function testRevertAcceptExhaustedOrder() public {
prankAdmin();
bytes32 orderEntity = world.createOrder(PublicMaterials.BUGS, 100, 100, ONE_MINUTE, 1);
Expand Down