From 5d31563b71776cbcec70ec1ba177030cffaa8c10 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sun, 5 Apr 2026 14:49:19 +0300 Subject: [PATCH 1/9] fix weird sneaking behaviour? --- .../core/src/handlers/player-auth-input.ts | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/core/src/handlers/player-auth-input.ts b/packages/core/src/handlers/player-auth-input.ts index 83cbd634..14d83ec8 100644 --- a/packages/core/src/handlers/player-auth-input.ts +++ b/packages/core/src/handlers/player-auth-input.ts @@ -326,81 +326,80 @@ class PlayerAuthInputHandler extends NetworkHandler { // Handle when a player sneaks case InputData.StartSneaking: case InputData.StopSneaking: { - // Get the sneaking flag from the player + // Derive the target sneaking state from the input action instead of + // blindly toggling. The client can send explicit start/stop actions + // that must remain in sync with placement/interact rules. const sneaking = player.flags.getActorFlag(ActorFlag.Sneaking) ?? false; + const nextSneaking = action === InputData.StartSneaking; - // Check if the player is already sneaking - if (sneaking === true) { - // Signal the player to stop sneaking + if (sneaking === nextSneaking) break; + + if (nextSneaking) { for (const trait of player.traits.values()) - trait.onStopSneaking?.(); + trait.onStartSneaking?.(); } else { - // Signal the player to start sneaking for (const trait of player.traits.values()) - trait.onStartSneaking?.(); + trait.onStopSneaking?.(); } - // Set the sneaking flag based on the action - player.flags.setActorFlag(ActorFlag.Sneaking, !sneaking); + player.flags.setActorFlag(ActorFlag.Sneaking, nextSneaking); break; } // Handle when a player sprints case InputData.StartSprinting: case InputData.StopSprinting: { - // Get the sprinting flag from the player const sprinting = player.flags.getActorFlag(ActorFlag.Sprinting) ?? false; + const nextSprinting = action === InputData.StartSprinting; - // Check if the player is already sprinting - if (sprinting === true) { - // Signal the player to stop sprinting + if (sprinting === nextSprinting) break; + + if (nextSprinting) { for (const trait of player.traits.values()) - trait.onStopSprinting?.(); + trait.onStartSprinting?.(); } else { - // Signal the player to start sprinting for (const trait of player.traits.values()) - trait.onStartSprinting?.(); + trait.onStopSprinting?.(); } - // Set the sprinting flag based on the action - player.flags.setActorFlag(ActorFlag.Sprinting, !sprinting); + player.flags.setActorFlag(ActorFlag.Sprinting, nextSprinting); break; } // Handle then a player swims case InputData.StartSwimming: case InputData.StopSwimming: { - // Get the swimming flag from the player const swimming = player.flags.getActorFlag(ActorFlag.Swimming) ?? false; + const nextSwimming = action === InputData.StartSwimming; - // Set the swimming flag based on the action - player.flags.setActorFlag(ActorFlag.Swimming, !swimming); + if (swimming === nextSwimming) break; + player.flags.setActorFlag(ActorFlag.Swimming, nextSwimming); break; } // Handle when a player crawls case InputData.StartCrawling: case InputData.StopCrawling: { - // Get the crawling flag from the player const crawling = player.flags.getActorFlag(ActorFlag.Crawling) ?? false; + const nextCrawling = action === InputData.StartCrawling; - // Set the crawling flag based on the action - player.flags.setActorFlag(ActorFlag.Crawling, !crawling); + if (crawling === nextCrawling) break; + player.flags.setActorFlag(ActorFlag.Crawling, nextCrawling); break; } // Handle when a player is gliding case InputData.StartGliding: case InputData.StopGliding: { - // Get the gliding flag from the player const gliding = player.flags.getActorFlag(ActorFlag.Gliding) ?? false; + const nextGliding = action === InputData.StartGliding; - // Set the gliding flag based on the action - player.flags.setActorFlag(ActorFlag.Gliding, !gliding); + if (gliding === nextGliding) break; + player.flags.setActorFlag(ActorFlag.Gliding, nextGliding); break; } From 64866d8184012b1eee1f56575212fb67238117e4 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sun, 5 Apr 2026 14:50:45 +0300 Subject: [PATCH 2/9] Block interactions in invalid container spaces --- .../core/src/handlers/item-stack-request.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/core/src/handlers/item-stack-request.ts b/packages/core/src/handlers/item-stack-request.ts index 60ee4df2..e644574e 100644 --- a/packages/core/src/handlers/item-stack-request.ts +++ b/packages/core/src/handlers/item-stack-request.ts @@ -24,6 +24,7 @@ import { } from "../events"; import { ItemStack } from "../item"; import { NetworkHandler } from "../network"; +import { Container } from ".."; class ItemStackRequestHandler extends NetworkHandler { public static readonly packet = Packet.ItemStackRequest; @@ -209,6 +210,12 @@ class ItemStackRequestHandler extends NetworkHandler { return null; } + // Reject invalid placements and restore the source item before returning. + if (!this.canPlaceItem(destination, destinationSlot, item)) { + source.addItem(item); + return null; + } + // Get the destination item stack. const destinationStack = destination.getItem(destinationSlot); @@ -236,6 +243,41 @@ class ItemStackRequestHandler extends NetworkHandler { ]; } + private canPlaceItem( + container: Container | null, + slot: number, + item: ItemStack + ): boolean { + if (!container) return false; + if (!this.canAccessSlot(container, slot)) return false; + + const existingItem = container.getItem(slot); + if (!existingItem) return true; + if (!existingItem.equals(item)) return false; + + return existingItem.getStackSize() + item.getStackSize() <= existingItem.maxStackSize; + } + + private canSwapItems( + source: Container | null, + sourceSlot: number, + destination: Container | null, + destinationSlot: number + ): boolean { + if (!source || !destination) return false; + if (!this.canAccessSlot(source, sourceSlot)) return false; + if (!this.canAccessSlot(destination, destinationSlot)) return false; + + return source.getItem(sourceSlot) !== null; + } + + private canAccessSlot( + container: Container | null, + slot: number + ): boolean { + return !!container && slot >= 0 && slot < container.getSize(); + } + private handleSwapAction( player: Player, action: ItemStackRequestActionSwap @@ -270,6 +312,10 @@ class ItemStackRequestHandler extends NetworkHandler { ); if (!signal.emit()) return null; + if (!this.canSwapItems(source, sourceSlot, destination, destinationSlot)) { + return null; + } + // Swap the items in the source and destination containers. source.swapItems(sourceSlot, destinationSlot, destination); From 1f529be52b3e7c244c6882ae8bfed678359ab621 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sun, 5 Apr 2026 14:58:41 +0300 Subject: [PATCH 3/9] So many damn container changes related to transactions --- .gitignore | 2 + packages/core/src/block/container.ts | 47 +++- packages/core/src/block/traits/chest.ts | 232 +++++++++++++----- packages/core/src/block/traits/inventory.ts | 37 ++- packages/core/src/block/traits/trait.ts | 10 +- packages/core/src/container.ts | 17 +- packages/core/src/entity/container.ts | 4 +- .../src/events/player-opened-container.ts | 4 +- .../src/handlers/inventory-transaction.ts | 6 +- 9 files changed, 266 insertions(+), 93 deletions(-) diff --git a/.gitignore b/.gitignore index 0b819e44..298cda36 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ devapp/permissions.json devapp/resource_packs devapp/behavior_packs +bds + # Swap the comments on the following lines if you don't wish to use zero-installs # Documentation here: https://yarnpkg.com/features/zero-installs # !.yarn/cache diff --git a/packages/core/src/block/container.ts b/packages/core/src/block/container.ts index debebf89..91194dda 100644 --- a/packages/core/src/block/container.ts +++ b/packages/core/src/block/container.ts @@ -1,15 +1,16 @@ import { + BlockPosition, + type IPosition, ContainerId, ContainerOpenPacket, ContainerType } from "@serenityjs/protocol"; import { Container } from "../container"; -import { ItemStack } from "../item"; -import { Player } from "../entity"; -import { PlayerOpenedContainerSignal } from "../events"; - -import { Block } from "./block"; +import { ItemStack } from "../item/stack"; +import { Player } from "../entity/player"; +import { PlayerOpenedContainerSignal } from "../events/player-opened-container"; +import type { Block } from "./block"; class BlockContainer extends Container { /** @@ -37,6 +38,19 @@ class BlockContainer extends Container { // Call the original update method super.update(); + // Call the onContainerUpdate method for the block traits + this.notifyTraits(); + } + + public updateSlot(slot: number): void { + // Call the original updateSlot method + super.updateSlot(slot); + + // Call the onContainerUpdate method for the block traits + this.notifyTraits(); + } + + private notifyTraits(): void { // Call the onContainerUpdate method for the block traits for (const trait of this.block.getAllTraits()) { try { @@ -56,7 +70,7 @@ class BlockContainer extends Container { } } - public show(player: Player): number { + public show(player: Player, position: IPosition = this.block.position): number { // Create a new PlayerOpenedContainerSignal const signal = new PlayerOpenedContainerSignal(player, this); @@ -72,9 +86,11 @@ class BlockContainer extends Container { // Assign the properties packet.identifier = identifier; packet.type = this.type; - packet.position = this.block.position; - packet.uniqueId = - this.type === ContainerType.Container ? -1n : player.uniqueId; + packet.position = position instanceof BlockPosition + ? position + : new BlockPosition(position.x, position.y, position.z); + // Vanilla/BDS uses -1 for block container windows, including hoppers. + packet.uniqueId = -1n; // Send the packet to the player player.send(packet); @@ -82,6 +98,19 @@ class BlockContainer extends Container { // Update the container this.update(); + // Some block container UIs (such as furnaces) ignore the initial full + // content snapshot until the window exists client-side. Re-send the slots + // on the next tick once the container is definitely open. dunno why its done like this here but i aint rewriting all that + if (this.type !== ContainerType.Container) { + this.block.dimension.schedule(1).on(() => { + if (!this.getAllOccupants().some(([occupant]) => occupant === player)) return; + + for (let slot = 0; slot < this.getSize(); slot++) { + this.updateSlot(slot); + } + }); + } + // Return the container identifier return identifier; } diff --git a/packages/core/src/block/traits/chest.ts b/packages/core/src/block/traits/chest.ts index 3a7cf273..9f90b8ba 100644 --- a/packages/core/src/block/traits/chest.ts +++ b/packages/core/src/block/traits/chest.ts @@ -1,4 +1,5 @@ import { + AbilityIndex, BlockEventPacket, BlockEventType, BlockPosition, @@ -7,7 +8,12 @@ import { } from "@serenityjs/protocol"; import { IntTag } from "@serenityjs/nbt"; -import { Block, BlockDestroyOptions, BlockInteractionOptions } from "../.."; +import { + Block, + BlockDestroyOptions, + BlockInteractionOptions, + BlockPlacementOptions +} from "../.."; import { BlockIdentifier } from "../../enums"; import { BlockInventoryTrait } from "./inventory"; @@ -19,6 +25,45 @@ class BlockChestTrait extends BlockInventoryTrait { BlockIdentifier.TrappedChest ]; + public static shouldBePairParent( + current: BlockPosition, + other: BlockPosition, + direction: string + ): boolean { + switch (direction) { + case "north": + return current.x > other.x; + case "south": + return current.x < other.x; + case "east": + return current.z > other.z; + case "west": + return current.z < other.z; + default: + if (current.x !== other.x) { + return current.x < other.x; + } + + return current.z < other.z; + } + } + + public static getPairCandidates( + block: Block, + direction: string + ): Array { + switch (direction) { + case "north": + case "south": + return [block.east(), block.west()]; + case "east": + case "west": + return [block.north(), block.south()]; + default: + return []; + } + } + public isPaired(): boolean { // Get the pairx and pairz storage entries const pairx = this.block.getStorageEntry("pairx"); @@ -78,32 +123,87 @@ class BlockChestTrait extends BlockInventoryTrait { this.container.setSize(isParent ? 54 : 27); } - public onUpdate(source?: Block): void { - // If there is no source, return - if (!source || source === this.block) return; + public onPlace(_options?: BlockPlacementOptions): void {} + + public onUpdate(_source?: Block): void { + // Preserve saved pairing and only repair invalid state. Double chests + // should pair when placed, not opportunistically on later world updates. + if (!this.isPaired()) return; + + const pairedPos = this.getPaired(); + if (!pairedPos) return this.clearPaired(); + + const paired = this.dimension.getBlock(pairedPos); + if (!paired.hasTrait(BlockChestTrait)) return this.clearPaired(); + + const trait = paired.getTrait(BlockChestTrait); + if (!trait.isPaired()) return this.clearPaired(); + + const reciprocal = trait.getPaired(); + if (!reciprocal || !reciprocal.equals(this.block.position)) { + return this.clearPaired(); + } + + const direction = this.block.getState("minecraft:cardinal_direction"); + const sourceDirection = paired.getState("minecraft:cardinal_direction"); + if (direction !== sourceDirection) { + this.clearPaired(); + trait.clearPaired(); + return; + } + + const validPairPositions = BlockChestTrait.getPairCandidates( + this.block, + String(direction) + ); + if (!validPairPositions.some((block) => block.position.equals(paired.position))) { + this.clearPaired(); + trait.clearPaired(); + return; + } + } + + public pairAfterPlacement(): void { + if (this.isPaired()) { + const pairedPos = this.getPaired(); + if (pairedPos) { + const paired = this.dimension.getBlock(pairedPos); + if ( + paired.hasTrait(BlockChestTrait) && + paired.getTrait(BlockChestTrait).getPaired()?.equals(this.block.position) + ) { + return; + } + } - // Check that the blocks are adjacent. - if (this.block.position.y !== source.position.y) return; + this.clearPaired(); + } - if (source.hasTrait(BlockChestTrait) && !this.isPaired()) { - // Get the chest trait from the source block - const trait = source.getTrait(BlockChestTrait); + const direction = this.block.getState("minecraft:cardinal_direction"); - // If the source chest is paired, return - if (!trait || trait?.isPaired()) return; + for (const neighbor of BlockChestTrait.getPairCandidates( + this.block, + String(direction) + )) { + if (!neighbor.hasTrait(BlockChestTrait)) continue; - // Verify that they are facing the same direction - const direction = this.block.getState("minecraft:cardinal_direction"); - const sourceDirection = source.getState("minecraft:cardinal_direction"); - if (direction !== sourceDirection) return; + const trait = neighbor.getTrait(BlockChestTrait); + if (!trait || trait.isPaired()) continue; - // Set the pairing - this.setPaired(source.position); + const sourceDirection = neighbor.getState("minecraft:cardinal_direction"); + if (direction !== sourceDirection) continue; + + this.setPaired(neighbor.position); trait.setPaired(this.block.position); - // Set the parent/child relationship - this.setIsPairParent(true); - trait.setIsPairParent(false); + const isParent = BlockChestTrait.shouldBePairParent( + this.block.position, + neighbor.position, + String(direction) + ); + this.setIsPairParent(isParent); + trait.setIsPairParent(!isParent); + return; } } @@ -181,31 +281,25 @@ class BlockChestTrait extends BlockInventoryTrait { super.onBreak(options); } - public onInteract({ cancel, origin }: BlockInteractionOptions): void { + public onInteract(options: BlockInteractionOptions): void { + const { cancel, origin } = options; if (cancel || !origin) return; if ( - this.isPaired() && - this.getIsPairParent() && - this.container.getSize() !== 54 + origin.isSneaking || + !origin.abilities.getAbility(AbilityIndex.OpenContainers) ) { - // Update the container size - this.container.setSize(54); + return; } - // Check if the chest is paired and this chest is the child - if (this.isPaired() && !this.getIsPairParent()) { - // Get the paired block from the dimension - const paired = this.dimension.getBlock(this.getPaired()!); - - // Get the chest trait from the paired block - const trait = paired.getTrait(BlockChestTrait); - - // Show the paired chest's container to the player - trait.onInteract({ cancel, origin }); - } else { - super.onInteract({ cancel, origin }); + const trait = this.getSharedContainerTrait(); + if (trait.isPaired() && trait.container.getSize() !== 54) { + trait.container.setSize(54); } + + // Open the canonical double chest container, but keep the clicked half as + // the window anchor so the client opens the correct block position. + trait.container.show(origin, this.block.position); } public onOpen(silent?: boolean): void { @@ -236,28 +330,15 @@ class BlockChestTrait extends BlockInventoryTrait { // If silent is true, return if (silent) return; - // Create a new BlockEventPacket - const event = new BlockEventPacket(); - event.position = this.block.position; - event.type = BlockEventType.ChangeState; - event.data = 1; + this.broadcastChestState(1, LevelSoundEvent.ChestOpen); - // Create a new LevelSoundEventPacket - const sound = new LevelSoundEventPacket(); - sound.position = BlockPosition.toVector3f(this.block.position); - sound.event = LevelSoundEvent.ChestOpen; - sound.data = this.block.permutation.networkId; - sound.actorIdentifier = String(); - sound.isBabyMob = false; - sound.isGlobal = false; - sound.uniqueActorId = -1n; - - // Broadcast the packets to the dimension - this.dimension.broadcast(event, sound); + if (this.isPaired()) { + const paired = this.dimension.getBlock(this.getPaired()!); + this.broadcastChestState(1, LevelSoundEvent.ChestOpen, paired); + } } public onClose(silent?: boolean): void { - // Check if the chest is paired and is the parent if (this.isPaired() && this.getIsPairParent()) { // Get the paired block from the dimension const paired = this.dimension.getBlock(this.getPaired()!); @@ -284,25 +365,48 @@ class BlockChestTrait extends BlockInventoryTrait { // If silent is true, return if (silent) return; - // Create a new BlockEventPacket + this.broadcastChestState(0, LevelSoundEvent.ChestClosed); + + if (this.isPaired()) { + const paired = this.dimension.getBlock(this.getPaired()!); + this.broadcastChestState(0, LevelSoundEvent.ChestClosed, paired); + } + } + + private broadcastChestState( + data: number, + soundEvent: LevelSoundEvent, + block: Block = this.block + ): void { const event = new BlockEventPacket(); - event.position = this.block.position; + event.position = block.position; event.type = BlockEventType.ChangeState; - event.data = 0; + event.data = data; - // Create a new LevelSoundEventPacket const sound = new LevelSoundEventPacket(); - sound.position = BlockPosition.toVector3f(this.block.position); - sound.event = LevelSoundEvent.ChestClosed; - sound.data = this.block.permutation.networkId; + sound.position = BlockPosition.toVector3f(block.position); + sound.event = soundEvent; + sound.data = block.permutation.networkId; sound.actorIdentifier = String(); sound.isBabyMob = false; sound.isGlobal = false; sound.uniqueActorId = -1n; - // Broadcast the packets to the dimension this.dimension.broadcast(event, sound); } + + private getSharedContainerTrait(): BlockChestTrait { + if (!this.isPaired() || this.getIsPairParent()) return this; + + const paired = this.getPaired(); + if (!paired) return this; + + const block = this.dimension.getBlock(paired); + if (!block.hasTrait(BlockChestTrait)) return this; + + const trait = block.getTrait(BlockChestTrait); + return trait.getIsPairParent() ? trait : this; + } } export { BlockChestTrait }; diff --git a/packages/core/src/block/traits/inventory.ts b/packages/core/src/block/traits/inventory.ts index 374687b2..6806ca5a 100644 --- a/packages/core/src/block/traits/inventory.ts +++ b/packages/core/src/block/traits/inventory.ts @@ -7,13 +7,13 @@ import { import { CompoundTag, IntTag, ListTag } from "@serenityjs/nbt"; import { BlockContainer } from "../container"; -import { Block } from "../block"; -import { +import type { Block } from "../block"; +import type { BlockDestroyOptions, BlockInteractionOptions, BlockInventoryTraitOptions } from "../../types"; -import { ItemStack } from "../../item"; +import { ItemStack } from "../../item/stack"; import { BlockTrait } from "./trait"; @@ -42,7 +42,7 @@ class BlockInventoryTrait extends BlockTrait { this.container = new BlockContainer( block, options?.type ?? ContainerType.Container, - 27 + options?.size ?? 27 ); } @@ -169,6 +169,35 @@ class BlockInventoryTrait extends BlockTrait { this.block.setStorageEntry("Items", items); } + public onContainerUpdate(container: BlockContainer): void { + // Check if the container is not the inventory container + if (container !== this.container) return; + + // Create a new items list tag + const items = new ListTag(); + + // Iterate over the container slots + for (let i = 0; i < this.container.getSize(); i++) { + // Get the item stack at the index + const itemStack = this.container.getItem(i); + + // Check if the item is null + if (!itemStack) continue; + + // Get the item stack level storage + const storage = itemStack.getStorage(); + + // Create a new int tag for the slot + storage.add(new IntTag(i, "Slot")); + + // Add the item stack storage to the items list tag + items.push(storage); + } + + // Add the items to the items list tag + this.block.setStorageEntry("Items", items); + } + public onAdd(): void { // Check if block has an items nbt property if (this.block.hasStorageEntry("Items")) { diff --git a/packages/core/src/block/traits/trait.ts b/packages/core/src/block/traits/trait.ts index f7706d2c..e56cc3d5 100644 --- a/packages/core/src/block/traits/trait.ts +++ b/packages/core/src/block/traits/trait.ts @@ -1,15 +1,15 @@ -import { Player } from "../../entity"; +import type { Player } from "../../entity/player"; import { BlockIdentifier } from "../../enums"; import { Trait } from "../../trait"; -import { Block } from "../block"; -import { Container } from "../../container"; -import { +import type { Container } from "../../container"; +import type { BlockDestroyOptions, BlockInteractionOptions, BlockPlacementOptions, JSONLikeObject } from "../../types"; -import { Dimension } from "../../world"; +import type { Dimension } from "../../world/dimension"; +import type { Block } from "../block"; import type { BlockTypeComponent } from "../identity"; diff --git a/packages/core/src/container.ts b/packages/core/src/container.ts index d7064b5f..a4473f95 100644 --- a/packages/core/src/container.ts +++ b/packages/core/src/container.ts @@ -1,4 +1,5 @@ import { + ContainerName, ContainerClosePacket, ContainerId, ContainerType, @@ -8,11 +9,11 @@ import { NetworkItemStackDescriptor } from "@serenityjs/protocol"; -import { ItemStack } from "./item"; -import { EntityContainer, type Player } from "./entity"; +import { ItemStack } from "./item/stack"; +import type { Player } from "./entity/player"; import { ItemIdentifier } from "./enums"; - -import { BlockContainer } from "."; +import type { BlockContainer } from "./block/container"; +import type { EntityContainer } from "./entity/container"; /** * Represents a container. @@ -72,7 +73,7 @@ class Container { * @returns Whether the container is an entity container. */ public isEntityContainer(): this is EntityContainer { - return this instanceof EntityContainer; + return "entity" in this; } /** @@ -80,7 +81,7 @@ class Container { * @returns Whether the container is a block container. */ public isBlockContainer(): this is BlockContainer { - return this instanceof BlockContainer; + return "block" in this; } /** @@ -361,6 +362,8 @@ class Container { packet.item = itemStack ? ItemStack.toNetworkStack(itemStack) : new NetworkItemStackDescriptor(0); + // Vanilla/BDS on proto-v944 sends identifier 0 here for these inventory + // sync packets, even for hopper windows. Match that behavior exactly. packet.fullContainerName = new FullContainerName(0, 0); packet.storageItem = new NetworkItemStackDescriptor(0); // Bundles ? @@ -395,6 +398,8 @@ class Container { const packet = new InventoryContentPacket(); // Set the properties of the packet. + // Vanilla/BDS on proto-v944 sends identifier 0 here for these inventory + // sync packets, even for hopper windows. Match that behavior exactly. packet.fullContainerName = new FullContainerName(0, 0); packet.storageItem = new NetworkItemStackDescriptor(0); // Bundles ? diff --git a/packages/core/src/entity/container.ts b/packages/core/src/entity/container.ts index a66f9666..5e010aac 100644 --- a/packages/core/src/entity/container.ts +++ b/packages/core/src/entity/container.ts @@ -10,8 +10,8 @@ import { } from "@serenityjs/protocol"; import { Container } from "../container"; -import { ItemStack } from "../item"; -import { PlayerOpenedContainerSignal } from "../events"; +import { ItemStack } from "../item/stack"; +import { PlayerOpenedContainerSignal } from "../events/player-opened-container"; import { Entity } from "./entity"; import { Player } from "./player"; diff --git a/packages/core/src/events/player-opened-container.ts b/packages/core/src/events/player-opened-container.ts index bf1877e3..06d8861f 100644 --- a/packages/core/src/events/player-opened-container.ts +++ b/packages/core/src/events/player-opened-container.ts @@ -1,5 +1,5 @@ -import { Container } from "../container"; -import { Player } from "../entity"; +import type { Container } from "../container"; +import type { Player } from "../entity/player"; import { WorldEvent } from "../enums"; import { EventSignal } from "./event-signal"; diff --git a/packages/core/src/handlers/inventory-transaction.ts b/packages/core/src/handlers/inventory-transaction.ts index 9ca8a486..6b9f633e 100644 --- a/packages/core/src/handlers/inventory-transaction.ts +++ b/packages/core/src/handlers/inventory-transaction.ts @@ -27,7 +27,7 @@ import { ItemStack, ItemStackUseOnBlockOptions } from "../item"; import { BlockIdentifier, EntityInteractMethod } from "../enums"; import { PlayerPlaceBlockSignal, PlayerStopUsingItemSignal } from "../events"; import { BlockPlacementOptions } from "../types"; -import { BlockLevelStorage } from ".."; +import { BlockChestTrait, BlockLevelStorage } from ".."; class InventoryTransactionHandler extends NetworkHandler { public static readonly packet = Packet.InventoryTransaction; @@ -364,6 +364,10 @@ class InventoryTransactionHandler extends NetworkHandler { return; } else { + if (resultant.hasTrait(BlockChestTrait)) { + resultant.getTrait(BlockChestTrait).pairAfterPlacement(); + } + // Decrement the stack if the player is in survival mode // This is done AFTER th e onPlace check to avoid losing items on canceled placements if (player.getGamemode() === Gamemode.Survival) From 2dd3fc3536d00ef376a60e034d9341caa83f1968 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sun, 5 Apr 2026 15:03:41 +0300 Subject: [PATCH 4/9] Better naming --- packages/core/src/block/traits/chest.ts | 71 ++++++++----------- .../src/handlers/inventory-transaction.ts | 2 +- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/packages/core/src/block/traits/chest.ts b/packages/core/src/block/traits/chest.ts index 9f90b8ba..18e0f657 100644 --- a/packages/core/src/block/traits/chest.ts +++ b/packages/core/src/block/traits/chest.ts @@ -25,43 +25,35 @@ class BlockChestTrait extends BlockInventoryTrait { BlockIdentifier.TrappedChest ]; + private static readonly pairAxis = { + north: "x", + south: "x", + east: "z", + west: "z" + } as const; + public static shouldBePairParent( current: BlockPosition, other: BlockPosition, direction: string ): boolean { - switch (direction) { - case "north": - return current.x > other.x; - case "south": - return current.x < other.x; - case "east": - return current.z > other.z; - case "west": - return current.z < other.z; - default: - if (current.x !== other.x) { - return current.x < other.x; - } + const axis = this.pairAxis[direction as keyof typeof this.pairAxis]; + if (!axis) return current.x !== other.x ? current.x < other.x : current.z < other.z; - return current.z < other.z; - } + return direction === "north" || direction === "east" + ? current[axis] > other[axis] + : current[axis] < other[axis]; } public static getPairCandidates( block: Block, direction: string ): Array { - switch (direction) { - case "north": - case "south": - return [block.east(), block.west()]; - case "east": - case "west": - return [block.north(), block.south()]; - default: - return []; - } + return this.pairAxis[direction as keyof typeof this.pairAxis] === "x" + ? [block.east(), block.west()] + : this.pairAxis[direction as keyof typeof this.pairAxis] === "z" + ? [block.north(), block.south()] + : []; } public isPaired(): boolean { @@ -163,21 +155,9 @@ class BlockChestTrait extends BlockInventoryTrait { } } - public pairAfterPlacement(): void { - if (this.isPaired()) { - const pairedPos = this.getPaired(); - if (pairedPos) { - const paired = this.dimension.getBlock(pairedPos); - if ( - paired.hasTrait(BlockChestTrait) && - paired.getTrait(BlockChestTrait).getPaired()?.equals(this.block.position) - ) { - return; - } - } - - this.clearPaired(); - } + public pair(): void { + if (this.hasValidPair()) return; + if (this.isPaired()) this.clearPaired(); const direction = this.block.getState("minecraft:cardinal_direction"); @@ -407,6 +387,17 @@ class BlockChestTrait extends BlockInventoryTrait { const trait = block.getTrait(BlockChestTrait); return trait.getIsPairParent() ? trait : this; } + + private hasValidPair(): boolean { + const pairedPos = this.getPaired(); + if (!pairedPos) return false; + + const paired = this.dimension.getBlock(pairedPos); + return ( + paired.hasTrait(BlockChestTrait) && + paired.getTrait(BlockChestTrait).getPaired()?.equals(this.block.position) === true + ); + } } export { BlockChestTrait }; diff --git a/packages/core/src/handlers/inventory-transaction.ts b/packages/core/src/handlers/inventory-transaction.ts index 6b9f633e..8f3f8551 100644 --- a/packages/core/src/handlers/inventory-transaction.ts +++ b/packages/core/src/handlers/inventory-transaction.ts @@ -365,7 +365,7 @@ class InventoryTransactionHandler extends NetworkHandler { return; } else { if (resultant.hasTrait(BlockChestTrait)) { - resultant.getTrait(BlockChestTrait).pairAfterPlacement(); + resultant.getTrait(BlockChestTrait).pair(); } // Decrement the stack if the player is in survival mode From f56e010479eaaa7a6420379739286d869195e7f4 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sun, 5 Apr 2026 15:11:10 +0300 Subject: [PATCH 5/9] shorten the func name --- packages/core/src/block/traits/chest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/block/traits/chest.ts b/packages/core/src/block/traits/chest.ts index 18e0f657..82dffc58 100644 --- a/packages/core/src/block/traits/chest.ts +++ b/packages/core/src/block/traits/chest.ts @@ -310,11 +310,11 @@ class BlockChestTrait extends BlockInventoryTrait { // If silent is true, return if (silent) return; - this.broadcastChestState(1, LevelSoundEvent.ChestOpen); + this.broadcastState(1, LevelSoundEvent.ChestOpen); if (this.isPaired()) { const paired = this.dimension.getBlock(this.getPaired()!); - this.broadcastChestState(1, LevelSoundEvent.ChestOpen, paired); + this.broadcastState(1, LevelSoundEvent.ChestOpen, paired); } } @@ -345,15 +345,15 @@ class BlockChestTrait extends BlockInventoryTrait { // If silent is true, return if (silent) return; - this.broadcastChestState(0, LevelSoundEvent.ChestClosed); + this.broadcastState(0, LevelSoundEvent.ChestClosed); if (this.isPaired()) { const paired = this.dimension.getBlock(this.getPaired()!); - this.broadcastChestState(0, LevelSoundEvent.ChestClosed, paired); + this.broadcastState(0, LevelSoundEvent.ChestClosed, paired); } } - private broadcastChestState( + private broadcastState( data: number, soundEvent: LevelSoundEvent, block: Block = this.block From 36dbecbb91d7e03a75c6c629c42f872db7571c54 Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Mon, 6 Apr 2026 21:49:56 +0300 Subject: [PATCH 6/9] Fix interactions --- .../core/src/handlers/inventory-transaction.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/handlers/inventory-transaction.ts b/packages/core/src/handlers/inventory-transaction.ts index 8f3f8551..fc34aa46 100644 --- a/packages/core/src/handlers/inventory-transaction.ts +++ b/packages/core/src/handlers/inventory-transaction.ts @@ -232,7 +232,7 @@ class InventoryTransactionHandler extends NetworkHandler { // Check if the client prediction failed to place the block if ( - !placingBlock || // If not placing a block, we use the item on the block + !results.placingBlock || // If not placing a block, we use the item on the block transaction.clientPrediction === PredictedResult.Failure ) { // Verify that the item stack exists @@ -380,6 +380,19 @@ class InventoryTransactionHandler extends NetworkHandler { // Handles when an item is used case ItemUseInventoryTransactionType.Use: { + const interacting = dimension.getBlock(transaction.blockPosition); + + const results = interacting.interact({ + origin: player, + clickedPosition: transaction.clickPosition, + clickedFace: transaction.face, + placingBlock: false + }); + + if (results.cancel || player.openedContainer) { + return; + } + // Get the players held item stack const stack = player.getHeldItem() as ItemStack; From edf33e3816ebcfde5ad9bd31712f15ed5afca79f Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Tue, 7 Apr 2026 00:28:16 +0300 Subject: [PATCH 7/9] Fix shutdown issues. --- bundle/index.ts | 4 ++- devapp/src/index.ts | 4 ++- packages/core/src/commands/console.ts | 20 ++++++++++++- packages/core/src/serenity.ts | 8 ++--- .../src/world/provider/leveldb/leveldb.ts | 30 ++++++++++++++++--- packages/core/src/world/worker/decorator.ts | 7 ++++- packages/core/src/world/world.ts | 2 +- packages/raknet/src/server/raknet.ts | 5 +++- 8 files changed, 66 insertions(+), 14 deletions(-) diff --git a/bundle/index.ts b/bundle/index.ts index 5e68d204..69e42b3a 100644 --- a/bundle/index.ts +++ b/bundle/index.ts @@ -34,5 +34,7 @@ if (isMainThread) { serenity.registerProvider(LevelDBProvider, { path: "./worlds" }); // Start the server - serenity.start(); + serenity.start().then(() => { }).catch((error) => { + serenity.logger.error("Failed to start SerenityJS server:", error); + }); } diff --git a/devapp/src/index.ts b/devapp/src/index.ts index 569b2973..afeb1af3 100644 --- a/devapp/src/index.ts +++ b/devapp/src/index.ts @@ -17,4 +17,6 @@ new Pipeline(serenity, { path: "./plugins" }); serenity.registerProvider(LevelDBProvider, { path: "./worlds" }); // Start the server -serenity.start(); +serenity.start().then(() => { }).catch((error) => { + serenity.logger.error("Failed to start SerenityJS server:", error); +}); \ No newline at end of file diff --git a/packages/core/src/commands/console.ts b/packages/core/src/commands/console.ts index 11510d14..b0de7934 100644 --- a/packages/core/src/commands/console.ts +++ b/packages/core/src/commands/console.ts @@ -13,6 +13,8 @@ class ConsoleInterface { terminal: true }); + private onLineHandler = this.onLine.bind(this); + public constructor(serenity: Serenity) { this.serenity = serenity; @@ -22,11 +24,27 @@ class ConsoleInterface { } stdin.resume(); - this.interface.on("line", this.onLine.bind(this)); + this.interface.on("line", this.onLineHandler); this.interface.setPrompt("> "); } + public close(): void { + // Remove all listeners + this.interface.removeAllListeners(); + + // Close the interface + this.interface.close(); + + // Pause stdin + stdin.pause(); + + // Reset raw mode if TTY + if (stdin instanceof ReadStream && stdin.isTTY) { + stdin.setRawMode(false); + } + } + protected async onLine(line: string): Promise { // Check if the line starts with a / // If so, slice the line and execute the command diff --git a/packages/core/src/serenity.ts b/packages/core/src/serenity.ts index 552cb982..6f7a12e3 100644 --- a/packages/core/src/serenity.ts +++ b/packages/core/src/serenity.ts @@ -378,9 +378,6 @@ class Serenity extends Emitter { * Stops the server and closes all connections */ public async stop(): Promise { - // Close the console interface - this.console.interface.close(); - // Emit the server shutdown event await this.emitAsync(ServerEvent.Stop, 0 as never); @@ -409,11 +406,14 @@ class Serenity extends Emitter { } // Shutdown all world providers - for (const world of this.worlds.values()) void world.provider.onShutdown(); + for (const world of this.worlds.values()) await world.provider.onShutdown(); // Write the permissions to the permissions path if (typeof this.properties.permissions === "string") this.writePermissions(this.properties.permissions); + + // Close the console interface + this.console.close(); } /** diff --git a/packages/core/src/world/provider/leveldb/leveldb.ts b/packages/core/src/world/provider/leveldb/leveldb.ts index 712b56a5..2394a79f 100644 --- a/packages/core/src/world/provider/leveldb/leveldb.ts +++ b/packages/core/src/world/provider/leveldb/leveldb.ts @@ -22,6 +22,7 @@ import { Structure } from "../../structure"; import { BlockLevelStorage } from "../../../block"; import { WorldProvider } from "../provider"; import { DimensionProperties, WorldProperties } from "../../types"; +import { ServerState } from "../../../enums"; import { LevelDBKeyBuilder } from "./key-builder"; @@ -87,8 +88,8 @@ class LevelDBProvider extends WorldProvider { // Check if the dimension has a generator with a worker. if (dimension.generator.worker) { - // Terminate the worker thread. - dimension.generator.worker.terminate(); + // Terminate the worker thread and wait for it to finish. + await dimension.generator.worker.terminate(); } } @@ -105,8 +106,8 @@ class LevelDBProvider extends WorldProvider { // Save all the world data. await this.onSave(); - // Close the database connection. - this.db.close(); + // Close the database connection and wait for it to finish. + await this.db.close(); } public async onStartup(): Promise { @@ -116,6 +117,13 @@ class LevelDBProvider extends WorldProvider { // Create a new method to hold the pregeneration logic. const pregenerate = async () => { for (const [, dimension] of this.world.dimensions) { + // Check if the server is shutting down, if so abort pregeneration + const serverState = this.world.serenity.state; + if (serverState === ServerState.ShuttingDown) { + this.world.logger.info("Aborting chunk pregeneration due to server shutdown."); + return; + } + // Fetch pregeneration options. const pregeneration = dimension.properties.chunkPregeneration ?? []; @@ -124,6 +132,13 @@ class LevelDBProvider extends WorldProvider { // Iterate through each pregeneration option. for (const { start, end, memoryLock } of pregeneration) { + // Check if the server is shutting down during pregeneration + const serverState = this.world.serenity.state; + if (serverState === ServerState.ShuttingDown) { + this.world.logger.info("Aborting chunk pregeneration due to server shutdown."); + return; + } + // Mask to chunk coordinates. const sx = start[0] >> 4; const sz = start[1] >> 4; @@ -132,6 +147,13 @@ class LevelDBProvider extends WorldProvider { // Iterate through each chunk in the area. for (let x = sx; x <= ex; x++) { + // Check if the server is shutting down during chunk generation + const serverState = this.world.serenity.state; + if (serverState === ServerState.ShuttingDown) { + this.world.logger.info("Aborting chunk pregeneration due to server shutdown."); + return; + } + for (let z = sz; z <= ez; z++) { // Create a new chunk and read it. let chunk = new Chunk(x, z, dimension.type); diff --git a/packages/core/src/world/worker/decorator.ts b/packages/core/src/world/worker/decorator.ts index 069c4385..401bdea0 100644 --- a/packages/core/src/world/worker/decorator.ts +++ b/packages/core/src/world/worker/decorator.ts @@ -32,7 +32,12 @@ function Worker(generator: typeof TerrainGenerator) { throw new Error("Worker can only be initialized in a worker thread"); // Create a new worker thread - return new WorkerThread(worker.path, { workerData: properties }); + const workerThread = new WorkerThread(worker.path, { workerData: properties }); + + // Unref the worker so it doesn't keep the process alive + workerThread.unref(); + + return workerThread; }; // Check if we are in the worker thread diff --git a/packages/core/src/world/world.ts b/packages/core/src/world/world.ts index e3e7ff92..a590cc53 100644 --- a/packages/core/src/world/world.ts +++ b/packages/core/src/world/world.ts @@ -226,7 +226,7 @@ class World extends Emitter { this.currentTick % (BigInt(this.properties.saveInterval) * 1200n) === 0n ) { // Save the world via the provider - this.provider.onSave(); + void this.provider.onSave(); } } diff --git a/packages/raknet/src/server/raknet.ts b/packages/raknet/src/server/raknet.ts index ddff7344..cff329cd 100644 --- a/packages/raknet/src/server/raknet.ts +++ b/packages/raknet/src/server/raknet.ts @@ -230,7 +230,10 @@ class Server extends Emitter { for (const connection of this.connections) connection.disconnect(); // Clear the interval - if (this.interval) this.interval = null; + if (this.interval) { + clearTimeout(this.interval); + this.interval = null; + } // Close the socket this.socket.close(); From 9a7bea2663e2930c5fe3fffaecdd7b3536c3c4ce Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Tue, 7 Apr 2026 00:59:15 +0300 Subject: [PATCH 8/9] Add loadWorld and unloadWorld --- packages/core/src/commands/operator/tp.ts | 8 +- packages/core/src/commands/operator/world.ts | 4 +- packages/core/src/entity/entity.ts | 6 +- packages/core/src/entity/player/player.ts | 17 ++- packages/core/src/entity/traits/item-stack.ts | 2 +- packages/core/src/handlers/player-action.ts | 2 +- packages/core/src/serenity.ts | 108 +++++++++++++++++- .../src/world/provider/leveldb/leveldb.ts | 16 ++- 8 files changed, 144 insertions(+), 19 deletions(-) diff --git a/packages/core/src/commands/operator/tp.ts b/packages/core/src/commands/operator/tp.ts index 3c7bc6a6..009f079d 100644 --- a/packages/core/src/commands/operator/tp.ts +++ b/packages/core/src/commands/operator/tp.ts @@ -31,7 +31,7 @@ const register = (world: World) => { const position = context.position.result as Vector3f; // Teleport the entity to the new location - origin.teleport(position); + void origin.teleport(position); } ); @@ -57,7 +57,7 @@ const register = (world: World) => { // Loop through all the targets for (const target of targets) { // Teleport the entity to the new location - target.teleport(other.position, other.dimension); + void target.teleport(other.position, other.dimension); } } ); @@ -78,12 +78,12 @@ const register = (world: World) => { // Loop through all the targets for (const target of targets) { // Teleport the entity to the new location - target.teleport(position); + void target.teleport(position); } } ); }, - () => {} + () => { } ); }; diff --git a/packages/core/src/commands/operator/world.ts b/packages/core/src/commands/operator/world.ts index 01d94aa0..e75345fe 100644 --- a/packages/core/src/commands/operator/world.ts +++ b/packages/core/src/commands/operator/world.ts @@ -56,7 +56,7 @@ const register = (world: World) => { position.z += 0.5; // Change the world of the target - target.teleport(position, dimension); + void target.teleport(position, dimension); // Append the message message.push( @@ -69,7 +69,7 @@ const register = (world: World) => { } ); }, - () => {} + () => { } ); }; diff --git a/packages/core/src/entity/entity.ts b/packages/core/src/entity/entity.ts index 506ae97e..b5cda936 100644 --- a/packages/core/src/entity/entity.ts +++ b/packages/core/src/entity/entity.ts @@ -714,7 +714,7 @@ class Entity { this.rotation.set(rotation); // Update the position of the entity - return this.teleport(this.position); + return void this.teleport(this.position); } /** @@ -1293,7 +1293,7 @@ class Entity { * @param position The position to teleport the entity to. * @param dimension The dimension to teleport the entity to; optional. */ - public teleport(position: Vector3f, dimension?: Dimension): void { + public async teleport(position: Vector3f, dimension?: Dimension): Promise { // Iterate over the traits of the entity for (const [identifier, trait] of this.traits) { // Attempt to trigger the onTeleport trait event @@ -1320,7 +1320,7 @@ class Entity { this.position = position; // Check if a dimension was provided - if (dimension) this.changeDimension(dimension); + if (dimension) await this.changeDimension(dimension); // Create a new MoveActorDeltaPacket const packet = new MoveActorDeltaPacket(); diff --git a/packages/core/src/entity/player/player.ts b/packages/core/src/entity/player/player.ts index 4ef74b76..d86291ad 100644 --- a/packages/core/src/entity/player/player.ts +++ b/packages/core/src/entity/player/player.ts @@ -612,7 +612,7 @@ class Player extends Entity { // Teleport the player to their position // This fixes an issue where the player is sometimes stuck in the ground - this.teleport(this.position, this.dimension); + void this.teleport(this.position, this.dimension); // Return the player return this; @@ -659,9 +659,9 @@ class Player extends Entity { * @param position The position to teleport the player to. * @param dimension The dimension to teleport the player to. */ - public teleport(position: Vector3f, dimension?: Dimension): void { + public async teleport(position: Vector3f, dimension?: Dimension): Promise { // Call the parent method to teleport the player - super.teleport(position, dimension); + await super.teleport(position, dimension); // Prepare the ridden runtime id let riddenRuntimeId = 0n; @@ -704,10 +704,19 @@ class Player extends Entity { * Changes the dimension of the player. * @param dimension The dimension to change the player to. */ - public changeDimension(dimension: Dimension): void { + public async changeDimension(dimension: Dimension): Promise { // Check if the dimension is the same as the current dimension if (this.dimension === dimension) return; + // Check if the target world is loaded, if not load it + const targetWorld = dimension.world; + if (!this.serenity.isWorldLoaded(targetWorld.identifier)) { + this.world.logger.info( + `Auto-loading world ${targetWorld.identifier} for player ${this.username}` + ); + await this.serenity.loadWorld(targetWorld.identifier); + } + // Despawn the player from the current dimension this.despawn({ changedDimensions: true }); diff --git a/packages/core/src/entity/traits/item-stack.ts b/packages/core/src/entity/traits/item-stack.ts index 55f5ec26..207d8e8b 100644 --- a/packages/core/src/entity/traits/item-stack.ts +++ b/packages/core/src/entity/traits/item-stack.ts @@ -132,7 +132,7 @@ class EntityItemStackTrait extends EntityTrait { */ public pickup(player: Player): void { // Teleport the item to the player - this.entity.teleport(player.position); + void this.entity.teleport(player.position); // Set the player as the target this.target = player; diff --git a/packages/core/src/handlers/player-action.ts b/packages/core/src/handlers/player-action.ts index f9a0baad..6318a1fb 100644 --- a/packages/core/src/handlers/player-action.ts +++ b/packages/core/src/handlers/player-action.ts @@ -61,7 +61,7 @@ class PlayerActionHandler extends NetworkHandler { const vector = new Vector3f(x, y, z); // Teleport the player back to the spawn point - return player.teleport(vector); + return void player.teleport(vector); } } } diff --git a/packages/core/src/serenity.ts b/packages/core/src/serenity.ts index 6f7a12e3..06e0e0b5 100644 --- a/packages/core/src/serenity.ts +++ b/packages/core/src/serenity.ts @@ -72,6 +72,11 @@ class Serenity extends Emitter { */ public readonly worlds = new Map(); + /** + * Track which worlds are fully loaded (chunks pregenerated, ready for players) + */ + private readonly loadedWorlds = new Set(); + /** * The registered providers and properties that are available to the server */ @@ -524,9 +529,10 @@ class Serenity extends Emitter { /** * Registers a world with the server * @param world The world to register + * @param skipStartup Whether to skip calling onStartup (for lazy loading) * @returns Whether the world was successfully registered or not */ - public registerWorld(world: World): boolean { + public registerWorld(world: World, skipStartup = false): boolean { // Check if the world is already registered if (this.worlds.has(world.identifier)) { // Log that the world is already registered @@ -542,8 +548,11 @@ class Serenity extends Emitter { // Log that the world has been registered this.logger.debug(`Registered world: ${world.identifier}`); - // Call the onStartup method of the world provider - void world.provider.onStartup(); + // Call the onStartup method of the world provider unless skipped + if (!skipStartup) { + void world.provider.onStartup(); + this.loadedWorlds.add(world.identifier); + } // Add the world to the worlds enum WorldEnum.options.push(world.identifier); @@ -571,6 +580,9 @@ class Serenity extends Emitter { // Call the onShutdown method of the world provider void world.provider.onShutdown(); + // Remove from loaded worlds set + this.loadedWorlds.delete(world.identifier); + // Remove all listeners of the world provider. world.removeAll(); @@ -586,6 +598,96 @@ class Serenity extends Emitter { return true; } + /** + * Loads a world (runs onStartup, pregeneration, etc.) + * @param identifier The identifier of the world to load + * @returns Whether the world was successfully loaded + */ + public async loadWorld(identifier: string): Promise { + // Get the world from the registered worlds + const world = this.worlds.get(identifier); + + // Check if the world exists + if (!world) { + this.logger.error(`Cannot load world: ${identifier} is not registered`); + return false; + } + + // Check if the world is already loaded + if (this.loadedWorlds.has(identifier)) { + this.logger.debug(`World ${identifier} is already loaded`); + return true; + } + + // Log that the world is being loaded + this.logger.info(`Loading world: ${identifier}`); + + // Call the onStartup method of the world provider + await world.provider.onStartup(); + + // Mark the world as loaded + this.loadedWorlds.add(identifier); + + // Log that the world has been loaded + this.logger.info(`World ${identifier} loaded successfully`); + + return true; + } + + /** + * Unloads a world (runs onShutdown, clears chunks, etc.) + * @param identifier The identifier of the world to unload + * @param force Whether to force unload even if players are in the world + * @returns Whether the world was successfully unloaded + */ + public async unloadWorld(identifier: string, force = false): Promise { + // Get the world from the registered worlds + const world = this.worlds.get(identifier); + + // Check if the world exists + if (!world) { + this.logger.error(`Cannot unload world: ${identifier} is not registered`); + return false; + } + + // Check if the world is loaded + if (!this.loadedWorlds.has(identifier)) { + this.logger.debug(`World ${identifier} is already unloaded`); + return true; + } + + // Check if there are players in the world + if (!force && world.getPlayers().length > 0) { + this.logger.error( + `Cannot unload world ${identifier}: players are still in the world. Use force=true to override.` + ); + return false; + } + + // Log that the world is being unloaded + this.logger.info(`Unloading world: ${identifier}`); + + // Call the onShutdown method of the world provider + await world.provider.onShutdown(); + + // Mark the world as unloaded + this.loadedWorlds.delete(identifier); + + // Log that the world has been unloaded + this.logger.info(`World ${identifier} unloaded successfully`); + + return true; + } + + /** + * Checks if a world is loaded + * @param identifier The identifier of the world to check + * @returns Whether the world is loaded + */ + public isWorldLoaded(identifier: string): boolean { + return this.loadedWorlds.has(identifier); + } + /** * Creates a new world at the provider's worlds directory. * @param provider The provider to use for the world. diff --git a/packages/core/src/world/provider/leveldb/leveldb.ts b/packages/core/src/world/provider/leveldb/leveldb.ts index 2394a79f..bf2590fc 100644 --- a/packages/core/src/world/provider/leveldb/leveldb.ts +++ b/packages/core/src/world/provider/leveldb/leveldb.ts @@ -813,6 +813,9 @@ class LevelDBProvider extends WorldProvider { return; } + // Get the spawn world identifier from serenity properties + const spawnWorldIdentifier = serenity.properties.spawnWorldIdentifier || "default"; + // Iterate over the world entries in the directory. for (const directory of directories) { // Get the path for the world. @@ -883,8 +886,19 @@ class LevelDBProvider extends WorldProvider { } } + // Determine if this is the spawn world + const isSpawnWorld = directory.name === spawnWorldIdentifier; + // Register the world with the serenity instance. - serenity.registerWorld(world); + // Only load (run onStartup) for the spawn world, others are lazy loaded + serenity.registerWorld(world, !isSpawnWorld); + + // Log whether the world was loaded or registered for lazy loading + if (isSpawnWorld) { + serenity.logger.info(`Loaded spawn world: ${directory.name}`); + } else { + serenity.logger.debug(`Registered world for lazy loading: ${directory.name}`); + } } } From 25e8c68d763f9ae814567a8bcabc3c21f1d8e7ee Mon Sep 17 00:00:00 2001 From: AnyBananaGAME Date: Sat, 2 May 2026 22:17:05 +0300 Subject: [PATCH 9/9] fix --- clones/PocketMine-MP | 1 + clones/PowerNukkitX | 1 + devapp/src/index.ts | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 160000 clones/PocketMine-MP create mode 160000 clones/PowerNukkitX diff --git a/clones/PocketMine-MP b/clones/PocketMine-MP new file mode 160000 index 00000000..d77520d2 --- /dev/null +++ b/clones/PocketMine-MP @@ -0,0 +1 @@ +Subproject commit d77520d210fcb967a02bc11817ad625393c8ebc6 diff --git a/clones/PowerNukkitX b/clones/PowerNukkitX new file mode 160000 index 00000000..712d7c79 --- /dev/null +++ b/clones/PowerNukkitX @@ -0,0 +1 @@ +Subproject commit 712d7c79863f229e510112093f03891542c5a731 diff --git a/devapp/src/index.ts b/devapp/src/index.ts index afeb1af3..943b3719 100644 --- a/devapp/src/index.ts +++ b/devapp/src/index.ts @@ -17,6 +17,6 @@ new Pipeline(serenity, { path: "./plugins" }); serenity.registerProvider(LevelDBProvider, { path: "./worlds" }); // Start the server -serenity.start().then(() => { }).catch((error) => { - serenity.logger.error("Failed to start SerenityJS server:", error); -}); \ No newline at end of file +serenity.start().catch((reason) => { + serenity.logger.error("Failed to start SerenityJS server:", reason); +});