From ce06e962c498aca0ff1f808a5a01e2df2acf020c Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Sat, 21 Dec 2024 03:32:59 +1100 Subject: [PATCH] whitelist (#2571) * whitelist * whitelist * fligter * rever * Fix/points (#2575) * fix hs contributions * Fix points on update * merge * Revert "merge" This reverts commit 252f6a0ac4b2bfa6f1202dbfc1bfe0771eff5711. * fix resource arrivals DDOS --------- Co-authored-by: Bob <80072466+bob0005@users.noreply.github.com> Co-authored-by: aymericdelab --- client/src/dojo/debouncedQueries.ts | 16 ++- client/src/dojo/queries.ts | 38 +++++++ client/src/dojo/setup.ts | 1 + .../hooks/helpers/use-resource-arrivals.tsx | 107 +++++++++--------- client/src/hooks/helpers/useGuilds.tsx | 1 + .../worldmap/players/PlayerList.tsx | 7 +- .../worldmap/players/PlayersPanel.tsx | 2 +- client/src/ui/layouts/World.tsx | 22 ++++ .../ui/modules/navigation/TopNavigation.tsx | 4 +- client/src/ui/modules/settings/Settings.tsx | 28 ++++- client/src/ui/modules/social/Social.tsx | 23 +++- .../world-structures/WorldStructuresMenu.tsx | 78 +------------ 12 files changed, 181 insertions(+), 146 deletions(-) diff --git a/client/src/dojo/debouncedQueries.ts b/client/src/dojo/debouncedQueries.ts index 0f256ba09..58558184e 100644 --- a/client/src/dojo/debouncedQueries.ts +++ b/client/src/dojo/debouncedQueries.ts @@ -3,6 +3,7 @@ import { ToriiClient } from "@dojoengine/torii-client"; import debounce from "lodash/debounce"; import { addArrivalsSubscription, + addHyperstructureSubscription, addMarketSubscription, addToSubscription, addToSubscriptionOneKeyModelbyRealmEntityId, @@ -55,7 +56,7 @@ class RequestQueue { const positionQueue = new RequestQueue(); const subscriptionQueue = new RequestQueue(); const marketQueue = new RequestQueue(); - +const hyperstructureQueue = new RequestQueue(); // Debounced functions that add to queues export const debouncedSyncPosition = debounce( async ( @@ -141,9 +142,22 @@ export const debouncedAddMarketSubscription = debounce( { leading: true }, ); +export const debouncedAddHyperstructureSubscription = debounce( + async ( + client: ToriiClient, + components: Component[], + onComplete?: () => void, + ) => { + await hyperstructureQueue.add(() => addHyperstructureSubscription(client, components), onComplete); + }, + 500, + { leading: true }, +); + // Utility function to clear all queues if needed export const clearAllQueues = () => { positionQueue.clear(); subscriptionQueue.clear(); marketQueue.clear(); + hyperstructureQueue.clear(); }; diff --git a/client/src/dojo/queries.ts b/client/src/dojo/queries.ts index 35cb30953..20f2e334d 100644 --- a/client/src/dojo/queries.ts +++ b/client/src/dojo/queries.ts @@ -153,6 +153,44 @@ export const addMarketSubscription = async ( console.log("MarketEnd", end - start); }; +export const addHyperstructureSubscription = async ( + client: ToriiClient, + components: Component[], +) => { + const start = performance.now(); + await getEntities( + client, + { + Composite: { + operator: "Or", + clauses: [ + { + Keys: { + keys: [undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-Epoch", "s0_eternum-Progress"], + }, + }, + { + Keys: { + keys: [undefined, undefined, undefined], + pattern_matching: "FixedLen", + models: ["s0_eternum-Contribution"], + }, + }, + ], + }, + }, + components as any, + [], + [], + 40_000, + false, + ); + const end = performance.now(); + console.log("HyperstructureEnd", end - start); +}; + export const addArrivalsSubscription = async ( client: ToriiClient, components: Component[], diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index 9fc3801b4..25d8fbf31 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -166,6 +166,7 @@ export async function setup(config: DojoConfig & { state: AppStore }) { "s0_eternum-Trade", "s0_eternum-Structure", "s0_eternum-Battle", + "s0_eternum-Guild", ], }, }, diff --git a/client/src/hooks/helpers/use-resource-arrivals.tsx b/client/src/hooks/helpers/use-resource-arrivals.tsx index dfb205ee5..4530c7223 100644 --- a/client/src/hooks/helpers/use-resource-arrivals.tsx +++ b/client/src/hooks/helpers/use-resource-arrivals.tsx @@ -1,20 +1,14 @@ import { ContractAddress, ID, Position } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; -import { - Entity, - Has, - HasValue, - NotValue, - defineQuery, - getComponentValue, - isComponentUpdate, - runQuery, -} from "@dojoengine/recs"; +import { Entity, Has, HasValue, NotValue, defineQuery, getComponentValue, isComponentUpdate } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useDojo } from "../context/DojoContext"; import useNextBlockTimestamp from "../useNextBlockTimestamp"; +const DONKEY_RESOURCE_TRACKER = 452312848583266388373324160190187140051835877600158453279131187530910662656n; +const LORDS_RESOURCE_TRACKER = 7237005577332262213973186563042994240829374041602535252466099000494570602496n; + export type ArrivalInfo = { entityId: ID; recipientEntityId: ID; @@ -23,17 +17,22 @@ export type ArrivalInfo = { isOwner: boolean; hasResources: boolean; isHome: boolean; - // resources: Resource[]; +}; + +const getCurrentDonkeyWeightMinimum = () => { + return Number(localStorage.getItem("WEIGHT_MINIMUM") || 0) * 1000; }; const usePlayerArrivals = () => { const { account: { account }, setup: { - components: { Position, Owner, EntityOwner, OwnedResourcesTracker, ArrivalTime, Weight, Resource, Structure }, + components: { Position, Owner, EntityOwner, OwnedResourcesTracker, ArrivalTime, Weight, Structure }, }, } = useDojo(); + const minWeight = getCurrentDonkeyWeightMinimum(); + const playerStructures = useEntityQuery([ Has(Structure), HasValue(Owner, { address: ContractAddress(account.address) }), @@ -48,68 +47,60 @@ const usePlayerArrivals = () => { const [entitiesWithInventory, setEntitiesWithInventory] = useState([]); - const queryFragments = [ - Has(Weight), - Has(ArrivalTime), - Has(EntityOwner), - NotValue(OwnedResourcesTracker, { resource_types: 0n }), - ]; - - const getArrivalsWithResourceOnPosition = useCallback((positions: Position[]) => { - const arrivals = positions.flatMap((position) => { - return Array.from(runQuery([HasValue(Position, { x: position.x, y: position.y }), ...queryFragments])); - }); - return arrivals; - }, []); + const hasMinWeight = useCallback( + (entity: Entity) => { + const weight = getComponentValue(Weight, entity); + return !!(weight?.value && Number(weight.value) >= minWeight); + }, + [minWeight], + ); const createArrivalInfo = useCallback( (id: Entity): ArrivalInfo | undefined => { + // Get required component values const position = getComponentValue(Position, id); - if (!position) return undefined; - const arrivalTime = getComponentValue(ArrivalTime, id); - if (!arrivalTime) return undefined; - + const ownedResourceTracker = getComponentValue(OwnedResourcesTracker, id); const entityOwner = getComponentValue(EntityOwner, id); - const ownerEntityId = getEntityIdFromKeys([BigInt(entityOwner?.entity_owner_id || 0)]); - const owner = getComponentValue(Owner, ownerEntityId); - if (owner?.address !== ContractAddress(account.address)) { - return undefined; - } + // Return early if missing required components + if (!position || !arrivalTime) return undefined; - const ownedResourceTracker = getComponentValue(OwnedResourcesTracker, id); + // Check if entity has special resource types that don't need weight check + const hasSpecialResources = + ownedResourceTracker?.resource_types === DONKEY_RESOURCE_TRACKER || + ownedResourceTracker?.resource_types === LORDS_RESOURCE_TRACKER; + + // Determine if entity meets weight requirements + const meetsWeightRequirement = hasSpecialResources || hasMinWeight(id); + + // Get owner information + const ownerEntityId = getEntityIdFromKeys([BigInt(entityOwner?.entity_owner_id || 0)]); + const owner = getComponentValue(Owner, ownerEntityId); + const isOwner = owner?.address === ContractAddress(account.address); - const hasResources = !!ownedResourceTracker && ownedResourceTracker.resource_types !== 0n; + // Check if entity has resources + const hasResources = + meetsWeightRequirement && !!ownedResourceTracker && ownedResourceTracker.resource_types !== 0n; + // Find matching player structure at position const playerStructurePosition = playerStructurePositions.find( (structurePosition) => structurePosition.x === position.x && structurePosition.y === position.y, ); - const isHome = !!playerStructurePosition; - return { entityId: position.entity_id, recipientEntityId: playerStructurePosition?.entityId || 0, arrivesAt: arrivalTime.arrives_at, - isOwner: true, + isOwner, position: { x: position.x, y: position.y }, hasResources, - isHome, + isHome: !!playerStructurePosition, }; }, [account, playerStructurePositions], ); - // initial load - useEffect(() => { - const arrivals = getArrivalsWithResourceOnPosition(playerStructurePositions) - .map(createArrivalInfo) - .filter((arrival): arrival is ArrivalInfo => arrival !== undefined) - .filter((arrival) => arrival.hasResources); - setEntitiesWithInventory(arrivals); - }, [playerStructurePositions, getArrivalsWithResourceOnPosition, createArrivalInfo]); - const isMine = useCallback( (entity: Entity) => { const entityOwner = getComponentValue(EntityOwner, entity); @@ -120,12 +111,21 @@ const usePlayerArrivals = () => { ); useEffect(() => { - const query = defineQuery([Has(Position), ...queryFragments], { runOnInit: false }); + const query = defineQuery( + [ + Has(Position), + Has(Weight), + Has(ArrivalTime), + Has(EntityOwner), + NotValue(OwnedResourcesTracker, { resource_types: 0n }), + ], + { runOnInit: false }, + ); const handleArrivalUpdate = (arrivals: ArrivalInfo[], newArrival: ArrivalInfo | undefined) => { if (!newArrival) return arrivals; - if (!newArrival.hasResources || !newArrival.isHome) { + if (!newArrival.hasResources || !newArrival.isHome || !newArrival.isOwner) { return arrivals.filter((arrival) => arrival.entityId !== newArrival.entityId); } @@ -144,10 +144,7 @@ const usePlayerArrivals = () => { isComponentUpdate(update, ArrivalTime) || isComponentUpdate(update, OwnedResourcesTracker) ) { - const isThisMine = isMine(update.entity); - - isThisMine && - setEntitiesWithInventory((arrivals) => handleArrivalUpdate(arrivals, createArrivalInfo(update.entity))); + setEntitiesWithInventory((arrivals) => handleArrivalUpdate(arrivals, createArrivalInfo(update.entity))); } }); diff --git a/client/src/hooks/helpers/useGuilds.tsx b/client/src/hooks/helpers/useGuilds.tsx index 94e1e6ff5..b197add82 100644 --- a/client/src/hooks/helpers/useGuilds.tsx +++ b/client/src/hooks/helpers/useGuilds.tsx @@ -308,5 +308,6 @@ export const useGuilds = () => { usePlayerWhitelist, getGuildFromEntityId, getPlayersInPlayersGuild, + getPlayerListInGuild, }; }; diff --git a/client/src/ui/components/worldmap/players/PlayerList.tsx b/client/src/ui/components/worldmap/players/PlayerList.tsx index 1431ef3d9..b36ef8622 100644 --- a/client/src/ui/components/worldmap/players/PlayerList.tsx +++ b/client/src/ui/components/worldmap/players/PlayerList.tsx @@ -38,11 +38,16 @@ export const PlayerList = ({ sort: "none", }); + const sortedPlayers = useMemo( + () => sortItems(players, activeSort, { sortKey: "rank", sort: "asc" }), + [players, activeSort], + ); + return (
- {sortItems(players, activeSort, { sortKey: "rank", sort: "asc" }).map((player) => ( + {sortedPlayers.map((player) => ( { const term = searchTerm.toLowerCase(); diff --git a/client/src/ui/layouts/World.tsx b/client/src/ui/layouts/World.tsx index 07d4c9eb3..5448f989e 100644 --- a/client/src/ui/layouts/World.tsx +++ b/client/src/ui/layouts/World.tsx @@ -5,6 +5,7 @@ import useUIStore from "../../hooks/store/useUIStore"; import { debounceAddResourceArrivals, + debouncedAddHyperstructureSubscription, debouncedAddMarketSubscription, debouncedAddToSubscription, debouncedAddToSubscriptionOneKey, @@ -229,6 +230,27 @@ export const World = ({ backgroundImage }: { backgroundImage: string }) => { fetch(); }, []); + useEffect(() => { + const fetch = async () => { + try { + setLoading(LoadingStateKey.Hyperstructure, true); + console.log("AddToSubscriptionStart - 4"); + await Promise.all([ + debouncedAddHyperstructureSubscription(dojo.network.toriiClient, dojo.network.contractComponents as any, () => + setLoading(LoadingStateKey.Hyperstructure, false), + ), + ]); + } catch (error) { + console.error("Fetch failed", error); + } finally { + // Ensure loading states are reset even if there's an error + setLoading(LoadingStateKey.Hyperstructure, false); + } + }; + + fetch(); + }, []); + const battleViewContent = useMemo( () => (
diff --git a/client/src/ui/modules/navigation/TopNavigation.tsx b/client/src/ui/modules/navigation/TopNavigation.tsx index b8ebb09ab..551b1137c 100644 --- a/client/src/ui/modules/navigation/TopNavigation.tsx +++ b/client/src/ui/modules/navigation/TopNavigation.tsx @@ -1,5 +1,4 @@ import { useGetAllPlayers } from "@/hooks/helpers/use-get-all-players"; - import { memo } from "react"; import { Rewards } from "../rewards/Rewards"; import { SettingsWindow } from "../settings/Settings"; @@ -7,12 +6,11 @@ import { Social } from "../social/Social"; export const TopMiddleNavigation = memo(() => { const getPlayers = useGetAllPlayers(); - const players = getPlayers(); return ( <>
- +
diff --git a/client/src/ui/modules/settings/Settings.tsx b/client/src/ui/modules/settings/Settings.tsx index 700b8e54d..90f532b42 100644 --- a/client/src/ui/modules/settings/Settings.tsx +++ b/client/src/ui/modules/settings/Settings.tsx @@ -1,8 +1,8 @@ +import { ReactComponent as Controller } from "@/assets/icons/Controller.svg"; import { ReactComponent as Copy } from "@/assets/icons/common/copy.svg"; import { ReactComponent as Next } from "@/assets/icons/common/fast-forward.svg"; import { ReactComponent as Muted } from "@/assets/icons/common/muted.svg"; import { ReactComponent as Unmuted } from "@/assets/icons/common/unmuted.svg"; -import { ReactComponent as Controller } from "@/assets/icons/Controller.svg"; import { ReactComponent as DojoMark } from "@/assets/icons/dojo-mark-full-dark.svg"; import { ReactComponent as RealmsWorld } from "@/assets/icons/rw-logo.svg"; import { useDojo } from "@/hooks/context/DojoContext"; @@ -18,7 +18,7 @@ import Button from "@/ui/elements/Button"; import { Checkbox } from "@/ui/elements/Checkbox"; import { Headline } from "@/ui/elements/Headline"; import { RangeInput } from "@/ui/elements/RangeInput"; -import { addressToNumber, displayAddress } from "@/ui/utils/utils"; +import { addressToNumber, currencyIntlFormat, displayAddress } from "@/ui/utils/utils"; import { ContractAddress } from "@bibliothecadao/eternum"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -42,6 +42,16 @@ export const SettingsWindow = () => { const isSoundOn = useUIStore((state) => state.isSoundOn); const toggleSound = useUIStore((state) => state.toggleSound); + const getCurrentDonkeyWeightMinimum = () => { + return Number(localStorage.getItem("WEIGHT_MINIMUM") || 0); + }; + + const [donkeyWeightLimit, setDonkeyWeightLimit] = useState(getCurrentDonkeyWeightMinimum()); + + useEffect(() => { + localStorage.setItem("WEIGHT_MINIMUM", donkeyWeightLimit.toString()); + }, [donkeyWeightLimit]); + const { toggleFullScreen, isFullScreen } = useScreenOrientation(); const [fullScreen, setFullScreen] = useState(isFullScreen()); @@ -61,7 +71,7 @@ export const SettingsWindow = () => { const isOpen = useUIStore((state) => state.isPopupOpen(settings)); - const GRAPHICS_SETTING = localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings || GraphicsSettings.HIGH; + const GRAPHICS_SETTING = (localStorage.getItem("GRAPHICS_SETTING") as GraphicsSettings) || GraphicsSettings.HIGH; return ( togglePopup(settings)} show={isOpen} title={settings}> @@ -145,6 +155,18 @@ export const SettingsWindow = () => { + + Donkey Settings + + diff --git a/client/src/ui/modules/social/Social.tsx b/client/src/ui/modules/social/Social.tsx index 92178c677..a38abdb77 100644 --- a/client/src/ui/modules/social/Social.tsx +++ b/client/src/ui/modules/social/Social.tsx @@ -1,5 +1,5 @@ import { useDojo } from "@/hooks/context/DojoContext"; -import { useHyperstructureData } from "@/hooks/store/useLeaderBoardStore"; +import { useHyperstructureData, useLeaderBoardStore } from "@/hooks/store/useLeaderBoardStore"; import useUIStore from "@/hooks/store/useUIStore"; import { HintSection } from "@/ui/components/hints/HintModal"; import { social } from "@/ui/components/navigation/Config"; @@ -12,11 +12,11 @@ import { Tabs } from "@/ui/elements/tab"; import { ContractAddress, ID, Player } from "@bibliothecadao/eternum"; import { useEntityQuery } from "@dojoengine/react"; import { Has } from "@dojoengine/recs"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { EndSeasonButton } from "./EndSeasonButton"; import { PlayerId } from "./PlayerId"; -export const Social = ({ players }: { players: Player[] }) => { +export const Social = ({ getPlayers }: { getPlayers: () => Player[] }) => { const { setup: { components: { @@ -26,7 +26,7 @@ export const Social = ({ players }: { players: Player[] }) => { } = useDojo(); const [selectedTab, setSelectedTab] = useState(0); const [isExpanded, setIsExpanded] = useState(false); - + const [isLoading, setIsLoading] = useState(false); const [selectedGuild, setSelectedGuild] = useState(0); const [selectedPlayer, setSelectedPlayer] = useState(0n); @@ -35,8 +35,21 @@ export const Social = ({ players }: { players: Player[] }) => { const gameEnded = useEntityQuery([Has(GameEnded)]); + const [players, setPlayers] = useState(() => getPlayers()); + const playersByRank = useLeaderBoardStore((state) => state.playersByRank); + const updateLeaderboard = useHyperstructureData(); + const handleUpdatePoints = () => { + setIsLoading(true); + updateLeaderboard(); + }; + + useEffect(() => { + setPlayers(getPlayers()); + setIsLoading(false); + }, [playersByRank]); + const viewGuildMembers = (guildEntityId: ID) => { if (selectedGuild === guildEntityId) { setSelectedPlayer(0n); @@ -112,7 +125,7 @@ export const Social = ({ players }: { players: Player[] }) => {
{gameEnded.length === 0 && } -
diff --git a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx index 22e5a2d58..2e55caf1e 100644 --- a/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx +++ b/client/src/ui/modules/world-structures/WorldStructuresMenu.tsx @@ -7,8 +7,6 @@ import { useFragmentMines } from "@/hooks/helpers/useFragmentMines"; import { useGuilds } from "@/hooks/helpers/useGuilds"; import { useHyperstructureProgress, useHyperstructures } from "@/hooks/helpers/useHyperstructures"; import { useResourceBalance } from "@/hooks/helpers/useResources"; -import useUIStore from "@/hooks/store/useUIStore"; -import { LoadingStateKey } from "@/hooks/store/useWorldLoading"; import { FragmentMinePanel } from "@/ui/components/fragmentMines/FragmentMinePanel"; import { HintSection } from "@/ui/components/hints/HintModal"; import { DisplayedAccess, HyperstructurePanel } from "@/ui/components/hyperstructures/HyperstructurePanel"; @@ -20,37 +18,15 @@ import { HintModalButton } from "@/ui/elements/HintModalButton"; import { ResourceIcon } from "@/ui/elements/ResourceIcon"; import { currencyFormat, currencyIntlFormat, divideByPrecision } from "@/ui/utils/utils"; import { BattleSide, ContractAddress, ID, ResourcesIds, findResourceById } from "@bibliothecadao/eternum"; -import { Metadata } from "@dojoengine/recs"; -import { S } from "@dojoengine/recs/dist/types-3444e4c1"; -import { getEntities } from "@dojoengine/state"; -import { ToriiClient } from "@dojoengine/torii-wasm"; import { ArrowRight } from "lucide-react"; -import { Component, useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Tabs } from "../../elements/tab"; export const WorldStructuresMenu = ({ className }: { className?: string }) => { const { account: { account }, - network: { toriiClient, contractComponents }, } = useDojo(); - const hyperstructuresLoaded = useUIStore((state) => state.loadingStates.hyperstructure); - const setLoading = useUIStore((state) => state.setLoading); - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(LoadingStateKey.Hyperstructure, false), - await fetchHyperstructureData(toriiClient, contractComponents as any, hyperstructuresLoaded, () => - setLoading(LoadingStateKey.Hyperstructure, true), - ); - } catch (error) { - console.error("Failed to fetch hyperstructure data:", error); - } - }; - fetchData(); - }, [toriiClient, contractComponents]); - const [selectedTab, setSelectedTab] = useState(0); const [showOnlyMine, setShowOnlyMine] = useState(false); @@ -141,14 +117,6 @@ export const WorldStructuresMenu = ({ className }: { className?: string }) => { [selectedTab, hyperstructures, fragmentMines, showOnlyMine, account.address, myHyperstructures], ); - if (hyperstructuresLoaded) { - return ( -
-
Loading structures...
-
- ); - } - return ( <> @@ -341,47 +309,3 @@ const EntityHeader = ({ entity }: { entity: any }) => {
); }; - -const fetchHyperstructureData = async ( - client: ToriiClient, - components: Component[], - isStructuresLoading: boolean, - onCompleted?: () => void, -) => { - if (!isStructuresLoading) { - return; - } - - console.log("Fetching hyperstructure data"); - await getEntities( - client, - { - Composite: { - operator: "Or", - clauses: [ - { - Keys: { - keys: [undefined, undefined], - pattern_matching: "VariableLen", - models: ["s0_eternum-Contribution"], - }, - }, - { - Keys: { - keys: [undefined, undefined, undefined], - pattern_matching: "VariableLen", - models: ["s0_eternum-Epoch", "s0_eternum-Progress"], - }, - }, - ], - }, - }, - components as any, - [], - [], - 40_000, - false, - ).finally(() => { - onCompleted?.(); - }); -};