Pilot: extract useInventoryFurni reducers to a pure module

The four useMessageEvent handlers in useInventoryFurniState (furniture
list add/update, list, removed, plus the dead post-it-placed listener)
were inlined as ~250 LOC of merge logic inside setGroupItems callbacks.
Three things change:

- The three meaningful reducers move to useInventoryFurni.reducers.ts
  as applyFurnitureListAddOrUpdate / applyFurnitureList /
  applyFurnitureListRemoved, plus two helpers clearUnseenFlags and
  refreshGroupItemsLocalization for the existing effect-driven mutations.
  Side effects (CreateLinkEvent, attemptItemPlacement, dispatchAdded)
  are passed in via a ctx object so the reducers stay easy to test.
- The module-level furniMsgFragments buffer becomes a useRef, removing
  a latent bug where two simultaneous client instances would have
  trampled each other's fragments.
- The empty FurniturePostItPlacedEvent handler is dropped (dead code).

useInventoryFurni still owns groupItems via useState so the existing
effect-driven setters (unseen flag reset, localization refresh) keep
working; the message handlers now call setGroupItems(prev =>
applyX(prev, event, ctx)) with the extracted reducers.
This commit is contained in:
simoleo89
2026-05-11 20:37:34 +02:00
parent 559d860a7b
commit 8b7bedf534
2 changed files with 244 additions and 205 deletions
@@ -0,0 +1,227 @@
import { CreateLinkEvent, FurnitureListAddOrUpdateEvent, FurnitureListEvent, FurnitureListItemParser, FurnitureListRemovedEvent } from '@nitrots/nitro-renderer';
import { CloneObject, FurnitureItem, GroupItem, UnseenItemCategory, addFurnitureItem, attemptItemPlacement, cancelRoomObjectPlacement, getAllItemIds, getPlacingItemId, mergeFurniFragments } from '../../api';
/**
* Pure reducers for furniture inventory state. Each takes the current
* GroupItem[] state plus the inbound event plus a context object carrying
* the cross-cutting helpers (unseen tracker, ui-event dispatcher).
*
* Side effects (CreateLinkEvent, attemptItemPlacement, dispatchAdded,
* cancelRoomObjectPlacement) are intentionally kept here to preserve the
* exact behavior of the original useInventoryFurni — they fire when the
* state transition demands them. The original code embedded them inside
* setGroupItems(prev => ...) and we mirror that.
*/
export interface FurniReducerContext {
isUnseen: (category: number, id: number) => boolean;
dispatchAdded: (id: number, type: number, category: number) => void;
fragments: { current: Map<number, FurnitureListItemParser>[] | null };
}
export const applyFurnitureListAddOrUpdate = (
state: GroupItem[],
event: FurnitureListAddOrUpdateEvent,
ctx: FurniReducerContext
): GroupItem[] =>
{
const parser = event.getParser();
const newValue = [ ...state ];
for(const item of parser.items)
{
let i = 0;
let groupItem: GroupItem = null;
while(i < newValue.length)
{
const group = newValue[i];
let j = 0;
while(j < group.items.length)
{
const furniture = group.items[j];
if(furniture.id === item.itemId)
{
furniture.update(item);
const newFurniture = [ ...group.items ];
newFurniture[j] = furniture;
group.items = newFurniture;
groupItem = group;
break;
}
j++;
}
if(groupItem) break;
i++;
}
if(groupItem)
{
groupItem.hasUnseenItems = true;
newValue[i] = CloneObject(groupItem);
}
else
{
const furniture = new FurnitureItem(item);
addFurnitureItem(newValue, furniture, ctx.isUnseen(UnseenItemCategory.FURNI, item.itemId));
ctx.dispatchAdded(furniture.id, furniture.type, furniture.category);
}
}
return newValue;
};
export const applyFurnitureList = (
state: GroupItem[],
event: FurnitureListEvent,
ctx: FurniReducerContext
): GroupItem[] =>
{
const parser = event.getParser();
if(!ctx.fragments.current) ctx.fragments.current = new Array(parser.totalFragments);
const fragment = mergeFurniFragments(parser.fragment, parser.totalFragments, parser.fragmentNumber, ctx.fragments.current);
if(!fragment) return state;
const newValue = [ ...state ];
const existingIds = getAllItemIds(newValue);
for(const existingId of existingIds)
{
if(fragment.get(existingId)) continue;
let index = 0;
while(index < newValue.length)
{
const group = newValue[index];
const item = group.remove(existingId);
if(!item)
{
index++;
continue;
}
if(getPlacingItemId() === item.ref)
{
cancelRoomObjectPlacement();
if(!attemptItemPlacement(group))
{
CreateLinkEvent('inventory/show');
}
}
if(group.getTotalCount() <= 0)
{
newValue.splice(index, 1);
group.dispose();
}
break;
}
}
for(const itemId of fragment.keys())
{
if(existingIds.indexOf(itemId) >= 0) continue;
const parserItem = fragment.get(itemId);
if(!parserItem) continue;
const item = new FurnitureItem(parserItem);
addFurnitureItem(newValue, item, ctx.isUnseen(UnseenItemCategory.FURNI, itemId));
ctx.dispatchAdded(item.id, item.type, item.category);
}
ctx.fragments.current = null;
return newValue;
};
export const applyFurnitureListRemoved = (
state: GroupItem[],
event: FurnitureListRemovedEvent
): GroupItem[] =>
{
const parser = event.getParser();
const newValue = [ ...state ];
let index = 0;
while(index < newValue.length)
{
const group = newValue[index];
const item = group.remove(parser.itemId);
if(!item)
{
index++;
continue;
}
if(getPlacingItemId() === item.ref)
{
cancelRoomObjectPlacement();
if(!attemptItemPlacement(group)) CreateLinkEvent('inventory/show');
}
if(group.getTotalCount() <= 0)
{
newValue.splice(index, 1);
group.dispose();
}
break;
}
return newValue;
};
export const clearUnseenFlags = (state: GroupItem[]): GroupItem[] =>
{
const newValue = [ ...state ];
for(const newGroup of newValue) newGroup.hasUnseenItems = false;
return newValue;
};
export const refreshGroupItemsLocalization = (state: GroupItem[]): GroupItem[] =>
{
if(!state?.length) return state;
return state.map(groupItem =>
{
const nextGroupItem = groupItem.clone();
nextGroupItem.refreshLocalization();
return nextGroupItem;
});
};
+17 -205
View File
@@ -1,19 +1,19 @@
import { CreateLinkEvent, FurnitureListAddOrUpdateEvent, FurnitureListComposer, FurnitureListEvent, FurnitureListInvalidateEvent, FurnitureListItemParser, FurnitureListRemovedEvent, FurniturePostItPlacedEvent } from '@nitrots/nitro-renderer';
import { useEffect, useState } from 'react';
import { FurnitureListAddOrUpdateEvent, FurnitureListComposer, FurnitureListEvent, FurnitureListInvalidateEvent, FurnitureListItemParser, FurnitureListRemovedEvent } from '@nitrots/nitro-renderer';
import { useEffect, useRef, useState } from 'react';
import { useBetween } from 'use-between';
import { CloneObject, DispatchUiEvent, FurnitureItem, GroupItem, SendMessageComposer, UnseenItemCategory, addFurnitureItem, attemptItemPlacement, cancelRoomObjectPlacement, getAllItemIds, getPlacingItemId, mergeFurniFragments } from '../../api';
import { DispatchUiEvent, GroupItem, SendMessageComposer, UnseenItemCategory } from '../../api';
import { InventoryFurniAddedEvent } from '../../events';
import { useMessageEvent } from '../events';
import { useSharedVisibility } from '../useSharedVisibility';
import { useInventoryUnseenTracker } from './useInventoryUnseenTracker';
let furniMsgFragments: Map<number, FurnitureListItemParser>[] = null;
import { applyFurnitureList, applyFurnitureListAddOrUpdate, applyFurnitureListRemoved, clearUnseenFlags, FurniReducerContext, refreshGroupItemsLocalization } from './useInventoryFurni.reducers';
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();
@@ -52,199 +52,30 @@ const useInventoryFurniState = () =>
return null;
};
const buildContext = (): FurniReducerContext => ({
isUnseen,
dispatchAdded: (id, type, category) => DispatchUiEvent(new InventoryFurniAddedEvent(id, type, category)),
fragments: fragmentsRef
});
useMessageEvent<FurnitureListAddOrUpdateEvent>(FurnitureListAddOrUpdateEvent, event =>
{
const parser = event.getParser();
setGroupItems(prevValue =>
{
const newValue = [ ...prevValue ];
for(const item of parser.items)
{
let i = 0;
let groupItem: GroupItem = null;
while(i < newValue.length)
{
const group = newValue[i];
let j = 0;
while(j < group.items.length)
{
const furniture = group.items[j];
if(furniture.id === item.itemId)
{
furniture.update(item);
const newFurniture = [ ...group.items ];
newFurniture[j] = furniture;
group.items = newFurniture;
groupItem = group;
break;
}
j++;
}
if(groupItem) break;
i++;
}
if(groupItem)
{
groupItem.hasUnseenItems = true;
newValue[i] = CloneObject(groupItem);
}
else
{
const furniture = new FurnitureItem(item);
addFurnitureItem(newValue, furniture, isUnseen(UnseenItemCategory.FURNI, item.itemId));
DispatchUiEvent(new InventoryFurniAddedEvent(furniture.id, furniture.type, furniture.category));
}
}
return newValue;
});
setGroupItems(prev => applyFurnitureListAddOrUpdate(prev, event, buildContext()));
});
useMessageEvent<FurnitureListEvent>(FurnitureListEvent, event =>
{
const parser = event.getParser();
if(!furniMsgFragments) furniMsgFragments = new Array(parser.totalFragments);
const fragment = mergeFurniFragments(parser.fragment, parser.totalFragments, parser.fragmentNumber, furniMsgFragments);
if(!fragment) return;
setGroupItems(prevValue =>
{
const newValue = [ ...prevValue ];
const existingIds = getAllItemIds(newValue);
for(const existingId of existingIds)
{
if(fragment.get(existingId)) continue;
let index = 0;
while(index < newValue.length)
{
const group = newValue[index];
const item = group.remove(existingId);
if(!item)
{
index++;
continue;
}
if(getPlacingItemId() === item.ref)
{
cancelRoomObjectPlacement();
if(!attemptItemPlacement(group))
{
CreateLinkEvent('inventory/show');
}
}
if(group.getTotalCount() <= 0)
{
newValue.splice(index, 1);
group.dispose();
}
break;
}
}
for(const itemId of fragment.keys())
{
if(existingIds.indexOf(itemId) >= 0) continue;
const parser = fragment.get(itemId);
if(!parser) continue;
const item = new FurnitureItem(parser);
addFurnitureItem(newValue, item, isUnseen(UnseenItemCategory.FURNI, itemId));
DispatchUiEvent(new InventoryFurniAddedEvent(item.id, item.type, item.category));
}
return newValue;
});
furniMsgFragments = null;
setGroupItems(prev => applyFurnitureList(prev, event, buildContext()));
});
useMessageEvent<FurnitureListInvalidateEvent>(FurnitureListInvalidateEvent, event =>
useMessageEvent<FurnitureListInvalidateEvent>(FurnitureListInvalidateEvent, () =>
{
setNeedsUpdate(true);
});
useMessageEvent<FurnitureListRemovedEvent>(FurnitureListRemovedEvent, event =>
{
const parser = event.getParser();
setGroupItems(prevValue =>
{
const newValue = [ ...prevValue ];
let index = 0;
while(index < newValue.length)
{
const group = newValue[index];
const item = group.remove(parser.itemId);
if(!item)
{
index++;
continue;
}
if(getPlacingItemId() === item.ref)
{
cancelRoomObjectPlacement();
if(!attemptItemPlacement(group)) CreateLinkEvent('inventory/show');
}
if(group.getTotalCount() <= 0)
{
newValue.splice(index, 1);
group.dispose();
}
break;
}
return newValue;
});
});
useMessageEvent<FurniturePostItPlacedEvent>(FurniturePostItPlacedEvent, event =>
{
setGroupItems(prev => applyFurnitureListRemoved(prev, event));
});
useEffect(() =>
@@ -271,14 +102,7 @@ const useInventoryFurniState = () =>
{
if(resetCategory(UnseenItemCategory.FURNI))
{
setGroupItems(prevValue =>
{
const newValue = [ ...prevValue ];
for(const newGroup of newValue) newGroup.hasUnseenItems = false;
return newValue;
});
setGroupItems(prev => clearUnseenFlags(prev));
}
};
}, [ isVisible, resetCategory ]);
@@ -296,19 +120,7 @@ const useInventoryFurniState = () =>
{
const refreshFurnitureLocalization = () =>
{
setGroupItems(prevValue =>
{
if(!prevValue?.length) return prevValue;
return prevValue.map(groupItem =>
{
const nextGroupItem = groupItem.clone();
nextGroupItem.refreshLocalization();
return nextGroupItem;
});
});
setGroupItems(prev => refreshGroupItemsLocalization(prev));
setSelectedItem(prevValue =>
{