diff --git a/client/.env.production b/client/.env.production index d5b551f1c..694ce0a1c 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,5 +1,5 @@ -VITE_PUBLIC_MASTER_ADDRESS= -VITE_PUBLIC_MASTER_PRIVATE_KEY= +VITE_PUBLIC_MASTER_ADDRESS=0x5013b17c43a2b664ec2a38aa45f6d891db1188622ec7cf320411321c3248fb5 +VITE_PUBLIC_MASTER_PRIVATE_KEY=0x5013b17c43a2b664ec2a38aa45f6d891db1188622ec7cf320411321c3248fb5 VITE_PUBLIC_WORLD_ADDRESS="0x5013b17c43a2b664ec2a38aa45f6d891db1188622ec7cf320411321c3248fb5" VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2" VITE_PUBLIC_FEE_TOKEN_ADDRESS=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 diff --git a/client/package.json b/client/package.json index d69af3fb3..30c37b8d0 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "@cartridge/connector": "0.5.5", "@cartridge/controller": "0.5.5", "@dojoengine/core": "1.0.1", + "@dojoengine/torii-wasm": "1.0.1", "@dojoengine/create-burner": "1.0.1", "@dojoengine/react": "1.0.1", "@dojoengine/recs": "^2.0.13", @@ -104,4 +105,4 @@ "vitest": "^2.0.5", "workbox-window": "^7.3.0" } -} \ No newline at end of file +} diff --git a/client/src/dojo/queries.ts b/client/src/dojo/queries.ts new file mode 100644 index 000000000..53d327a86 --- /dev/null +++ b/client/src/dojo/queries.ts @@ -0,0 +1,150 @@ +// onload -> fetch single key entities + +import { Component, Metadata, Schema } from "@dojoengine/recs"; +import { setEntities } from "@dojoengine/state"; +import { Clause, EntityKeysClause, PatternMatching, Subscription, ToriiClient } from "@dojoengine/torii-client"; + +// on hexception -> fetch below queries based on entityID + +// background sync after load -> + +export const getEntities = async ( + client: ToriiClient, + clause: Clause | undefined, + components: Component[], + limit: number = 100, + logging: boolean = false, +) => { + if (logging) console.log("Starting getEntities"); + let offset = 0; + let continueFetching = true; + + while (continueFetching) { + const entities = await client.getEntities({ + limit, + offset, + clause, + dont_include_hashed_keys: false, + }); + + console.log("entities", entities); + + if (logging) console.log(`Fetched ${entities} entities`); + + setEntities(entities, components, logging); + + if (Object.keys(entities).length < limit) { + continueFetching = false; + } else { + offset += limit; + } + } +}; + +export const syncEntitiesEternum = async ( + client: ToriiClient, + components: Component[], + entityKeyClause: EntityKeysClause[], + logging: boolean = true, +) => { + if (logging) console.log("Starting syncEntities"); + return await client.onEntityUpdated(entityKeyClause, (fetchedEntities: any, data: any) => { + if (logging) console.log("Entity updated", fetchedEntities); + + setEntities({ [fetchedEntities]: data }, components, logging); + }); +}; + +export const addToSubscription = async ( + client: ToriiClient, + syncObject: { sync: Subscription; clauses: EntityKeysClause[] }, + components: Component[], + entityID: string, + position?: { x: number; y: number }, +) => { + console.log("position", syncObject); + + await getEntities(client, { ...(entityQueryOneKey(entityID) as Clause) }, components, 1000, false); + + await getEntities(client, { ...(entityQueryTwoKey(entityID) as Clause) }, components, 1000, false); + + await getEntities(client, { ...(entityQueryThreeKey(entityID) as Clause) }, components, 1000, false); + + await getEntities( + client, + { + Keys: { + keys: [String(position?.x || 0), String(position?.y || 0), undefined, undefined], + pattern_matching: "FixedLen", + models: [], + }, + }, + components, + 1000, + false, + ); + const positionClause: EntityKeysClause = { + Keys: { + keys: [String(position?.x || 0), String(position?.y || 0), undefined, undefined], + pattern_matching: "FixedLen" as PatternMatching, + models: [], + }, + }; + + const newSubscriptions = [ + { ...entityQueryOneKey(entityID) }, + { ...entityQueryTwoKey(entityID) }, + { ...entityQueryThreeKey(entityID) }, + { ...entityQueryFourKey(position?.x || 0, position?.y || 0) }, + positionClause, + ...syncObject.clauses, + ]; + + try { + await client.updateEntitySubscription(syncObject.sync, newSubscriptions); + } catch (error) { + console.log("error", error); + } + + syncObject.clauses = newSubscriptions; +}; + +const entityQueryOneKey = (entityID: string) => { + return { + Keys: { + keys: [entityID], + pattern_matching: "FixedLen", + models: [], + }, + } as EntityKeysClause; +}; + +const entityQueryTwoKey = (entityID: string) => { + return { + Keys: { + keys: [entityID, undefined], + pattern_matching: "FixedLen", + models: [], + }, + } as EntityKeysClause; +}; + +const entityQueryThreeKey = (entityID: string) => { + return { + Keys: { + keys: [entityID, undefined, undefined], + pattern_matching: "FixedLen", + models: [], + }, + } as EntityKeysClause; +}; + +const entityQueryFourKey = (x: number, y: number) => { + return { + Keys: { + keys: [String(x), String(y), undefined, undefined], + pattern_matching: "FixedLen", + models: [], + }, + } as EntityKeysClause; +}; diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index 1143c47f4..7d0e3645b 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -1,5 +1,7 @@ +import { WORLD_CONFIG_ID } from "@bibliothecadao/eternum"; import { DojoConfig } from "@dojoengine/core"; import { getSyncEntities, getSyncEvents } from "@dojoengine/state"; +import { Clause } from "@dojoengine/torii-client"; import { createClientComponents } from "./createClientComponents"; import { createSystemCalls } from "./createSystemCalls"; import { ClientConfigManager } from "./modelManager/ConfigManager"; @@ -14,8 +16,148 @@ export async function setup({ ...config }: DojoConfig) { const components = createClientComponents(network); const systemCalls = createSystemCalls(network); + const singleKeyQuery = { + Keys: { + keys: [undefined], + pattern_matching: "FixedLen", + models: [ + "s0_eternum-AddressName", + "s0_eternum-Army", + "s0_eternum-ArrivalTime", + "s0_eternum-Bank", + "s0_eternum-BankConfig", + "s0_eternum-Battle", + "s0_eternum-BattleConfig", + "s0_eternum-Building", + "s0_eternum-BuildingCategoryPopConfig", + "s0_eternum-BuildingConfig", + "s0_eternum-BuildingGeneralConfig", + "s0_eternum-BuildingQuantityv2", + "s0_eternum-CapacityCategory", + "s0_eternum-CapacityConfig", + "s0_eternum-Contribution", + "s0_eternum-DetachedResource", + "s0_eternum-EntityName", + "s0_eternum-EntityOwner", + "s0_eternum-Epoch", + "s0_eternum-Guild", + "s0_eternum-GuildMember", + "s0_eternum-GuildWhitelist", + "s0_eternum-Health", + "s0_eternum-Hyperstructure", + "s0_eternum-HyperstructureConfig", + "s0_eternum-HyperstructureResourceConfig", + "s0_eternum-Leaderboard", + "s0_eternum-LeaderboardEntry", + "s0_eternum-LeaderboardRegistered", + "s0_eternum-LeaderboardRewardClaimed", + "s0_eternum-LevelingConfig", + "s0_eternum-Liquidity", + "s0_eternum-MapConfig", + + "s0_eternum-MercenariesConfig", + "s0_eternum-Message", + "s0_eternum-Movable", + "s0_eternum-Orders", + "s0_eternum-OwnedResourcesTracker", + "s0_eternum-Owner", + "s0_eternum-Population", + "s0_eternum-PopulationConfig", + "s0_eternum-Position", + "s0_eternum-ProductionConfig", + "s0_eternum-ProductionDeadline", + "s0_eternum-ProductionInput", + "s0_eternum-ProductionOutput", + + "s0_eternum-Protectee", + "s0_eternum-Protector", + "s0_eternum-Quantity", + "s0_eternum-QuantityTracker", + + "s0_eternum-QuestConfig", + "s0_eternum-QuestRewardConfig", + // "s0_eternum-Realm", + ], + }, + }; + + const twoKeyQuery = { + Keys: { + keys: ["763", undefined], + pattern_matching: "FixedLen", + models: [ + // 's0_eternum-Tile', - done + // 's0_eternum-BuildingQuantityv2', - done with Realm + // // 's0_eternum-Resource', - done with Realm + // // 's0_eternum-Production', - done with Realm + // 's0_eternum-GuildWhitelist', - add to subscription from somewhere + // 's0_eternum-Progress', - done + // 's0_eternum-HyperstructureContribution', - done + // 's0_eternum-Epoch', - done + // // 's0_eternum-QuestBonus', - done + // // "s0_eternum-Progress", - done + // // "s0_eternum-Market", + // 's0_eternum-Quest', + // 's0_eternum-Position', + ], + }, + }; + + const threeKeyQuery = { + Keys: { + keys: [undefined, undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-BuildingConfig", "s0_eternum-Liquidity"], + }, + }; + + const fourKeyQuery = { + Keys: { + keys: [undefined, undefined, undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-Building"], + }, + }; + + const clauses: Clause[] = [ + { + Keys: { + keys: [undefined], + pattern_matching: "FixedLen", + models: [], + }, + }, + { + Keys: { + keys: [WORLD_CONFIG_ID.toString(), undefined], + pattern_matching: "VariableLen", + models: [], + }, + }, + { + Keys: { + keys: [WORLD_CONFIG_ID.toString()], + pattern_matching: "VariableLen", + models: [], + }, + }, + ]; + // fetch all existing entities from torii - const sync = await getSyncEntities(network.toriiClient, network.contractComponents as any, undefined, [], 20_000); + const sync = await getSyncEntities( + network.toriiClient, + network.contractComponents as any, + { Composite: { operator: "Or", clauses: [...clauses] } }, + [], + 5000, + true, + ); + + const syncObject = { + sync, + clauses: [...clauses], + }; + const eventSync = getSyncEvents( network.toriiClient, network.contractComponents.events as any, @@ -32,7 +174,7 @@ export async function setup({ ...config }: DojoConfig) { network, components, systemCalls, - sync, + syncObject, eventSync, }; } diff --git a/client/src/hooks/store/useWorldLoading.tsx b/client/src/hooks/store/useWorldLoading.tsx new file mode 100644 index 000000000..52c654754 --- /dev/null +++ b/client/src/hooks/store/useWorldLoading.tsx @@ -0,0 +1,12 @@ +// World loading state +import { create } from "zustand"; + +interface WorldState { + isWorldLoading: boolean; + setWorldLoading: (loading: boolean) => void; +} + +export const useWorldStore = create((set) => ({ + isWorldLoading: true, + setWorldLoading: (loading: boolean) => set({ isWorldLoading: loading }), +})); \ No newline at end of file diff --git a/client/src/three/scenes/HexagonScene.ts b/client/src/three/scenes/HexagonScene.ts index a7529871e..ce6c45bc6 100644 --- a/client/src/three/scenes/HexagonScene.ts +++ b/client/src/three/scenes/HexagonScene.ts @@ -258,6 +258,8 @@ export abstract class HexagonScene { const { row, col } = this.getHexFromWorldPosition(position); + console.log("row", row, col); + return { row, col, x: position.x, z: position.z }; } diff --git a/client/src/three/scenes/Worldmap.ts b/client/src/three/scenes/Worldmap.ts index b1718bfbc..5b5cd5c12 100644 --- a/client/src/three/scenes/Worldmap.ts +++ b/client/src/three/scenes/Worldmap.ts @@ -1,6 +1,3 @@ -import * as THREE from "three"; -import { Raycaster } from "three"; - import { ArmyMovementManager, TravelPaths } from "@/dojo/modelManager/ArmyMovementManager"; import { TileManager } from "@/dojo/modelManager/TileManager"; import { SetupResult } from "@/dojo/setup"; @@ -13,7 +10,11 @@ import { UNDEFINED_STRUCTURE_ENTITY_ID } from "@/ui/constants"; import { LeftView } from "@/ui/modules/navigation/LeftNavigationModule"; import { getWorldPositionForHex } from "@/ui/utils/utils"; import { BiomeType, getNeighborOffsets, ID } from "@bibliothecadao/eternum"; +import { getSyncEntities } from "@dojoengine/state"; +import * as torii from "@dojoengine/torii-client"; import throttle from "lodash/throttle"; +import * as THREE from "three"; +import { Raycaster } from "three"; import { MapControls } from "three/examples/jsm/controls/MapControls"; import { SceneManager } from "../SceneManager"; import { ArmyManager } from "../components/ArmyManager"; @@ -41,6 +42,8 @@ export default class WorldmapScene extends HexagonScene { private currentChunk: string = "null"; + private subscription: torii.Subscription | null = null; + private armyManager: ArmyManager; private structureManager: StructureManager; private battleManager: BattleManager; @@ -56,6 +59,8 @@ export default class WorldmapScene extends HexagonScene { private cachedMatrices: Map> = new Map(); + dojo: SetupResult; + constructor( dojoContext: SetupResult, raycaster: Raycaster, @@ -65,6 +70,8 @@ export default class WorldmapScene extends HexagonScene { ) { super(SceneName.WorldMap, controls, dojoContext, mouse, raycaster, sceneManager); + this.dojo = dojoContext; + this.GUIFolder.add(this, "moveCameraToURLLocation"); this.biome = new Biome(); @@ -503,6 +510,8 @@ export default class WorldmapScene extends HexagonScene { const globalRow = startRow + row; const globalCol = startCol + col; + + const isExplored = this.exploredTiles.get(globalCol)?.has(globalRow) || false; if (!isExplored) { @@ -565,9 +574,13 @@ export default class WorldmapScene extends HexagonScene { const hexPositions: THREE.Vector3[] = []; const batchSize = 25; // Adjust batch size as needed let currentIndex = 0; + let hashedTiles: string[] = []; - const processBatch = () => { + const processBatch = async () => { const endIndex = Math.min(currentIndex + batchSize, rows * cols); + + + for (let i = currentIndex; i < endIndex; i++) { const row = Math.floor(i / cols) - rows / 2; const col = (i % cols) - cols / 2; @@ -575,6 +588,10 @@ export default class WorldmapScene extends HexagonScene { const globalRow = startRow + row; const globalCol = startCol + col; + const hashedTile = torii.poseidonHash([(startCol + col + FELT_CENTER).toString(), (startRow + row + FELT_CENTER).toString()]); + + hashedTiles.push(hashedTile); + hexPositions.push(new THREE.Vector3(dummy.position.x, dummy.position.y, dummy.position.z)); const pos = getWorldPositionForHex({ row: globalRow, col: globalCol }); dummy.position.copy(pos); @@ -622,6 +639,7 @@ export default class WorldmapScene extends HexagonScene { } } + currentIndex = endIndex; if (currentIndex < rows * cols) { requestAnimationFrame(processBatch); @@ -640,9 +658,24 @@ export default class WorldmapScene extends HexagonScene { Promise.all(this.modelLoadPromises).then(() => { requestAnimationFrame(processBatch); + this.computeTileEntities(hashedTiles); }); } + private async computeTileEntities(hashedTiles: string[]) { + if (this.subscription) this.subscription.cancel(); + + const sub = await getSyncEntities(this.dojo.network.toriiClient, this.dojo.network.contractComponents as any, undefined, [ + { + HashedKeys: hashedTiles + }, + ]); + + console.log("entities", sub); + this.subscription = sub; + } + + private getExploredHexesForCurrentChunk() { const chunkKey = this.currentChunk.split(","); const startRow = parseInt(chunkKey[0]); diff --git a/client/src/ui/components/resources/EntityResourceTable.tsx b/client/src/ui/components/resources/EntityResourceTable.tsx index 37a04668b..4a713a625 100644 --- a/client/src/ui/components/resources/EntityResourceTable.tsx +++ b/client/src/ui/components/resources/EntityResourceTable.tsx @@ -23,6 +23,7 @@ export const EntityResourceTable = ({ entityId }: { entityId: ID | undefined }) return multiplyByPrecision(quantity * storehouseCapacityKg + storehouseCapacityKg); }, [quantity, entityId]); + if (!entityId || entityId === 0) { return
No Entity Selected
; } diff --git a/client/src/ui/components/trading/MarketModal.tsx b/client/src/ui/components/trading/MarketModal.tsx index e6001befc..d388d544c 100644 --- a/client/src/ui/components/trading/MarketModal.tsx +++ b/client/src/ui/components/trading/MarketModal.tsx @@ -26,9 +26,9 @@ import { currencyFormat, getEntityIdFromKeys } from "@/ui/utils/utils"; import { ID, ResourcesIds } from "@bibliothecadao/eternum"; import { useComponentValue } from "@dojoengine/react"; import { Suspense, lazy, useMemo, useState } from "react"; +import { ModalContainer } from "../ModalContainer"; import { HintModal } from "../hints/HintModal"; import { TroopDisplay } from "../military/TroopChip"; -import { ModalContainer } from "../ModalContainer"; const MarketResourceSidebar = lazy(() => import("./MarketResourceSideBar").then((module) => ({ default: module.MarketResourceSidebar })), diff --git a/client/src/ui/layouts/World.tsx b/client/src/ui/layouts/World.tsx index 5b5bc256c..dcf279a95 100644 --- a/client/src/ui/layouts/World.tsx +++ b/client/src/ui/layouts/World.tsx @@ -1,14 +1,20 @@ import { Leva } from "leva"; -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { Redirect } from "wouter"; import useUIStore from "../../hooks/store/useUIStore"; +import { addToSubscription } from "@/dojo/queries"; +import { useDojo } from "@/hooks/context/DojoContext"; import { useStructureEntityId } from "@/hooks/helpers/useStructureEntityId"; import { useFetchBlockchainData } from "@/hooks/store/useBlockchainStore"; +import { useWorldStore } from "@/hooks/store/useWorldLoading"; +import { useComponentValue } from "@dojoengine/react"; +import { EntityKeysClause, Subscription } from "@dojoengine/torii-client"; +import { getEntityIdFromKeys } from "@dojoengine/utils"; import { env } from "../../../env"; import { IS_MOBILE } from "../config"; -import { LoadingScreen } from "../modules/LoadingScreen"; import { LoadingOroborus } from "../modules/loading-oroborus"; +import { LoadingScreen } from "../modules/LoadingScreen"; // Lazy load components const SelectedArmy = lazy(() => @@ -91,6 +97,31 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { useFetchBlockchainData(); useStructureEntityId(); + // We could optimise this deeper.... + + const worldLoading = useWorldStore((state) => state.isWorldLoading); + const setWorldLoading = useWorldStore((state) => state.setWorldLoading); + + const dojo = useDojo(); + const structureEntityId = useUIStore((state) => state.structureEntityId); + const position = useComponentValue(dojo.setup.components.Position, getEntityIdFromKeys([BigInt(structureEntityId)])); + + useEffect(() => { + setWorldLoading(true); + const fetch = async () => { + await addToSubscription( + dojo.setup.network.toriiClient, + dojo.setup.syncObject as { sync: Subscription; clauses: EntityKeysClause[] }, + dojo.setup.network.contractComponents as any, + structureEntityId.toString(), + { x: position?.x || 0, y: position?.y || 0 }, + ); + }; + fetch(); + console.log("world loading", worldLoading); + setWorldLoading(false); + }, [structureEntityId]); + return (
{ diff --git a/client/src/ui/modules/navigation/LeftNavigationModule.tsx b/client/src/ui/modules/navigation/LeftNavigationModule.tsx index 0fd85355e..6a3282aa8 100644 --- a/client/src/ui/modules/navigation/LeftNavigationModule.tsx +++ b/client/src/ui/modules/navigation/LeftNavigationModule.tsx @@ -217,6 +217,10 @@ export const LeftNavigationModule = memo(() => { visible: { x: "0%", transition: { duration: 0.5 } }, }; + + + + return (
diff --git a/client/src/ui/modules/navigation/RightNavigationModule.tsx b/client/src/ui/modules/navigation/RightNavigationModule.tsx index 6998d3403..4532fe384 100644 --- a/client/src/ui/modules/navigation/RightNavigationModule.tsx +++ b/client/src/ui/modules/navigation/RightNavigationModule.tsx @@ -21,6 +21,8 @@ export const RightNavigationModule = () => { const view = useUIStore((state) => state.rightNavigationView); const setView = useUIStore((state) => state.setRightNavigationView); + + const navigation = useMemo( () => [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 033831bc9..0c237c5ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ importers: '@dojoengine/torii-client': specifier: 1.0.1 version: 1.0.1 + '@dojoengine/torii-wasm': + specifier: 1.0.1 + version: 1.0.1 '@dojoengine/utils': specifier: 1.0.1 version: 1.0.1(starknet@6.11.0(encoding@0.1.13))(typescript@5.6.3)(zod@3.23.8)