Repository: BibliothecaDAO/eternum
Pull request #3294: fix
Author: login: ponderingdemocritus
Created: 2025-05-19T03:58:15Z | Merged: 2025-05-19T04:07:22Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Added a new script to run the game app in mainnet mode.
	- Introduced new styles and animations for buttons, scrolling, gradients, and tooltips.
	- Enhanced event data with decoded names and unique identifiers.

- **Improvements**
	- Switched data fetching in army and structure detail views to use react-query for better performance and reliability.
	- Increased the event stream size limit to handle more concurrent events.
	- Improved event stream with deduplication and refined UI layout.
	- Updated an environment variable to use a new API endpoint.
	- Added new dependencies for data fetching and SDK integration.

- **Bug Fixes**
	- Improved safety in user and room filtering and chat features to prevent errors when data is missing or incomplete.

- **Style**
	- Minor UI spacing adjustment in the navigation menu.

- **Chores**
	- Added console logs for debugging in chat user lookup.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts b/client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts
index 6366fca166b..4d5b5ed0a96 100644
--- a/client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts
+++ b/client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts
@@ -43,5 +43,5 @@ export const filterRoomsBySearch = (rooms: Room[], searchText: string): Room[] =
 export const filterUsersBySearch = (users: User[], searchText: string): User[] => {
   if (!searchText.trim()) return users;
 
-  return users.filter((user) => (user.username || user.id).toLowerCase().includes(searchText.toLowerCase()));
+  return users?.filter((user) => (user?.username || user?.id).toLowerCase().includes(searchText.toLowerCase()));
 };
diff --git a/client/apps/game/.env.mainnet b/client/apps/game/.env.mainnet
index ba8941dcd13..ac760278b38 100644
--- a/client/apps/game/.env.mainnet
+++ b/client/apps/game/.env.mainnet
@@ -3,7 +3,7 @@ VITE_PUBLIC_MASTER_PRIVATE_KEY=0x5013b17c43a2b664ec2a38aa45f6d891db1188622ec7cf3
 VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2"
 VITE_PUBLIC_FEE_TOKEN_ADDRESS=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
 
-VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-2/torii
+VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-3/torii
 VITE_PUBLIC_NODE_URL=https://api.cartridge.gg/x/starknet/mainnet
 VITE_PUBLIC_GAME_VERSION="v1.0.0"
 VITE_PUBLIC_GRAPHICS_DEV=false
diff --git a/client/apps/game/package.json b/client/apps/game/package.json
index ee311689cd3..906be2d823c 100644
--- a/client/apps/game/package.json
+++ b/client/apps/game/package.json
@@ -33,15 +33,16 @@
     "@solana/web3.js": "catalog:",
     "@starknet-react/chains": "catalog:",
     "@starknet-react/core": "catalog:",
+    "@tanstack/react-query": "^5.76.0",
     "@vercel/analytics": "^1.2.2",
     "buffer": "^6.0.3",
     "graphql-request": "^7.1.2",
     "gsap": "^3.12.5",
     "lil-gui": "^0.19.2",
     "lodash": "^4.17.21",
+    "metagame-sdk": "0.0.19",
     "open": "catalog:",
     "platform": "^1.3.6",
-    "metagame-sdk": "0.0.19",
     "postprocessing": "^6.36.2",
     "react-beautiful-dnd": "^13.1.1",
     "react-draggable": "^4.4.6",
diff --git a/client/apps/game/src/index.css b/client/apps/game/src/index.css
index 5074b8497d4..78433903210 100644
--- a/client/apps/game/src/index.css
+++ b/client/apps/game/src/index.css
@@ -2,6 +2,148 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
+
+@layer components {
+  .btn {
+    @apply px-4 py-2 rounded-md font-bold text-white transition-all duration-200 ease-in-out;
+  }
+  .btn-primary {
+    @apply bg-blue-500 hover:bg-blue-600;
+  }
+  .btn-secondary {
+    @apply bg-gray-500 hover:bg-gray-600;
+  }
+}
+
+@keyframes scroll {
+  0% {
+    transform: translateX(100%);
+  }
+  100% {
+    transform: translateX(-100%);
+  }
+}
+
+.animate-scroll {
+  animation: scroll 30s linear infinite;
+}
+
+/* Add any other global styles or overrides here */
+body {
+  overscroll-behavior: none;
+  /* Additional global styles */
+}
+
+/* width */
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+  @apply bg-brown/10;
+  /* background: #f1f1f1; */
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+  @apply bg-gold/30;
+  border-radius: 10px;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+  @apply bg-gold/50;
+}
+
+.clip-angled {
+  clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 20px 100%, 0 calc(100% - 20px));
+}
+
+.clip-angled-tlb {
+  clip-path: polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px);
+}
+
+.clip-angled-br {
+  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%);
+}
+
+.clip-angled-tr {
+  clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);
+}
+
+.clip-angled-bl {
+  clip-path: polygon(0 0, 100% 0, 100% 100%, 20px 100%, 0 calc(100% - 20px));
+}
+
+.gold-gradient-text {
+  background: linear-gradient(172deg, #fef08a 0%, #d4af37 100%);
+  background-clip: text;
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+.tooltip-text-color {
+  color: #fef08a !important;
+}
+
+.tooltip-arrow-color {
+  border-color: #fef08a !important;
+}
+
+.select-item-blue {
+  background: linear-gradient(90deg, #0000ff 0%, #0000aa 100%);
+}
+
+.move-button-animation {
+  animation: moveButton 0.5s infinite alternate;
+}
+
+@keyframes moveButton {
+  from {
+    transform: translateY(-2px);
+  }
+  to {
+    transform: translateY(2px);
+  }
+}
+
+.loading-animation {
+  filter: drop-shadow(0 0 0.75rem crimson);
+  animation: loadingAnimation 1s infinite alternate;
+}
+
+@keyframes loadingAnimation {
+  from {
+    filter: drop-shadow(0 0 0.2rem #fde047);
+  }
+  to {
+    filter: drop-shadow(0 0 1rem #fde047);
+  }
+}
+
+.bordered-text {
+  -webkit-text-stroke: 1.5px black; /* For WebKit browsers */
+  text-stroke: 1.5px black; /* Standard property */
+}
+
+.highlight-central-cell {
+  animation: highlightPulse 2s infinite;
+}
+
+@keyframes highlightPulse {
+  0% {
+    box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.7); /* Gold color with some transparency */
+  }
+  70% {
+    box-shadow: 0 0 20px 10px rgba(255, 215, 0, 0); /* Expand shadow and fade out */
+  }
+  100% {
+    box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); /* Reset shadow */
+  }
+}
+
 html,
 body {
   overflow: hidden;
diff --git a/client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx b/client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
index b5d3ed6d18c..85298bac4b9 100644
--- a/client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
+++ b/client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
@@ -6,10 +6,10 @@ import { getBlockTimestamp } from "@/utils/timestamp";
 import { getAddressName, getGuildFromPlayerAddress, StaminaManager } from "@bibliothecadao/eternum";
 import { useDojo } from "@bibliothecadao/react";
 import { getExplorerFromToriiClient, getStructureFromToriiClient } from "@bibliothecadao/torii-client";
-import { ClientComponents, ContractAddress, GuildInfo, ID, TroopTier, TroopType } from "@bibliothecadao/types";
-import { ComponentValue } from "@dojoengine/recs";
-import { MessageCircle, Trash2 } from "lucide-react";
-import { memo, useEffect, useState } from "react";
+import { ContractAddress, ID, TroopTier, TroopType } from "@bibliothecadao/types";
+import { useQuery } from "@tanstack/react-query";
+import { Loader, MessageCircle, Trash2 } from "lucide-react";
+import { memo, useMemo, useState } from "react";
 import { TroopChip } from "../../military/troop-chip";
 import { InventoryResources } from "../../resources/inventory-resources";
 import { ArmyWarning } from "../armies/army-warning";
@@ -32,26 +32,71 @@ export const ArmyEntityDetail = memo(
       },
     } = useDojo();
 
-    const [explorer, setExplorer] = useState<ComponentValue<ClientComponents["ExplorerTroops"]["schema"]> | undefined>(
-      undefined,
-    );
-    const [explorerResources, setExplorerResources] = useState<
-      ComponentValue<ClientComponents["Resource"]["schema"]> | undefined
-    >(undefined);
-    const [structureResources, setStructureResources] = useState<
-      ComponentValue<ClientComponents["Resource"]["schema"]> | undefined
-    >(undefined);
-    const [structure, setStructure] = useState<
-      ComponentValue<ClientComponents["Structure"]["schema"]> | null | undefined
-    >(undefined);
     const userAddress = ContractAddress(account.address);
-    const [addressName, setAddressName] = useState<string | undefined>(undefined);
-    const [playerGuild, setPlayerGuild] = useState<GuildInfo | undefined>(undefined);
-    const [isAlly, setIsAlly] = useState(false);
-    const [stamina, setStamina] = useState<{ amount: bigint; updated_tick: bigint } | undefined>(undefined);
-    const [maxStamina, setMaxStamina] = useState<number | undefined>(undefined);
     const [isLoadingDelete, setIsLoadingDelete] = useState(false);
 
+    const {
+      data: explorerData,
+      isLoading: isLoadingExplorer,
+      // error: explorerError, // Available if error handling is needed
+    } = useQuery({
+      queryKey: ["explorer", String(armyEntityId)],
+      queryFn: async () => {
+        if (!toriiClient || !armyEntityId) return undefined;
+        return getExplorerFromToriiClient(toriiClient, armyEntityId);
+      },
+      staleTime: 30000, // 30 seconds
+    });
+
+    const explorer = explorerData?.explorer;
+    const explorerResources = explorerData?.resources;
+
+    const {
+      data: structureData,
+      isLoading: isLoadingStructure,
+      // error: structureError, // Available if error handling is needed
+    } = useQuery({
+      queryKey: ["structure", explorer?.owner ? String(explorer.owner) : undefined],
+      queryFn: async () => {
+        if (!toriiClient || !explorer?.owner) return undefined;
+        return getStructureFromToriiClient(toriiClient, explorer.owner);
+      },
+      enabled: !!explorer?.owner,
+      staleTime: 30000, // 30 seconds
+    });
+
+    const structure = structureData?.structure;
+    const structureResources = structureData?.resources;
+
+    const derivedData = useMemo(() => {
+      if (!explorer) return undefined;
+
+      const { currentArmiesTick } = getBlockTimestamp();
+      const stamina = StaminaManager.getStamina(explorer.troops, currentArmiesTick);
+      const maxStamina = StaminaManager.getMaxStamina(
+        explorer.troops.category as TroopType,
+        explorer.troops.tier as TroopTier,
+      );
+
+      const guild = structure ? getGuildFromPlayerAddress(ContractAddress(structure.owner), components) : undefined;
+      const userGuild = getGuildFromPlayerAddress(userAddress, components);
+      const isMine = structure?.owner === userAddress;
+      const isAlly = isMine || (guild && userGuild && guild.entityId === userGuild.entityId) || false;
+
+      const addressName = structure?.owner
+        ? getAddressName(structure?.owner, components)
+        : getCharacterName(explorer.troops.tier as TroopTier, explorer.troops.category as TroopType, armyEntityId);
+
+      return {
+        stamina,
+        maxStamina,
+        playerGuild: guild,
+        isAlly,
+        addressName,
+        isMine,
+      };
+    }, [explorer, structure, components, userAddress, armyEntityId]);
+
     const handleDeleteExplorer = async () => {
       setIsLoadingDelete(true);
       try {
@@ -66,56 +111,13 @@ export const ArmyEntityDetail = memo(
       }
     };
 
-    useEffect(() => {
-      const fetch = async () => {
-        const { explorer, resources: explorerResources } = await getExplorerFromToriiClient(toriiClient, armyEntityId);
-        if (!explorer) {
-          return {
-            explorer: undefined,
-            explorerResources: undefined,
-            structure: undefined,
-            structureResources: undefined,
-          };
-        }
-        const { structure, resources: structureResources } = await getStructureFromToriiClient(
-          toriiClient,
-          explorer.owner,
-        );
-
-        const { currentArmiesTick } = getBlockTimestamp();
-        const stamina = StaminaManager.getStamina(explorer.troops, currentArmiesTick);
-        const maxStamina = StaminaManager.getMaxStamina(
-          explorer.troops.category as TroopType,
-          explorer.troops.tier as TroopTier,
-        );
-
-        const guild = structure ? getGuildFromPlayerAddress(ContractAddress(structure.owner), components) : undefined;
-        const userGuild = getGuildFromPlayerAddress(userAddress, components);
-        const isAlly = isMine || (guild && userGuild && guild.entityId === userGuild.entityId) || false;
-        setIsAlly(isAlly);
-        setAddressName(
-          structure?.owner
-            ? getAddressName(structure?.owner, components)
-            : getCharacterName(explorer.troops.tier as TroopTier, explorer.troops.category as TroopType, armyEntityId),
-        );
-        setStamina(stamina);
-        setMaxStamina(maxStamina);
-        setPlayerGuild(guild);
-        setExplorer(explorer);
-        setExplorerResources(explorerResources);
-        setStructure(structure);
-        setStructureResources(structureResources);
-      };
-      fetch();
-    }, [armyEntityId]);
-
     const { openChat, selectDirectMessageRecipient, getUserIdByUsername } = useChatStore((state) => state.actions);
 
     const handleChatClick = () => {
-      if (isMine) {
+      if (derivedData?.isMine) {
         openChat();
       } else {
-        const userId = getUserIdByUsername(addressName || "");
+        const userId = getUserIdByUsername(derivedData?.addressName || "");
 
         if (userId) {
           selectDirectMessageRecipient(userId);
@@ -123,36 +125,18 @@ export const ArmyEntityDetail = memo(
       }
     };
 
-    useEffect(() => {
-      const fetchStructure = async () => {
-        if (!explorer) return;
-        const result = await getStructureFromToriiClient(toriiClient, explorer.owner);
-
-        // If a structure object is returned, use it; otherwise explicitly mark as null (fetched, but none found)
-        if (result.structure !== undefined) {
-          // Found (may still be undefined if Torii entity did not include Structure model)
-          if (result.structure) {
-            setStructure(result.structure);
-          } else {
-            setStructure(null);
-          }
-          // Always set resources even if undefined to stop further fetching attempts
-          setStructureResources(result.resources);
-        } else {
-          // In unexpected scenario (shouldn't happen), mark as null to avoid indefinite loading
-          setStructure(null);
-        }
-      };
-      fetchStructure();
-    }, [explorer]);
-
-    const isMine = structure?.owner === userAddress;
-
-    // Precompute common class strings
     const headerTextClass = compact ? "text-base" : "text-lg";
     const smallTextClass = compact ? "text-xxs" : "text-xs";
 
-    if (!explorer) return null;
+    if (isLoadingExplorer || (explorer?.owner && isLoadingStructure)) {
+      return (
+        <div className="flex items-center justify-center h-full mt-2 ">
+          <Loader className="animate-spin" />
+        </div>
+      );
+    }
+
+    if (!explorer || !derivedData) return null;
 
     return (
       <div className={`flex flex-col ${compact ? "gap-1" : "gap-2"} ${className}`}>
@@ -160,24 +144,26 @@ export const ArmyEntityDetail = memo(
         <div className={`flex items-center justify-between border-b border-gold/30 ${compact ? "pb-1" : "pb-2"} gap-2`}>
           <div className="flex flex-col">
             <h4 className={`${headerTextClass} font-bold`}>
-              {addressName} <span className="text-xs text-gold/80">({armyEntityId})</span>
+              {derivedData.addressName} <span className="text-xs text-gold/80">({armyEntityId})</span>
             </h4>
-            {playerGuild && (
+            {derivedData.playerGuild && (
               <div className="text-xs text-gold/80">
                 {"< "}
-                {playerGuild.name}
+                {derivedData.playerGuild.name}
                 {" >"}
               </div>
             )}
           </div>
           <div className="flex items-center gap-2">
-            <div className={`px-2 py-1 rounded text-xs font-bold ${isAlly ? "bg-green/20" : "bg-red/20"}`}>
-              {isAlly ? "Ally" : "Enemy"}
+            <div className={`px-2 py-1 rounded text-xs font-bold ${derivedData.isAlly ? "bg-green/20" : "bg-red/20"}`}>
+              {derivedData.isAlly ? "Ally" : "Enemy"}
             </div>
-            <button onClick={handleChatClick} className="p-1 rounded hover:bg-gold/10 transition" title="Chat">
-              <MessageCircle />
-            </button>
-            {isMine && (
+            {derivedData.addressName !== undefined && (
+              <button onClick={handleChatClick} className="p-1 rounded hover:bg-gold/10 transition" title="Chat">
+                <MessageCircle />
+              </button>
+            )}
+            {derivedData.isMine && (
               <button
                 onClick={handleDeleteExplorer}
                 className={`p-1 rounded bg-red-600/80 hover:bg-red-700 transition text-white flex items-center ${
@@ -199,26 +185,6 @@ export const ArmyEntityDetail = memo(
 
         <div className="flex flex-col gap-2 w-full">
           <div className="flex flex-col w-full gap-2">
-            {/* Army name - made more prominent */}
-            {/* {explorer && (
-              <div className="flex flex-col gap-0.5">
-                <div className="bg-gold/10 rounded-sm px-2 py-0.5 border-l-4 border-gold">
-                  <h6 className={`${compact ? "text-base" : "text-lg"} font-bold truncate`}>
-                    {getCharacterName(
-                      explorer.troops.tier as TroopTier,
-                      explorer.troops.category as TroopType,
-                      armyEntityId,
-                    )}
-                  </h6>
-                </div>
-                <div
-                  className={`${compact ? "text-xs" : "text-sm"} font-semibold text-gold/90 uppercase tracking-wide`}
-                >
-                  Army
-                </div>
-              </div>
-            )} */}
-
             {/* Army warnings */}
             {structureResources && explorerResources && (
               <ArmyWarning
@@ -232,11 +198,11 @@ export const ArmyEntityDetail = memo(
             <div className="flex flex-col gap-1 mt-1 bg-gray-800/40 rounded p-2 border border-gold/20">
               <div className="flex items-center justify-between gap-2">
                 <div className={`${smallTextClass} font-bold text-gold/90 uppercase`}>STAMINA</div>
-                {stamina && maxStamina && (
+                {derivedData.stamina && derivedData.maxStamina && (
                   <StaminaResource
                     entityId={armyEntityId}
-                    stamina={stamina}
-                    maxStamina={maxStamina}
+                    stamina={derivedData.stamina}
+                    maxStamina={derivedData.maxStamina}
                     className="flex-1"
                   />
                 )}
diff --git a/client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx b/client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx
index ca9b2fb13fc..ab21e0d50ab 100644
--- a/client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx
+++ b/client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx
@@ -12,12 +12,11 @@ import { useChatStore } from "@/ui/modules/ws-chat/useChatStore";
 import { displayAddress } from "@/ui/utils/utils";
 import { useDojo } from "@bibliothecadao/react";
 import { getStructureFromToriiClient } from "@bibliothecadao/torii-client";
-import { ClientComponents, ContractAddress, GuildInfo, ID, MERCENARIES, StructureType } from "@bibliothecadao/types";
-import { ComponentValue } from "@dojoengine/recs";
-import { MessageCircle } from "lucide-react";
-import { memo, useEffect, useMemo, useState } from "react";
+import { ContractAddress, ID, MERCENARIES, StructureType } from "@bibliothecadao/types";
+import { useQuery } from "@tanstack/react-query";
+import { Loader, MessageCircle } from "lucide-react";
+import { memo, useMemo } from "react";
 import { CompactDefenseDisplay } from "../../military/compact-defense-display";
-import { DefenseTroop } from "../../military/structure-defence";
 import { InventoryResources } from "../../resources/inventory-resources";
 import { RealmResourcesIO } from "../../resources/realm-resources-io";
 import { ImmunityTimer } from "../structures/immunity-timer";
@@ -45,31 +44,55 @@ export const StructureEntityDetail = memo(
     } = useDojo();
 
     const userAddress = ContractAddress(account.account.address);
-    const [structure, setStructure] = useState<ComponentValue<ClientComponents["Structure"]["schema"]> | null>(null);
-    const [resources, setResources] = useState<ComponentValue<ClientComponents["Resource"]["schema"]> | null>(null);
-    const [playerGuild, setPlayerGuild] = useState<GuildInfo | undefined>(undefined);
-    const [isAlly, setIsAlly] = useState<boolean>(false);
-    const [guards, setGuards] = useState<DefenseTroop[]>([]);
-    const [addressName, setAddressName] = useState<string | undefined>(undefined);
-
-    useEffect(() => {
-      const fetchStructure = async () => {
+
+    const {
+      data: structureDetails,
+      isLoading: isLoadingStructure,
+      // error: structureError, // Can be used for error UI
+    } = useQuery({
+      queryKey: ["structureDetails", String(structureEntityId), String(userAddress)],
+      queryFn: async () => {
+        if (!toriiClient || !structureEntityId || !components || !userAddress) return null;
+
         const { structure, resources } = await getStructureFromToriiClient(toriiClient, structureEntityId);
-        if (!structure || !resources) return;
+        if (!structure)
+          return {
+            structure: null,
+            resources: null,
+            playerGuild: undefined,
+            guards: [],
+            isAlly: false,
+            addressName: MERCENARIES,
+            isMine: false,
+          };
+
+        const isMine = structure.owner === userAddress;
         const guild = getGuildFromPlayerAddress(ContractAddress(structure.owner), components);
-        const guards = structure ? getGuardsByStructure(structure).filter((guard) => guard.troops.count > 0n) : [];
+        const guards = getGuardsByStructure(structure).filter((guard) => guard.troops.count > 0n);
         const userGuild = getGuildFromPlayerAddress(userAddress, components);
         const isAlly = isMine || (guild && userGuild && guild.entityId === userGuild.entityId) || false;
-        setIsAlly(isAlly);
-        // explorer troops that are roaming the world are always daydreams agents if they are not owned by a player
-        setAddressName(structure?.owner ? getAddressName(structure?.owner, components) : MERCENARIES);
-        setStructure(structure);
-        setResources(resources);
-        setPlayerGuild(guild);
-        setGuards(guards);
-      };
-      fetchStructure();
-    }, [structureEntityId, userAddress]);
+        const addressName = structure.owner ? getAddressName(structure.owner, components) : MERCENARIES;
+
+        return {
+          structure,
+          resources,
+          playerGuild: guild,
+          guards,
+          isAlly,
+          addressName,
+          isMine,
+        };
+      },
+      staleTime: 30000, // 30 seconds
+    });
+
+    const structure = structureDetails?.structure;
+    const resources = structureDetails?.resources;
+    const playerGuild = structureDetails?.playerGuild;
+    const guards = structureDetails?.guards || [];
+    const isAlly = structureDetails?.isAlly || false;
+    const addressName = structureDetails?.addressName;
+    const isMine = structureDetails?.isMine || false;
 
     const isRealmOrVillage =
       structure?.base.category === StructureType.Realm || structure?.base.category === StructureType.Village;
@@ -83,12 +106,9 @@ export const StructureEntityDetail = memo(
     // Precompute common class strings for consistency with ArmyEntityDetail
     const smallTextClass = compact ? "text-xxs" : "text-xs";
 
-    const isMine = useMemo(() => {
-      return structure?.owner === userAddress;
-    }, [structure?.owner, userAddress]);
-
     const { openChat, selectDirectMessageRecipient, getUserIdByUsername } = useChatStore((state) => state.actions);
 
+    console.log("CHAT_DEBUG: addressName", addressName);
     const handleChatClick = () => {
       if (isMine) {
         openChat();
@@ -113,7 +133,15 @@ export const StructureEntityDetail = memo(
       return unpackValue(structure?.resources_packed || 0n);
     }, [structure]);
 
-    if (!structure) return null;
+    if (isLoadingStructure) {
+      return (
+        <div className="flex items-center justify-center h-full mt-2 ">
+          <Loader className="animate-spin" />
+        </div>
+      );
+    }
+
+    if (!structure || !structureDetails) return null;
 
     return (
       <div className={`flex flex-col ${compact ? "gap-1" : "gap-2"} ${className}`}>
@@ -137,9 +165,11 @@ export const StructureEntityDetail = memo(
             >
               {isAlly ? "Ally" : "Enemy"}
             </div>
-            <button onClick={handleChatClick}>
-              <MessageCircle />
-            </button>
+            {addressName !== undefined && (
+              <button onClick={handleChatClick} className="p-1 rounded hover:bg-gold/10 transition" title="Chat">
+                <MessageCircle />
+              </button>
+            )}
           </div>
         </div>
 
diff --git a/client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx b/client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx
index 4504829a144..4b0e54d29d0 100644
--- a/client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx
+++ b/client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx
@@ -209,6 +209,7 @@ export const TopLeftNavigation = memo(({ structures }: { structures: PlayerStruc
             </span>
           </div>
         </div>
+
         <SecondaryMenuItems />
       </motion.div>
     </div>
diff --git a/client/apps/game/src/ui/modules/stream/constants.tsx b/client/apps/game/src/ui/modules/stream/constants.tsx
index 3022c498672..ce55689f9e7 100644
--- a/client/apps/game/src/ui/modules/stream/constants.tsx
+++ b/client/apps/game/src/ui/modules/stream/constants.tsx
@@ -1,2 +1,2 @@
-export const EVENT_STREAM_SIZE = 8;
+export const EVENT_STREAM_SIZE = 20;
 export const EVENT_NOTIF_STORAGE_KEY = "eventNotif";
diff --git a/client/apps/game/src/ui/modules/stream/event-stream.tsx b/client/apps/game/src/ui/modules/stream/event-stream.tsx
index 6e55ccb4846..b946dee474f 100644
--- a/client/apps/game/src/ui/modules/stream/event-stream.tsx
+++ b/client/apps/game/src/ui/modules/stream/event-stream.tsx
@@ -1,12 +1,9 @@
-import { Position } from "@/types/position";
-import { NavigateToPositionIcon } from "@/ui/components/military/army-chip";
-import { ViewOnMapIcon } from "@/ui/elements/view-on-map-icon";
-import { ContractAddress, ID, world } from "@bibliothecadao/types";
-import { getAddressFromStructureEntity } from "@bibliothecadao/eternum";
+import { getAddressFromStructureEntity, getEntityIdFromKeys } from "@bibliothecadao/eternum";
 import { useDojo } from "@bibliothecadao/react";
+import { ContractAddress, ID, world } from "@bibliothecadao/types";
 import { Component, defineComponentSystem, Entity, getComponentValue, World } from "@dojoengine/recs";
 import { useCallback, useEffect, useState } from "react";
-import { MessageIcon } from "../social/player-id";
+import { shortString } from "starknet";
 import { EVENT_NOTIF_STORAGE_KEY, EVENT_STREAM_SIZE } from "./constants";
 import { eventDetails } from "./event-details";
 import { EventData, EventType } from "./types";
@@ -21,29 +18,36 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
   const [activeTab, setActiveTab] = useState<"all" | "personal">("all");
   const [hasNewEvents, setHasNewEvents] = useState(false);
 
-  // todo: fix this
   const createEvent = useCallback(
     (entity: Entity, component: Component<any>, eventType: EventType): EventData | undefined => {
       const componentValue = getComponentValue(component, entity);
 
+      console.log(componentValue);
+
       const owner = undefined;
 
       const to = eventDetails[eventType].to?.(componentValue! as any, (id: ID) =>
         getAddressFromStructureEntity(id, components),
       );
+
       const isPersonal = to === ContractAddress(account.address);
 
+      const name = shortString.decodeShortString(
+        getComponentValue(components.AddressName, getEntityIdFromKeys([BigInt(to ?? 0)]))?.name.toString() ?? "",
+      );
+
       return {
         to,
         action: eventDetails[eventType].getAction(componentValue! as any, isPersonal),
-        name: "",
+        name: name,
         eventType,
         timestamp: componentValue?.timestamp || 0,
         position: { x: 0, y: 0 },
         address: owner,
+        entityId: entity,
       };
     },
-    [account.address],
+    [account.address, components],
   );
 
   const createEventStream = (world: World, eventType: EventType) => {
@@ -56,6 +60,11 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
         const event = createEvent(update.entity, component, eventType);
         if (!event) return;
         setEventList((prev) => {
+          const eventExists = prev.some((e) => e.entityId === event.entityId && e.timestamp === event.timestamp);
+          if (eventExists) {
+            return prev;
+          }
+
           const newEvents = [event, ...prev];
           if (event.to === ContractAddress(account.address)) {
             const lastSeen = localStorage.getItem(EVENT_NOTIF_STORAGE_KEY) || "0";
@@ -63,7 +72,7 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
               setHasNewEvents(true);
             }
           }
-          return newEvents;
+          return newEvents.slice(0, EVENT_STREAM_SIZE * 2);
         });
       },
       { runOnInit: true },
@@ -93,17 +102,19 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
   });
 
   return (
-    <div className={`w-full md:justify-start justify-end pointer-events-auto ${hideChat ? "h-0 hidden" : "h-[20vh]"}`}>
-      <div className={`flex flex-row text-sm text-center md:justify-start justify-end ${hideChat ? "hidden" : ""}`}>
-        <div className="flex ml-2">
+    <div className={`w-full pointer-events-auto flex flex-col ${hideChat ? "h-0 hidden" : "h-full"}`}>
+      <div
+        className={`flex flex-row text-xs text-center md:justify-start justify-end flex-shrink-0 ${hideChat ? "hidden" : "h-[2rem]"}`}
+      >
+        <div className="flex ml-2 h6 pb-0.5">
           <div
-            className={`px-3 py-1 cursor-pointer ${activeTab === "all" ? "bg-brown/40 text-gold" : "bg-brown/20"}`}
+            className={`px-3 cursor-pointer ${activeTab === "all" ? "bg-brown/40 text-gold" : "bg-brown/20"}`}
             onClick={() => setActiveTab("all")}
           >
             All
           </div>
           <div
-            className={`px-3 py-1 cursor-pointer relative ${
+            className={`px-3 cursor-pointer relative ${
               activeTab === "personal" ? "bg-brown/40 text-gold" : "bg-brown/20"
             }`}
             onClick={() => setActiveTab("personal")}
@@ -120,18 +131,18 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
       </div>
 
       <div
-        className={`bg-brown/90 bg-hex-bg rounded-bl-2xl p-1 rounded-tr border border-gold/40 overflow-y-auto ${hideChat ? "hidden" : "h-[calc(100%-2rem)]"}`}
+        className={`bg-brown/90 px-2 text-xs rounded-tr border border-gold/40 overflow-y-auto ${hideChat ? "hidden" : "flex-grow"}`}
       >
         {filteredEvents
           .sort((a, b) => a.timestamp - b.timestamp)
           .slice(-EVENT_STREAM_SIZE)
-          .map((event, index) => {
+          .map((event) => {
             const { emoji, color } = eventDetails[event.eventType as keyof typeof eventDetails];
             return (
               <div
-                className={`hover:bg-brown/20 w-full rounded flex flex-row gap-2 justify-between `}
+                className={`hover:bg-brown/20 w-full rounded flex flex-row justify-between text-xs `}
                 style={{ color }}
-                key={index}
+                key={`${event.entityId}-${event.timestamp}`}
               >
                 <div className="flex items-center space-x-2 flex-grow overflow-hidden">
                   <span className="text-lg flex-shrink-0" style={{ fill: color }}>
@@ -144,15 +155,15 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
                   <span className="opacity-70 text-xs whitespace-nowrap">
                     [{new Date(event.timestamp * 1000).toLocaleTimeString()}]
                   </span>
-                  {event.position && (
+                  {/* {event.position && (
                     <div className="hidden md:flex">
                       <ViewOnMapIcon hideTooltip={true} position={new Position(event.position)} />
                       <NavigateToPositionIcon hideTooltip={true} position={new Position(event.position)} />
                     </div>
-                  )}
-                  <div className="hidden md:block">
+                  )} */}
+                  {/* <div className="hidden md:block">
                     <MessageIcon playerName={event.name} selectedPlayer={event.address ?? BigInt(0)} />
-                  </div>
+                  </div> */}
                 </div>
               </div>
             );
diff --git a/client/apps/game/src/ui/modules/stream/types.tsx b/client/apps/game/src/ui/modules/stream/types.tsx
index ef36a33c213..aa561f1b47a 100644
--- a/client/apps/game/src/ui/modules/stream/types.tsx
+++ b/client/apps/game/src/ui/modules/stream/types.tsx
@@ -15,4 +15,5 @@ export interface EventData {
   timestamp: number;
   position: Position | undefined;
   address: ContractAddress | undefined;
+  entityId: string | number;
 }
diff --git a/client/apps/game/src/ui/modules/ws-chat/chat.tsx b/client/apps/game/src/ui/modules/ws-chat/chat.tsx
index 54cc6a3f887..a2cbeb6a681 100644
--- a/client/apps/game/src/ui/modules/ws-chat/chat.tsx
+++ b/client/apps/game/src/ui/modules/ws-chat/chat.tsx
@@ -754,7 +754,7 @@ function ChatModule() {
                               className={`flex items-center px-2 py-1 cursor-pointer transition-colors   ${
                                 user.id === userId
                                   ? "bg-orange-600/20 border-l-2 border-orange "
-                                  : user.id === directMessageRecipientId
+                                  : user?.id === directMessageRecipientId
                                     ? "bg-orange/10 border-l-2 border-orange"
                                     : "hover:bg-gray/50 hover:border-l-2 hover:border-gray-500 active:bg-orange/10 active:border-orange"
                               }`}
@@ -800,7 +800,7 @@ function ChatModule() {
                           <li
                             key={user.id}
                             className={`flex items-center px-2 cursor-pointer transition-colors opacity-60 ${
-                              user.id === directMessageRecipientId
+                              user?.id === directMessageRecipientId
                                 ? "bg-gray-800/30 border-l-2 border-gray-500"
                                 : "hover:bg-gray-800/30 hover:border-l-2 hover:border-gray-600 active:bg-gray-800/50 active:border-gray-500"
                             }`}
@@ -847,7 +847,7 @@ function ChatModule() {
             <div className="truncate h6">
               {directMessageRecipientId ? (
                 `Chat with ${
-                  [...onlineUsers, ...offlineUsers].find((user) => user.id === directMessageRecipientId)?.username ||
+                  [...onlineUsers, ...offlineUsers].find((user) => user?.id === directMessageRecipientId)?.username ||
                   directMessageRecipientId
                 }`
               ) : activeRoomId ? (
diff --git a/client/apps/game/src/ui/modules/ws-chat/useChatStore.ts b/client/apps/game/src/ui/modules/ws-chat/useChatStore.ts
index 8141b4e0c75..8846c03df1d 100644
--- a/client/apps/game/src/ui/modules/ws-chat/useChatStore.ts
+++ b/client/apps/game/src/ui/modules/ws-chat/useChatStore.ts
@@ -71,7 +71,8 @@ export const useChatStore = create<ChatStoreState>((set, get) => ({
     getUserIdByUsername: (username: string) => {
       const allUsers = [...get().onlineUsers, ...get().offlineUsers];
 
-      const user = allUsers.filter((user) => user.username === username);
+      console.log("user", username);
+      const user = allUsers.filter((user) => user?.username === username);
       console.log("user", user);
       return user[0]?.id || null;
     },
diff --git a/client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts b/client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts
index aea64be4444..3525cb36b7c 100644
--- a/client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts
+++ b/client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts
@@ -36,12 +36,12 @@ export const sortMessagesByTime = (messages: Message[]): Message[] => {
 export const filterRoomsBySearch = (rooms: Room[], searchText: string): Room[] => {
   if (!searchText.trim()) return rooms;
 
-  return rooms.filter((room) => (room.name || room.id).toLowerCase().includes(searchText.toLowerCase()));
+  return rooms.filter((room) => (room.name || room.id)?.toLowerCase().includes(searchText.toLowerCase()));
 };
 
 // Filter users based on search input
 export const filterUsersBySearch = (users: User[], searchText: string): User[] => {
   if (!searchText.trim()) return users;
 
-  return users.filter((user) => (user.username || user.id).toLowerCase().includes(searchText.toLowerCase()));
+  return users.filter((user) => (user?.username || user?.id)?.toLowerCase().includes(searchText.toLowerCase()));
 };
diff --git a/package.json b/package.json
index c0453b72b3e..50856d019d2 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
     "build:mobile": "pnpm --dir ./client/apps/eternum-mobile build",
     "clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && find . -name 'pnpm-lock.yaml' -type f -delete",
     "dev": "pnpm --dir ./client/apps/game dev",
+    "dev:mainnet": "NODE_ENV=mainnet pnpm --dir ./client/apps/game dev",
     "dev:docs": "cd ./client/apps/game-docs && vocs dev",
     "dev:landing": "pnpm --dir ./client/apps/landing dev",
     "dev:mobile": "pnpm --dir ./client/apps/eternum-mobile dev",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5cb2b2f77b8..fe56906514d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -452,6 +452,9 @@ importers:
       '@starknet-react/core':
         specifier: 'catalog:'
         version: 3.7.2(bufferutil@4.0.9)(get-starknet-core@4.0.0)(react@18.3.1)(starknet@6.23.1(encoding@0.1.13))(typescript@5.8.3)(utf-8-validate@5.0.10)
+      '@tanstack/react-query':
+        specifier: ^5.76.0
+        version: 5.76.0(react@18.3.1)
       '@vercel/analytics':
         specifier: ^1.2.2
         version: 1.5.0(next@15.3.2(@babel/core@7.27.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>client/apps/game/src/ui/modules/ws-chat/useChatStore.ts (1)</summary><blockquote>

`74-74`: **Remove debugging console.log statement.**

This debugging statement should be removed before merging to production.

```diff
-      console.log("user", username);
```

</blockquote></details>
<details>
<summary>client/apps/game/package.json (1)</summary><blockquote>

`43-43`: **Note the fixed version for metagame-sdk**

You've added metagame-sdk with a fixed version (0.0.19) rather than using the caret (^) notation used for react-query. This is fine if you need this specific version, but consider keeping version notation consistent across dependencies where possible.

</blockquote></details>
<details>
<summary>client/apps/game/src/index.css (2)</summary><blockquote>

`18-30`: **Duplicate `@keyframes scroll` definitions – only the last one is used**

Defining the same animation name twice is confusing and may hide intent.  
Rename one of them or consolidate into a single definition to avoid accidental overrides.



Also applies to: 246-253

---

`127-129`: **`text-stroke` is non-standard – triggers lint error**

Browsers currently only support the WebKit prefixed version.  
Keep `-webkit-text-stroke` and drop the unprefixed property (or use an `@supports` block) to silence static-analysis noise.

```diff
-  -webkit-text-stroke: 1.5px black; /* For WebKit browsers */
-  text-stroke: 1.5px black; /* Standard property */
+  -webkit-text-stroke: 1.5px black;
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 128-128: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cf3abc44e98dc68ca920989b19c0160c0d00486d and f14de18b2d634a87689db9e481dee14892cefe01.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts` (1 hunks)
* `client/apps/game/.env.mainnet` (1 hunks)
* `client/apps/game/package.json` (1 hunks)
* `client/apps/game/src/index.css` (1 hunks)
* `client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx` (4 hunks)
* `client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx` (5 hunks)
* `client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx` (1 hunks)
* `client/apps/game/src/ui/modules/stream/constants.tsx` (1 hunks)
* `client/apps/game/src/ui/modules/stream/event-stream.tsx` (6 hunks)
* `client/apps/game/src/ui/modules/stream/types.tsx` (1 hunks)
* `client/apps/game/src/ui/modules/ws-chat/chat.tsx` (3 hunks)
* `client/apps/game/src/ui/modules/ws-chat/useChatStore.ts` (1 hunks)
* `client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts` (1 hunks)
* `package.json` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts (1)</summary><blockquote>

<details>
<summary>client/apps/game/src/ui/modules/ws-chat/types/user.ts (1)</summary>

* `User` (2-6)

</details>

</blockquote></details>
<details>
<summary>client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx (5)</summary><blockquote>

<details>
<summary>packages/torii-client/src/queries/structure.ts (1)</summary>

* `getStructureFromToriiClient` (72-116)

</details>
<details>
<summary>packages/types/src/constants/troops.ts (1)</summary>

* `MERCENARIES` (3-3)

</details>
<details>
<summary>packages/core/src/utils/guild.ts (1)</summary>

* `getGuildFromPlayerAddress` (73-81)

</details>
<details>
<summary>packages/core/src/utils/army.ts (1)</summary>

* `getGuardsByStructure` (148-186)

</details>
<details>
<summary>packages/core/src/utils/entities.ts (1)</summary>

* `getAddressName` (99-107)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts</summary>

[error] 46-46: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:


(lint/correctness/noUnsafeOptionalChaining)

</details>
<details>
<summary>client/apps/game/src/index.css</summary>

[error] 128-128: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: test-various (troop_management)
* GitHub Check: test-various (quest)
* GitHub Check: test-season-pass

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx (1)</summary>

`212-212`: **Formatting adjustment only: blank line inserted before `<SecondaryMenuItems />`.**

This is a purely stylistic change that does not impact functionality or behavior. Approving as-is.

</details>
<details>
<summary>client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts (2)</summary>

`39-39`: **Good defensive programming with optional chaining!**

Adding optional chaining to prevent runtime errors when calling `.toLowerCase()` on potentially undefined values is a good practice.

---

`46-46`: **Good defensive programming with optional chaining!**

This change properly handles potential null/undefined values when accessing user properties. According to the User interface, username is optional so this null safety is appropriate.

</details>
<details>
<summary>client/apps/game/src/ui/modules/ws-chat/chat.tsx (3)</summary>

`757-757`: **Good null safety for conditional styling!**

Adding optional chaining when checking user ID prevents potential runtime errors if user is null or undefined.

---

`803-803`: **Good null safety for offline users!**

Consistent with the change for online users, adding optional chaining here prevents runtime errors if user objects are null or undefined.

---

`850-851`: **Good null safety for username lookup!**

Adding optional chaining to the user lookup prevents potential runtime errors if a user object is null or undefined, ensuring the chat header can display safely.

</details>
<details>
<summary>client/apps/game/src/ui/modules/ws-chat/useChatStore.ts (1)</summary>

`75-75`: **Good null safety for username filtering!**

Adding optional chaining for the username property is consistent with similar changes in other files and prevents potential runtime errors.

</details>
<details>
<summary>client/apps/game/package.json (1)</summary>

`36-36`: **Good addition of react-query for improved data fetching**

Adding `@tanstack/react-query` is a great architectural choice that will provide improved data fetching, caching, and state management compared to manual approaches. This should help centralize data fetching logic and improve loading state handling across components.

</details>
<details>
<summary>client/apps/game/src/ui/modules/stream/types.tsx (1)</summary>

`18-18`: <details>
<summary>✅ Verification successful</summary>

**Ensure all EventData implementations provide the new required entityId field**

Adding an entityId field to EventData is a good enhancement for event identification and deduplication. However, since this is a required field in the interface, make sure all code that creates EventData objects has been updated to provide this field.



The flexible type (string | number) allows for different identifier formats but might lead to inconsistent handling. Consider standardizing to a single type if possible:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for EventData creation patterns to verify the entityId field is being provided
rg -A 5 "EventData\s*[=:]" --type typescript
```

Length of output: 81

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all references to EventData across .ts and .tsx files to verify entityId is provided
rg -n "EventData" --glob "*.ts" --glob "*.tsx"
```

Length of output: 512

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the first 200 lines to inspect EventData object creation
sed -n '1,200p' client/apps/game/src/ui/modules/stream/event-stream.tsx
```

Length of output: 6675

---



</details>

**All EventData instances include the new entityId field — no further updates required**

Verified that the only factory for `EventData` in `client/apps/game/src/ui/modules/stream/event-stream.tsx` (the `createEvent` callback) always returns an object with the new `entityId` property, and no other instantiations of `EventData` exist in the codebase.

</details>
<details>
<summary>client/apps/game/src/ui/modules/stream/constants.tsx (1)</summary>

`1-1`: **Monitor performance with increased event stream size**

Increasing the EVENT_STREAM_SIZE from 8 to 20 provides a better user experience by showing more event history. However, be mindful of potential performance implications, especially if these events trigger complex UI rendering or expensive operations.



Consider implementing monitoring to ensure that the increased buffer size doesn't negatively impact UI responsiveness on lower-end devices.

</details>
<details>
<summary>client/apps/game/src/ui/modules/stream/event-stream.tsx (1)</summary>

`33-34`: **Type mismatch could mark personal events as non-personal**

`to` is a `bigint`; `ContractAddress(account.address)` returns a hex string.  
Strict equality will always be false.

Either normalise both sides to `bigint` or `string`:

```diff
-      const isPersonal = to === ContractAddress(account.address);
+      const isPersonal = to === BigInt(ContractAddress(account.address));
```

</details>
<details>
<summary>client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx (6)</summary>

`48-87`: **Consider guarding the query with `enabled` & avoid returning `null`**  

`useQuery` will mark the query as **fulfilled** the moment the promise resolves, even when the resolver returns `null`.  
If any of the dependencies (`toriiClient`, `components`, `userAddress`) are still undefined on the first render the resolver will still run and immediately cache `null`, forcing a refetch only after the `staleTime` expires.

```diff
-  } = useQuery({
+  } = useQuery({
       queryKey: ["structureDetails", String(structureEntityId), String(userAddress)],
-      queryFn: async () => {
-        if (!toriiClient || !structureEntityId || !components || !userAddress) return null;
+      enabled: !!toriiClient && !!components && !!userAddress && !!structureEntityId,
+      queryFn: async () => {
         …
       },
       staleTime: 30000, // 30 seconds
     });
```

This prevents an unnecessary network request, avoids caching `null`, and removes the need for the early-return inside the `queryFn`.  
[ suggest_essential_refactor ]

---

`71-72`: **Runtime error: `guard.troops.count` is incompatible with current data shape**  

`getGuardsByStructure` returns `troops` as a `bigint`, but the filter assumes an object with a `count` field.  
At runtime this will throw: **“Cannot read properties of bigint (reading 'count')”**.

```diff
-const guards = getGuardsByStructure(structure).filter((guard) => guard.troops.count > 0n);
+const guards = getGuardsByStructure(structure).filter((guard) => {
+  const troopCount =
+    typeof guard.troops === "bigint" ? guard.troops : guard.troops?.count ?? 0n;
+  return troopCount > 0n;
+});
```

Please align the expected shape (or adjust `getGuardsByStructure` to return an object with a `count` property).  
[ raise_critical_issue ]

---

`100-101`: **Incorrect property used to derive structure type name**  

Earlier checks use `structure.base.category`, but here the plain `structure?.category` is passed to `getStructureTypeName`. Unless the model defines both, this will likely be `undefined`, resulting in an empty type label.

```diff
-const structureTypeName = structure ? getStructureTypeName(structure?.category) : undefined;
+const structureTypeName = structure ? getStructureTypeName(structure.base.category) : undefined;
```

Aligning the accessor keeps the component consistent with the realm/village and hyperstructure checks above.  
[ raise_critical_issue ]

---

`24-31`: **`showButtons` is typed as boolean but rendered as children**  

`showButtons?: boolean` suggests a simple toggle, yet later it is interpolated as JSX:  

```tsx
{showButtons && <div …>{showButtons}</div>}
```

Passing a boolean into JSX will render `true`/`false` as text.

Recommended fix:

```diff
-export interface StructureEntityDetailProps {
+export interface StructureEntityDetailProps {
   …
-  showButtons?: boolean;
+  /**
+   * React element(s) to render as action buttons. Pass `undefined`/`null`
+   * to hide them.
+   */
+  actionButtons?: React.ReactNode;
 }
 …
-{showButtons && <div className="flex …">{showButtons}</div>}
+{actionButtons && <div className="flex …">{actionButtons}</div>}
```

This clarifies intent and prevents accidental boolean rendering.  
[ suggest_essential_refactor ]


Also applies to: 248-249

---

`111-112`: **Remove leftover debug `console.log`**  

`console.log("CHAT_DEBUG: addressName", addressName);` will spam production logs. Please remove or wrap with a development guard.  
[ suggest_nitpick ]

---

`168-173`: **Chat button shown for “Bandits”/unknown owners**  

The button currently appears when `addressName !== undefined`, which includes the `"Bandits"` placeholder. Opening a DM with a non-player address will silently fail.

```diff
-{addressName !== undefined && (
+{addressName && addressName !== MERCENARIES && (
```

Optionally hide the button when the target is the current user (`isMine`).  
[ suggest_optional_refactor ]

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T04:02:56Z

Review comments:
diff_hunk: @@ -43,5 +43,5 @@ export const filterRoomsBySearch = (rooms: Room[], searchText: string): Room[] =
 export const filterUsersBySearch = (users: User[], searchText: string): User[] => {
   if (!searchText.trim()) return users;
 
-  return users.filter((user) => (user.username || user.id).toLowerCase().includes(searchText.toLowerCase()));
+  return users?.filter((user) => (user?.username || user?.id).toLowerCase().includes(searchText.toLowerCase()));
path: client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix unsafe optional chaining that could lead to errors.**

The optional chaining on `users?.filter` can lead to a TypeError. If `users` is undefined, the expression returns undefined (not an empty array), and any code using the result would fail.


Try one of these safer approaches:

```diff
-  return users?.filter((user) => (user?.username || user?.id).toLowerCase().includes(searchText.toLowerCase()));
+  // Option 1: Check for undefined users at the top
+  if (!users) return [];
+  return users.filter((user) => (user?.username || user?.id)?.toLowerCase().includes(searchText.toLowerCase()));

+  // Option 2: Ensure a valid return value
+  return users?.filter((user) => (user?.username || user?.id)?.toLowerCase().includes(searchText.toLowerCase())) || [];
```

Notice I also added optional chaining to the `toLowerCase()` call, as in the game version, since `(user?.username || user?.id)` could still be undefined.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 46-46: Unsafe usage of optional chaining.

If it short-circuits with 'undefined' the evaluation will throw TypeError here:


(lint/correctness/noUnsafeOptionalChaining)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts at line
46, the use of optional chaining on users?.filter can cause a TypeError if users
is undefined because it returns undefined instead of an empty array. To fix
this, ensure users is defined before calling filter by using a default empty
array or a conditional check. Also, add optional chaining to the toLowerCase()
calls on (user?.username || user?.id) to safely handle cases where these values
might be undefined.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,7 +3,7 @@ VITE_PUBLIC_MASTER_PRIVATE_KEY=0x5013b17c43a2b664ec2a38aa45f6d891db1188622ec7cf3
 VITE_PUBLIC_ACCOUNT_CLASS_HASH="0x07dc7899aa655b0aae51eadff6d801a58e97dd99cf4666ee59e704249e51adf2"
 VITE_PUBLIC_FEE_TOKEN_ADDRESS=0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
 
-VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-2/torii
+VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-3/torii
path: client/apps/game/.env.mainnet
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Update all mainnet endpoint references for consistency**

You've updated the TORII endpoint to point to mainnet-3, but VITE_PUBLIC_TORII_RELAY (line 10) and VITE_PUBLIC_SLOT (line 15) still reference mainnet-2. This inconsistency might cause unexpected behavior.


Consider updating all references to maintain consistency:
```diff
VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-3/torii
-VITE_PUBLIC_TORII_RELAY=/dns4/api.cartridge.gg/tcp/443/x-parity-wss/%2Fx%2Feternum-game-mainnet-2%2Ftorii%2Fwss
+VITE_PUBLIC_TORII_RELAY=/dns4/api.cartridge.gg/tcp/443/x-parity-wss/%2Fx%2Feternum-game-mainnet-3%2Ftorii%2Fwss
-VITE_PUBLIC_SLOT=eternum-game-mainnet-2
+VITE_PUBLIC_SLOT=eternum-game-mainnet-3
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
VITE_PUBLIC_TORII=https://api.cartridge.gg/x/eternum-game-mainnet-3/torii
VITE_PUBLIC_TORII_RELAY=/dns4/api.cartridge.gg/tcp/443/x-parity-wss/%2Fx%2Feternum-game-mainnet-3%2Ftorii%2Fwss
VITE_PUBLIC_SLOT=eternum-game-mainnet-3
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/game/.env.mainnet at lines 6, 10, and 15, the mainnet endpoint
references are inconsistent; line 6 uses mainnet-3 while lines 10 and 15 still
use mainnet-2. Update the values of VITE_PUBLIC_TORII_RELAY and VITE_PUBLIC_SLOT
on lines 10 and 15 to use mainnet-3 endpoints to ensure all mainnet references
are consistent.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,6 +10,7 @@
     "build:mobile": "pnpm --dir ./client/apps/eternum-mobile build",
     "clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && find . -name 'pnpm-lock.yaml' -type f -delete",
     "dev": "pnpm --dir ./client/apps/game dev",
+    "dev:mainnet": "NODE_ENV=mainnet pnpm --dir ./client/apps/game dev",
path: package.json
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use a cross-platform way to set `NODE_ENV` in npm scripts**

`NODE_ENV=mainnet` works on Unix-like shells but fails on Windows (`cmd.exe` / PowerShell).  
Consider using `cross-env` (already a transitive dep in many stacks) or an npm script wrapper so the command works for every contributor and in CI.

```diff
-    "dev:mainnet": "NODE_ENV=mainnet pnpm --dir ./client/apps/game dev",
+    "dev:mainnet": "cross-env NODE_ENV=mainnet pnpm --dir ./client/apps/game dev",
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    "dev:mainnet": "cross-env NODE_ENV=mainnet pnpm --dir ./client/apps/game dev",
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In package.json at line 13, the script sets NODE_ENV using a Unix-specific
syntax that fails on Windows. To fix this, modify the script to use the
cross-platform 'cross-env' package by prefixing the command with 'cross-env',
ensuring NODE_ENV is set correctly on all platforms including Windows and CI
environments.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,29 +18,36 @@ export const EventStream = ({ hideChat }: { hideChat: boolean }) => {
   const [activeTab, setActiveTab] = useState<"all" | "personal">("all");
   const [hasNewEvents, setHasNewEvents] = useState(false);
 
-  // todo: fix this
   const createEvent = useCallback(
     (entity: Entity, component: Component<any>, eventType: EventType): EventData | undefined => {
       const componentValue = getComponentValue(component, entity);
 
+      console.log(componentValue);
+
path: client/apps/game/src/ui/modules/stream/event-stream.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove leftover debug logging**

`console.log` in hot render paths floods the browser console and hurts performance.  
Please delete or guard it behind a debug flag.

```diff
-      console.log(componentValue);
+
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/game/src/ui/modules/stream/event-stream.tsx at lines 25 to 26,
remove the console.log statement used for debugging as it can flood the browser
console and degrade performance. Either delete this line entirely or wrap it in
a conditional check that only logs when a debug flag is enabled, ensuring it
does not run in production or normal usage.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,29 +18,36 @@
   const [activeTab, setActiveTab] = useState<"all" | "personal">("all");
   const [hasNewEvents, setHasNewEvents] = useState(false);
 
-  // todo: fix this
   const createEvent = useCallback(
     (entity: Entity, component: Component<any>, eventType: EventType): EventData | undefined => {
       const componentValue = getComponentValue(component, entity);
 
+      console.log(componentValue);
+
       const owner = undefined;
 
       const to = eventDetails[eventType].to?.(componentValue! as any, (id: ID) =>
         getAddressFromStructureEntity(id, components),
       );
+
       const isPersonal = to === ContractAddress(account.address);
 
+      const name = shortString.decodeShortString(
+        getComponentValue(components.AddressName, getEntityIdFromKeys([BigInt(to ?? 0)]))?.name.toString() ?? "",
+      );
path: client/apps/game/src/ui/modules/stream/event-stream.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Potentially incorrect decoding of `AddressName.name`**

`shortString.decodeShortString` expects a _hex-encoded_ string (`0x…`).  
`toString()` on a `bigint` produces a **decimal** string, so names will be mis-decoded (often throwing or returning gibberish).

Convert the bigint to hex (or store names as plain strings) before decoding:

```diff
-        getComponentValue(components.AddressName, getEntityIdFromKeys([BigInt(to ?? 0)]))?.name.toString() ?? "",
+        (() => {
+          const raw = getComponentValue(
+            components.AddressName,
+            getEntityIdFromKeys([BigInt(to ?? 0)]),
+          )?.name;
+          return raw ? shortString.decodeShortString(`0x${raw.toString(16)}`) : "";
+        })(),
```



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/game/src/ui/modules/stream/event-stream.tsx around lines 35 to
37, the code calls shortString.decodeShortString on a string obtained by
toString() on a bigint, which produces a decimal string instead of the expected
hex-encoded string. To fix this, convert the bigint value to a hex string
(prefixed with "0x") before passing it to decodeShortString, ensuring the input
matches the expected hex format for correct decoding.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,26 +32,71 @@ export const ArmyEntityDetail = memo(
       },
     } = useDojo();
 
-    const [explorer, setExplorer] = useState<ComponentValue<ClientComponents["ExplorerTroops"]["schema"]> | undefined>(
-      undefined,
-    );
-    const [explorerResources, setExplorerResources] = useState<
-      ComponentValue<ClientComponents["Resource"]["schema"]> | undefined
-    >(undefined);
-    const [structureResources, setStructureResources] = useState<
-      ComponentValue<ClientComponents["Resource"]["schema"]> | undefined
-    >(undefined);
-    const [structure, setStructure] = useState<
-      ComponentValue<ClientComponents["Structure"]["schema"]> | null | undefined
-    >(undefined);
     const userAddress = ContractAddress(account.address);
-    const [addressName, setAddressName] = useState<string | undefined>(undefined);
-    const [playerGuild, setPlayerGuild] = useState<GuildInfo | undefined>(undefined);
-    const [isAlly, setIsAlly] = useState(false);
-    const [stamina, setStamina] = useState<{ amount: bigint; updated_tick: bigint } | undefined>(undefined);
-    const [maxStamina, setMaxStamina] = useState<number | undefined>(undefined);
     const [isLoadingDelete, setIsLoadingDelete] = useState(false);
 
+    const {
+      data: explorerData,
+      isLoading: isLoadingExplorer,
+      // error: explorerError, // Available if error handling is needed
+    } = useQuery({
+      queryKey: ["explorer", String(armyEntityId)],
+      queryFn: async () => {
+        if (!toriiClient || !armyEntityId) return undefined;
+        return getExplorerFromToriiClient(toriiClient, armyEntityId);
+      },
+      staleTime: 30000, // 30 seconds
+    });
+
+    const explorer = explorerData?.explorer;
+    const explorerResources = explorerData?.resources;
+
+    const {
+      data: structureData,
+      isLoading: isLoadingStructure,
+      // error: structureError, // Available if error handling is needed
+    } = useQuery({
+      queryKey: ["structure", explorer?.owner ? String(explorer.owner) : undefined],
+      queryFn: async () => {
+        if (!toriiClient || !explorer?.owner) return undefined;
+        return getStructureFromToriiClient(toriiClient, explorer.owner);
+      },
+      enabled: !!explorer?.owner,
+      staleTime: 30000, // 30 seconds
path: client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid `undefined` inside React-Query `queryKey`**

Including `undefined` leads to keys like `["structure", undefined]` which collide for multiple armies and break caching.  
Use a stable key only when the owner exists.

```diff
-      queryKey: ["structure", explorer?.owner ? String(explorer.owner) : undefined],
+      queryKey: explorer?.owner ? ["structure", String(explorer.owner)] : [],
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
      queryKey: explorer?.owner ? ["structure", String(explorer.owner)] : [],
      queryFn: async () => {
        if (!toriiClient || !explorer?.owner) return undefined;
        return getStructureFromToriiClient(toriiClient, explorer.owner);
      },
      enabled: !!explorer?.owner,
      staleTime: 30000, // 30 seconds
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
around lines 59 to 65, the queryKey array includes undefined when explorer.owner
is missing, causing cache collisions in React-Query. Fix this by only including
the owner in the queryKey when it exists, ensuring the key is stable and does
not contain undefined values.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,11 +198,11 @@
             <div className="flex flex-col gap-1 mt-1 bg-gray-800/40 rounded p-2 border border-gold/20">
               <div className="flex items-center justify-between gap-2">
                 <div className={`${smallTextClass} font-bold text-gold/90 uppercase`}>STAMINA</div>
-                {stamina && maxStamina && (
+                {derivedData.stamina && derivedData.maxStamina && (
                   <StaminaResource
                     entityId={armyEntityId}
-                    stamina={stamina}
-                    maxStamina={maxStamina}
+                    stamina={derivedData.stamina}
+                    maxStamina={derivedData.maxStamina}
                     className="flex-1"
path: client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`stamina` of `0` is hidden**

The JSX condition treats `0` as falsy, so completely exhausted armies display no bar.  
Check for `null/undefined` instead.

```diff
-                {derivedData.stamina && derivedData.maxStamina && (
+                {derivedData.stamina !== undefined && derivedData.maxStamina !== undefined && (
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx
around lines 201 to 206, the JSX condition currently hides the stamina bar when
stamina is 0 because it treats 0 as falsy. To fix this, change the condition to
explicitly check that derivedData.stamina and derivedData.maxStamina are not
null or undefined instead of relying on truthiness, so that a stamina value of 0
still renders the StaminaResource component.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T04:02:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-19T03:58:20Z
body: [vc]: #95eRtS+77CIEnrPcRbJMbL7aiAb11/7MufE819LeBqM=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJldGVybnVtIiwicm9vdERpcmVjdG9yeSI6bnVsbCwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImV0ZXJudW0tZ2l0LWxvYWYtZml4LTMtbG9vdC1iaWJsaW90aGVjYS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sb290LWJpYmxpb3RoZWNhL2V0ZXJudW0vOEhGRXZWZXZDNTJ4Z3ZOZ1dDZVJNTERLa0RIdSIsInByZXZpZXdVcmwiOiJldGVybnVtLWdpdC1sb2FmLWZpeC0zLWxvb3QtYmlibGlvdGhlY2EudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6ImV0ZXJudW0tbGFuZGluZyIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2xvb3QtYmlibGlvdGhlY2EvZXRlcm51bS1sYW5kaW5nL0V3elcyUUdNYkNMS1hoN2VvSFpvdXV6Q3BzWHYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9LHsibmFtZSI6ImV0ZXJudW0tZG9jcyIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJldGVybnVtLWRvY3MtZ2l0LWxvYWYtZml4LTMtbG9vdC1iaWJsaW90aGVjYS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sb290LWJpYmxpb3RoZWNhL2V0ZXJudW0tZG9jcy8yWDdNcmh3Q0daVnhSVDc0SnE3YjU0MlVhSGJzIiwicHJldmlld1VybCI6ImV0ZXJudW0tZG9jcy1naXQtbG9hZi1maXgtMy1sb290LWJpYmxpb3RoZWNhLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJldGVybnVtLW1vYmlsZSIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJldGVybnVtLW1vYmlsZS1naXQtbG9hZi1maXgtMy1sb290LWJpYmxpb3RoZWNhLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2xvb3QtYmlibGlvdGhlY2EvZXRlcm51bS1tb2JpbGUvODkxaWd3TFd5bUQ4TEdGUTJYUnNwaG5KeUdFWiIsInByZXZpZXdVcmwiOiJldGVybnVtLW1vYmlsZS1naXQtbG9hZi1maXgtMy1sb290LWJpYmxpb3RoZWNhLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJldGVybnVtLWluZnJhIiwicm9vdERpcmVjdG9yeSI6bnVsbCwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImV0ZXJudW0taW5mcmEtZ2l0LWxvYWYtZml4LTMtbG9vdC1iaWJsaW90aGVjYS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9sb290LWJpYmxpb3RoZWNhL2V0ZXJudW0taW5mcmEvNG1yMUQ0VzRSSlZGWGhhR3pFYVp6U2F5cGp3TiIsInByZXZpZXdVcmwiOiJldGVybnVtLWluZnJhLWdpdC1sb2FmLWZpeC0zLWxvb3QtYmlibGlvdGhlY2EudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **eternum** | ✅ Ready ([Inspect](https://vercel.com/loot-bibliotheca/eternum/8HFEvVevC52xgvNgWCeRMLDKkDHu)) | [Visit Preview](https://eternum-git-loaf-fix-3-loot-bibliotheca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/eternum-git-loaf-fix-3-loot-bibliotheca.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 3:58am |
| **eternum-docs** | ✅ Ready ([Inspect](https://vercel.com/loot-bibliotheca/eternum-docs/2X7MrhwCGZVxRT74Jq7b542UaHbs)) | [Visit Preview](https://eternum-docs-git-loaf-fix-3-loot-bibliotheca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/eternum-docs-git-loaf-fix-3-loot-bibliotheca.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 3:58am |
| **eternum-infra** | ✅ Ready ([Inspect](https://vercel.com/loot-bibliotheca/eternum-infra/4mr1D4W4RJVFXhaGzEaZzSaypjwN)) | [Visit Preview](https://eternum-infra-git-loaf-fix-3-loot-bibliotheca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/eternum-infra-git-loaf-fix-3-loot-bibliotheca.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 3:58am |
| **eternum-mobile** | ✅ Ready ([Inspect](https://vercel.com/loot-bibliotheca/eternum-mobile/891igwLWymD8LGFQ2XRsphnJyGEZ)) | [Visit Preview](https://eternum-mobile-git-loaf-fix-3-loot-bibliotheca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/eternum-mobile-git-loaf-fix-3-loot-bibliotheca.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 3:58am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **eternum-landing** | ⬜️ Ignored ([Inspect](https://vercel.com/loot-bibliotheca/eternum-landing/EwzW2QGMbCLKXh7eoHZouuzCpsXv)) |  |  | May 19, 2025 3:58am |
</details>






author_association: NONE
user: login: mentatbot[bot]
created_at: 2025-05-19T03:58:21Z
body: You are out of Mentat credits. You can view and refresh your credits [here](https://mentat.ai/settings).

<sub>To disable this message on pr open, disable `Always Review` on my [settings page](https://mentat.ai/gh/BibliothecaDAO/eternum/settings)</sub>
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-19T03:58:22Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces several improvements and refactors across the codebase. Key changes include enhanced null safety in user and room filtering utilities, a major refactor of world map entity detail components to use `react-query` for data fetching, updates to event streaming logic and UI, the addition of new CSS styles and animations, dependency updates, and new scripts and environment configurations.

## Changes

| File(s) | Change Summary |
|---------|---------------|
| `client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts`, `client/apps/game/src/ui/modules/ws-chat/utils/filterUtils.ts` | Added optional chaining to user and room property access in filtering functions to prevent errors from null or undefined values. |
| `client/apps/game/.env.mainnet` | Updated the `VITE_PUBLIC_TORII` environment variable to a new API endpoint URL. |
| `client/apps/game/package.json` | Added `@tanstack/react-query` and `metagame-sdk` dependencies; reordered dependency list. |
| `client/apps/game/src/index.css` | Introduced new styles, animations, and utility classes for buttons, scrollbars, gradients, clip paths, and text effects. |
| `client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx` | Refactored to use `react-query` for data fetching, replaced multiple `useEffect`/`useState` hooks with centralized memoized derived data, and simplified loading and error handling. |
| `client/apps/game/src/ui/components/worldmap/entities/structure-entity-detail.tsx` | Refactored to use a single `useQuery` for fetching and computing all structure-related data, removed manual state management, and updated UI rendering logic. |
| `client/apps/game/src/ui/modules/navigation/top-left-navigation.tsx` | Added a blank line before `<SecondaryMenuItems />` for formatting; no logic changes. |
| `client/apps/game/src/ui/modules/stream/constants.tsx` | Increased `EVENT_STREAM_SIZE` constant from 8 to 20. |
| `client/apps/game/src/ui/modules/stream/event-stream.tsx` | Enhanced event data with decoded names and entity IDs, added deduplication, adjusted UI layout, and updated event list size management. |
| `client/apps/game/src/ui/modules/stream/types.tsx` | Added `entityId` property to the `EventData` interface. |
| `client/apps/game/src/ui/modules/ws-chat/chat.tsx` | Applied optional chaining to user ID checks when rendering user lists and chat headers to prevent runtime errors. |
| `client/apps/game/src/ui/modules/ws-chat/useChatStore.ts` | Added console logs and null checks in `getUserIdByUsername` for safer user lookup. |
| `package.json` | Added a `dev:mainnet` script to run the game app in mainnet development mode. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant UI
    participant ReactQuery
    participant API

    UI->>ReactQuery: useQuery(fetchEntityData)
    ReactQuery->>API: Fetch explorer/structure data
    API-->>ReactQuery: Return data
    ReactQuery-->>UI: Provide data, loading, error states
    UI->>UI: Compute derivedData via useMemo
    UI->>UI: Render based on derivedData and loading state
```

```mermaid
sequenceDiagram
    participant EventStream
    participant API
    participant Utils

    EventStream->>API: Fetch event data
    API-->>EventStream: Return events
    EventStream->>Utils: Decode entity name, get entityId
    Utils-->>EventStream: Return name, entityId
    EventStream->>EventStream: Deduplicate events, update list
    EventStream->>UI: Render updated event stream
```

## Poem

> A bunny hops through code anew,  
> With queries fetched and styles that grew.  
> Events deduped, the scrollbars gleam,  
> Buttons shimmer, gradients beam.  
> Mainnet scripts and safety checks—  
> This rabbit’s work is quite complex!  
> 🐇✨

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>client/apps/eternum-mobile/src/features/ws-chat/lib/filterUtils.ts</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.26.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>client/apps/game/src/ui/components/worldmap/entities/army-entity-detail.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.26.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> <details>
> <summary>client/apps/game/src/index.css</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.26.0
> 
> ESLint couldn't find an eslint.config.(js|mjs|cjs) file.
> 
> From ESLint v9.0.0, the default configuration file is now eslint.config.js.
> If you are using a .eslintrc.* file, please follow the migration guide
> to update your configuration file to the new format:
> 
> https://eslint.org/docs/latest/use/configure/migration-guide
> 
> If you still have problems after following the migration guide, please stop by
> https://eslint.org/chat/help to chat with the team.
> 
> 
> </details>
> 
> + 8 others
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=BibliothecaDAO/eternum&utm_content=3294):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-19T04:00:01Z
body: Failed to generate code suggestions for PR
author_association: CONTRIBUTOR