Description
Bug description
When assigning stateview tags to my player class if the @view() tag is attached to a array schema that contains another type of class it's as if the whole class is marked with the state view().
My Player Class contains 3 "inventory" classes each of which contain the "inventory item" class. When the Player's inventory classes are tagged they operate as expected preventing any other client from viewing those inventory classes however then the inventory items inside of a buildings chest inventory disapears. However when I remove the view tag from those "inventory" classes while they are still added to other parts of the player schema the chest functions as normal. It's as if the tag is propogating to the entire class instead of just the objects used inside of the "inventory classes"
Player initialization
onJoin(client: any, options: any, auth: any): void {
this.state.players.set(client.sessionId, player);
client.view = new StateView();
client.view.add(player, 1);
client.view.add(player.inventory, 1)
client.view.add(player.pchest, 1)
client.view.add(player.equipment, 1)
//etc
Player Class
export class Player extends Schema {
// ...
@type("string") id: string; // Player ID (can be any unique identifier)
@view(1) @type(Inventory) inventory = new Inventory();
@view(1) @type(Inventory) pchest = new Inventory(20);
@view(1) @type("string") interactHook: string = "";
@view(1) @type(Equipment) equipment = new Equipment()
// ... etc
Inventory Class (methods included)
import { Schema, type, MapSchema} from '@colyseus/schema';
import { InventoryItem } from './inventoryitem';
export class Inventory extends Schema{
@type({ map: InventoryItem })
slots = new MapSchema<InventoryItem>();
@type('number') version : number;
// Total number of available slots.
MAX_SLOTS: number
/**
* Returns the first available slot index (0 .. MAX_SLOTS-1)
* or null if all slots are taken.
*/
constructor(slots: number = 41){
super();
this.version = 0;
this.MAX_SLOTS = slots;
}
getFreeSlot(): number | null {
for (let i = 0; i < this.MAX_SLOTS; i++) {
const key = i.toString();
// We cast to any to allow bracket notation.
/*
if (!((this.slots as any)[key])) {
return i;
}
*/
if (!this.slots.get(key)) {
return i;
}
}
return null;
}
Version(): void{
this.version ++;
if(this.version>50){
this.version = 0
}
}
/**
* Adds an item to the inventory.
* If targetSlot is provided, it will attempt to put it there (stacking if possible,
* or swapping if the slot is already occupied).
* Otherwise, it will first try to add to any partially filled stack (if stackable),
* then use the first free slot.
*/
addItem(newItem: InventoryItem, targetSlot?: number): void{
// --- If a specific slot is requested ---
if (targetSlot !== undefined) {
const key = targetSlot.toString();
//const slotItem: InventoryItem = (this.slots as any)[key];
const slotItem = this.slots.get(key);
if (!slotItem) {
//(this.slots as any)[key] = newItem;
this.slots.set(key, newItem);
return;
} else {
// If the slot is occupied and the items can stack, add to the stack.
if (
slotItem.name === newItem.name &&
slotItem.maxStack > 1 &&
slotItem.quantity < slotItem.maxStack
) {
const availableSpace = slotItem.maxStack - slotItem.quantity;
const addAmount = Math.min(availableSpace, newItem.quantity);
slotItem.quantity += addAmount;
newItem.quantity -= addAmount;
// If there’s any quantity left, try adding it normally.
if (newItem.quantity > 0) {
this.addItem(newItem);
}
return;
} else {
// Otherwise, if stacking isn’t possible, try swapping.
const freeSlot = this.getFreeSlot();
if (freeSlot !== null) {
this.slots.set(freeSlot.toString(), slotItem);
this.slots.set(key, newItem);
} else {
//console.log("No free slot to swap with at slot", targetSlot);
}
return; // No items added, but the original item remains.
}
}
}
// --- No specific slot requested: first try stacking ---
if (newItem.maxStack > 1) {
// Using MapSchema's forEach: note that we cannot break early.
this.slots.forEach((slotItem: InventoryItem, key: string) => {
if (
slotItem &&
slotItem.name === newItem.name &&
slotItem.maxStack > 1 &&
slotItem.quantity < slotItem.maxStack &&
newItem.quantity > 0
) {
const space = slotItem.maxStack - slotItem.quantity;
const toAdd = Math.min(space, newItem.quantity);
slotItem.quantity += toAdd;
newItem.quantity -= toAdd;
}
});
}
// --- Place any remaining quantity into free slots ---
while (newItem.quantity > 0) {
const freeSlot = this.getFreeSlot();
if (freeSlot === null) {
//console.log("Inventory full. Could not add all items; remaining:", newItem.quantity);
break;
}
// For stackable items, add up to the maxStack amount per slot; otherwise add one.
const toAdd = newItem.maxStack > 1 ? Math.min(newItem.maxStack, newItem.quantity) : 1;
this.slots.set(freeSlot.toString(), new InventoryItem(
newItem.name,
toAdd,
newItem.durability,
newItem.maxDurability,
newItem.maxStack,
newItem.equipSlot
));
newItem.quantity -= toAdd;
}
this.Version();
return;
}
/**
* Moves an item from one slot to another.
* - If the target slot is empty, the item is moved.
* - If the target slot contains the same stackable item and there’s room, the stacks merge.
* - Otherwise, the items are swapped.
*/
moveItem(fromSlot: number, toSlot: number): void {
// Special case for picking up an item (-1 destination)
if (toSlot === -1) {
return;
}
const fromKey = fromSlot.toString();
const toKey = toSlot.toString();
const fromItem = this.slots.get(fromKey);
const toItem = this.slots.get(toKey);
if (!fromItem) {
return;
}
this.Version();
// Case 1: Empty destination slot
if (!toItem) {
// Create a NEW item with the same properties instead of moving the reference
const newItem = new InventoryItem(
fromItem.name,
fromItem.quantity,
fromItem.durability,
fromItem.maxDurability,
fromItem.maxStack,
fromItem.equipSlot
);
// Set the new item in the destination slot
this.slots.set(toKey, newItem);
// Remove the original item
this.slots.delete(fromKey);
return;
}
// Case 2: Same item type, stackable
if (
fromItem.name === toItem.name &&
fromItem.maxStack > 1 &&
toItem.quantity < toItem.maxStack
) {
const space = toItem.maxStack - toItem.quantity;
const toMove = Math.min(space, fromItem.quantity);
toItem.quantity += toMove;
fromItem.quantity -= toMove;
if (fromItem.quantity === 0) {
this.slots.delete(fromKey);
}
return;
}
// Case 3: Different item types or not stackable - swap them
// Create NEW items for the swap
const newToItem = new InventoryItem(
fromItem.name,
fromItem.quantity,
fromItem.durability,
fromItem.maxDurability,
fromItem.maxStack,
fromItem.equipSlot
);
const newFromItem = new InventoryItem(
toItem.name,
toItem.quantity,
toItem.durability,
toItem.maxDurability,
toItem.maxStack,
toItem.equipSlot
);
// First remove both items to avoid reference collisions
this.slots.delete(fromKey);
this.slots.delete(toKey);
// Then add the new items
this.slots.set(toKey, newToItem);
this.slots.set(fromKey, newFromItem);
}
/**
* Returns the total quantity of a given item across all inventory slots.
*/
getTotalQuantity(itemName: string): number {
let total = 0;
this.slots.forEach((slot: InventoryItem) => {
if (slot.name === itemName) {
total += slot.quantity;
}
});
return total;
}
removeItem(itemName: string, quantity: number): boolean {
// First, check that there is enough total quantity.
const total = this.getTotalQuantity(itemName);
if (total < quantity) {
return false;
}
this.Version();
// Iterate over all slots using forEach.
// forEach doesn’t support breaking out early, so we simply check if nothing remains to remove.
this.slots.forEach((slot: InventoryItem, key: string) => {
if (quantity <= 0) return; // Nothing left to remove.
if (slot.name === itemName) {
if (slot.quantity <= quantity) {
// Use up the entire stack.
quantity -= slot.quantity;
//delete (this.slots as any)[key];
this.slots.delete(key);
} else {
// Remove only part of the stack.
slot.quantity -= quantity;
quantity = 0;
}
}
});
return true;
}
removeSlot(itemslot: number): boolean{
const itemKey = itemslot.toString();
const item = this.slots.get(itemKey);
if(!item){
return false
}
this.Version();
this.slots.delete(itemKey);
return true
}
}
InventoryItem Class
import { Schema, type} from '@colyseus/schema';
import ITEMS from '../data/items/items.json';
export class InventoryItem extends Schema {
// Item identifier (e.g. "wood", "stone", "sword")
@type("string")
name: string;
// How many of this item are in this stack.
@type("number")
quantity: number;
// For items with durability (e.g. tools, weapons). If not used, set to 0.
@type("number")
durability: number;
// Maximum durability (if applicable).
@type("number")
maxDurability: number;
// Maximum number allowed per stack. For example, wood may be 999, while a sword might be 1.
maxStack: number;
//does it equip to any certain slots:
@type("string")
equipSlot: string;
constructor(
name: string,
quantity: number | null,
durability: number | null = null,
maxDurability: number | null = null,
maxStack: number | null = null,
equipSlot: string | null = null,
) {
super();
this.name = name;
this.quantity = quantity ?? 1;
this.maxDurability = maxDurability ?? (ITEMS[name as keyof typeof ITEMS] ?? {maxDurability: 0}).maxDurability;
this.durability = durability ?? this.maxDurability;
this.maxStack = (ITEMS[name as keyof typeof ITEMS] ?? {maxStack: 999}).maxStack;
this.equipSlot = (ITEMS[name as keyof typeof ITEMS] ?? {equipSlot: ""}).equipSlot;
//modify item stats based on name
}
}
Building Class
import { Schema, type} from '@colyseus/schema';
import type { SpatialHashing } from './spatialhashing';
import { repairbox, chest } from './buildingPlugins';
export class Building extends Schema {
@type("string") id: string;
@type( chest ) chest?: chest;
chest Class
export class chest extends Schema {
@type(Inventory) inventory = new Inventory();
safe: boolean = false
}
The equipment class operates similarly to the inventory class
Optional: Minimal reproduction
No response