useInventoryFurni.ts 6.6 Ko
Télécharger le fichier
import {
FurnitureListAddOrUpdateEvent,
FurnitureListComposer,
FurnitureListEvent,
FurnitureListInvalidateEvent,
FurnitureListItemParser,
FurnitureListRemovedEvent,
FurnitureListRemoveMultipleEvent
} from '@octane/renderer';
import { useEffect, useRef, useState } from 'react';
import { registerSharedHook, useSharedHook } from '@/state/useSharedHook';
import { DispatchUiEvent, getGroupItemKey, GroupItem, mergeFurniFragments, SendMessageComposer, UnseenItemCategory } from '../../api';
import { InventoryFurniAddedEvent } from '../../events';
import { useMessageEvent } from '../events';
import { useSharedVisibility } from '../useSharedVisibility';
import {
applyFurnitureListAddOrUpdate,
applyFurnitureListRemoved,
applyFurnitureListRemovedMany,
applyMergedFurnitureList,
clearUnseenFlags,
FurniReducerContext,
refreshGroupItemsLocalization
} from './useInventoryFurni.reducers';
import { useInventoryUnseenTracker } from './useInventoryUnseenTracker';
const useInventoryFurniState = () => {
const [needsUpdate, setNeedsUpdate] = useState(true);
const [groupItems, setGroupItems] = useState<GroupItem[]>([]);
const [selectedItem, setSelectedItem] = useState<GroupItem>(null);
const fragmentsRef = useRef<Map<number, FurnitureListItemParser>[] | null>(null);
const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility();
const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker();
const getItemsByType = (type: number) => {
if (!groupItems || !groupItems.length) return;
return groupItems.filter((i) => i.type === type);
};
const getWallItemById = (id: number) => {
if (!groupItems || !groupItems.length) return;
for (const groupItem of groupItems) {
const item = groupItem.getItemById(id);
if (item && item.isWallItem) return groupItem;
}
return null;
};
const getFloorItemById = (id: number) => {
if (!groupItems || !groupItems.length) return;
for (const groupItem of groupItems) {
const item = groupItem.getItemById(id);
if (item && !item.isWallItem) return groupItem;
}
return null;
};
// The reducers queue side effects (placement cancel, "furni added" events). They run on the latest list here, in
// the event handler, and the result is committed once: inside a state updater React may run them twice.
const groupItemsRef = useRef(groupItems);
groupItemsRef.current = groupItems;
const commitGroupItems = (next: GroupItem[]) => {
if (next === groupItemsRef.current) return;
groupItemsRef.current = next;
setGroupItems(next);
};
const buildContext = (): FurniReducerContext => ({
isUnseen,
dispatchAdded: (id, type, category) => queueMicrotask(() => DispatchUiEvent(new InventoryFurniAddedEvent(id, type, category)))
});
useMessageEvent<FurnitureListAddOrUpdateEvent>(FurnitureListAddOrUpdateEvent, (event) => {
commitGroupItems(applyFurnitureListAddOrUpdate(groupItemsRef.current, event, buildContext()));
});
useMessageEvent<FurnitureListEvent>(FurnitureListEvent, (event) => {
const parser = event.getParser();
// Accumulate multi-fragment inventories HERE, in the event handler (runs once per packet),
// not inside the setState updater (React double-invokes updaters). Only when the last
// fragment completes the merge do we hand the finished map to the pure reducer.
if (!fragmentsRef.current) fragmentsRef.current = new Array(parser.totalFragments);
const merged = mergeFurniFragments(parser.fragment, parser.totalFragments, parser.fragmentNumber, fragmentsRef.current);
if (!merged) return;
fragmentsRef.current = null;
const context = buildContext();
commitGroupItems(applyMergedFurnitureList(groupItemsRef.current, merged, context));
});
useMessageEvent<FurnitureListInvalidateEvent>(FurnitureListInvalidateEvent, () => {
setNeedsUpdate(true);
});
useMessageEvent<FurnitureListRemovedEvent>(FurnitureListRemovedEvent, (event) => {
commitGroupItems(applyFurnitureListRemoved(groupItemsRef.current, event));
});
useMessageEvent<FurnitureListRemoveMultipleEvent>(FurnitureListRemoveMultipleEvent, (event) => {
const itemIds = event.getParser().stripIds;
commitGroupItems(applyFurnitureListRemovedMany(groupItemsRef.current, itemIds));
});
useEffect(() => {
if (!groupItems || !groupItems.length) return;
setSelectedItem((prevValue) => {
if (!prevValue) return groupItems[0];
// Same reference still present - keep it.
if (groupItems.indexOf(prevValue) !== -1) return prevValue;
// The reducers replace touched groups with clones, so the old reference goes stale on
// any inventory change. Reconcile by stable identity instead of dropping to groupItems[0],
// so the selection (and preview panel) doesn't jump to the first tile on every update.
const key = getGroupItemKey(prevValue);
const match = groupItems.find((group) => getGroupItemKey(group) === key);
return match ?? groupItems[0];
});
}, [groupItems]);
useEffect(() => {
if (!isVisible) return;
return () => {
if (resetCategory(UnseenItemCategory.FURNI)) {
setGroupItems((prev) => clearUnseenFlags(prev));
}
};
}, [isVisible, resetCategory]);
useEffect(() => {
if (!isVisible || !needsUpdate) return;
SendMessageComposer(new FurnitureListComposer());
setNeedsUpdate(false);
}, [isVisible, needsUpdate]);
useEffect(() => {
const refreshFurnitureLocalization = () => {
setGroupItems((prev) => refreshGroupItemsLocalization(prev));
setSelectedItem((prevValue) => {
if (!prevValue) return prevValue;
const nextGroupItem = prevValue.clone();
nextGroupItem.refreshLocalization();
return nextGroupItem;
});
};
window.addEventListener('octane-localization-updated', refreshFurnitureLocalization);
return () => window.removeEventListener('octane-localization-updated', refreshFurnitureLocalization);
}, []);
return { isVisible, groupItems, setGroupItems, selectedItem, setSelectedItem, activate, deactivate, getWallItemById, getFloorItemById, getItemsByType };
};
export const useInventoryFurni = () => useSharedHook(useInventoryFurniState);
registerSharedHook(useInventoryFurniState);
