From 20e9495c5832ce670a96bb42b402cff5bbc84250 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:44:36 +0300 Subject: [PATCH 01/14] refactor: prepare to merge with folkbe --- src/modules/anticheat/anti-piston-abuse.ts | 29 +++++++++++++++++++ .../anticheat/anti-wither-bedrock-kill.ts | 22 ++++++++++++++ src/modules/anticheat/dupe.ts | 26 ----------------- src/modules/anticheat/forbidden-items.ts | 6 ++-- src/modules/anticheat/index.ts | 3 +- src/modules/anticheat/log-provider.ts | 16 ++++++++++ 6 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 src/modules/anticheat/anti-piston-abuse.ts create mode 100644 src/modules/anticheat/anti-wither-bedrock-kill.ts delete mode 100644 src/modules/anticheat/dupe.ts create mode 100644 src/modules/anticheat/log-provider.ts diff --git a/src/modules/anticheat/anti-piston-abuse.ts b/src/modules/anticheat/anti-piston-abuse.ts new file mode 100644 index 00000000..ab5e4967 --- /dev/null +++ b/src/modules/anticheat/anti-piston-abuse.ts @@ -0,0 +1,29 @@ +import { system, world } from '@minecraft/server' +import { MinecraftBlockTypes } from '@minecraft/vanilla-data' +import { Vec } from 'lib/vector' +import { antiCheatLog } from './log-provider' + +world.afterEvents.pistonActivate.subscribe(event => { + const locations = event.piston.getAttachedBlocksLocations() + + system.runTimeout( + () => { + if (!event.block.isValid) return + + for (const location of locations) { + const block = event.block.dimension.getBlock(location) + if (block?.typeId !== MinecraftBlockTypes.Hopper) continue + + const nearbyPlayers = event.block.dimension.getPlayers({ location: event.block.location, maxDistance: 20 }) + const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') + + antiCheatLog(`ПОРШЕНЬ ДЮП ${Vec.string(event.block.location)}\n${nearbyPlayersNames}`) + + event.block.dimension.createExplosion(event.block.location, 5, { breaksBlocks: true }) + return + } + }, + 'piston dupe prevent', + 2, + ) +}) diff --git a/src/modules/anticheat/anti-wither-bedrock-kill.ts b/src/modules/anticheat/anti-wither-bedrock-kill.ts new file mode 100644 index 00000000..401bdce0 --- /dev/null +++ b/src/modules/anticheat/anti-wither-bedrock-kill.ts @@ -0,0 +1,22 @@ +import { world } from '@minecraft/server' +import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' +import { Vec } from 'lib/vector' +import { antiCheatLog } from './log-provider' + +world.afterEvents.entitySpawn.subscribe(event => { + const { entity } = event + + if (entity.typeId !== MinecraftEntityTypes.Wither) return + + const { location } = entity + const block = entity.dimension.getBlock(location) + + if (block?.typeId !== MinecraftBlockTypes.Bedrock) return + + const nearbyPlayers = event.entity.dimension.getPlayers({ location, maxDistance: 20 }) + const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') + + antiCheatLog(`ОБНАРУЖЕН АБУЗ ВИЗЕРА ${Vec.string(location)}\n${nearbyPlayersNames}`) + + entity.remove() +}) diff --git a/src/modules/anticheat/dupe.ts b/src/modules/anticheat/dupe.ts deleted file mode 100644 index f8a9c96c..00000000 --- a/src/modules/anticheat/dupe.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { system, world } from '@minecraft/server' -import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' - -world.afterEvents.pistonActivate.subscribe(event => { - const blocks = event.piston.getAttachedBlocksLocations() - - system.runTimeout( - () => { - if (!event.block.isValid) return - for (const blockl of blocks) { - const block = event.block.dimension.getBlock(blockl) - if (block?.typeId === MinecraftBlockTypes.Hopper) { - const nearbyPlayers = event.block.dimension.getPlayers({ location: event.block.location, maxDistance: 20 }) - console.warn( - `PISTON DUPE DETECTED!!! ${Vec.string(event.block.location)}\n${nearbyPlayers.map(e => e.name).join('\n')}`, - ) - event.block.dimension.createExplosion(event.block.location, 1, { breaksBlocks: true }) - return - } - } - }, - 'piston dupe prevent', - 2, - ) -}) diff --git a/src/modules/anticheat/forbidden-items.ts b/src/modules/anticheat/forbidden-items.ts index df991f6f..ede3430a 100644 --- a/src/modules/anticheat/forbidden-items.ts +++ b/src/modules/anticheat/forbidden-items.ts @@ -1,7 +1,7 @@ import { system, world } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' import { actionGuard, ActionGuardOrder, isNotPlaying } from 'lib' -import { createLogger } from 'lib/utils/logger' +import { antiCheatLogger } from './log-provider' const forbiddenItems: string[] = [ MinecraftItemTypes.Barrier, @@ -13,7 +13,9 @@ const forbiddenItems: string[] = [ MinecraftItemTypes.RepeatingCommandBlock, ] -const logger = createLogger('AntiCheat') +const logger = antiCheatLogger + +// TODO Use inventorySlotChange + scan on startup and player join function interval() { try { diff --git a/src/modules/anticheat/index.ts b/src/modules/anticheat/index.ts index a01b86d2..7db9223a 100644 --- a/src/modules/anticheat/index.ts +++ b/src/modules/anticheat/index.ts @@ -1,3 +1,4 @@ -import './dupe' +import './anti-piston-abuse' +import './anti-wither-bedrock-kill' import './forbidden-items' import './whitelist' diff --git a/src/modules/anticheat/log-provider.ts b/src/modules/anticheat/log-provider.ts new file mode 100644 index 00000000..4ae0bbb3 --- /dev/null +++ b/src/modules/anticheat/log-provider.ts @@ -0,0 +1,16 @@ +import { createLogger } from 'lib/utils/logger' + +export const antiCheatLogger = createLogger('anticheat') + +export function antiCheatLog(text: string) { + if (!log) return antiCheatLogger.warn('No provider: ', text) + + antiCheatLogger.warn(text) + log(text) +} + +let log: null | ((text: string) => void) = null + +export function registerAntiCheatLogProvider(provider: typeof log) { + log = provider +} From 00025b468f5962585bce71b33f3c845c432c9117 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:14:51 +0300 Subject: [PATCH 02/14] fix: migrate to direct imports instead of lib --- src/lib.ts | 102 +++++++++--------- src/lib/i18n/intl.test.ts | 2 - src/lib/location.test.ts | 2 +- src/lib/region/database.test.ts | 1 - src/lib/region/kinds/region.test.ts | 1 - src/lib/shop/buttons/sellable-item.test.ts | 1 - src/lib/utils/logger.test.ts | 1 - src/lib/utils/structure.test.ts | 2 +- src/modules/anticheat/forbidden-items.ts | 2 +- src/modules/anticheat/whitelist.ts | 2 +- src/modules/commands/camera.ts | 1 - src/modules/commands/db.ts | 2 +- src/modules/commands/gamemode.ts | 2 +- src/modules/commands/help.ts | 2 +- src/modules/commands/items.ts | 1 - src/modules/commands/pid.ts | 2 +- src/modules/commands/role.ts | 1 - src/modules/commands/rtp.ts | 1 - src/modules/commands/scores.ts | 1 - src/modules/commands/send.ts | 1 - src/modules/commands/sit.ts | 1 - src/modules/commands/socials.ts | 1 - src/modules/commands/tp.ts | 2 +- src/modules/commands/version.ts | 1 - .../features/break-place-outside-of-region.ts | 2 +- src/modules/indicator/health.ts | 1 - src/modules/indicator/player-name-tag.ts | 2 +- src/modules/indicator/pvp.ts | 2 +- src/modules/indicator/test-damage.ts | 2 +- src/modules/minigames/BattleRoyal/index.ts | 2 +- src/modules/minigames/BattleRoyal/var.ts | 1 - src/modules/minigames/Builder.ts | 1 - src/modules/places/anarchy/airdrop.ts | 2 +- src/modules/places/anarchy/anarchy.ts | 2 +- src/modules/places/anarchy/quartz.ts | 2 +- src/modules/places/base/actions/create.ts | 2 +- src/modules/places/base/base-menu.ts | 2 +- src/modules/places/base/region.ts | 2 +- src/modules/places/dungeons/command.ts | 2 +- src/modules/places/dungeons/custom-dungeon.ts | 2 +- src/modules/places/dungeons/loot.ts | 1 - .../places/lib/city-investigating-quest.ts | 1 - src/modules/places/lib/city.ts | 1 - src/modules/places/mineshaft/algo.ts | 2 +- .../places/mineshaft/mineshaft-region.ts | 4 +- src/modules/places/mineshaft/ore-collector.ts | 2 +- src/modules/places/spawn.ts | 1 - src/modules/places/stone-quarry/furnacer.ts | 2 +- src/modules/places/stone-quarry/gunsmith.ts | 2 +- .../places/stone-quarry/stone-quarry.ts | 2 +- .../places/stone-quarry/wither.boss.ts | 2 +- src/modules/places/tech-city/golem.boss.ts | 4 +- src/modules/places/tech-city/tech-city.ts | 1 - .../places/village-of-explorers/mage.ts | 2 +- .../places/village-of-explorers/slime.boss.ts | 2 +- .../village-of-explorers.ts | 1 - .../village-of-miners/village-of-miners.ts | 1 - src/modules/pvp/cannon.ts | 2 +- src/modules/pvp/explosible-entities.ts | 2 +- src/modules/pvp/explosible-fireworks.ts | 2 +- src/modules/pvp/fireball.ts | 2 +- src/modules/pvp/ice-bomb.ts | 2 +- src/modules/pvp/item-ability.ts | 2 +- src/modules/pvp/raid.ts | 2 +- src/modules/pvp/throwable-tnt.ts | 2 +- src/modules/quests/daily/index.ts | 7 +- src/modules/quests/learning/airdrop.ts | 1 - src/modules/quests/learning/learning.ts | 3 +- src/modules/survival/cleanup.ts | 2 +- .../survival/death-quest-and-gravestone.ts | 2 +- src/modules/survival/locked-features.ts | 1 - src/modules/survival/random-teleport.ts | 1 - src/modules/survival/realtime.ts | 2 +- src/modules/survival/sidebar.ts | 2 +- src/modules/survival/speedrun/target.ts | 2 +- src/modules/test/edit-structure.ts | 2 +- src/modules/test/load-chunks.ts | 2 +- src/modules/test/minimap.ts | 1 - src/modules/test/properties.ts | 2 +- src/modules/test/simulatedPlayer.ts | 3 +- src/modules/test/test.ts | 35 +++--- src/modules/wiki/wiki.ts | 3 +- src/modules/world-edit/commands/general/id.ts | 1 - .../commands/region/set/set-selection.ts | 2 +- .../region/set/use-block-selection.ts | 7 +- .../commands/region/set/use-replace-mode.ts | 3 +- .../world-edit/commands/selection/chunk.ts | 2 +- .../world-edit/commands/selection/expand.ts | 2 +- .../world-edit/commands/selection/pos1.ts | 2 +- .../world-edit/commands/selection/pos2.ts | 2 +- .../world-edit/commands/selection/size.ts | 2 +- .../world-edit/lib/world-edit-multi-tool.ts | 2 +- .../world-edit/lib/world-edit-tool-brush.ts | 5 +- src/modules/world-edit/lib/world-edit-tool.ts | 2 +- src/modules/world-edit/lib/world-edit.ts | 2 +- src/modules/world-edit/menu.ts | 7 +- src/modules/world-edit/settings.ts | 2 +- src/modules/world-edit/tools/brush.ts | 2 +- src/modules/world-edit/tools/create-region.ts | 2 +- src/modules/world-edit/tools/dash.ts | 2 +- src/modules/world-edit/tools/debug-stick.ts | 3 +- src/modules/world-edit/tools/multi-brush.ts | 2 +- src/modules/world-edit/tools/randomizer.ts | 2 +- src/modules/world-edit/tools/shovel.ts | 3 +- src/modules/world-edit/tools/smooth.ts | 5 +- src/modules/world-edit/tools/tool.ts | 5 +- src/modules/world-edit/utils/blocks-set.ts | 3 +- .../world-edit/utils/default-block-sets.ts | 2 +- src/test/utils.ts | 2 +- 109 files changed, 173 insertions(+), 185 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index 38eab696..02540c45 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -5,54 +5,54 @@ import 'lib/load/message1' import 'lib/database/properties' // Database -export * from 'lib/database/inventory' -export * from 'lib/database/player' -export * from 'lib/database/scoreboard' -export * from 'lib/database/utils' - -// Command -export * from 'lib/command/index' - -// Lib -export * from 'lib/roles' -export * from 'lib/util' -export * from 'lib/vector' - -export * from 'lib/action' -export * from 'lib/cooldown' -export * from 'lib/enchantments' -export * from 'lib/event-signal' -export * from 'lib/i18n/lang' -export * from 'lib/location' -export * from 'lib/mail' -export * from 'lib/player-join' -export * from 'lib/portals' -export * from 'lib/rpg/airdrop' -export * from 'lib/rpg/boss' -export * from 'lib/rpg/leaderboard' -export * from 'lib/rpg/loot-table' -export * from 'lib/rpg/menu' -export * from 'lib/settings' -export * from 'lib/sidebar' -export * from 'lib/temporary' -export * from 'lib/utils/game' -export * from 'lib/utils/ms' -export * from 'lib/utils/search' - -// Region -export * from 'lib/region/index' - -// Form -export * from 'lib/form/action' -export * from 'lib/form/array' -export * from 'lib/form/chest' -export * from 'lib/form/message' -export * from 'lib/form/modal' -export * from 'lib/form/npc' -export * from 'lib/form/utils' - -// Extension exports -export * from 'lib/extensions/extend' -export * from 'lib/extensions/item-stack' -export * from 'lib/extensions/system' -export * from 'lib/load/extensions' +// export * from 'lib/database/inventory' +// export * from 'lib/database/player' +// export * from 'lib/database/scoreboard' +// export * from 'lib/database/utils' + +// // Command +// export * from 'lib/command/index' + +// // Lib +// export * from 'lib/roles' +// export * from 'lib/util' +// export * from 'lib/vector' + +// export * from 'lib/action' +// export * from 'lib/cooldown' +// export * from 'lib/enchantments' +// export * from 'lib/event-signal' +// export * from 'lib/i18n/lang' +// export * from 'lib/location' +// export * from 'lib/mail' +// export * from 'lib/player-join' +// export * from 'lib/portals' +// export * from 'lib/rpg/airdrop' +// export * from 'lib/rpg/boss' +// export * from 'lib/rpg/leaderboard' +// export * from 'lib/rpg/loot-table' +// export * from 'lib/rpg/menu' +// export * from 'lib/settings' +// export * from 'lib/sidebar' +// export * from 'lib/temporary' +// export * from 'lib/utils/game' +// export * from 'lib/utils/ms' +// export * from 'lib/utils/search' + +// // Region +// export * from 'lib/region/index' + +// // Form +// export * from 'lib/form/action' +// export * from 'lib/form/array' +// export * from 'lib/form/chest' +// export * from 'lib/form/message' +// export * from 'lib/form/modal' +// export * from 'lib/form/npc' +// export * from 'lib/form/utils' + +// // Extension exports +// export * from 'lib/extensions/extend' +// export * from 'lib/extensions/item-stack' +// export * from 'lib/extensions/system' +// export * from 'lib/load/extensions' diff --git a/src/lib/i18n/intl.test.ts b/src/lib/i18n/intl.test.ts index 7371e90c..668ea535 100644 --- a/src/lib/i18n/intl.test.ts +++ b/src/lib/i18n/intl.test.ts @@ -1,4 +1,3 @@ -import { ms } from 'lib' import { Language, supportedLanguages } from 'lib/assets/lang' import { intlListFormat, intlRemaining } from './intl' import { i18n } from './text' @@ -62,4 +61,3 @@ describe('intlRemaining', () => { ).toMatchInlineSnapshot(`"1,001 days, 6 hours, 4 minutes"`) }) }) - diff --git a/src/lib/location.test.ts b/src/lib/location.test.ts index 79fa85ed..7148ddad 100644 --- a/src/lib/location.test.ts +++ b/src/lib/location.test.ts @@ -1,6 +1,6 @@ import { Player } from '@minecraft/server' -import { Vec } from 'lib' import 'lib/database/scoreboard' +import { Vec } from 'lib/vector' import { location, locationWithRadius, locationWithRotation, migrateLocationName } from './location' import { Group } from './rpg/place' import { Settings } from './settings' diff --git a/src/lib/region/database.test.ts b/src/lib/region/database.test.ts index 5707ab60..06cff14e 100644 --- a/src/lib/region/database.test.ts +++ b/src/lib/region/database.test.ts @@ -1,4 +1,3 @@ -import { Region, RegionIsSaveable } from 'lib' import { ChunkCubeArea } from './areas/chunk-cube' import { SphereArea } from './areas/sphere' import { diff --git a/src/lib/region/kinds/region.test.ts b/src/lib/region/kinds/region.test.ts index 6621893f..ea4f35f8 100644 --- a/src/lib/region/kinds/region.test.ts +++ b/src/lib/region/kinds/region.test.ts @@ -1,4 +1,3 @@ -import { RegionDatabase, registerSaveableRegion } from 'lib' import { createPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { TEST_clearDatabase, TEST_createPlayer } from 'test/utils' diff --git a/src/lib/shop/buttons/sellable-item.test.ts b/src/lib/shop/buttons/sellable-item.test.ts index 700bccc6..a5e4920e 100644 --- a/src/lib/shop/buttons/sellable-item.test.ts +++ b/src/lib/shop/buttons/sellable-item.test.ts @@ -2,7 +2,6 @@ import { MinecraftItemTypes } from '@minecraft/vanilla-data' import { TEST_createPlayer, TEST_onFormOpen } from 'test/utils' import { Shop } from '../shop' -import { doNothing } from 'lib' import 'lib/database/scoreboard' describe('sellableItem', () => { diff --git a/src/lib/utils/logger.test.ts b/src/lib/utils/logger.test.ts index 95217c9f..a9b17868 100644 --- a/src/lib/utils/logger.test.ts +++ b/src/lib/utils/logger.test.ts @@ -1,4 +1,3 @@ -import { util } from 'lib' import { TEST_createPlayer } from 'test/utils' import { createLogger } from './logger' diff --git a/src/lib/utils/structure.test.ts b/src/lib/utils/structure.test.ts index 681af47e..2d81fe43 100644 --- a/src/lib/utils/structure.test.ts +++ b/src/lib/utils/structure.test.ts @@ -1,5 +1,5 @@ import { StructureRotation } from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { structureLikeRotate, toAbsolute, toRelative } from './structure' describe('structureLikeRotate', () => { diff --git a/src/modules/anticheat/forbidden-items.ts b/src/modules/anticheat/forbidden-items.ts index ede3430a..842fd6ea 100644 --- a/src/modules/anticheat/forbidden-items.ts +++ b/src/modules/anticheat/forbidden-items.ts @@ -1,6 +1,6 @@ import { system, world } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' -import { actionGuard, ActionGuardOrder, isNotPlaying } from 'lib' + import { antiCheatLogger } from './log-provider' const forbiddenItems: string[] = [ diff --git a/src/modules/anticheat/whitelist.ts b/src/modules/anticheat/whitelist.ts index 2ad51064..2295b663 100644 --- a/src/modules/anticheat/whitelist.ts +++ b/src/modules/anticheat/whitelist.ts @@ -1,5 +1,5 @@ import { system, world } from '@minecraft/server' -import { DEFAULT_ROLE, is, ROLES, Settings } from 'lib' + import { defaultLang } from 'lib/assets/lang' import { noI18n } from 'lib/i18n/text' import { createLogger } from 'lib/utils/logger' diff --git a/src/modules/commands/camera.ts b/src/modules/commands/camera.ts index 6f620866..db086d64 100644 --- a/src/modules/commands/camera.ts +++ b/src/modules/commands/camera.ts @@ -1,4 +1,3 @@ -import { restorePlayerCamera } from 'lib' import { i18n } from 'lib/i18n/text' new Command('camera').setDescription(i18n`Возвращает камеру в исходное состояние`).executes(ctx => { diff --git a/src/modules/commands/db.ts b/src/modules/commands/db.ts index 193cef67..a07297a7 100644 --- a/src/modules/commands/db.ts +++ b/src/modules/commands/db.ts @@ -1,7 +1,7 @@ /* i18n-ignore */ import { Player, system, world } from '@minecraft/server' -import { ArrayForm, ROLES, getRole, inspect, util } from 'lib' + import { UnknownTable, getProvider } from 'lib/database/abstract' import { ActionForm } from 'lib/form/action' import { ModalForm } from 'lib/form/modal' diff --git a/src/modules/commands/gamemode.ts b/src/modules/commands/gamemode.ts index cfe527e0..e7cb6cc5 100644 --- a/src/modules/commands/gamemode.ts +++ b/src/modules/commands/gamemode.ts @@ -2,7 +2,7 @@ import { GameMode } from '@minecraft/server' import { MinecraftEffectTypes } from '@minecraft/vanilla-data' -import { is, isNotPlaying, Temporary } from 'lib' + import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' import { WeakPlayerMap } from 'lib/weak-player-storage' diff --git a/src/modules/commands/help.ts b/src/modules/commands/help.ts index b9c9de48..b1ad7246 100644 --- a/src/modules/commands/help.ts +++ b/src/modules/commands/help.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { ROLES, getRole } from 'lib' + import { defaultLang } from 'lib/assets/lang' import { CmdLet } from 'lib/command/cmdlet' import { Command } from 'lib/command/index' diff --git a/src/modules/commands/items.ts b/src/modules/commands/items.ts index fc8bffc7..8ffef114 100644 --- a/src/modules/commands/items.ts +++ b/src/modules/commands/items.ts @@ -1,4 +1,3 @@ -import { ArrayForm, langToken, translateToken } from 'lib' import { noI18n } from 'lib/i18n/text' import { customItems } from 'lib/rpg/custom-item' diff --git a/src/modules/commands/pid.ts b/src/modules/commands/pid.ts index 2d6650d7..e40f00d8 100644 --- a/src/modules/commands/pid.ts +++ b/src/modules/commands/pid.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { is, ModalForm } from 'lib' + import { selectPlayer } from 'lib/form/select-player' import { i18n, noI18n } from 'lib/i18n/text' diff --git a/src/modules/commands/role.ts b/src/modules/commands/role.ts index 20fd51c6..578ef499 100644 --- a/src/modules/commands/role.ts +++ b/src/modules/commands/role.ts @@ -1,5 +1,4 @@ import { Player, world } from '@minecraft/server' -import { FormCallback, ROLES, getRole, setRole } from 'lib' import { ArrayForm } from 'lib/form/array' import { ModalForm } from 'lib/form/modal' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/commands/rtp.ts b/src/modules/commands/rtp.ts index 5793bf91..7dc3bf60 100644 --- a/src/modules/commands/rtp.ts +++ b/src/modules/commands/rtp.ts @@ -1,6 +1,5 @@ import { Player, TicksPerSecond } from '@minecraft/server' import { MinecraftEffectTypes } from '@minecraft/vanilla-data' -import { LockAction, Vec } from 'lib' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' import { WeakPlayerMap } from 'lib/weak-player-storage' diff --git a/src/modules/commands/scores.ts b/src/modules/commands/scores.ts index 6f9c5aae..ddbb7166 100644 --- a/src/modules/commands/scores.ts +++ b/src/modules/commands/scores.ts @@ -1,7 +1,6 @@ /* i18n-ignore */ import { Player, ScoreboardIdentityType, ScoreboardObjective, world } from '@minecraft/server' -import { ActionForm, BUTTON, Leaderboard, ModalForm, noBoolean } from 'lib' import { defaultLang } from 'lib/assets/lang' import { ScoreboardDB } from 'lib/database/scoreboard' import { ArrayForm } from 'lib/form/array' diff --git a/src/modules/commands/send.ts b/src/modules/commands/send.ts index 609685c0..8dec660b 100644 --- a/src/modules/commands/send.ts +++ b/src/modules/commands/send.ts @@ -1,7 +1,6 @@ /* i18n-ignore */ import { Player, ScoreName, world } from '@minecraft/server' -import { ActionForm, Mail, ModalForm } from 'lib' import { createSelectPlayerMenu } from 'lib/form/select-player' import { i18n } from 'lib/i18n/text' import { Rewards } from 'lib/utils/rewards' diff --git a/src/modules/commands/sit.ts b/src/modules/commands/sit.ts index d4bffdfa..270bc42f 100644 --- a/src/modules/commands/sit.ts +++ b/src/modules/commands/sit.ts @@ -1,5 +1,4 @@ import { system, world } from '@minecraft/server' -import { LockAction } from 'lib' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/commands/socials.ts b/src/modules/commands/socials.ts index b235dc2e..dbf9ea49 100644 --- a/src/modules/commands/socials.ts +++ b/src/modules/commands/socials.ts @@ -1,5 +1,4 @@ import { system, TicksPerSecond, world } from '@minecraft/server' -import { Settings } from 'lib' import { emoji } from 'lib/assets/emoji' const socials = [ diff --git a/src/modules/commands/tp.ts b/src/modules/commands/tp.ts index d64035e3..2b28daf8 100644 --- a/src/modules/commands/tp.ts +++ b/src/modules/commands/tp.ts @@ -1,11 +1,11 @@ import { world } from '@minecraft/server' -import { Vec } from 'lib' import { form } from 'lib/form/new' import { debounceMenu } from 'lib/form/utils' import { getFullname } from 'lib/get-fullname' import { i18n, i18nPlural, noI18n } from 'lib/i18n/text' import { isNotPlaying } from 'lib/utils/game' import { VectorInDimension } from 'lib/utils/point' +import { Vec } from 'lib/vector' import { SafePlace } from 'modules/places/lib/safe-place' import { Spawn } from 'modules/places/spawn' import { StoneQuarry } from 'modules/places/stone-quarry/stone-quarry' diff --git a/src/modules/commands/version.ts b/src/modules/commands/version.ts index d8a27c93..1c7ce592 100644 --- a/src/modules/commands/version.ts +++ b/src/modules/commands/version.ts @@ -1,4 +1,3 @@ -import { is } from 'lib' import { i18n, textTable } from 'lib/i18n/text' new Command('version') diff --git a/src/modules/features/break-place-outside-of-region.ts b/src/modules/features/break-place-outside-of-region.ts index 91548c56..1eaff45f 100644 --- a/src/modules/features/break-place-outside-of-region.ts +++ b/src/modules/features/break-place-outside-of-region.ts @@ -1,6 +1,6 @@ import { Player, system } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Cooldown, ms } from 'lib' + import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' import { actionGuard, ActionGuardOrder, BLOCK_CONTAINERS, DOORS, GATES, SWITCHES, TRAPDOORS } from 'lib/region/index' diff --git a/src/modules/indicator/health.ts b/src/modules/indicator/health.ts index 3b254d99..685adf15 100644 --- a/src/modules/indicator/health.ts +++ b/src/modules/indicator/health.ts @@ -1,7 +1,6 @@ import { Entity, system, world } from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Boss, ms, Vec } from 'lib' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { ClosingChatSet } from 'lib/extensions/player' diff --git a/src/modules/indicator/player-name-tag.ts b/src/modules/indicator/player-name-tag.ts index 2fc53739..9bb3d64f 100644 --- a/src/modules/indicator/player-name-tag.ts +++ b/src/modules/indicator/player-name-tag.ts @@ -1,5 +1,5 @@ import { Entity, Player, system } from '@minecraft/server' -import { isNotPlaying } from 'lib' + import { getFullname } from 'lib/get-fullname' export const PlayerNameTagModifiers: ((player: Player) => string | false)[] = [ diff --git a/src/modules/indicator/pvp.ts b/src/modules/indicator/pvp.ts index c841f596..8f59026f 100644 --- a/src/modules/indicator/pvp.ts +++ b/src/modules/indicator/pvp.ts @@ -1,5 +1,5 @@ import { EntityDamageCause, EntityHurtAfterEvent, Player, system, world } from '@minecraft/server' -import { Boss, BossArenaRegion, LockAction, ms, Settings } from 'lib' + import { emoji } from 'lib/assets/emoji' import { Core } from 'lib/extensions/core' import { ActionbarPriority } from 'lib/extensions/on-screen-display' diff --git a/src/modules/indicator/test-damage.ts b/src/modules/indicator/test-damage.ts index 647a198d..df4f84f4 100644 --- a/src/modules/indicator/test-damage.ts +++ b/src/modules/indicator/test-damage.ts @@ -3,7 +3,7 @@ import { EnchantmentType, EquipmentSlot, ItemStack, Player } from '@minecraft/server' import { registerAsync } from '@minecraft/server-gametest' import { MinecraftEnchantmentTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Enchantments, isKeyof, Temporary } from 'lib' + import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n } from 'lib/i18n/text' import { TestStructures } from 'test/constants' diff --git a/src/modules/minigames/BattleRoyal/index.ts b/src/modules/minigames/BattleRoyal/index.ts index 8421e0ff..09fa2f82 100644 --- a/src/modules/minigames/BattleRoyal/index.ts +++ b/src/modules/minigames/BattleRoyal/index.ts @@ -3,7 +3,7 @@ // TODO Update import { Player, system, world } from '@minecraft/server' -import { Command } from 'lib' + import { br } from './game' import { BATTLE_ROYAL_EVENTS, BR_QUENE } from './var' diff --git a/src/modules/minigames/BattleRoyal/var.ts b/src/modules/minigames/BattleRoyal/var.ts index 950143f0..1a4431ad 100644 --- a/src/modules/minigames/BattleRoyal/var.ts +++ b/src/modules/minigames/BattleRoyal/var.ts @@ -1,4 +1,3 @@ -import { Settings } from 'lib' import { table } from 'lib/database/abstract' import { EventSignal } from 'lib/event-signal' diff --git a/src/modules/minigames/Builder.ts b/src/modules/minigames/Builder.ts index 28b72de6..0aedf39e 100644 --- a/src/modules/minigames/Builder.ts +++ b/src/modules/minigames/Builder.ts @@ -1,5 +1,4 @@ import { Player } from '@minecraft/server' -import { LockAction, Sidebar } from 'lib' // TODO Add minigame place diff --git a/src/modules/places/anarchy/airdrop.ts b/src/modules/places/anarchy/airdrop.ts index 6d8358ae..12c5958b 100644 --- a/src/modules/places/anarchy/airdrop.ts +++ b/src/modules/places/anarchy/airdrop.ts @@ -1,5 +1,5 @@ import { LocationInUnloadedChunkError, system, world } from '@minecraft/server' -import { Airdrop, isNotPlaying, Loot, Vec } from 'lib' + import { Items } from 'lib/assets/custom-items' import { i18n } from 'lib/i18n/text' import { Anarchy } from 'modules/places/anarchy/anarchy' diff --git a/src/modules/places/anarchy/anarchy.ts b/src/modules/places/anarchy/anarchy.ts index de3fb0d2..c034236f 100644 --- a/src/modules/places/anarchy/anarchy.ts +++ b/src/modules/places/anarchy/anarchy.ts @@ -1,5 +1,5 @@ import { GameMode, Player } from '@minecraft/server' -import { EventSignal, InventoryStore, Portal, ValidLocation, Vec, location } from 'lib' + import { consoleLang } from 'lib/assets/lang' import { i18n, noI18n } from 'lib/i18n/text' import { isNotPlaying } from 'lib/utils/game' diff --git a/src/modules/places/anarchy/quartz.ts b/src/modules/places/anarchy/quartz.ts index 77337cdd..5c478311 100644 --- a/src/modules/places/anarchy/quartz.ts +++ b/src/modules/places/anarchy/quartz.ts @@ -1,7 +1,7 @@ import { ItemStack, system } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEffectTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { isKeyof, ms } from 'lib' + import { i18n } from 'lib/i18n/text' import { RegionEvents } from 'lib/region/events' import { actionGuard, ActionGuardOrder, disableAdventureNear, Region, RegionPermissions } from 'lib/region/index' diff --git a/src/modules/places/base/actions/create.ts b/src/modules/places/base/actions/create.ts index dd88d0f6..a563edd3 100644 --- a/src/modules/places/base/actions/create.ts +++ b/src/modules/places/base/actions/create.ts @@ -1,5 +1,5 @@ import { Block, Player, system, world } from '@minecraft/server' -import { actionGuard, ActionGuardOrder, LockAction, Region, Vec } from 'lib' + import { i18n } from 'lib/i18n/text' import { SphereArea } from 'lib/region/areas/sphere' import { askForExitingNewbieMode, isNewbie } from 'lib/rpg/newbie' diff --git a/src/modules/places/base/base-menu.ts b/src/modules/places/base/base-menu.ts index 54a7b3d4..4ded283e 100644 --- a/src/modules/places/base/base-menu.ts +++ b/src/modules/places/base/base-menu.ts @@ -1,7 +1,7 @@ -import { Vec } from 'lib' import { form } from 'lib/form/new' import { i18n } from 'lib/i18n/text' import { editRegionPermissions, manageRegionMembers } from 'lib/region/form' +import { Vec } from 'lib/vector' import { baseRottingButton } from './actions/rotting' import { baseUpgradeButton } from './actions/upgrade' import { BaseRegion } from './region' diff --git a/src/modules/places/base/region.ts b/src/modules/places/base/region.ts index bf8e4c24..d3d2703c 100644 --- a/src/modules/places/base/region.ts +++ b/src/modules/places/base/region.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { disableAdventureNear } from 'lib' + import { i18n, noI18n } from 'lib/i18n/text' import { SphereArea } from 'lib/region/areas/sphere' import { registerRegionType } from 'lib/region/command' diff --git a/src/modules/places/dungeons/command.ts b/src/modules/places/dungeons/command.ts index 6ef97eb7..4d7bafb2 100644 --- a/src/modules/places/dungeons/command.ts +++ b/src/modules/places/dungeons/command.ts @@ -1,7 +1,7 @@ /* i18n-ignore */ import { MolangVariableMap, Player, StructureRotation, system, world } from '@minecraft/server' -import { ArrayForm, isKeyof, Vec } from 'lib' + import { Items } from 'lib/assets/custom-items' import { StructureDungeonsId } from 'lib/assets/structures' import { ItemLoreSchema } from 'lib/database/item-stack' diff --git a/src/modules/places/dungeons/custom-dungeon.ts b/src/modules/places/dungeons/custom-dungeon.ts index d527d1af..baa32e06 100644 --- a/src/modules/places/dungeons/custom-dungeon.ts +++ b/src/modules/places/dungeons/custom-dungeon.ts @@ -1,6 +1,6 @@ import { Block, GameMode, Player, StructureRotation, system, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { is, ModalForm, ms, RegionCreationOptions, registerRegionType, registerSaveableRegion, Vec } from 'lib' + import { StructureDungeonsId, StructureFile } from 'lib/assets/structures' import { i18n, noI18n } from 'lib/i18n/text' import { Area } from 'lib/region/areas/area' diff --git a/src/modules/places/dungeons/loot.ts b/src/modules/places/dungeons/loot.ts index ddc6d672..a651963a 100644 --- a/src/modules/places/dungeons/loot.ts +++ b/src/modules/places/dungeons/loot.ts @@ -1,4 +1,3 @@ -import { Loot, LootTable } from 'lib' import { Items } from 'lib/assets/custom-items' import { StructureDungeonsId } from 'lib/assets/structures' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/places/lib/city-investigating-quest.ts b/src/modules/places/lib/city-investigating-quest.ts index 0dc363ca..bccdc601 100644 --- a/src/modules/places/lib/city-investigating-quest.ts +++ b/src/modules/places/lib/city-investigating-quest.ts @@ -1,4 +1,3 @@ -import { isNotPlaying } from 'lib' import { i18n } from 'lib/i18n/text' import { Quest } from 'lib/quest' import { RegionEvents } from 'lib/region/events' diff --git a/src/modules/places/lib/city.ts b/src/modules/places/lib/city.ts index b9e6fc65..319f048f 100644 --- a/src/modules/places/lib/city.ts +++ b/src/modules/places/lib/city.ts @@ -1,4 +1,3 @@ -import { location, LootTable } from 'lib' import { Crate } from 'lib/crates/crate' import { Cutscene } from 'lib/cutscene' import { i18n, i18nShared } from 'lib/i18n/text' diff --git a/src/modules/places/mineshaft/algo.ts b/src/modules/places/mineshaft/algo.ts index 31eac897..7fd1eecb 100644 --- a/src/modules/places/mineshaft/algo.ts +++ b/src/modules/places/mineshaft/algo.ts @@ -1,7 +1,7 @@ import { Block, Dimension, Player } from '@minecraft/server' import { MinecraftBlockTypes as b, MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' import { EventSignal } from 'lib/event-signal' +import { Vec } from 'lib/vector' import { getEdgeBlocksOf } from './get-edge-blocks-of' import { MineshaftRegion } from './mineshaft-region' import { Ore, OreCollector, OreEntry } from './ore-collector' diff --git a/src/modules/places/mineshaft/mineshaft-region.ts b/src/modules/places/mineshaft/mineshaft-region.ts index 578326dd..261e8c87 100644 --- a/src/modules/places/mineshaft/mineshaft-region.ts +++ b/src/modules/places/mineshaft/mineshaft-region.ts @@ -1,12 +1,12 @@ import { LocationOutOfWorldBoundariesError, Player, PlayerBreakBlockBeforeEvent, system } from '@minecraft/server' -import { ActionForm, ms, registerRegionType, Vec } from 'lib' + +import { NewFormCreator } from 'lib/form/new' import { i18n, noI18n } from 'lib/i18n/text' import { registerSaveableRegion } from 'lib/region/database' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { createLogger } from 'lib/utils/logger' import { MineareaRegion } from '../../../lib/region/kinds/minearea' import { ores, placeOre } from './algo' -import { NewFormCreator } from 'lib/form/new' const logger = createLogger('Shaft') diff --git a/src/modules/places/mineshaft/ore-collector.ts b/src/modules/places/mineshaft/ore-collector.ts index 24968e12..ad3fd97f 100644 --- a/src/modules/places/mineshaft/ore-collector.ts +++ b/src/modules/places/mineshaft/ore-collector.ts @@ -1,5 +1,5 @@ import { MinecraftBlockTypes as b } from '@minecraft/vanilla-data' -import { stringifyError } from 'lib' + import { i18n } from 'lib/i18n/text' import { selectByChance } from 'lib/rpg/random' diff --git a/src/modules/places/spawn.ts b/src/modules/places/spawn.ts index d43275b1..1a1ae58a 100644 --- a/src/modules/places/spawn.ts +++ b/src/modules/places/spawn.ts @@ -1,7 +1,6 @@ import { GameMode, Player, system, world } from '@minecraft/server' import { MinecraftEffectTypes } from '@minecraft/vanilla-data' -import { InventoryStore, Portal, Settings, locationWithRotation, util } from 'lib' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' import { Join } from 'lib/player-join' diff --git a/src/modules/places/stone-quarry/furnacer.ts b/src/modules/places/stone-quarry/furnacer.ts index 63082ae0..e2d45697 100644 --- a/src/modules/places/stone-quarry/furnacer.ts +++ b/src/modules/places/stone-quarry/furnacer.ts @@ -1,6 +1,6 @@ import { ContainerSlot, Player, TicksPerSecond, system, world } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Vec, getAuxOrTexture, ms } from 'lib' + import { Sounds } from 'lib/assets/custom-sounds' import { defaultLang } from 'lib/assets/lang' import { table } from 'lib/database/abstract' diff --git a/src/modules/places/stone-quarry/gunsmith.ts b/src/modules/places/stone-quarry/gunsmith.ts index 59c96ce1..1473ecc0 100644 --- a/src/modules/places/stone-quarry/gunsmith.ts +++ b/src/modules/places/stone-quarry/gunsmith.ts @@ -1,6 +1,6 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' import { MinecraftItemTypes as i, MinecraftBlockTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { translateTypeId } from 'lib' + import { i18n, i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' import { rollChance } from 'lib/rpg/random' diff --git a/src/modules/places/stone-quarry/stone-quarry.ts b/src/modules/places/stone-quarry/stone-quarry.ts index 3521ddbc..7fd7788e 100644 --- a/src/modules/places/stone-quarry/stone-quarry.ts +++ b/src/modules/places/stone-quarry/stone-quarry.ts @@ -1,5 +1,5 @@ import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Loot } from 'lib' + import { i18n, i18nShared } from 'lib/i18n/text' import { AuntZina } from 'modules/places/stone-quarry/aunt-zina' import { Barman } from 'modules/places/stone-quarry/barman' diff --git a/src/modules/places/stone-quarry/wither.boss.ts b/src/modules/places/stone-quarry/wither.boss.ts index e156eeeb..1b24de34 100644 --- a/src/modules/places/stone-quarry/wither.boss.ts +++ b/src/modules/places/stone-quarry/wither.boss.ts @@ -1,6 +1,6 @@ import { BlockTypes, EntityComponentTypes } from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Boss, Loot, ms } from 'lib' + import { i18nShared, noI18n } from 'lib/i18n/text' import { BigRegionStructure } from 'lib/region/big-structure' import { Group } from 'lib/rpg/place' diff --git a/src/modules/places/tech-city/golem.boss.ts b/src/modules/places/tech-city/golem.boss.ts index 856da568..ef99674c 100644 --- a/src/modules/places/tech-city/golem.boss.ts +++ b/src/modules/places/tech-city/golem.boss.ts @@ -1,7 +1,7 @@ import { world } from '@minecraft/server' import { MinecraftEffectTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Boss, Loot, ms, Vec } from 'lib' -import { i18n, i18nShared } from 'lib/i18n/text' + +import { i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' import { Chip } from './engineer' diff --git a/src/modules/places/tech-city/tech-city.ts b/src/modules/places/tech-city/tech-city.ts index 0039e20b..baf5119b 100644 --- a/src/modules/places/tech-city/tech-city.ts +++ b/src/modules/places/tech-city/tech-city.ts @@ -1,4 +1,3 @@ -import { Loot } from 'lib' import { i18n, i18nShared } from 'lib/i18n/text' import { CutArea } from 'lib/region/areas/cut' import { CannonItem, CannonShellItem } from 'modules/pvp/cannon' diff --git a/src/modules/places/village-of-explorers/mage.ts b/src/modules/places/village-of-explorers/mage.ts index 40a05d04..5d220e3e 100644 --- a/src/modules/places/village-of-explorers/mage.ts +++ b/src/modules/places/village-of-explorers/mage.ts @@ -8,7 +8,7 @@ import { MinecraftPotionEffectTypes, MinecraftPotionModifierTypes, } from '@minecraft/vanilla-data' -import { addNamespace, doNothing, Enchantments, getAuxOrTexture } from 'lib' + import { Sounds } from 'lib/assets/custom-sounds' import { translateEnchantment, translateTypeId } from 'lib/i18n/lang' import { i18n, i18nShared } from 'lib/i18n/text' diff --git a/src/modules/places/village-of-explorers/slime.boss.ts b/src/modules/places/village-of-explorers/slime.boss.ts index 1e30d323..888213bb 100644 --- a/src/modules/places/village-of-explorers/slime.boss.ts +++ b/src/modules/places/village-of-explorers/slime.boss.ts @@ -1,6 +1,6 @@ import { world } from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Loot, ms } from 'lib' + import { i18nShared } from 'lib/i18n/text' import { Boss } from 'lib/rpg/boss' import { Group } from 'lib/rpg/place' diff --git a/src/modules/places/village-of-explorers/village-of-explorers.ts b/src/modules/places/village-of-explorers/village-of-explorers.ts index 2719c235..c7d5ef40 100644 --- a/src/modules/places/village-of-explorers/village-of-explorers.ts +++ b/src/modules/places/village-of-explorers/village-of-explorers.ts @@ -1,4 +1,3 @@ -import { Loot } from 'lib' import { i18n, i18nShared } from 'lib/i18n/text' import { City } from '../lib/city' import { Butcher } from '../lib/npc/butcher' diff --git a/src/modules/places/village-of-miners/village-of-miners.ts b/src/modules/places/village-of-miners/village-of-miners.ts index 7ae21441..6a7e3b74 100644 --- a/src/modules/places/village-of-miners/village-of-miners.ts +++ b/src/modules/places/village-of-miners/village-of-miners.ts @@ -1,4 +1,3 @@ -import { Loot, migrateLocationName } from 'lib' import { i18n, i18nShared } from 'lib/i18n/text' import { City } from '../lib/city' import { Butcher } from '../lib/npc/butcher' diff --git a/src/modules/pvp/cannon.ts b/src/modules/pvp/cannon.ts index a0f58169..47d1f47e 100644 --- a/src/modules/pvp/cannon.ts +++ b/src/modules/pvp/cannon.ts @@ -1,6 +1,6 @@ import { EntityComponentTypes, Player, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { actionGuard, ActionGuardOrder, Cooldown, ms, Vec } from 'lib' + import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { i18n } from 'lib/i18n/text' import { CustomItemWithBlueprint } from 'lib/rpg/custom-item' diff --git a/src/modules/pvp/explosible-entities.ts b/src/modules/pvp/explosible-entities.ts index e844f5b8..2f951599 100644 --- a/src/modules/pvp/explosible-entities.ts +++ b/src/modules/pvp/explosible-entities.ts @@ -1,6 +1,6 @@ import { Entity, EntityDamageCause, ExplosionOptions, Player, system } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { getEdgeBlocksOf } from 'modules/places/mineshaft/get-edge-blocks-of' import { createBlockExplosionChecker } from './raid' diff --git a/src/modules/pvp/explosible-fireworks.ts b/src/modules/pvp/explosible-fireworks.ts index b7ad2f0d..725888fc 100644 --- a/src/modules/pvp/explosible-fireworks.ts +++ b/src/modules/pvp/explosible-fireworks.ts @@ -1,6 +1,6 @@ import { Entity, Player, system, world } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { explosibleEntities, ExplosibleEntityOptions } from './explosible-entities' const fireworks = new Set<{ date: number; entity: Entity }>() diff --git a/src/modules/pvp/fireball.ts b/src/modules/pvp/fireball.ts index 9902be62..66019118 100644 --- a/src/modules/pvp/fireball.ts +++ b/src/modules/pvp/fireball.ts @@ -1,10 +1,10 @@ import { ItemStack, system, world } from '@minecraft/server' -import { Vec } from 'lib' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { Items } from 'lib/assets/custom-items' import { i18n } from 'lib/i18n/text' import { customItems } from 'lib/rpg/custom-item' +import { Vec } from 'lib/vector' import { explosibleEntities, ExplosibleEntityOptions } from './explosible-entities' import { decreaseMainhandItemCount } from './throwable-tnt' diff --git a/src/modules/pvp/ice-bomb.ts b/src/modules/pvp/ice-bomb.ts index bc2b4f7d..16339b86 100644 --- a/src/modules/pvp/ice-bomb.ts +++ b/src/modules/pvp/ice-bomb.ts @@ -1,6 +1,6 @@ import { Entity, EntityComponentTypes, ItemStack, Player, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Vec, ms } from 'lib' + import { i18n } from 'lib/i18n/text' import { customItems } from 'lib/rpg/custom-item' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' diff --git a/src/modules/pvp/item-ability.ts b/src/modules/pvp/item-ability.ts index f3d67ce0..a7816abb 100644 --- a/src/modules/pvp/item-ability.ts +++ b/src/modules/pvp/item-ability.ts @@ -1,5 +1,5 @@ import { EntityDamageCause, world } from '@minecraft/server' -import { isKeyof } from 'lib' + import { defaultLang } from 'lib/assets/lang' import { ItemLoreSchema } from 'lib/database/item-stack' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' diff --git a/src/modules/pvp/raid.ts b/src/modules/pvp/raid.ts index 88ad87ba..f1a1c04e 100644 --- a/src/modules/pvp/raid.ts +++ b/src/modules/pvp/raid.ts @@ -1,5 +1,5 @@ import { Block, Entity, system, world } from '@minecraft/server' -import { LockAction, ms, Region } from 'lib' + import { ScoreboardDB } from 'lib/database/scoreboard' import { i18n } from 'lib/i18n/text' import { MineareaRegion } from 'lib/region/kinds/minearea' diff --git a/src/modules/pvp/throwable-tnt.ts b/src/modules/pvp/throwable-tnt.ts index c3c81019..0f1efec8 100644 --- a/src/modules/pvp/throwable-tnt.ts +++ b/src/modules/pvp/throwable-tnt.ts @@ -1,8 +1,8 @@ import { GameMode, Player, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' import { Cooldown } from 'lib/cooldown' import { ms } from 'lib/utils/ms' +import { Vec } from 'lib/vector' import { explosibleEntities, ExplosibleEntityOptions } from './explosible-entities' const cooldown = new Cooldown(ms.from('sec', 3)) diff --git a/src/modules/quests/daily/index.ts b/src/modules/quests/daily/index.ts index 0381e3ac..78a581ef 100644 --- a/src/modules/quests/daily/index.ts +++ b/src/modules/quests/daily/index.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { doNothing, noNullable } from 'lib' + import { table } from 'lib/database/abstract' import { form } from 'lib/form/new' import { intlListFormat } from 'lib/i18n/intl' @@ -114,7 +114,10 @@ questMenuCustomButtons.subscribe(({ player, form }) => { }) ) { const playerDb = db.get(player.id) - form.button(i18n.accent`Ежедневные задания`.badge(dailyQuests - playerDb.today).to(player.lang), dailyQuestsForm.show) + form.button( + i18n.accent`Ежедневные задания`.badge(dailyQuests - playerDb.today).to(player.lang), + dailyQuestsForm.show, + ) } }) diff --git a/src/modules/quests/learning/airdrop.ts b/src/modules/quests/learning/airdrop.ts index 9e5ed914..f2b988b0 100644 --- a/src/modules/quests/learning/airdrop.ts +++ b/src/modules/quests/learning/airdrop.ts @@ -1,4 +1,3 @@ -import { Loot } from 'lib' import { Items } from 'lib/assets/custom-items' export default new Loot('starter') diff --git a/src/modules/quests/learning/learning.ts b/src/modules/quests/learning/learning.ts index c9667ac2..17cdc55e 100644 --- a/src/modules/quests/learning/learning.ts +++ b/src/modules/quests/learning/learning.ts @@ -1,8 +1,7 @@ import { EquipmentSlot, ItemStack, system } from '@minecraft/server' -import { ActionForm, ActionGuardOrder, location, Temporary, Vec } from 'lib' import { MinecraftBlockTypes as b, MinecraftBlockTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { actionGuard } from 'lib' + import { Sounds } from 'lib/assets/custom-sounds' import { Join } from 'lib/player-join' import { Quest } from 'lib/quest/index' diff --git a/src/modules/survival/cleanup.ts b/src/modules/survival/cleanup.ts index ffec3c72..70a9c674 100644 --- a/src/modules/survival/cleanup.ts +++ b/src/modules/survival/cleanup.ts @@ -9,7 +9,7 @@ // TicksPerSecond, // world, // } from '@minecraft/server' -// import { ms, Settings } from 'lib' +// // import { i18n } from 'lib/i18n/text' // import { createLogger } from 'lib/utils/logger' // import { gravestoneEntityTypeId, gravestoneGetOwner } from './death-quest-and-gravestone' diff --git a/src/modules/survival/death-quest-and-gravestone.ts b/src/modules/survival/death-quest-and-gravestone.ts index 8831b875..96b10938 100644 --- a/src/modules/survival/death-quest-and-gravestone.ts +++ b/src/modules/survival/death-quest-and-gravestone.ts @@ -1,5 +1,5 @@ import { Entity, Player, system, world } from '@minecraft/server' -import { actionGuard, Cooldown, EventSignal, inventoryIsEmpty, ms, Settings, Vec } from 'lib' + import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' import { Quest } from 'lib/quest/quest' diff --git a/src/modules/survival/locked-features.ts b/src/modules/survival/locked-features.ts index 999690bd..2c711be6 100644 --- a/src/modules/survival/locked-features.ts +++ b/src/modules/survival/locked-features.ts @@ -1,4 +1,3 @@ -import { actionGuard, ActionGuardOrder } from 'lib' import { intlListFormat } from 'lib/i18n/intl' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/survival/random-teleport.ts b/src/modules/survival/random-teleport.ts index a047d718..159bc97e 100644 --- a/src/modules/survival/random-teleport.ts +++ b/src/modules/survival/random-teleport.ts @@ -12,7 +12,6 @@ import { } from '@minecraft/server' import { MinecraftEffectTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { LockAction, Vec, util } from 'lib' const RTP_ELYTRA = new ItemStack(MinecraftItemTypes.Elytra, 1).setInfo( '§6Элитра перемещения', diff --git a/src/modules/survival/realtime.ts b/src/modules/survival/realtime.ts index 708e792d..6b4de424 100644 --- a/src/modules/survival/realtime.ts +++ b/src/modules/survival/realtime.ts @@ -1,5 +1,5 @@ import { TicksPerDay, TimeOfDay, system, world } from '@minecraft/server' -import { Settings } from 'lib' + import { noI18n } from 'lib/i18n/text' const MinutesPerDay = 24 * 60 diff --git a/src/modules/survival/sidebar.ts b/src/modules/survival/sidebar.ts index 8f84075f..439b5d13 100644 --- a/src/modules/survival/sidebar.ts +++ b/src/modules/survival/sidebar.ts @@ -1,5 +1,5 @@ import { Player, system, TicksPerSecond, world } from '@minecraft/server' -import { Menu, Region, separateNumberWithDots, Settings, Sidebar } from 'lib' + import { emoji } from 'lib/assets/emoji' import { i18n } from 'lib/i18n/text' import { Quest } from 'lib/quest/quest' diff --git a/src/modules/survival/speedrun/target.ts b/src/modules/survival/speedrun/target.ts index 39b6d333..99f76122 100644 --- a/src/modules/survival/speedrun/target.ts +++ b/src/modules/survival/speedrun/target.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { InventoryInterval, ScoreboardDB } from 'lib' + import { defaultLang } from 'lib/assets/lang' import { form } from 'lib/form/new' import { i18n, i18nShared } from 'lib/i18n/text' diff --git a/src/modules/test/edit-structure.ts b/src/modules/test/edit-structure.ts index 3d3657d0..5a0089af 100644 --- a/src/modules/test/edit-structure.ts +++ b/src/modules/test/edit-structure.ts @@ -1,6 +1,6 @@ import { BlockVolume, LocationInUnloadedChunkError, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Region, Vec } from 'lib' + import { StructureDungeonsId } from 'lib/assets/structures' import { form } from 'lib/form/new' import { noI18n } from 'lib/i18n/text' diff --git a/src/modules/test/load-chunks.ts b/src/modules/test/load-chunks.ts index daaac399..2e2f8a70 100644 --- a/src/modules/test/load-chunks.ts +++ b/src/modules/test/load-chunks.ts @@ -1,8 +1,8 @@ import { Block, system } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n } from 'lib/i18n/text' +import { Vec } from 'lib/vector' new Command('chunkload') .setPermissions('curator') diff --git a/src/modules/test/minimap.ts b/src/modules/test/minimap.ts index aa15f17b..8e28f032 100644 --- a/src/modules/test/minimap.ts +++ b/src/modules/test/minimap.ts @@ -1,6 +1,5 @@ import { RGBA, system, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { removeNamespace } from 'lib' system.afterEvents.scriptEventReceive.subscribe( ({ id }) => { diff --git a/src/modules/test/properties.ts b/src/modules/test/properties.ts index 5d452c60..4f6f5092 100644 --- a/src/modules/test/properties.ts +++ b/src/modules/test/properties.ts @@ -1,5 +1,5 @@ import { Player } from '@minecraft/server' -import { ArrayForm } from 'lib' + import { playerJson } from 'lib/assets/player-json' new Command('props') diff --git a/src/modules/test/simulatedPlayer.ts b/src/modules/test/simulatedPlayer.ts index dbf150e5..039df256 100644 --- a/src/modules/test/simulatedPlayer.ts +++ b/src/modules/test/simulatedPlayer.ts @@ -4,7 +4,8 @@ import { GameMode, system, world } from '@minecraft/server' import * as GameTest from '@minecraft/server-gametest' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec, util } from 'lib' +import { util } from 'lib/util' +import { Vec } from 'lib/vector' import { TestStructures } from 'test/constants' const time = 9999999 diff --git a/src/modules/test/test.ts b/src/modules/test/test.ts index 8aae5981..b49bd409 100644 --- a/src/modules/test/test.ts +++ b/src/modules/test/test.ts @@ -10,42 +10,35 @@ import { MinecraftItemTypes, MinecraftPotionEffectTypes, } from '@minecraft/vanilla-data' -import { - Airdrop, - BUTTON, - ChestForm, - DatabaseUtils, - FormNpc, - LootTable, - Mail, - Region, - RoadRegion, - SafeAreaRegion, - Settings, - Vec, - getAuxOrTexture, - getAuxTextureOrPotionAux, - inspect, - is, - isKeyof, - restorePlayerCamera, - util, -} from 'lib' + import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { CommandContext } from 'lib/command/context' import { parseArguments } from 'lib/command/utils' import { Cutscene } from 'lib/cutscene' +import { DatabaseUtils } from 'lib/database/utils' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { ActionForm } from 'lib/form/action' +import { ChestForm, getAuxOrTexture, getAuxTextureOrPotionAux } from 'lib/form/chest' import { MessageForm } from 'lib/form/message' import { ModalForm } from 'lib/form/modal' import { form } from 'lib/form/new' +import { FormNpc } from 'lib/form/npc' +import { BUTTON } from 'lib/form/utils' import { i18n, noI18n } from 'lib/i18n/text' +import { Mail } from 'lib/mail' +import { Region, RoadRegion, SafeAreaRegion } from 'lib/region' import { MineareaRegion } from 'lib/region/kinds/minearea' +import { is } from 'lib/roles' +import { Airdrop } from 'lib/rpg/airdrop' +import { LootTable } from 'lib/rpg/loot-table' import { Compass } from 'lib/rpg/menu' import { setMinimapNpcPosition } from 'lib/rpg/minimap' +import { Settings } from 'lib/settings' +import { inspect, isKeyof, util } from 'lib/util' +import { restorePlayerCamera } from 'lib/utils/game' import { toPoint } from 'lib/utils/point' import { Rewards } from 'lib/utils/rewards' +import { Vec } from 'lib/vector' import { requestAirdrop } from 'modules/places/anarchy/airdrop' import { BaseRegion } from 'modules/places/base/region' import { skipForBlending } from 'modules/world-edit/utils/blending' diff --git a/src/modules/wiki/wiki.ts b/src/modules/wiki/wiki.ts index 1b308f8c..fd66f442 100644 --- a/src/modules/wiki/wiki.ts +++ b/src/modules/wiki/wiki.ts @@ -1,6 +1,7 @@ import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { getAuxOrTexture, langToken } from 'lib' +import { getAuxOrTexture } from 'lib/form/chest' import { form } from 'lib/form/new' +import { langToken } from 'lib/i18n/lang' import { i18n, textTable } from 'lib/i18n/text' import { selectByChance } from 'lib/rpg/random' import { ores } from 'modules/places/mineshaft/algo' diff --git a/src/modules/world-edit/commands/general/id.ts b/src/modules/world-edit/commands/general/id.ts index d0666c37..7088a441 100644 --- a/src/modules/world-edit/commands/general/id.ts +++ b/src/modules/world-edit/commands/general/id.ts @@ -1,6 +1,5 @@ import {} from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Vec, inspect } from 'lib' const root = new Command('id').setDescription('Выдает айди').setPermissions('builder').setGroup('we') diff --git a/src/modules/world-edit/commands/region/set/set-selection.ts b/src/modules/world-edit/commands/region/set/set-selection.ts index a031b3a4..b020bae2 100644 --- a/src/modules/world-edit/commands/region/set/set-selection.ts +++ b/src/modules/world-edit/commands/region/set/set-selection.ts @@ -1,7 +1,7 @@ import { Player } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { BUTTON } from 'lib' + import { ChestForm } from 'lib/form/chest' import { WeakPlayerMap } from 'lib/weak-player-storage' import { ReplaceMode } from 'modules/world-edit/utils/blocks-set' diff --git a/src/modules/world-edit/commands/region/set/use-block-selection.ts b/src/modules/world-edit/commands/region/set/use-block-selection.ts index 064c5152..157746c7 100644 --- a/src/modules/world-edit/commands/region/set/use-block-selection.ts +++ b/src/modules/world-edit/commands/region/set/use-block-selection.ts @@ -1,7 +1,12 @@ import { BlockPermutation, BlockTypes, Player } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { ActionForm, BUTTON, ChestForm, ModalForm, inspect, translateTypeId } from 'lib' +import { ActionForm } from 'lib/form/action' +import { ChestForm } from 'lib/form/chest' +import { ModalForm } from 'lib/form/modal' +import { BUTTON } from 'lib/form/utils' +import { translateTypeId } from 'lib/i18n/lang' import { i18n } from 'lib/i18n/text' +import { inspect } from 'lib/util' import { WeakPlayerMap } from 'lib/weak-player-storage' import { WEeditBlockStatesMenu } from 'modules/world-edit/menu' import { diff --git a/src/modules/world-edit/commands/region/set/use-replace-mode.ts b/src/modules/world-edit/commands/region/set/use-replace-mode.ts index d2349549..0978a2cd 100644 --- a/src/modules/world-edit/commands/region/set/use-replace-mode.ts +++ b/src/modules/world-edit/commands/region/set/use-replace-mode.ts @@ -1,6 +1,7 @@ import { Player } from '@minecraft/server' -import { BUTTON, settingsModal } from 'lib' +import { BUTTON } from 'lib/form/utils' import { noI18n } from 'lib/i18n/text' +import { settingsModal } from 'lib/settings' import { WeakPlayerMap } from 'lib/weak-player-storage' import { getReplaceMode, ReplaceMode } from 'modules/world-edit/utils/blocks-set' import { REPLACE_MODES } from 'modules/world-edit/utils/default-block-sets' diff --git a/src/modules/world-edit/commands/selection/chunk.ts b/src/modules/world-edit/commands/selection/chunk.ts index e3cd7e32..82d50708 100644 --- a/src/modules/world-edit/commands/selection/chunk.ts +++ b/src/modules/world-edit/commands/selection/chunk.ts @@ -1,5 +1,5 @@ import { Entity, Player } from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { WorldEdit } from '../../lib/world-edit' /** diff --git a/src/modules/world-edit/commands/selection/expand.ts b/src/modules/world-edit/commands/selection/expand.ts index 8ae49a02..17133e31 100644 --- a/src/modules/world-edit/commands/selection/expand.ts +++ b/src/modules/world-edit/commands/selection/expand.ts @@ -1,5 +1,5 @@ import {} from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { WorldEdit } from '../../lib/world-edit' export class SelectionManager { diff --git a/src/modules/world-edit/commands/selection/pos1.ts b/src/modules/world-edit/commands/selection/pos1.ts index 5d1d07cd..1ffc01f9 100644 --- a/src/modules/world-edit/commands/selection/pos1.ts +++ b/src/modules/world-edit/commands/selection/pos1.ts @@ -1,5 +1,5 @@ import {} from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { WorldEdit } from '../../lib/world-edit' new Command('pos1') diff --git a/src/modules/world-edit/commands/selection/pos2.ts b/src/modules/world-edit/commands/selection/pos2.ts index 9f4b23d8..d8ca260d 100644 --- a/src/modules/world-edit/commands/selection/pos2.ts +++ b/src/modules/world-edit/commands/selection/pos2.ts @@ -1,5 +1,5 @@ import {} from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { WorldEdit } from '../../lib/world-edit' new Command('pos2') diff --git a/src/modules/world-edit/commands/selection/size.ts b/src/modules/world-edit/commands/selection/size.ts index 24b1c12b..63d4880f 100644 --- a/src/modules/world-edit/commands/selection/size.ts +++ b/src/modules/world-edit/commands/selection/size.ts @@ -1,7 +1,7 @@ import {} from '@minecraft/server' -import { Vec } from 'lib' import { CommandContext } from 'lib/command/context' import { i18n } from 'lib/i18n/text' +import { Vec } from 'lib/vector' import { WorldEdit } from 'modules/world-edit/lib/world-edit' function getSelection(ctx: CommandContext) { diff --git a/src/modules/world-edit/lib/world-edit-multi-tool.ts b/src/modules/world-edit/lib/world-edit-multi-tool.ts index e0714cb4..4e8a9a84 100644 --- a/src/modules/world-edit/lib/world-edit-multi-tool.ts +++ b/src/modules/world-edit/lib/world-edit-multi-tool.ts @@ -1,5 +1,5 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' -import { ArrayForm, ask, BUTTON, doNothing, ModalForm } from 'lib' + import { noI18n } from 'lib/i18n/text' import { WorldEditTool } from './world-edit-tool' diff --git a/src/modules/world-edit/lib/world-edit-tool-brush.ts b/src/modules/world-edit/lib/world-edit-tool-brush.ts index 20032c6f..d0defd09 100644 --- a/src/modules/world-edit/lib/world-edit-tool-brush.ts +++ b/src/modules/world-edit/lib/world-edit-tool-brush.ts @@ -1,6 +1,7 @@ import { BlockRaycastHit, ItemStack, Player } from '@minecraft/server' -import { isLocationError } from 'lib' +import { noI18n } from 'lib/i18n/text' import stringifyError from 'lib/utils/error' +import { isLocationError } from 'lib/utils/game' import { worldEditPlayerSettings } from 'modules/world-edit/settings' import { BlocksSetRef } from '../utils/blocks-set' import { WorldEditTool } from './world-edit-tool' @@ -29,7 +30,7 @@ export abstract class WorldEditToolBrush extends Wor if (!this.isOurBrush(storage)) return const hit = player.getBlockFromViewDirection({ maxDistance: storage.maxDistance }) - const fail = (reason: string) => player.fail(`§7Кисть§f: §c${reason}`) + const fail = (reason: string) => player.fail(noI18n.error`Кисть: ${reason}`) if (!hit) return fail('Блок слишком далеко.') try { diff --git a/src/modules/world-edit/lib/world-edit-tool.ts b/src/modules/world-edit/lib/world-edit-tool.ts index 2822227a..c5015d43 100644 --- a/src/modules/world-edit/lib/world-edit-tool.ts +++ b/src/modules/world-edit/lib/world-edit-tool.ts @@ -8,7 +8,7 @@ import { system, world, } from '@minecraft/server' -import { Command, inspect, isKeyof, noBoolean, stringify, util } from 'lib' + import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n, textUnitColorize } from 'lib/i18n/text' import { BlocksSetRef, stringifyBlocksSetRef } from 'modules/world-edit/utils/blocks-set' diff --git a/src/modules/world-edit/lib/world-edit.ts b/src/modules/world-edit/lib/world-edit.ts index 35d25f20..ec23d6b1 100644 --- a/src/modules/world-edit/lib/world-edit.ts +++ b/src/modules/world-edit/lib/world-edit.ts @@ -1,5 +1,5 @@ import { BlockPermutation, Player, StructureMirrorAxis, StructureRotation, system, world } from '@minecraft/server' -import { Vec, ask, getRole, isLocationError } from 'lib' + import { Sounds } from 'lib/assets/custom-sounds' import { table } from 'lib/database/abstract' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/world-edit/menu.ts b/src/modules/world-edit/menu.ts index 19cac62d..fb8eefd5 100644 --- a/src/modules/world-edit/menu.ts +++ b/src/modules/world-edit/menu.ts @@ -1,13 +1,18 @@ import { BlockStates, BlockTypes, Player, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { ActionForm, BUTTON, FormCallback, ModalForm, Vec, inspect, is, noNullable, stringify } from 'lib' import { Sounds } from 'lib/assets/custom-sounds' +import { ActionForm } from 'lib/form/action' import { ArrayForm } from 'lib/form/array' import { ChestButtonOptions, ChestForm } from 'lib/form/chest' import { ask } from 'lib/form/message' +import { ModalForm } from 'lib/form/modal' +import { BUTTON, FormCallback } from 'lib/form/utils' import { translateTypeId } from 'lib/i18n/lang' import { i18n } from 'lib/i18n/text' +import { is } from 'lib/roles' +import { inspect, noNullable, stringify } from 'lib/util' +import { Vec } from 'lib/vector' import { WorldEdit } from 'modules/world-edit/lib/world-edit' import { weRandomizerTool } from 'modules/world-edit/tools/randomizer' import { diff --git a/src/modules/world-edit/settings.ts b/src/modules/world-edit/settings.ts index dfac7b35..aaec9bd6 100644 --- a/src/modules/world-edit/settings.ts +++ b/src/modules/world-edit/settings.ts @@ -1,4 +1,4 @@ -import { Settings } from 'lib' +import { Settings } from 'lib/settings' export const worldEditPlayerSettings = Settings.player('§6World§dEdit\n§7Настройки строителя мира', 'we', { noBrushParticles: { diff --git a/src/modules/world-edit/tools/brush.ts b/src/modules/world-edit/tools/brush.ts index d4ed4fd1..77d540b5 100644 --- a/src/modules/world-edit/tools/brush.ts +++ b/src/modules/world-edit/tools/brush.ts @@ -1,5 +1,5 @@ import { ContainerSlot, Entity, Player, system, world } from '@minecraft/server' -import { ModalForm, Vec, is, isKeyof, isLocationError } from 'lib' + import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { Items } from 'lib/assets/custom-items' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/world-edit/tools/create-region.ts b/src/modules/world-edit/tools/create-region.ts index 391536e8..16a0f6f8 100644 --- a/src/modules/world-edit/tools/create-region.ts +++ b/src/modules/world-edit/tools/create-region.ts @@ -1,5 +1,5 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' -import { ModalForm, Region, regionTypes, Vec } from 'lib' + import { Items } from 'lib/assets/custom-items' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n } from 'lib/i18n/text' diff --git a/src/modules/world-edit/tools/dash.ts b/src/modules/world-edit/tools/dash.ts index e0359056..59f9d599 100644 --- a/src/modules/world-edit/tools/dash.ts +++ b/src/modules/world-edit/tools/dash.ts @@ -1,5 +1,5 @@ import { world } from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { Items } from 'lib/assets/custom-items' world.afterEvents.itemUse.subscribe(({ itemStack, source }) => { diff --git a/src/modules/world-edit/tools/debug-stick.ts b/src/modules/world-edit/tools/debug-stick.ts index 574381a2..64a534b9 100644 --- a/src/modules/world-edit/tools/debug-stick.ts +++ b/src/modules/world-edit/tools/debug-stick.ts @@ -1,9 +1,10 @@ import { Block, BlockStates, ContainerSlot, ItemStack, Player } from '@minecraft/server' import { BlockStateSuperset } from '@minecraft/vanilla-data' -import { ModalForm, Vec } from 'lib' import { Items } from 'lib/assets/custom-items' import { ActionbarPriority } from 'lib/extensions/on-screen-display' +import { ModalForm } from 'lib/form/modal' import { i18n, noI18n } from 'lib/i18n/text' +import { Vec } from 'lib/vector' import { WorldEditTool } from '../lib/world-edit-tool' import { WEeditBlockStatesMenu } from '../menu' diff --git a/src/modules/world-edit/tools/multi-brush.ts b/src/modules/world-edit/tools/multi-brush.ts index 0a82c15b..d547db9f 100644 --- a/src/modules/world-edit/tools/multi-brush.ts +++ b/src/modules/world-edit/tools/multi-brush.ts @@ -1,5 +1,5 @@ import { Direction, ItemStack, Player } from '@minecraft/server' -import { Vec } from 'lib' +import { Vec } from 'lib/vector' import { Items } from 'lib/assets/custom-items' import { ToolsDataStorage, WorldEditMultiTool } from '../lib/world-edit-multi-tool' import { WorldEditTool } from '../lib/world-edit-tool' diff --git a/src/modules/world-edit/tools/randomizer.ts b/src/modules/world-edit/tools/randomizer.ts index ba094e95..0bd59421 100644 --- a/src/modules/world-edit/tools/randomizer.ts +++ b/src/modules/world-edit/tools/randomizer.ts @@ -1,7 +1,7 @@ import { ContainerSlot, ItemStack, Player, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { ModalForm } from 'lib' +import { ModalForm } from 'lib/form/modal' import { i18n } from 'lib/i18n/text' import { isNotPlaying } from 'lib/utils/game' import { WorldEditTool } from '../lib/world-edit-tool' diff --git a/src/modules/world-edit/tools/shovel.ts b/src/modules/world-edit/tools/shovel.ts index 7a1ac7f2..83b25a98 100644 --- a/src/modules/world-edit/tools/shovel.ts +++ b/src/modules/world-edit/tools/shovel.ts @@ -1,8 +1,9 @@ import { ContainerSlot, ItemStack, Player, world } from '@minecraft/server' -import { ModalForm, Vec } from 'lib' import { Items } from 'lib/assets/custom-items' import { ActionbarPriority } from 'lib/extensions/on-screen-display' +import { ModalForm } from 'lib/form/modal' import { i18n } from 'lib/i18n/text' +import { Vec } from 'lib/vector' import { WorldEdit } from 'modules/world-edit/lib/world-edit' import { WorldEditTool } from '../lib/world-edit-tool' import { skipForBlending } from '../utils/blending' diff --git a/src/modules/world-edit/tools/smooth.ts b/src/modules/world-edit/tools/smooth.ts index 893390e5..3e3c674a 100644 --- a/src/modules/world-edit/tools/smooth.ts +++ b/src/modules/world-edit/tools/smooth.ts @@ -1,8 +1,11 @@ import { Block, BlockPermutation, ContainerSlot, Player, system, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { is, ModalForm, util, Vec } from 'lib' import { Items } from 'lib/assets/custom-items' +import { ModalForm } from 'lib/form/modal' +import { is } from 'lib/roles' +import { util } from 'lib/util' +import { Vec } from 'lib/vector' import { WorldEdit } from 'modules/world-edit/lib/world-edit' import { BlocksSetRef, diff --git a/src/modules/world-edit/tools/tool.ts b/src/modules/world-edit/tools/tool.ts index 5fa63539..261075b7 100644 --- a/src/modules/world-edit/tools/tool.ts +++ b/src/modules/world-edit/tools/tool.ts @@ -1,8 +1,11 @@ import { ContainerSlot, MolangVariableMap, Player, system, world } from '@minecraft/server' -import { ActionForm, ModalForm, Vec, inspect } from 'lib' import { Items } from 'lib/assets/custom-items' import { ListParticles } from 'lib/assets/particles' import { ListSounds } from 'lib/assets/sounds' +import { ActionForm } from 'lib/form/action' +import { ModalForm } from 'lib/form/modal' +import { inspect } from 'lib/utils/inspect' +import { Vec } from 'lib/vector' import { WorldEditTool } from '../lib/world-edit-tool' const actions: Record = { diff --git a/src/modules/world-edit/utils/blocks-set.ts b/src/modules/world-edit/utils/blocks-set.ts index 7fe4f884..0e33c7eb 100644 --- a/src/modules/world-edit/utils/blocks-set.ts +++ b/src/modules/world-edit/utils/blocks-set.ts @@ -1,10 +1,11 @@ import { Block, BlockPermutation, Player } from '@minecraft/server' import { BlockStateSuperset } from '@minecraft/vanilla-data' -import { noNullable, translateTypeId } from 'lib' import { table } from 'lib/database/abstract' import { DEFAULT_BLOCK_SETS, DEFAULT_REPLACE_TARGET_SETS, REPLACE_MODES } from './default-block-sets' import { Language } from 'lib/assets/lang' +import { noNullable } from 'lib/util' +import { translateTypeId } from 'lib/i18n/lang' export type BlockStateWeight = [...Parameters, number] diff --git a/src/modules/world-edit/utils/default-block-sets.ts b/src/modules/world-edit/utils/default-block-sets.ts index faa69353..85048a35 100644 --- a/src/modules/world-edit/utils/default-block-sets.ts +++ b/src/modules/world-edit/utils/default-block-sets.ts @@ -1,6 +1,6 @@ import { BlockPermutation, BlockTypes, LiquidType } from '@minecraft/server' import { BlockStateSuperset, MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { noNullable } from 'lib' +import { noNullable } from 'lib/util' import { BlockStateWeight, BlocksSets, diff --git a/src/test/utils.ts b/src/test/utils.ts index a8c15f11..ca9a52f4 100644 --- a/src/test/utils.ts +++ b/src/test/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Player, system } from '@minecraft/server' -import { EventSignal } from 'lib' +import { EventSignal } from 'lib/event-signal' import type { Table } from 'lib/database/abstract' import type { TestFormCallback, TFD } from 'test/__mocks__/minecraft_server-ui' From 16c935272a9233229360ee495d181e84966f1a8d Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:36:20 +0300 Subject: [PATCH 03/14] fix: migrate to direct imports instead of lib --- eslint.config.js | 1 + src/lib/command/index.ts | 6 +- src/lib/i18n/intl.test.ts | 1 + src/lib/i18n/text.test.ts | Bin 12245 -> 12276 bytes src/lib/region/database.test.ts | 2 + src/lib/region/kinds/region.test.ts | 2 + src/lib/shop/buttons/sellable-item.test.ts | 1 + src/lib/utils/logger.test.ts | 1 + src/modules/anticheat/forbidden-items.ts | 3 + src/modules/anticheat/whitelist.ts | 4 + src/modules/commands/camera.ts | 1 + src/modules/commands/db.ts | 5 + src/modules/commands/gamemode.ts | 3 + src/modules/commands/help.ts | 2 + src/modules/commands/items.ts | 3 + src/modules/commands/mail.ts | 294 +++++++++--------- src/modules/commands/pid.ts | 2 + src/modules/commands/player.ts | 117 ++++--- src/modules/commands/role.ts | 3 +- src/modules/commands/rtp.ts | 2 + src/modules/commands/scores.ts | 5 + src/modules/commands/send.ts | 3 + src/modules/commands/sit.ts | 1 + src/modules/commands/socials.ts | 1 + src/modules/commands/stats.ts | 115 +++---- src/modules/commands/version.ts | 1 + src/modules/commands/wipe.ts | 24 +- .../features/break-place-outside-of-region.ts | 2 + src/modules/indicator/health.ts | 3 + src/modules/indicator/player-name-tag.ts | 1 + src/modules/indicator/pvp.ts | 5 + src/modules/indicator/test-damage.ts | 3 + src/modules/minigames/BattleRoyal/var.ts | 1 + src/modules/minigames/Builder.ts | 2 + src/modules/places/anarchy/airdrop.ts | 4 + src/modules/places/anarchy/anarchy.ts | 6 + src/modules/places/anarchy/quartz.ts | 2 + src/modules/places/base/actions/create.ts | 5 + src/modules/places/base/actions/rotting.ts | 20 +- src/modules/places/base/region.ts | 1 + src/modules/places/dungeons/command.ts | 3 + src/modules/places/dungeons/custom-dungeon.ts | 7 + src/modules/places/dungeons/dungeon.ts | 17 +- src/modules/places/dungeons/loot.ts | 2 + src/modules/places/dungeons/warden.ts | 26 +- .../places/lib/city-investigating-quest.ts | 1 + src/modules/places/lib/city.ts | 2 + src/modules/places/lib/safe-place.ts | 20 +- .../places/mineshaft/mineshaft-region.ts | 3 + src/modules/places/mineshaft/ore-collector.ts | 1 + src/modules/places/spawn.ts | 5 + src/modules/places/stone-quarry/furnacer.ts | 3 + src/modules/places/stone-quarry/gunsmith.ts | 1 + .../places/stone-quarry/stone-quarry.ts | 1 + .../places/stone-quarry/wither.boss.ts | 3 + src/modules/places/tech-city/golem.boss.ts | 4 + src/modules/places/tech-city/tech-city.ts | 1 + .../places/village-of-explorers/mage.ts | 4 + .../places/village-of-explorers/slime.boss.ts | 2 + .../village-of-explorers.ts | 1 + .../village-of-miners/village-of-miners.ts | 2 + src/modules/pvp/cannon.ts | 5 + src/modules/pvp/ice-bomb.ts | 2 + src/modules/pvp/item-ability.ts | 1 + src/modules/pvp/raid.ts | 3 + src/modules/quests/daily/index.ts | 1 + src/modules/quests/learning/airdrop.ts | 1 + src/modules/quests/learning/learning.ts | 6 + .../survival/death-quest-and-gravestone.ts | 8 +- src/modules/survival/locked-features.ts | 2 + src/modules/survival/menu.ts | 167 ++++------ src/modules/survival/random-teleport.ts | 3 + src/modules/survival/realtime.ts | 1 + src/modules/survival/recurring-events.ts | 135 ++++---- src/modules/survival/sidebar.ts | 5 + src/modules/survival/speedrun/target.ts | 2 + src/modules/test/edit-structure.ts | 2 + src/modules/test/minimap.ts | 1 + src/modules/test/properties.ts | 1 + src/modules/world-edit/commands/general/id.ts | 2 + .../commands/region/set/set-selection.ts | 1 + .../world-edit/lib/world-edit-multi-tool.ts | 5 + src/modules/world-edit/lib/world-edit-tool.ts | 2 + src/modules/world-edit/lib/world-edit.ts | 4 + src/modules/world-edit/tools/brush.ts | 5 + src/modules/world-edit/tools/create-region.ts | 4 + 86 files changed, 606 insertions(+), 530 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 53d59f86..26d6d842 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -37,6 +37,7 @@ export default ts.config( 'no-empty': 'off', 'no-console': 'off', 'no-undef': 'off', + 'no-var': 'off', 'prefer-const': 'warn', 'lines-between-class-members': 'off', '@typescript-eslint/no-explicit-any': 'off', diff --git a/src/lib/command/index.ts b/src/lib/command/index.ts index fcbf235c..d5af01ec 100644 --- a/src/lib/command/index.ts +++ b/src/lib/command/index.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/unified-signatures */ import { ChatSendAfterEvent, Player, system, world } from '@minecraft/server' -import { defaultLang, Language } from 'lib/assets/lang' +import { Language } from 'lib/assets/lang' import { i18n, noI18n } from 'lib/i18n/text' import { stringifyError } from 'lib/util' import { stringifySymbol } from 'lib/utils/inspect' @@ -72,8 +72,8 @@ export class Command v => v.sys.type.matches(args[i]!).success || (!args[i] && v.sys.type.optional), ) if (!child && !args[i] && start.sys.callback) return 'success' - if (!child) return commandSyntaxFail(event.sender, command, args, i), 'fail' - if (!child.sys.requires(event.sender)) return commandNoPermissions(event.sender, child), 'fail' + if (!child) return (commandSyntaxFail(event.sender, command, args, i), 'fail') + if (!child.sys.requires(event.sender)) return (commandNoPermissions(event.sender, child), 'fail') childs.push(child) return getChilds(child, i + 1) } diff --git a/src/lib/i18n/intl.test.ts b/src/lib/i18n/intl.test.ts index 668ea535..918b8962 100644 --- a/src/lib/i18n/intl.test.ts +++ b/src/lib/i18n/intl.test.ts @@ -1,6 +1,7 @@ import { Language, supportedLanguages } from 'lib/assets/lang' import { intlListFormat, intlRemaining } from './intl' import { i18n } from './text' +import { ms } from 'lib/utils/ms' describe('intlListFormat', () => { it('should translate', () => { diff --git a/src/lib/i18n/text.test.ts b/src/lib/i18n/text.test.ts index b18a7baa62cc8a37e882013cee5138687f5bc740..01ed24c3df14f6aedb831932007f2d8486239135 100644 GIT binary patch delta 137 zcmcZ_|0RAx@Wk9=vD{*XT7|Tt{9Fa~oXjNs(vr-aV*T8Wm*25ZE)ZjxJfDek@->ba zPEB4e1u&Sb$SW~HwW@Wf+$6W!}KzI@0&d4Uwq5&)_!&n(FRDyyFSfm2!tMG9nQ^<;T2`AIBdlUPJ2>#*=nuHfPU zGMQ?1;9>$mzD=zThE4^ASWH1I>dP& diff --git a/src/lib/region/database.test.ts b/src/lib/region/database.test.ts index 06cff14e..8614e471 100644 --- a/src/lib/region/database.test.ts +++ b/src/lib/region/database.test.ts @@ -7,6 +7,8 @@ import { restoreRegionFromJSON, TEST_clearSaveableRegions, } from './database' +import { RegionIsSaveable } from './kinds/region' +import { Region } from './kinds/region' class TestK1Region extends Region { method() {} diff --git a/src/lib/region/kinds/region.test.ts b/src/lib/region/kinds/region.test.ts index ea4f35f8..b36e9b2f 100644 --- a/src/lib/region/kinds/region.test.ts +++ b/src/lib/region/kinds/region.test.ts @@ -3,6 +3,8 @@ import { Vec } from 'lib/vector' import { TEST_clearDatabase, TEST_createPlayer } from 'test/utils' import { SphereArea } from '../areas/sphere' import { Region } from './region' +import { registerSaveableRegion } from '../database' +import { RegionDatabase } from '../database' describe('Region', () => { beforeAll(() => { diff --git a/src/lib/shop/buttons/sellable-item.test.ts b/src/lib/shop/buttons/sellable-item.test.ts index a5e4920e..6031851c 100644 --- a/src/lib/shop/buttons/sellable-item.test.ts +++ b/src/lib/shop/buttons/sellable-item.test.ts @@ -3,6 +3,7 @@ import { TEST_createPlayer, TEST_onFormOpen } from 'test/utils' import { Shop } from '../shop' import 'lib/database/scoreboard' +import { doNothing } from 'lib/util' describe('sellableItem', () => { it('should sell items', () => { diff --git a/src/lib/utils/logger.test.ts b/src/lib/utils/logger.test.ts index a9b17868..4b2a2377 100644 --- a/src/lib/utils/logger.test.ts +++ b/src/lib/utils/logger.test.ts @@ -1,5 +1,6 @@ import { TEST_createPlayer } from 'test/utils' import { createLogger } from './logger' +import { util } from 'lib/util' describe('Logger', () => { it('should create logger that prints debug info', () => { diff --git a/src/modules/anticheat/forbidden-items.ts b/src/modules/anticheat/forbidden-items.ts index 842fd6ea..4c5064c7 100644 --- a/src/modules/anticheat/forbidden-items.ts +++ b/src/modules/anticheat/forbidden-items.ts @@ -2,6 +2,9 @@ import { system, world } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' import { antiCheatLogger } from './log-provider' +import { ActionGuardOrder } from 'lib/region' +import { actionGuard } from 'lib/region' +import { isNotPlaying } from 'lib/utils/game' const forbiddenItems: string[] = [ MinecraftItemTypes.Barrier, diff --git a/src/modules/anticheat/whitelist.ts b/src/modules/anticheat/whitelist.ts index 2295b663..5a510746 100644 --- a/src/modules/anticheat/whitelist.ts +++ b/src/modules/anticheat/whitelist.ts @@ -2,6 +2,10 @@ import { system, world } from '@minecraft/server' import { defaultLang } from 'lib/assets/lang' import { noI18n } from 'lib/i18n/text' +import { is } from 'lib/roles' +import { DEFAULT_ROLE } from 'lib/roles' +import { ROLES } from 'lib/roles' +import { Settings } from 'lib/settings' import { createLogger } from 'lib/utils/logger' // Delay execution to move whitelist settings to the end of the settings menu diff --git a/src/modules/commands/camera.ts b/src/modules/commands/camera.ts index db086d64..c19dbed8 100644 --- a/src/modules/commands/camera.ts +++ b/src/modules/commands/camera.ts @@ -1,4 +1,5 @@ import { i18n } from 'lib/i18n/text' +import { restorePlayerCamera } from 'lib/utils/game' new Command('camera').setDescription(i18n`Возвращает камеру в исходное состояние`).executes(ctx => { restorePlayerCamera(ctx.player, 1) diff --git a/src/modules/commands/db.ts b/src/modules/commands/db.ts index a07297a7..47145ec4 100644 --- a/src/modules/commands/db.ts +++ b/src/modules/commands/db.ts @@ -7,6 +7,11 @@ import { ActionForm } from 'lib/form/action' import { ModalForm } from 'lib/form/modal' import { i18n, noI18n } from 'lib/i18n/text' import { stringifyBenchmarkResult } from './stringifyBenchmarkReult' +import { util } from 'lib/util' +import { inspect } from 'lib/util' +import { getRole } from 'lib/roles' +import { ROLES } from 'lib/roles' +import { ArrayForm } from 'lib/form/array' new Command('db') .setDescription('Просматривает базу данных') diff --git a/src/modules/commands/gamemode.ts b/src/modules/commands/gamemode.ts index e7cb6cc5..24c90533 100644 --- a/src/modules/commands/gamemode.ts +++ b/src/modules/commands/gamemode.ts @@ -5,6 +5,9 @@ import { MinecraftEffectTypes } from '@minecraft/vanilla-data' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' +import { Temporary } from 'lib/temporary' +import { is } from 'lib/roles' +import { isNotPlaying } from 'lib/utils/game' import { WeakPlayerMap } from 'lib/weak-player-storage' function fastGamemode(mode: GameMode, shorname: string) { diff --git a/src/modules/commands/help.ts b/src/modules/commands/help.ts index b1ad7246..0944ea87 100644 --- a/src/modules/commands/help.ts +++ b/src/modules/commands/help.ts @@ -5,6 +5,8 @@ import { CmdLet } from 'lib/command/cmdlet' import { Command } from 'lib/command/index' import { commandNoPermissions, commandNotFound } from 'lib/command/utils' import { i18n, noI18n } from 'lib/i18n/text' +import { ROLES } from 'lib/roles' +import { getRole } from 'lib/roles' const help = new Command('help') .setDescription(i18n`Выводит список команд`) diff --git a/src/modules/commands/items.ts b/src/modules/commands/items.ts index 8ffef114..5dd0b082 100644 --- a/src/modules/commands/items.ts +++ b/src/modules/commands/items.ts @@ -1,3 +1,6 @@ +import { langToken } from 'lib/i18n/lang' +import { translateToken } from 'lib/i18n/lang' +import { ArrayForm } from 'lib/form/array' import { noI18n } from 'lib/i18n/text' import { customItems } from 'lib/rpg/custom-item' diff --git a/src/modules/commands/mail.ts b/src/modules/commands/mail.ts index 27713121..30479e24 100644 --- a/src/modules/commands/mail.ts +++ b/src/modules/commands/mail.ts @@ -1,169 +1,155 @@ -import { Player } from "@minecraft/server"; -import { ActionForm, ArrayForm, Mail, Menu, Settings, ask } from "lib"; -import { i18n, i18nPlural } from "lib/i18n/text"; -import { Join } from "lib/player-join"; -import { Rewards } from "lib/utils/rewards"; - -const command = new Command("mail") - .setDescription(i18n`Посмотреть входящие сообщения почты`) - .setPermissions("member") - .executes((ctx) => mailMenu(ctx.player)); +import { Player } from '@minecraft/server' +import { ActionForm } from 'lib/form/action' +import { ArrayForm } from 'lib/form/array' +import { ask } from 'lib/form/message' +import { i18n, i18nPlural } from 'lib/i18n/text' +import { Mail } from 'lib/mail' +import { Join } from 'lib/player-join' +import { Menu } from 'lib/rpg/menu' +import { Settings } from 'lib/settings' +import { Rewards } from 'lib/utils/rewards' + +const command = new Command('mail') + .setDescription(i18n`Посмотреть входящие сообщения почты`) + .setPermissions('member') + .executes(ctx => mailMenu(ctx.player)) const getSettings = Settings.player(...Menu.settings, { - mailReadOnOpen: { - name: i18n`Читать письмо при открытии`, - description: i18n`Помечать ли письмо прочитанным при открытии`, - value: true, - }, - mailClaimOnDelete: { - name: i18n`Собирать награды при удалении`, - description: i18n`Собирать ли награды при удалении письма`, - value: true, - }, -}); + mailReadOnOpen: { + name: i18n`Читать письмо при открытии`, + description: i18n`Помечать ли письмо прочитанным при открытии`, + value: true, + }, + mailClaimOnDelete: { + name: i18n`Собирать награды при удалении`, + description: i18n`Собирать ли награды при удалении письма`, + value: true, + }, +}) const getJoinSettings = Settings.player(...Join.settings.extend, { - unreadMails: { - name: i18n`Почта`, - description: i18n`Показывать ли при входе сообщение с кол-вом непрочитанных`, - value: true, - }, -}); + unreadMails: { + name: i18n`Почта`, + description: i18n`Показывать ли при входе сообщение с кол-вом непрочитанных`, + value: true, + }, +}) export function mailMenu(player: Player, back?: VoidFunction) { - new ArrayForm( - i18n`Почта`.badge(Mail.getUnreadMessagesCount(player.id)), - Mail.getLetters(player.id) - ) - .filters({ - unread: { - name: i18n`Непрочитанные`, - description: i18n`Показывать только непрочитанные сообщения`, - value: false, - }, - unclaimed: { - name: i18n`Несобранные награды`, - description: i18n`У письма есть несобранные награды`, - value: false, - }, - sort: { - name: i18n`Соритровать по`, - value: [ - ["date", i18n`Дате`], - ["name", i18n`Имени`], - ], - }, - }) - .button(({ letter, index }) => { - const name = `${letter.read ? "§7" : "§f"}${letter.title}${ - letter.read ? "\n§8" : "§c*\n§7" - }${letter.content}`; - return [ - name, - () => { - letterDetailsMenu({ letter, index }, player); - if (getSettings(player).mailReadOnOpen) - Mail.readMessage(player.id, index); - }, - ]; - }) - .sort((keys, filters) => { - if (filters.unread) keys = keys.filter((letter) => !letter.letter.read); - - if (filters.unclaimed) - keys = keys.filter((letter) => !letter.letter.rewardsClaimed); - - filters.sort === "name" - ? keys.sort((letterA, letterB) => - letterA.letter.title.localeCompare(letterB.letter.title) - ) - : keys.reverse(); - - return keys; - }) - .back(back) - .show(player); + new ArrayForm(i18n`Почта`.badge(Mail.getUnreadMessagesCount(player.id)), Mail.getLetters(player.id)) + .filters({ + unread: { + name: i18n`Непрочитанные`, + description: i18n`Показывать только непрочитанные сообщения`, + value: false, + }, + unclaimed: { + name: i18n`Несобранные награды`, + description: i18n`У письма есть несобранные награды`, + value: false, + }, + sort: { + name: i18n`Соритровать по`, + value: [ + ['date', i18n`Дате`], + ['name', i18n`Имени`], + ], + }, + }) + .button(({ letter, index }) => { + const name = `${letter.read ? '§7' : '§f'}${letter.title}${letter.read ? '\n§8' : '§c*\n§7'}${letter.content}` + return [ + name, + () => { + letterDetailsMenu({ letter, index }, player) + if (getSettings(player).mailReadOnOpen) Mail.readMessage(player.id, index) + }, + ] + }) + .sort((keys, filters) => { + if (filters.unread) keys = keys.filter(letter => !letter.letter.read) + + if (filters.unclaimed) keys = keys.filter(letter => !letter.letter.rewardsClaimed) + + filters.sort === 'name' + ? keys.sort((letterA, letterB) => letterA.letter.title.localeCompare(letterB.letter.title)) + : keys.reverse() + + return keys + }) + .back(back) + .show(player) } function letterDetailsMenu( - { letter, index }: ReturnType<(typeof Mail)["getLetters"]>[number], - player: Player, - back = () => mailMenu(player), - message = "" + { letter, index }: ReturnType<(typeof Mail)['getLetters']>[number], + player: Player, + back = () => mailMenu(player), + message = '', ) { - const settings = getSettings(player); - // TODO Fix collors - // TODO Rewrite to use new form - const form = new ActionForm( - letter.title, - i18n`${message}${letter.content}\n\n§l§fНаграды:§r\n${Rewards.restore( - letter.rewards - ).toString(player)}`.to(player.lang) - ).addButtonBack(back, player.lang); - - if (!letter.rewardsClaimed && letter.rewards.length) - if (player.database.inv !== "anarchy") { - form.button(i18n.disabled`Забрать награду`.to(player.lang), () => - letterDetailsMenu( - { letter, index }, - player, - back, - i18n.error`Вы не можете забрать награды не находясь на анархии`.to( - player.lang - ) - ) - ); - } else { - form.button(i18n`Забрать награду`.to(player.lang), () => { - Mail.claimRewards(player, index); - letterDetailsMenu( - { letter, index }, - player, - back, - message + i18n.success`Награда успешно забрана!\n\n`.to(player.lang) - ); - }); - } - - if (!letter.read && !settings.mailReadOnOpen) - form.button(i18n`Пометить как прочитанное`.to(player.lang), () => { - Mail.readMessage(player.id, index); - back(); - }); - - let deleteDescription = i18n.error`Удалить письмо?`.to(player.lang); - if (!letter.rewardsClaimed) { - if (getSettings(player).mailClaimOnDelete) { - deleteDescription += i18n` Все награды будут собраны автоматически`.to( - player.lang - ); - } else { - deleteDescription += - i18n` Вы потеряете все награды, прикрепленные к письму!`.to( - player.lang - ); - } - } - - form.button(i18n.error`Удалить письмо`.to(player.lang), null, () => { - ask(player, deleteDescription, i18n`Удалить`, () => { - if (getSettings(player).mailClaimOnDelete) - Mail.claimRewards(player, index); - Mail.deleteMessage(player, index); - back(); - }); - }); - - form.show(player); + const settings = getSettings(player) + // TODO Fix collors + // TODO Rewrite to use new form + const form = new ActionForm( + letter.title, + i18n`${message}${letter.content}\n\n§l§fНаграды:§r\n${Rewards.restore(letter.rewards).toString(player)}`.to( + player.lang, + ), + ).addButtonBack(back, player.lang) + + if (!letter.rewardsClaimed && letter.rewards.length) + if (player.database.inv !== 'anarchy') { + form.button(i18n.disabled`Забрать награду`.to(player.lang), () => + letterDetailsMenu( + { letter, index }, + player, + back, + i18n.error`Вы не можете забрать награды не находясь на анархии`.to(player.lang), + ), + ) + } else { + form.button(i18n`Забрать награду`.to(player.lang), () => { + Mail.claimRewards(player, index) + letterDetailsMenu( + { letter, index }, + player, + back, + message + i18n.success`Награда успешно забрана!\n\n`.to(player.lang), + ) + }) + } + + if (!letter.read && !settings.mailReadOnOpen) + form.button(i18n`Пометить как прочитанное`.to(player.lang), () => { + Mail.readMessage(player.id, index) + back() + }) + + let deleteDescription = i18n.error`Удалить письмо?`.to(player.lang) + if (!letter.rewardsClaimed) { + if (getSettings(player).mailClaimOnDelete) { + deleteDescription += i18n` Все награды будут собраны автоматически`.to(player.lang) + } else { + deleteDescription += i18n` Вы потеряете все награды, прикрепленные к письму!`.to(player.lang) + } + } + + form.button(i18n.error`Удалить письмо`.to(player.lang), null, () => { + ask(player, deleteDescription, i18n`Удалить`, () => { + if (getSettings(player).mailClaimOnDelete) Mail.claimRewards(player, index) + Mail.deleteMessage(player, index) + back() + }) + }) + + form.show(player) } Join.onMoveAfterJoin.subscribe(({ player }) => { - if (!getJoinSettings(player).unreadMails) return; + if (!getJoinSettings(player).unreadMails) return - const unreadCount = Mail.getUnreadMessagesCount(player.id); - if (unreadCount === 0) return; + const unreadCount = Mail.getUnreadMessagesCount(player.id) + if (unreadCount === 0) return - player.info( - i18n.join`${i18n.header`Почта:`} ${i18nPlural`У вас ${unreadCount} непрочитанных сообщений!`} ${command}` - ); -}); + player.info(i18n.join`${i18n.header`Почта:`} ${i18nPlural`У вас ${unreadCount} непрочитанных сообщений!`} ${command}`) +}) diff --git a/src/modules/commands/pid.ts b/src/modules/commands/pid.ts index e40f00d8..ffe7201e 100644 --- a/src/modules/commands/pid.ts +++ b/src/modules/commands/pid.ts @@ -1,7 +1,9 @@ import { Player } from '@minecraft/server' +import { ModalForm } from 'lib/form/modal' import { selectPlayer } from 'lib/form/select-player' import { i18n, noI18n } from 'lib/i18n/text' +import { is } from 'lib/roles' new Command('pid') .setDescription(i18n`Выдает ваш айди`) diff --git a/src/modules/commands/player.ts b/src/modules/commands/player.ts index 4a6416cd..79c5e29c 100644 --- a/src/modules/commands/player.ts +++ b/src/modules/commands/player.ts @@ -1,67 +1,58 @@ -import { Player } from "@minecraft/server"; -import { is, Portal, stringify } from "lib"; -import { Achievement } from "lib/achievements/achievement"; -import { LoreForm } from "lib/form/lore"; -import { form } from "lib/form/new"; -import { selectPlayer } from "lib/form/select-player"; -import { i18n } from "lib/i18n/text"; -import { statsForm } from "./stats"; +import { Player } from '@minecraft/server' +import { Achievement } from 'lib/achievements/achievement' +import { LoreForm } from 'lib/form/lore' +import { form } from 'lib/form/new' +import { selectPlayer } from 'lib/form/select-player' +import { i18n } from 'lib/i18n/text' +import { Portal } from 'lib/portals' +import { is } from 'lib/roles' +import { stringify } from 'lib/util' +import { statsForm } from './stats' -new Command("player") - .setAliases("p", "profile") - .setPermissions("everybody") - .setDescription(i18n`Общее меню игрока`) - .executes((ctx) => playerMenu({ targetId: ctx.player.id }).command(ctx)); +new Command('player') + .setAliases('p', 'profile') + .setPermissions('everybody') + .setDescription(i18n`Общее меню игрока`) + .executes(ctx => playerMenu({ targetId: ctx.player.id }).command(ctx)) -const playerMenu = form.params<{ targetId: string }>( - (f, { player, params: { targetId }, self }) => { - const moder = is(player.id, "moderator"); - const db = Player.database.getImmutable(targetId); - f.title(db.name ?? targetId); +const playerMenu = form.params<{ targetId: string }>((f, { player, params: { targetId }, self }) => { + const moder = is(player.id, 'moderator') + const db = Player.database.getImmutable(targetId) + f.title(db.name ?? targetId) - if (moder) { - f.button(i18n`Другие игроки`, () => { - selectPlayer(player, i18n`открыть его меню`.to(player.lang), self).then( - (target) => { - playerMenu({ targetId: target.id }).show(player, self); - } - ); - }); - } - f.button(i18n`Статистика`, statsForm({ targetId })); + if (moder) { + f.button(i18n`Другие игроки`, () => { + selectPlayer(player, i18n`открыть его меню`.to(player.lang), self).then(target => { + playerMenu({ targetId: target.id }).show(player, self) + }) + }) + } + f.button(i18n`Статистика`, statsForm({ targetId })) - f.button( - i18n`Задания` - .badge(db.quests?.active.length) - .size(db.quests?.completed.length), - form((f) => f.body(stringify(db.quests))) - ); - f.button( - form((f) => { - const all = Achievement.list.length; - const completed = db.achivs?.s.filter((e) => !!e.r).length ?? 0; - f.title( - i18n`Достижения ${completed}/${all} (${( - (completed / all) * - 100 - ).toFixed(0)}%%)` - ); - f.body(stringify(db.achivs)); - }) - ); - f.button( - form((f) => { - const portals = db.unlockedPortals; - f.title(i18n`Порталы ${portals?.length ?? 0}/${Portal.portals.size}`); - f.body(portals?.join("\n") ?? ""); - }) - ); - f.button( - form((f) => { - const lore = LoreForm.getAll(targetId); - f.title(i18n`Лор прочитан`.size(lore.length)); - f.body(lore.map((e) => stringify(e)).join("\n")); - }) - ); - } -); + f.button( + i18n`Задания`.badge(db.quests?.active.length).size(db.quests?.completed.length), + form(f => f.body(stringify(db.quests))), + ) + f.button( + form(f => { + const all = Achievement.list.length + const completed = db.achivs?.s.filter(e => !!e.r).length ?? 0 + f.title(i18n`Достижения ${completed}/${all} (${((completed / all) * 100).toFixed(0)}%%)`) + f.body(stringify(db.achivs)) + }), + ) + f.button( + form(f => { + const portals = db.unlockedPortals + f.title(i18n`Порталы ${portals?.length ?? 0}/${Portal.portals.size}`) + f.body(portals?.join('\n') ?? '') + }), + ) + f.button( + form(f => { + const lore = LoreForm.getAll(targetId) + f.title(i18n`Лор прочитан`.size(lore.length)) + f.body(lore.map(e => stringify(e)).join('\n')) + }), + ) +}) diff --git a/src/modules/commands/role.ts b/src/modules/commands/role.ts index 578ef499..7bea5e37 100644 --- a/src/modules/commands/role.ts +++ b/src/modules/commands/role.ts @@ -1,8 +1,9 @@ import { Player, world } from '@minecraft/server' import { ArrayForm } from 'lib/form/array' import { ModalForm } from 'lib/form/modal' +import { FormCallback } from 'lib/form/utils' import { i18n } from 'lib/i18n/text' -import { WHO_CAN_CHANGE } from 'lib/roles' +import { getRole, setRole, ROLES, WHO_CAN_CHANGE } from 'lib/roles' const FULL_HIERARCHY = Object.keys(ROLES) diff --git a/src/modules/commands/rtp.ts b/src/modules/commands/rtp.ts index 7dc3bf60..f559c9aa 100644 --- a/src/modules/commands/rtp.ts +++ b/src/modules/commands/rtp.ts @@ -1,7 +1,9 @@ import { Player, TicksPerSecond } from '@minecraft/server' import { MinecraftEffectTypes } from '@minecraft/vanilla-data' +import { LockAction } from 'lib/action' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' +import { Vec } from 'lib/vector' import { WeakPlayerMap } from 'lib/weak-player-storage' import { randomLocationInAnarchy } from 'modules/places/anarchy/random-location-in-anarchy' diff --git a/src/modules/commands/scores.ts b/src/modules/commands/scores.ts index ddbb7166..cf8eb503 100644 --- a/src/modules/commands/scores.ts +++ b/src/modules/commands/scores.ts @@ -3,9 +3,14 @@ import { Player, ScoreboardIdentityType, ScoreboardObjective, world } from '@minecraft/server' import { defaultLang } from 'lib/assets/lang' import { ScoreboardDB } from 'lib/database/scoreboard' +import { ActionForm } from 'lib/form/action' import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' import { selectPlayer } from 'lib/form/select-player' +import { BUTTON } from 'lib/form/utils' import { i18n, noI18n, textTable } from 'lib/i18n/text' +import { noBoolean } from 'lib/util' +import { Leaderboard } from 'lib/rpg/leaderboard' new Command('scores') .setDescription('Управляет счетом игроков (монеты, листья)') diff --git a/src/modules/commands/send.ts b/src/modules/commands/send.ts index 8dec660b..adfbd217 100644 --- a/src/modules/commands/send.ts +++ b/src/modules/commands/send.ts @@ -1,8 +1,11 @@ /* i18n-ignore */ import { Player, ScoreName, world } from '@minecraft/server' +import { ActionForm } from 'lib/form/action' +import { ModalForm } from 'lib/form/modal' import { createSelectPlayerMenu } from 'lib/form/select-player' import { i18n } from 'lib/i18n/text' +import { Mail } from 'lib/mail' import { Rewards } from 'lib/utils/rewards' interface SendState { diff --git a/src/modules/commands/sit.ts b/src/modules/commands/sit.ts index 270bc42f..9162f35e 100644 --- a/src/modules/commands/sit.ts +++ b/src/modules/commands/sit.ts @@ -1,4 +1,5 @@ import { system, world } from '@minecraft/server' +import { LockAction } from 'lib/action' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' diff --git a/src/modules/commands/socials.ts b/src/modules/commands/socials.ts index dbf9ea49..2ae4f5be 100644 --- a/src/modules/commands/socials.ts +++ b/src/modules/commands/socials.ts @@ -1,5 +1,6 @@ import { system, TicksPerSecond, world } from '@minecraft/server' import { emoji } from 'lib/assets/emoji' +import { Settings } from 'lib/settings' const socials = [ [`${emoji.custom.socials.discord} §9Discord§7: §b§ldsc.gg/lushway`, 'discord'], diff --git a/src/modules/commands/stats.ts b/src/modules/commands/stats.ts index 85828234..a6a0c79a 100644 --- a/src/modules/commands/stats.ts +++ b/src/modules/commands/stats.ts @@ -1,81 +1,50 @@ -import { Player, ScoreName, ScoreNames } from "@minecraft/server"; -import { - capitalize, - ScoreboardDB, - scoreboardDisplayNames, - scoreboardObjectiveNames, -} from "lib"; -import { form } from "lib/form/new"; -import { i18n, textTable } from "lib/i18n/text"; +import { Player, ScoreName, ScoreNames } from '@minecraft/server' +import { ScoreboardDB, scoreboardDisplayNames, scoreboardObjectiveNames } from 'lib/database/scoreboard' +import { form } from 'lib/form/new' +import { i18n, textTable } from 'lib/i18n/text' +import { capitalize } from 'lib/util' -new Command("stats") - .setDescription(i18n`Показывает статистику по игре`) - .executes((ctx) => statsForm({}).command(ctx)); +new Command('stats').setDescription(i18n`Показывает статистику по игре`).executes(ctx => statsForm({}).command(ctx)) -export const statsForm = form.params<{ targetId?: string }>( - (f, { player, params: { targetId = player.id } }) => { - const scores = ScoreboardDB.getOrCreateProxyFor(targetId); +export const statsForm = form.params<{ targetId?: string }>((f, { player, params: { targetId = player.id } }) => { + const scores = ScoreboardDB.getOrCreateProxyFor(targetId) - f.title(i18n.header`Статистика игрока ${Player.nameOrUnknown(targetId)}`); - f.body( - textTable([ - [ - scoreboardDisplayNames.totalOnlineTime, - formatDate(scores.totalOnlineTime), - ], - [ - scoreboardDisplayNames.anarchyOnlineTime, - formatDate(scores.anarchyOnlineTime), - ], - "", - [ - scoreboardDisplayNames.lastSeenDate, - i18n.time(Date.now() - scores.lastSeenDate * 1000), - ], - [ - scoreboardDisplayNames.anarchyLastSeenDate, - i18n.time(Date.now() - scores.anarchyLastSeenDate * 1000), - ], - "", - ...statsTable( - scores, - (key) => key, - (n) => n.to(player.lang) - ), - "", - ...statsTable( - scores, - (key) => `anarchy${capitalize(key)}`, - (n) => i18n`Анархия ${n}`.to(player.lang) - ), - ]) - ); - } -); + f.title(i18n.header`Статистика игрока ${Player.nameOrUnknown(targetId)}`) + f.body( + textTable([ + [scoreboardDisplayNames.totalOnlineTime, formatDate(scores.totalOnlineTime)], + [scoreboardDisplayNames.anarchyOnlineTime, formatDate(scores.anarchyOnlineTime)], + '', + [scoreboardDisplayNames.lastSeenDate, i18n.time(Date.now() - scores.lastSeenDate * 1000)], + [scoreboardDisplayNames.anarchyLastSeenDate, i18n.time(Date.now() - scores.anarchyLastSeenDate * 1000)], + '', + ...statsTable( + scores, + key => key, + n => n.to(player.lang), + ), + '', + ...statsTable( + scores, + key => `anarchy${capitalize(key)}`, + n => i18n`Анархия ${n}`.to(player.lang), + ), + ]), + ) +}) function formatDate(date: number) { - return i18n.hhmmss(date); + return i18n.hhmmss(date) } -function statsTable( - s: Player["scores"], - getKey: (k: ScoreNames.Stat) => ScoreName, - getN: (n: Text) => string -) { - const table: Text.Table[number][] = []; - for (const key of scoreboardObjectiveNames.stats) { - const k = getKey(key); - table.push([getN(scoreboardDisplayNames[k]), s[k]]); - if (key === "kills") - table.push([ - getN(i18n`Убийств/Смертей`), - s[getKey("kills")] / s[getKey("deaths")], - ]); - if (key === "damageGive") - table.push([ - getN(i18n`Нанесено/Получено`), - s[getKey("damageGive")] / s[getKey("damageRecieve")], - ]); - } - return table satisfies Text.Table; +function statsTable(s: Player['scores'], getKey: (k: ScoreNames.Stat) => ScoreName, getN: (n: Text) => string) { + const table: Text.Table[number][] = [] + for (const key of scoreboardObjectiveNames.stats) { + const k = getKey(key) + table.push([getN(scoreboardDisplayNames[k]), s[k]]) + if (key === 'kills') table.push([getN(i18n`Убийств/Смертей`), s[getKey('kills')] / s[getKey('deaths')]]) + if (key === 'damageGive') + table.push([getN(i18n`Нанесено/Получено`), s[getKey('damageGive')] / s[getKey('damageRecieve')]]) + } + return table satisfies Text.Table } diff --git a/src/modules/commands/version.ts b/src/modules/commands/version.ts index 1c7ce592..5e396c57 100644 --- a/src/modules/commands/version.ts +++ b/src/modules/commands/version.ts @@ -1,4 +1,5 @@ import { i18n, textTable } from 'lib/i18n/text' +import { is } from 'lib/roles' new Command('version') .setAliases('v') diff --git a/src/modules/commands/wipe.ts b/src/modules/commands/wipe.ts index 35e9c3b8..2aa323e8 100644 --- a/src/modules/commands/wipe.ts +++ b/src/modules/commands/wipe.ts @@ -1,22 +1,20 @@ import { GameMode, Player, PlayerDatabase, ScoreNames, ShortcutDimensions, system, world } from '@minecraft/server' -import { - Airdrop, - ArrayForm, - BUTTON, - Compass, - InventoryStore, - is, - Join, - ModalForm, - pick, - scoreboardObjectiveNames, - sizeOf, -} from 'lib' + import { table } from 'lib/database/abstract' +import { InventoryStore } from 'lib/database/inventory' +import { scoreboardObjectiveNames } from 'lib/database/scoreboard' +import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' import { form, NewFormCallback, NewFormCreator } from 'lib/form/new' +import { BUTTON } from 'lib/form/utils' import { i18n, noI18n } from 'lib/i18n/text' +import { Join } from 'lib/player-join' import { Quest } from 'lib/quest' +import { is } from 'lib/roles' +import { Airdrop } from 'lib/rpg/airdrop' +import { Compass } from 'lib/rpg/menu' import { enterNewbieMode, isNewbie } from 'lib/rpg/newbie' +import { pick, sizeOf } from 'lib/util' import { Anarchy } from 'modules/places/anarchy/anarchy' import { Spawn } from 'modules/places/spawn' import { updateBuilderStatus } from 'modules/world-edit/builder' diff --git a/src/modules/features/break-place-outside-of-region.ts b/src/modules/features/break-place-outside-of-region.ts index 1eaff45f..5f4e364a 100644 --- a/src/modules/features/break-place-outside-of-region.ts +++ b/src/modules/features/break-place-outside-of-region.ts @@ -1,10 +1,12 @@ import { Player, system } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' +import { Cooldown } from 'lib/cooldown' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' import { actionGuard, ActionGuardOrder, BLOCK_CONTAINERS, DOORS, GATES, SWITCHES, TRAPDOORS } from 'lib/region/index' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' +import { ms } from 'lib/utils/ms' import { BaseRegion } from 'modules/places/base/region' const INTERACTABLE = DOORS.concat(SWITCHES, TRAPDOORS, BLOCK_CONTAINERS, GATES) diff --git a/src/modules/indicator/health.ts b/src/modules/indicator/health.ts index 685adf15..892ab621 100644 --- a/src/modules/indicator/health.ts +++ b/src/modules/indicator/health.ts @@ -5,7 +5,10 @@ import { MinecraftEntityTypes } from '@minecraft/vanilla-data' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { ClosingChatSet } from 'lib/extensions/player' import { NOT_MOB_ENTITIES } from 'lib/region/config' +import { Boss } from 'lib/rpg/boss' import { isNotPlaying } from 'lib/utils/game' +import { Vec } from 'lib/vector' +import { ms } from 'lib/utils/ms' import { PlayerNameTagModifiers, setNameTag } from 'modules/indicator/player-name-tag' interface BaseHurtEntity { diff --git a/src/modules/indicator/player-name-tag.ts b/src/modules/indicator/player-name-tag.ts index 9bb3d64f..64495267 100644 --- a/src/modules/indicator/player-name-tag.ts +++ b/src/modules/indicator/player-name-tag.ts @@ -1,6 +1,7 @@ import { Entity, Player, system } from '@minecraft/server' import { getFullname } from 'lib/get-fullname' +import { isNotPlaying } from 'lib/utils/game' export const PlayerNameTagModifiers: ((player: Player) => string | false)[] = [ player => { diff --git a/src/modules/indicator/pvp.ts b/src/modules/indicator/pvp.ts index 8f59026f..25c2468e 100644 --- a/src/modules/indicator/pvp.ts +++ b/src/modules/indicator/pvp.ts @@ -1,10 +1,15 @@ import { EntityDamageCause, EntityHurtAfterEvent, Player, system, world } from '@minecraft/server' +import { LockAction } from 'lib/action' import { emoji } from 'lib/assets/emoji' import { Core } from 'lib/extensions/core' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' +import { BossArenaRegion } from 'lib/region' import { RegionEvents } from 'lib/region/events' +import { Boss } from 'lib/rpg/boss' +import { ms } from 'lib/utils/ms' +import { Settings } from 'lib/settings' import { WeakPlayerMap } from 'lib/weak-player-storage' import { Anarchy } from 'modules/places/anarchy/anarchy' diff --git a/src/modules/indicator/test-damage.ts b/src/modules/indicator/test-damage.ts index df4f84f4..80c5477d 100644 --- a/src/modules/indicator/test-damage.ts +++ b/src/modules/indicator/test-damage.ts @@ -3,9 +3,12 @@ import { EnchantmentType, EquipmentSlot, ItemStack, Player } from '@minecraft/server' import { registerAsync } from '@minecraft/server-gametest' import { MinecraftEnchantmentTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' +import { Enchantments } from 'lib/enchantments' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n } from 'lib/i18n/text' +import { isKeyof } from 'lib/util' +import { Temporary } from 'lib/temporary' import { TestStructures } from 'test/constants' const players: Player[] = [] diff --git a/src/modules/minigames/BattleRoyal/var.ts b/src/modules/minigames/BattleRoyal/var.ts index 1a4431ad..e95ae6df 100644 --- a/src/modules/minigames/BattleRoyal/var.ts +++ b/src/modules/minigames/BattleRoyal/var.ts @@ -1,5 +1,6 @@ import { table } from 'lib/database/abstract' import { EventSignal } from 'lib/event-signal' +import { Settings } from 'lib/settings' export const BATTLE_ROYAL_EVENTS = { join: new EventSignal(), diff --git a/src/modules/minigames/Builder.ts b/src/modules/minigames/Builder.ts index 0aedf39e..7e278bf7 100644 --- a/src/modules/minigames/Builder.ts +++ b/src/modules/minigames/Builder.ts @@ -1,4 +1,6 @@ import { Player } from '@minecraft/server' +import { LockAction } from 'lib/action' +import { Sidebar } from 'lib/sidebar' // TODO Add minigame place diff --git a/src/modules/places/anarchy/airdrop.ts b/src/modules/places/anarchy/airdrop.ts index 12c5958b..381748ad 100644 --- a/src/modules/places/anarchy/airdrop.ts +++ b/src/modules/places/anarchy/airdrop.ts @@ -5,6 +5,10 @@ import { i18n } from 'lib/i18n/text' import { Anarchy } from 'modules/places/anarchy/anarchy' import { CannonItem, CannonShellItem } from '../../pvp/cannon' import { randomLocationInAnarchy } from './random-location-in-anarchy' +import { Vec } from 'lib/vector' +import { isNotPlaying } from 'lib/utils/game' +import { Airdrop } from 'lib/rpg/airdrop' +import { Loot } from 'lib/rpg/loot-table' const base = new Loot('base_airdrop') .item('Gunpowder') diff --git a/src/modules/places/anarchy/anarchy.ts b/src/modules/places/anarchy/anarchy.ts index c034236f..080a84c0 100644 --- a/src/modules/places/anarchy/anarchy.ts +++ b/src/modules/places/anarchy/anarchy.ts @@ -12,6 +12,12 @@ import { Spawn } from 'modules/places/spawn' import { showSurvivalHud } from 'modules/survival/sidebar' import { AreaWithInventory } from '../lib/area-with-inventory' import { RadioactiveZone } from './radioactive-zone' +import { EventSignal } from 'lib/event-signal' +import { Vec } from 'lib/vector' +import { ValidLocation } from 'lib/location' +import { InventoryStore } from 'lib/database/inventory' +import { location } from 'lib/location' +import { Portal } from 'lib/portals' import('./airdrop') class AnarchyBuilder extends AreaWithInventory { diff --git a/src/modules/places/anarchy/quartz.ts b/src/modules/places/anarchy/quartz.ts index 5c478311..7fa302ae 100644 --- a/src/modules/places/anarchy/quartz.ts +++ b/src/modules/places/anarchy/quartz.ts @@ -7,6 +7,8 @@ import { RegionEvents } from 'lib/region/events' import { actionGuard, ActionGuardOrder, disableAdventureNear, Region, RegionPermissions } from 'lib/region/index' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { TechCity } from '../tech-city/tech-city' +import { ms } from 'lib/utils/ms' +import { isKeyof } from 'lib/util' export class QuartzMineRegion extends Region { protected priority = 100 diff --git a/src/modules/places/base/actions/create.ts b/src/modules/places/base/actions/create.ts index a563edd3..a59318e7 100644 --- a/src/modules/places/base/actions/create.ts +++ b/src/modules/places/base/actions/create.ts @@ -7,6 +7,11 @@ import { BaseItem, baseLogger } from '../base' import { baseLevels } from '../base-levels' import { baseCommand } from '../base-menu' import { BaseRegion } from '../region' +import { Vec } from 'lib/vector' +import { Region } from 'lib/region' +import { LockAction } from 'lib/action' +import { ActionGuardOrder } from 'lib/region' +import { actionGuard } from 'lib/region' actionGuard((_, __, ctx) => { if ( diff --git a/src/modules/places/base/actions/rotting.ts b/src/modules/places/base/actions/rotting.ts index c6569861..6122178d 100644 --- a/src/modules/places/base/actions/rotting.ts +++ b/src/modules/places/base/actions/rotting.ts @@ -1,17 +1,6 @@ import { Block, BlockPermutation, ContainerSlot, Player, system } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { - actionGuard, - ActionGuardOrder, - Cooldown, - getBlockStatus, - isEmpty, - isLocationError, - isNotPlaying, - Mail, - ms, - Vec, -} from 'lib' + import { table } from 'lib/database/abstract' import { form } from 'lib/form/new' import { Message } from 'lib/i18n/message' @@ -21,6 +10,13 @@ import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { itemNameXCount } from 'lib/utils/item-name-x-count' import { spawnParticlesInArea } from 'modules/world-edit/config' import { BaseRegion, RottingState } from '../region' +import { Cooldown } from 'lib/cooldown' +import { Mail } from 'lib/mail' +import { actionGuard, ActionGuardOrder } from 'lib/region' +import { isEmpty } from 'lib/util' +import { getBlockStatus, isLocationError, isNotPlaying } from 'lib/utils/game' +import { ms } from 'lib/utils/ms' +import { Vec } from 'lib/vector' const takeMaterialsTime = __DEV__ ? ms.from('day', 1) : ms.from('day', 1) const blocksReviseTime = __DEV__ ? ms.from('min', 1) : ms.from('min', 2) diff --git a/src/modules/places/base/region.ts b/src/modules/places/base/region.ts index d3d2703c..afa4d17d 100644 --- a/src/modules/places/base/region.ts +++ b/src/modules/places/base/region.ts @@ -7,6 +7,7 @@ import { registerSaveableRegion } from 'lib/region/database' import { RegionWithStructure } from 'lib/region/kinds/with-structure' import { getSafeFromRottingTime, materialsToRString } from './actions/rotting' import { baseLevels } from './base-levels' +import { disableAdventureNear } from 'lib/region' interface BaseLDB extends JsonObject { level: number diff --git a/src/modules/places/dungeons/command.ts b/src/modules/places/dungeons/command.ts index 4d7bafb2..627a3575 100644 --- a/src/modules/places/dungeons/command.ts +++ b/src/modules/places/dungeons/command.ts @@ -11,6 +11,9 @@ import { SphereArea } from 'lib/region/areas/sphere' import { DungeonRegion } from 'modules/places/dungeons/dungeon' import { CustomDungeonRegion } from './custom-dungeon' import { Dungeon } from './loot' +import { Vec } from 'lib/vector' +import { ArrayForm } from 'lib/form/array' +import { isKeyof } from 'lib/util' const toolSchema = new ItemLoreSchema('dungeonCreationTool', Items.WeTool) .property('type', String) diff --git a/src/modules/places/dungeons/custom-dungeon.ts b/src/modules/places/dungeons/custom-dungeon.ts index baa32e06..1fab2d0e 100644 --- a/src/modules/places/dungeons/custom-dungeon.ts +++ b/src/modules/places/dungeons/custom-dungeon.ts @@ -6,6 +6,13 @@ import { i18n, noI18n } from 'lib/i18n/text' import { Area } from 'lib/region/areas/area' import { DungeonRegion, DungeonRegionDatabase } from './dungeon' import { Dungeon } from './loot' +import { Vec } from 'lib/vector' +import { ModalForm } from 'lib/form/modal' +import { is } from 'lib/roles' +import { registerRegionType } from 'lib/region' +import { registerSaveableRegion } from 'lib/region' +import { ms } from 'lib/utils/ms' +import { RegionCreationOptions } from 'lib/region' interface CustomDungeonRegionDatabase extends DungeonRegionDatabase { chestLoot: { diff --git a/src/modules/places/dungeons/dungeon.ts b/src/modules/places/dungeons/dungeon.ts index 1b03de3b..f5c8b622 100644 --- a/src/modules/places/dungeons/dungeon.ts +++ b/src/modules/places/dungeons/dungeon.ts @@ -1,15 +1,6 @@ import { EntityTypes, Player, StructureRotation, StructureSaveMode, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { - adventureModeRegions, - Cooldown, - isKeyof, - LootTable, - ms, - registerRegionType, - registerSaveableRegion, - Vec, -} from 'lib' + import { StructureDungeonsId, StructureFile, structureFiles } from 'lib/assets/structures' import { NewFormCreator } from 'lib/form/new' import { i18n, noI18n } from 'lib/i18n/text' @@ -20,6 +11,12 @@ import { Region, RegionCreationOptions, RegionPermissions } from 'lib/region/kin import { createLogger } from 'lib/utils/logger' import { structureLikeRotate, structureLikeRotateRelative, toAbsolute, toRelative } from 'lib/utils/structure' import { Dungeon } from './loot' +import { Cooldown } from 'lib/cooldown' +import { registerSaveableRegion, registerRegionType, adventureModeRegions } from 'lib/region' +import { LootTable } from 'lib/rpg/loot-table' +import { isKeyof } from 'lib/util' +import { ms } from 'lib/utils/ms' +import { Vec } from 'lib/vector' const logger = createLogger('dungeon') diff --git a/src/modules/places/dungeons/loot.ts b/src/modules/places/dungeons/loot.ts index a651963a..00b4b2e0 100644 --- a/src/modules/places/dungeons/loot.ts +++ b/src/modules/places/dungeons/loot.ts @@ -5,6 +5,8 @@ import { CannonItem, CannonShellItem } from 'modules/pvp/cannon' import { FireBallItem } from 'modules/pvp/fireball' import { IceBombItem } from 'modules/pvp/ice-bomb' import { BaseItem } from '../base/base' +import { LootTable } from 'lib/rpg/loot-table' +import { Loot } from 'lib/rpg/loot-table' const defaultLoot = new Loot('dungeon_default_loot') .itemStack(CannonShellItem.blueprint) diff --git a/src/modules/places/dungeons/warden.ts b/src/modules/places/dungeons/warden.ts index ac2034a7..df1b850d 100644 --- a/src/modules/places/dungeons/warden.ts +++ b/src/modules/places/dungeons/warden.ts @@ -1,24 +1,24 @@ import { system } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { - actionGuard, - ActionGuardOrder, - disableAdventureNear, - fromMsToTicks, - ms, - PVP_ENTITIES, - Region, - RegionPermissions, - registerRegionType, - registerSaveableRegion, - Vec, -} from 'lib' + import { form } from 'lib/form/new' import { i18n, noI18n } from 'lib/i18n/text' import { anyPlayerNearRegion } from 'lib/player-move' import { rollChance } from 'lib/rpg/random' import { createLogger } from 'lib/utils/logger' import { BaseItem } from '../base/base' +import { + Region, + RegionPermissions, + PVP_ENTITIES, + registerSaveableRegion, + registerRegionType, + disableAdventureNear, + actionGuard, + ActionGuardOrder, +} from 'lib/region' +import { fromMsToTicks, ms } from 'lib/utils/ms' +import { Vec } from 'lib/vector' const logger = createLogger('warden') diff --git a/src/modules/places/lib/city-investigating-quest.ts b/src/modules/places/lib/city-investigating-quest.ts index bccdc601..d584fbb3 100644 --- a/src/modules/places/lib/city-investigating-quest.ts +++ b/src/modules/places/lib/city-investigating-quest.ts @@ -2,6 +2,7 @@ import { i18n } from 'lib/i18n/text' import { Quest } from 'lib/quest' import { RegionEvents } from 'lib/region/events' import { City } from './city' +import { isNotPlaying } from 'lib/utils/game' export class CityInvestigating { static list: CityInvestigating[] = [] diff --git a/src/modules/places/lib/city.ts b/src/modules/places/lib/city.ts index 319f048f..fd08de68 100644 --- a/src/modules/places/lib/city.ts +++ b/src/modules/places/lib/city.ts @@ -7,6 +7,8 @@ import { Npc } from 'lib/rpg/npc' import { Jeweler } from 'modules/places/lib/npc/jeweler' import { Scavenger } from './npc/scavenger' import { SafePlace } from './safe-place' +import { location } from 'lib/location' +import { LootTable } from 'lib/rpg/loot-table' export abstract class City extends SafePlace { protected createKits(normalLoot: LootTable, donutLoot: LootTable) { diff --git a/src/modules/places/lib/safe-place.ts b/src/modules/places/lib/safe-place.ts index b933a270..158e98ab 100644 --- a/src/modules/places/lib/safe-place.ts +++ b/src/modules/places/lib/safe-place.ts @@ -1,28 +1,22 @@ import { Player, system, TicksPerSecond } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { - actionGuard, - ActionGuardOrder, - ArrayForm, - debounceMenu, - location, - locationWithRadius, - locationWithRotation, - Portal, - SafeAreaRegion, - Vec, - Vector3Radius, -} from 'lib' + import { Sounds } from 'lib/assets/custom-sounds' import { emoji } from 'lib/assets/emoji' +import { ArrayForm } from 'lib/form/array' +import { debounceMenu } from 'lib/form/utils' import { SharedI18nMessage } from 'lib/i18n/message' import { i18n, noI18n } from 'lib/i18n/text' +import { locationWithRadius, locationWithRotation, location, Vector3Radius } from 'lib/location' +import { Portal } from 'lib/portals' +import { SafeAreaRegion, actionGuard, ActionGuardOrder } from 'lib/region' import { SphereArea } from 'lib/region/areas/sphere' import { RegionEvents } from 'lib/region/events' import { Group } from 'lib/rpg/place' import { MultiCost } from 'lib/shop/cost' import { ErrorCost } from 'lib/shop/cost/cost' import { Product } from 'lib/shop/product' +import { Vec } from 'lib/vector' export class SafePlace { static places: SafePlace[] = [] diff --git a/src/modules/places/mineshaft/mineshaft-region.ts b/src/modules/places/mineshaft/mineshaft-region.ts index 261e8c87..cf1d5e4b 100644 --- a/src/modules/places/mineshaft/mineshaft-region.ts +++ b/src/modules/places/mineshaft/mineshaft-region.ts @@ -7,6 +7,9 @@ import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { createLogger } from 'lib/utils/logger' import { MineareaRegion } from '../../../lib/region/kinds/minearea' import { ores, placeOre } from './algo' +import { registerRegionType } from 'lib/region' +import { Vec } from 'lib/vector' +import { ms } from 'lib/utils/ms' const logger = createLogger('Shaft') diff --git a/src/modules/places/mineshaft/ore-collector.ts b/src/modules/places/mineshaft/ore-collector.ts index ad3fd97f..a32f8a2c 100644 --- a/src/modules/places/mineshaft/ore-collector.ts +++ b/src/modules/places/mineshaft/ore-collector.ts @@ -2,6 +2,7 @@ import { MinecraftBlockTypes as b } from '@minecraft/vanilla-data' import { i18n } from 'lib/i18n/text' import { selectByChance } from 'lib/rpg/random' +import { stringifyError } from 'lib/util' export class Ore { private types: string[] = [] diff --git a/src/modules/places/spawn.ts b/src/modules/places/spawn.ts index 1a1ae58a..c22d54b2 100644 --- a/src/modules/places/spawn.ts +++ b/src/modules/places/spawn.ts @@ -13,6 +13,11 @@ import { isNotPlaying } from 'lib/utils/game' import { createLogger } from 'lib/utils/logger' import { showSurvivalHud } from 'modules/survival/sidebar' import { AreaWithInventory } from './lib/area-with-inventory' +import { InventoryStore } from 'lib/database/inventory' +import { util } from 'lib/util' +import { Settings } from 'lib/settings' +import { locationWithRotation } from 'lib/location' +import { Portal } from 'lib/portals' class SpawnBuilder extends AreaWithInventory { group = new Group('common', i18nShared`Общее`) diff --git a/src/modules/places/stone-quarry/furnacer.ts b/src/modules/places/stone-quarry/furnacer.ts index e2d45697..3b0f2ff4 100644 --- a/src/modules/places/stone-quarry/furnacer.ts +++ b/src/modules/places/stone-quarry/furnacer.ts @@ -12,6 +12,9 @@ import { FreeCost, MoneyCost } from 'lib/shop/cost' import { ShopNpc } from 'lib/shop/npc' import { lockBlockPriorToNpc } from 'modules/survival/locked-features' import { StoneQuarry } from './stone-quarry' +import { Vec } from 'lib/vector' +import { getAuxOrTexture } from 'lib/form/chest' +import { ms } from 'lib/utils/ms' const furnaceExpireTime = ms.from('hour', 1) const furnaceExpireTimeText = i18n`Ключ теперь привязан к этой печке! В течении часа вы можете открывать ее с помощью этого ключа!` diff --git a/src/modules/places/stone-quarry/gunsmith.ts b/src/modules/places/stone-quarry/gunsmith.ts index 1473ecc0..8aeffb32 100644 --- a/src/modules/places/stone-quarry/gunsmith.ts +++ b/src/modules/places/stone-quarry/gunsmith.ts @@ -1,5 +1,6 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' import { MinecraftItemTypes as i, MinecraftBlockTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' +import { translateTypeId } from 'lib/i18n/lang' import { i18n, i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' diff --git a/src/modules/places/stone-quarry/stone-quarry.ts b/src/modules/places/stone-quarry/stone-quarry.ts index 7fd7788e..cc158b49 100644 --- a/src/modules/places/stone-quarry/stone-quarry.ts +++ b/src/modules/places/stone-quarry/stone-quarry.ts @@ -12,6 +12,7 @@ import { Woodman } from '../lib/npc/woodman' import { Furnacer } from './furnacer' import { Gunsmith } from './gunsmith' import { createBossWither } from './wither.boss' +import { Loot } from 'lib/rpg/loot-table' class StoneQuarryBuilder extends City { constructor() { diff --git a/src/modules/places/stone-quarry/wither.boss.ts b/src/modules/places/stone-quarry/wither.boss.ts index 1b24de34..15bc9024 100644 --- a/src/modules/places/stone-quarry/wither.boss.ts +++ b/src/modules/places/stone-quarry/wither.boss.ts @@ -3,7 +3,10 @@ import { MinecraftEntityTypes } from '@minecraft/vanilla-data' import { i18nShared, noI18n } from 'lib/i18n/text' import { BigRegionStructure } from 'lib/region/big-structure' +import { Loot } from 'lib/rpg/loot-table' +import { Boss } from 'lib/rpg/boss' import { Group } from 'lib/rpg/place' +import { ms } from 'lib/utils/ms' export function createBossWither(group: Group) { const boss = Boss.create() diff --git a/src/modules/places/tech-city/golem.boss.ts b/src/modules/places/tech-city/golem.boss.ts index ef99674c..fac6d0e5 100644 --- a/src/modules/places/tech-city/golem.boss.ts +++ b/src/modules/places/tech-city/golem.boss.ts @@ -4,6 +4,10 @@ import { MinecraftEffectTypes, MinecraftEntityTypes } from '@minecraft/vanilla-d import { i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' import { Chip } from './engineer' +import { Vec } from 'lib/vector' +import { ms } from 'lib/utils/ms' +import { Loot } from 'lib/rpg/loot-table' +import { Boss } from 'lib/rpg/boss' export function createBossGolem(group: Group) { const boss = Boss.create() diff --git a/src/modules/places/tech-city/tech-city.ts b/src/modules/places/tech-city/tech-city.ts index baf5119b..53b66704 100644 --- a/src/modules/places/tech-city/tech-city.ts +++ b/src/modules/places/tech-city/tech-city.ts @@ -10,6 +10,7 @@ import { Stoner } from '../lib/npc/stoner' import { Woodman } from '../lib/npc/woodman' import { Engineer } from './engineer' import { createBossGolem } from './golem.boss' +import { Loot } from 'lib/rpg/loot-table' class TechCityBuilder extends City { constructor() { diff --git a/src/modules/places/village-of-explorers/mage.ts b/src/modules/places/village-of-explorers/mage.ts index 5d220e3e..cbfd96b8 100644 --- a/src/modules/places/village-of-explorers/mage.ts +++ b/src/modules/places/village-of-explorers/mage.ts @@ -10,6 +10,8 @@ import { } from '@minecraft/vanilla-data' import { Sounds } from 'lib/assets/custom-sounds' +import { Enchantments } from 'lib/enchantments' +import { getAuxOrTexture } from 'lib/form/chest' import { translateEnchantment, translateTypeId } from 'lib/i18n/lang' import { i18n, i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' @@ -17,6 +19,8 @@ import { Cost, MoneyCost, MultiCost } from 'lib/shop/cost' import { ErrorCost, FreeCost } from 'lib/shop/cost/cost' import { ShopFormSection } from 'lib/shop/form' import { ShopNpc } from 'lib/shop/npc' +import { addNamespace } from 'lib/util' +import { doNothing } from 'lib/util' import { copyAllItemPropertiesExceptEnchants } from 'lib/utils/game' import { FireBallItem } from 'modules/pvp/fireball' import { IceBombItem } from 'modules/pvp/ice-bomb' diff --git a/src/modules/places/village-of-explorers/slime.boss.ts b/src/modules/places/village-of-explorers/slime.boss.ts index 888213bb..4527e527 100644 --- a/src/modules/places/village-of-explorers/slime.boss.ts +++ b/src/modules/places/village-of-explorers/slime.boss.ts @@ -5,6 +5,8 @@ import { i18nShared } from 'lib/i18n/text' import { Boss } from 'lib/rpg/boss' import { Group } from 'lib/rpg/place' import { MagicSlimeBall } from './items' +import { ms } from 'lib/utils/ms' +import { Loot } from 'lib/rpg/loot-table' export function createBossSlime(group: Group) { const boss = Boss.create() diff --git a/src/modules/places/village-of-explorers/village-of-explorers.ts b/src/modules/places/village-of-explorers/village-of-explorers.ts index c7d5ef40..4e33ffa7 100644 --- a/src/modules/places/village-of-explorers/village-of-explorers.ts +++ b/src/modules/places/village-of-explorers/village-of-explorers.ts @@ -8,6 +8,7 @@ import { techCityInvestigating } from '../tech-city/quests/investigating' import { MagicSlimeBall } from './items' import { Mage } from './mage' import { createBossSlime } from './slime.boss' +import { Loot } from 'lib/rpg/loot-table' class VillageOfExporersBuilder extends City { constructor() { diff --git a/src/modules/places/village-of-miners/village-of-miners.ts b/src/modules/places/village-of-miners/village-of-miners.ts index 6a7e3b74..89b44f77 100644 --- a/src/modules/places/village-of-miners/village-of-miners.ts +++ b/src/modules/places/village-of-miners/village-of-miners.ts @@ -6,6 +6,8 @@ import { Stoner } from '../lib/npc/stoner' import { Woodman } from '../lib/npc/woodman' import { stoneQuarryInvestigating } from '../stone-quarry/quests/investigating' import { createMineQuests } from './quests/mine-x-blocks' +import { Loot } from 'lib/rpg/loot-table' +import { migrateLocationName } from 'lib/location' class VillageOfMinersBuilder extends City { constructor() { diff --git a/src/modules/pvp/cannon.ts b/src/modules/pvp/cannon.ts index 47d1f47e..2ff2d800 100644 --- a/src/modules/pvp/cannon.ts +++ b/src/modules/pvp/cannon.ts @@ -6,6 +6,11 @@ import { i18n } from 'lib/i18n/text' import { CustomItemWithBlueprint } from 'lib/rpg/custom-item' import { explosibleEntities, ExplosibleEntityOptions } from './explosible-entities' import { decreaseMainhandItemCount } from './throwable-tnt' +import { Vec } from 'lib/vector' +import { ActionGuardOrder } from 'lib/region' +import { actionGuard } from 'lib/region' +import { ms } from 'lib/utils/ms' +import { Cooldown } from 'lib/cooldown' export const CannonItem = new CustomItemWithBlueprint('cannon') .typeId('lw:cannon_spawn_egg') diff --git a/src/modules/pvp/ice-bomb.ts b/src/modules/pvp/ice-bomb.ts index 16339b86..3e8dd092 100644 --- a/src/modules/pvp/ice-bomb.ts +++ b/src/modules/pvp/ice-bomb.ts @@ -4,7 +4,9 @@ import { MinecraftBlockTypes, MinecraftEntityTypes, MinecraftItemTypes } from '@ import { i18n } from 'lib/i18n/text' import { customItems } from 'lib/rpg/custom-item' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' +import { ms } from 'lib/utils/ms' import { toPoint } from 'lib/utils/point' +import { Vec } from 'lib/vector' import { WeakPlayerSet } from 'lib/weak-player-storage' import { BaseRegion } from 'modules/places/base/region' import { getEdgeBlocksOf } from 'modules/places/mineshaft/get-edge-blocks-of' diff --git a/src/modules/pvp/item-ability.ts b/src/modules/pvp/item-ability.ts index a7816abb..24fe2349 100644 --- a/src/modules/pvp/item-ability.ts +++ b/src/modules/pvp/item-ability.ts @@ -4,6 +4,7 @@ import { defaultLang } from 'lib/assets/lang' import { ItemLoreSchema } from 'lib/database/item-stack' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' import { rollChance } from 'lib/rpg/random' +import { isKeyof } from 'lib/util' import { createLogger } from 'lib/utils/logger' const logger = createLogger('ItemAbility') diff --git a/src/modules/pvp/raid.ts b/src/modules/pvp/raid.ts index f1a1c04e..125918ea 100644 --- a/src/modules/pvp/raid.ts +++ b/src/modules/pvp/raid.ts @@ -1,9 +1,12 @@ import { Block, Entity, system, world } from '@minecraft/server' +import { LockAction } from 'lib/action' import { ScoreboardDB } from 'lib/database/scoreboard' import { i18n } from 'lib/i18n/text' +import { Region } from 'lib/region' import { MineareaRegion } from 'lib/region/kinds/minearea' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' +import { ms } from 'lib/utils/ms' import { BaseRegion } from 'modules/places/base/region' const notify = new Map() diff --git a/src/modules/quests/daily/index.ts b/src/modules/quests/daily/index.ts index 78a581ef..dad779dd 100644 --- a/src/modules/quests/daily/index.ts +++ b/src/modules/quests/daily/index.ts @@ -7,6 +7,7 @@ import { i18n, textTable } from 'lib/i18n/text' import { questMenuCustomButtons } from 'lib/quest/menu' import { DailyQuest } from 'lib/quest/quest' import { RecurringEvent } from 'lib/recurring-event' +import { noNullable } from 'lib/util' import later from 'lib/utils/later' import { City } from 'modules/places/lib/city' import { CityInvestigating } from 'modules/places/lib/city-investigating-quest' diff --git a/src/modules/quests/learning/airdrop.ts b/src/modules/quests/learning/airdrop.ts index f2b988b0..3ea7f351 100644 --- a/src/modules/quests/learning/airdrop.ts +++ b/src/modules/quests/learning/airdrop.ts @@ -1,4 +1,5 @@ import { Items } from 'lib/assets/custom-items' +import { Loot } from 'lib/rpg/loot-table' export default new Loot('starter') .item('WoodenSword') diff --git a/src/modules/quests/learning/learning.ts b/src/modules/quests/learning/learning.ts index 17cdc55e..c813918d 100644 --- a/src/modules/quests/learning/learning.ts +++ b/src/modules/quests/learning/learning.ts @@ -23,6 +23,12 @@ import { OrePlace, ores } from 'modules/places/mineshaft/algo' import { Spawn } from 'modules/places/spawn' import { VillageOfMiners } from 'modules/places/village-of-miners/village-of-miners' import airdropTable from './airdrop' +import { ActionForm } from 'lib/form/action' +import { Temporary } from 'lib/temporary' +import { ActionGuardOrder } from 'lib/region' +import { actionGuard } from 'lib/region' +import { location } from 'lib/location' +import { Vec } from 'lib/vector' const logger = createLogger('Learning Quest') diff --git a/src/modules/survival/death-quest-and-gravestone.ts b/src/modules/survival/death-quest-and-gravestone.ts index 96b10938..2fffdcef 100644 --- a/src/modules/survival/death-quest-and-gravestone.ts +++ b/src/modules/survival/death-quest-and-gravestone.ts @@ -1,11 +1,17 @@ import { Entity, Player, system, world } from '@minecraft/server' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +import { EventSignal } from 'lib/event-signal' +import { Cooldown } from 'lib/cooldown' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' import { Quest } from 'lib/quest/quest' -import { ActionGuardOrder, forceAllowSpawnInRegion, Region } from 'lib/region' +import { actionGuard, ActionGuardOrder, forceAllowSpawnInRegion, Region } from 'lib/region' import { SphereArea } from 'lib/region/areas/sphere' +import { inventoryIsEmpty } from 'lib/rpg/airdrop' import { noGroup } from 'lib/rpg/place' +import { Settings } from 'lib/settings' +import { Vec } from 'lib/vector' +import { ms } from 'lib/utils/ms' import { SafePlace } from 'modules/places/lib/safe-place' import { Spawn } from 'modules/places/spawn' diff --git a/src/modules/survival/locked-features.ts b/src/modules/survival/locked-features.ts index 2c711be6..b223a72c 100644 --- a/src/modules/survival/locked-features.ts +++ b/src/modules/survival/locked-features.ts @@ -1,5 +1,7 @@ import { intlListFormat } from 'lib/i18n/intl' import { i18n } from 'lib/i18n/text' +import { ActionGuardOrder } from 'lib/region' +import { actionGuard } from 'lib/region' const blocked: Record = {} diff --git a/src/modules/survival/menu.ts b/src/modules/survival/menu.ts index 8af7e67e..cbee4223 100644 --- a/src/modules/survival/menu.ts +++ b/src/modules/survival/menu.ts @@ -1,114 +1,81 @@ -import { Player } from "@minecraft/server"; -import { BUTTON, doNothing } from "lib"; -import { - achievementsForm, - achievementsFormName, -} from "lib/achievements/command"; -import { clanMenu } from "lib/clan/menu"; -import { Core } from "lib/extensions/core"; -import { form } from "lib/form/new"; -import { i18n } from "lib/i18n/text"; -import { Mail } from "lib/mail"; -import { Join } from "lib/player-join"; -import { questsMenu } from "lib/quest/menu"; -import { Menu } from "lib/rpg/menu"; -import { playerSettingsMenu } from "lib/settings"; -import { mailMenu } from "modules/commands/mail"; -import { statsForm } from "modules/commands/stats"; -import { baseMenu } from "modules/places/base/base-menu"; -import { wiki } from "modules/wiki/wiki"; -import { Anarchy } from "../places/anarchy/anarchy"; -import { Spawn } from "../places/spawn"; -import { recurForm } from "./recurring-events"; -import { speedrunForm } from "./speedrun/target"; +import { Player } from '@minecraft/server' +import { achievementsForm, achievementsFormName } from 'lib/achievements/command' +import { clanMenu } from 'lib/clan/menu' +import { Core } from 'lib/extensions/core' +import { form } from 'lib/form/new' +import { i18n } from 'lib/i18n/text' +import { Mail } from 'lib/mail' +import { Join } from 'lib/player-join' +import { questsMenu } from 'lib/quest/menu' +import { Menu } from 'lib/rpg/menu' +import { playerSettingsMenu } from 'lib/settings' +import { mailMenu } from 'modules/commands/mail' +import { statsForm } from 'modules/commands/stats' +import { baseMenu } from 'modules/places/base/base-menu' +import { wiki } from 'modules/wiki/wiki' +import { Anarchy } from '../places/anarchy/anarchy' +import { Spawn } from '../places/spawn' +import { recurForm } from './recurring-events' +import { speedrunForm } from './speedrun/target' +import { BUTTON } from 'lib/form/utils' +import { doNothing } from 'lib/util' function tp( - player: Player, - place: InventoryTypeName, - inv: InventoryTypeName, - color = "§9", - text = i18n`Спавн`, - extra: Text = "" + player: Player, + place: InventoryTypeName, + inv: InventoryTypeName, + color = '§9', + text = i18n`Спавн`, + extra: Text = '', ) { - const here = inv === place; - if (here) - extra = i18n`${extra ? extra.to(player.lang) + " " : ""}§8Вы тут`.to( - player.lang - ); - if (extra) extra = "\n" + extra.to(player.lang); - const prefix = here ? "§7" : color; - return `${prefix}> ${inv === place ? "§7" : "§r§f"}${text.to( - player.lang - )} ${prefix}<${extra}`; + const here = inv === place + if (here) extra = i18n`${extra ? extra.to(player.lang) + ' ' : ''}§8Вы тут`.to(player.lang) + if (extra) extra = '\n' + extra.to(player.lang) + const prefix = here ? '§7' : color + return `${prefix}> ${inv === place ? '§7' : '§r§f'}${text.to(player.lang)} ${prefix}<${extra}` } Menu.form = form((f, { player, self }) => { - const inv = player.database.inv; - f.title(Core.name, "§c§u§s§r"); - f.button( - tp(player, "spawn", inv, "§9", i18n`Спавн`), - "textures/ui/worldsIcon", - () => { - Spawn.portal?.teleport(player); - } - ) - .button( - tp(player, "anarchy", inv, "§c", i18n`Анархия`), - "textures/blocks/tnt_side", - () => { - Anarchy.portal?.teleport(player); - } - ) - .button( - tp(player, "mg", inv, `§6`, i18n`Миниигры`, i18n`§7СКОРО!`), - "textures/blocks/bedrock", - self - ); + const inv = player.database.inv + f.title(Core.name, '§c§u§s§r') + f.button(tp(player, 'spawn', inv, '§9', i18n`Спавн`), 'textures/ui/worldsIcon', () => { + Spawn.portal?.teleport(player) + }) + .button(tp(player, 'anarchy', inv, '§c', i18n`Анархия`), 'textures/blocks/tnt_side', () => { + Anarchy.portal?.teleport(player) + }) + .button(tp(player, 'mg', inv, `§6`, i18n`Миниигры`, i18n`§7СКОРО!`), 'textures/blocks/bedrock', self) - if (player.database.inv === "anarchy") { - f.button( - i18n`Задания`.badge(player.database.quests?.active.length), - "textures/ui/sidebar_icons/genre", - () => questsMenu(player, self) - ); + if (player.database.inv === 'anarchy') { + f.button(i18n`Задания`.badge(player.database.quests?.active.length), 'textures/ui/sidebar_icons/genre', () => + questsMenu(player, self), + ) - f.button( - achievementsFormName(player), - "textures/blocks/gold_block", - achievementsForm - ); + f.button(achievementsFormName(player), 'textures/blocks/gold_block', achievementsForm) - f.button(i18n`База`, "textures/blocks/barrel_side", baseMenu({})); - const [clanText, clan] = clanMenu(player, self); - f.button(clanText, "textures/ui/FriendsIcon", clan); - } + f.button(i18n`База`, 'textures/blocks/barrel_side', baseMenu({})) + const [clanText, clan] = clanMenu(player, self) + f.button(clanText, 'textures/ui/FriendsIcon', clan) + } - f.button( - i18n.nocolor`§6Донат\n§7СКОРО!`, - "textures/ui/permissions_op_crown", - self - ) - .button( - i18n.nocolor`§fПочта`.badge(Mail.getUnreadMessagesCount(player.id)), - "textures/ui/feedIcon", - () => mailMenu(player, self) - ) - .button(i18n.nocolor`§bВики`, BUTTON.search, wiki.show) + f.button(i18n.nocolor`§6Донат\n§7СКОРО!`, 'textures/ui/permissions_op_crown', self) + .button(i18n.nocolor`§fПочта`.badge(Mail.getUnreadMessagesCount(player.id)), 'textures/ui/feedIcon', () => + mailMenu(player, self), + ) + .button(i18n.nocolor`§bВики`, BUTTON.search, wiki.show) - .button(i18n.nocolor`§7Настройки`, BUTTON.settings, () => - playerSettingsMenu(player, self) - ) - .button(i18n`Еще`, BUTTON[">"], secondPage); -}); + .button(i18n.nocolor`§7Настройки`, BUTTON.settings, () => playerSettingsMenu(player, self)) + .button(i18n`Еще`, BUTTON['>'], secondPage) +}) -const secondPage = form((f) => { - f.title(Core.name, "§c§u§s§r"); - f.button(i18n`Цели`, BUTTON["?"], speedrunForm); - f.button(i18n`Лидеры`, BUTTON["?"], doNothing); - f.button(i18n`События`, BUTTON["?"], recurForm); - f.button(i18n`Статистика`, BUTTON["?"], statsForm({})); -}); +const secondPage = form(f => { + f.title(Core.name, '§c§u§s§r') + f.button(i18n`Цели`, BUTTON['?'], speedrunForm) + f.button(i18n`Лидеры`, BUTTON['?'], doNothing) + f.button(i18n`События`, BUTTON['?'], recurForm) + f.button(i18n`Статистика`, BUTTON['?'], statsForm({})) +}) Join.onMoveAfterJoin.subscribe(({ player, firstJoin }) => { - if (firstJoin) Menu.item.give(player, { mode: "ensure" }); -}); + if (firstJoin) Menu.item.give(player, { mode: 'ensure' }) +}) diff --git a/src/modules/survival/random-teleport.ts b/src/modules/survival/random-teleport.ts index 159bc97e..50173534 100644 --- a/src/modules/survival/random-teleport.ts +++ b/src/modules/survival/random-teleport.ts @@ -12,6 +12,9 @@ import { } from '@minecraft/server' import { MinecraftEffectTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' +import { util } from 'lib/util' +import { Vec } from 'lib/vector' +import { LockAction } from 'lib/action' const RTP_ELYTRA = new ItemStack(MinecraftItemTypes.Elytra, 1).setInfo( '§6Элитра перемещения', diff --git a/src/modules/survival/realtime.ts b/src/modules/survival/realtime.ts index 6b4de424..56442903 100644 --- a/src/modules/survival/realtime.ts +++ b/src/modules/survival/realtime.ts @@ -1,6 +1,7 @@ import { TicksPerDay, TimeOfDay, system, world } from '@minecraft/server' import { noI18n } from 'lib/i18n/text' +import { Settings } from 'lib/settings' const MinutesPerDay = 24 * 60 const Offset = 6000 diff --git a/src/modules/survival/recurring-events.ts b/src/modules/survival/recurring-events.ts index a720d683..c6810049 100644 --- a/src/modules/survival/recurring-events.ts +++ b/src/modules/survival/recurring-events.ts @@ -1,14 +1,12 @@ -import { Player, TicksPerSecond, world } from "@minecraft/server"; -import { - MinecraftEffectTypes, - MinecraftEffectTypesUnion, -} from "@minecraft/vanilla-data"; -import { ms, RoadRegion } from "lib"; -import { form } from "lib/form/new"; -import { i18n } from "lib/i18n/text"; -import { DurationalRecurringEvent } from "lib/recurring-event"; -import { RegionEvents } from "lib/region/events"; -import later from "lib/utils/later"; +import { Player, TicksPerSecond, world } from '@minecraft/server' +import { MinecraftEffectTypes, MinecraftEffectTypesUnion } from '@minecraft/vanilla-data' +import { form } from 'lib/form/new' +import { i18n } from 'lib/i18n/text' +import { DurationalRecurringEvent } from 'lib/recurring-event' +import { RoadRegion } from 'lib/region' +import { RegionEvents } from 'lib/region/events' +import later from 'lib/utils/later' +import { ms } from 'lib/utils/ms' // TODO Add settings for players to not apply effects on them // TODO Add command to show menu to view events @@ -17,75 +15,64 @@ import later from "lib/utils/later"; // TODO Add chat notification class RecurringEffect { - static all: RecurringEffect[] = []; + static all: RecurringEffect[] = [] - readonly event: DurationalRecurringEvent; + readonly event: DurationalRecurringEvent - constructor( - readonly effectType: MinecraftEffectTypesUnion, - readonly startingOn: number, - filter?: (p: Player) => boolean, - readonly amplifier = 2 - ) { - RecurringEffect.all.push(this); - this.event = new DurationalRecurringEvent( - `effect${effectType}`, - later.parse.recur().every(5).hour().startingOn(startingOn), - ms.from("min", 10), - () => ({}), - (_, ctx) => { - for (const player of world.getAllPlayers()) { - player.success( - i18n.success`Событие! ${effectType} силой ${amplifier} на ${10} минут` - ); - } - ctx.temp.system.runInterval( - () => { - for (const player of world.getAllPlayers()) { - if (filter && !filter(player)) continue; + constructor( + readonly effectType: MinecraftEffectTypesUnion, + readonly startingOn: number, + filter?: (p: Player) => boolean, + readonly amplifier = 2, + ) { + RecurringEffect.all.push(this) + this.event = new DurationalRecurringEvent( + `effect${effectType}`, + later.parse.recur().every(5).hour().startingOn(startingOn), + ms.from('min', 10), + () => ({}), + (_, ctx) => { + for (const player of world.getAllPlayers()) { + player.success(i18n.success`Событие! ${effectType} силой ${amplifier} на ${10} минут`) + } + ctx.temp.system.runInterval( + () => { + for (const player of world.getAllPlayers()) { + if (filter && !filter(player)) continue - player.addEffect( - MinecraftEffectTypes[effectType], - TicksPerSecond * 3, - { - amplifier, - showParticles: false, - } - ); - } - }, - `effect${effectType}`, - TicksPerSecond * 2 - ); - } - ); - } + player.addEffect(MinecraftEffectTypes[effectType], TicksPerSecond * 3, { + amplifier, + showParticles: false, + }) + } + }, + `effect${effectType}`, + TicksPerSecond * 2, + ) + }, + ) + } } -new RecurringEffect("Haste", 1); -new RecurringEffect("HealthBoost", 2); +new RecurringEffect('Haste', 1) +new RecurringEffect('HealthBoost', 2) new RecurringEffect( - "Speed", - 3, - (p) => - RegionEvents.playerInRegionsCache - .get(p) - ?.some((e) => e instanceof RoadRegion) ?? false, - 4 -); + 'Speed', + 3, + p => RegionEvents.playerInRegionsCache.get(p)?.some(e => e instanceof RoadRegion) ?? false, + 4, +) export const recurForm = form((f, { self }) => { - f.title(i18n`События`); - f.body(i18n`Время: ${new Date().toHHMMSS()}`); + f.title(i18n`События`) + f.body(i18n`Время: ${new Date().toHHMMSS()}`) - const now = Date.now(); - for (const event of RecurringEffect.all) { - const next = event.event.getNextOccurances(1)[0] ?? new Date(); - f.button( - i18n`${event.effectType} ${event.amplifier + 1}\nЧерез ${i18n.time( - next.getTime() - now - )} (${next.toHHMM()})`, - self - ); - } -}); + const now = Date.now() + for (const event of RecurringEffect.all) { + const next = event.event.getNextOccurances(1)[0] ?? new Date() + f.button( + i18n`${event.effectType} ${event.amplifier + 1}\nЧерез ${i18n.time(next.getTime() - now)} (${next.toHHMM()})`, + self, + ) + } +}) diff --git a/src/modules/survival/sidebar.ts b/src/modules/survival/sidebar.ts index 439b5d13..9eb4e227 100644 --- a/src/modules/survival/sidebar.ts +++ b/src/modules/survival/sidebar.ts @@ -3,6 +3,11 @@ import { Player, system, TicksPerSecond, world } from '@minecraft/server' import { emoji } from 'lib/assets/emoji' import { i18n } from 'lib/i18n/text' import { Quest } from 'lib/quest/quest' +import { separateNumberWithDots } from 'lib/util' +import { Region } from 'lib/region' +import { Sidebar } from 'lib/sidebar' +import { Menu } from 'lib/rpg/menu' +import { Settings } from 'lib/settings' import { Minigame } from 'modules/minigames/Builder' import { BaseRegion } from 'modules/places/base/region' diff --git a/src/modules/survival/speedrun/target.ts b/src/modules/survival/speedrun/target.ts index 99f76122..848333d0 100644 --- a/src/modules/survival/speedrun/target.ts +++ b/src/modules/survival/speedrun/target.ts @@ -1,6 +1,8 @@ import { Player } from '@minecraft/server' +import { InventoryInterval } from 'lib/action' import { defaultLang } from 'lib/assets/lang' +import { ScoreboardDB } from 'lib/database/scoreboard' import { form } from 'lib/form/new' import { i18n, i18nShared } from 'lib/i18n/text' import { BaseItem } from 'modules/places/base/base' diff --git a/src/modules/test/edit-structure.ts b/src/modules/test/edit-structure.ts index 5a0089af..1b9ff3db 100644 --- a/src/modules/test/edit-structure.ts +++ b/src/modules/test/edit-structure.ts @@ -4,6 +4,8 @@ import { MinecraftBlockTypes } from '@minecraft/vanilla-data' import { StructureDungeonsId } from 'lib/assets/structures' import { form } from 'lib/form/new' import { noI18n } from 'lib/i18n/text' +import { Region } from 'lib/region' +import { Vec } from 'lib/vector' const f = form((f, { player }) => { for (const [name, id] of Object.entries(StructureDungeonsId)) { diff --git a/src/modules/test/minimap.ts b/src/modules/test/minimap.ts index 8e28f032..a7615966 100644 --- a/src/modules/test/minimap.ts +++ b/src/modules/test/minimap.ts @@ -1,5 +1,6 @@ import { RGBA, system, world } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' +import { removeNamespace } from 'lib/util' system.afterEvents.scriptEventReceive.subscribe( ({ id }) => { diff --git a/src/modules/test/properties.ts b/src/modules/test/properties.ts index 4f6f5092..506cb02f 100644 --- a/src/modules/test/properties.ts +++ b/src/modules/test/properties.ts @@ -1,6 +1,7 @@ import { Player } from '@minecraft/server' import { playerJson } from 'lib/assets/player-json' +import { ArrayForm } from 'lib/form/array' new Command('props') .setDescription('Player properties menu') diff --git a/src/modules/world-edit/commands/general/id.ts b/src/modules/world-edit/commands/general/id.ts index 7088a441..c64e3009 100644 --- a/src/modules/world-edit/commands/general/id.ts +++ b/src/modules/world-edit/commands/general/id.ts @@ -1,5 +1,7 @@ import {} from '@minecraft/server' import { MinecraftEntityTypes } from '@minecraft/vanilla-data' +import { Vec } from 'lib/vector' +import { inspect } from 'lib/util' const root = new Command('id').setDescription('Выдает айди').setPermissions('builder').setGroup('we') diff --git a/src/modules/world-edit/commands/region/set/set-selection.ts b/src/modules/world-edit/commands/region/set/set-selection.ts index b020bae2..cfc3c274 100644 --- a/src/modules/world-edit/commands/region/set/set-selection.ts +++ b/src/modules/world-edit/commands/region/set/set-selection.ts @@ -8,6 +8,7 @@ import { ReplaceMode } from 'modules/world-edit/utils/blocks-set' import { WorldEdit } from '../../../lib/world-edit' import { SelectedBlock, useBlockSelection } from './use-block-selection' import { useReplaceMode } from './use-replace-mode' +import { BUTTON } from 'lib/form/utils' const selection = { block: new WeakPlayerMap(), diff --git a/src/modules/world-edit/lib/world-edit-multi-tool.ts b/src/modules/world-edit/lib/world-edit-multi-tool.ts index 4e8a9a84..4ab2058d 100644 --- a/src/modules/world-edit/lib/world-edit-multi-tool.ts +++ b/src/modules/world-edit/lib/world-edit-multi-tool.ts @@ -2,6 +2,11 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' import { noI18n } from 'lib/i18n/text' import { WorldEditTool } from './world-edit-tool' +import { doNothing } from 'lib/util' +import { ModalForm } from 'lib/form/modal' +import { ask } from 'lib/form/message' +import { BUTTON } from 'lib/form/utils' +import { ArrayForm } from 'lib/form/array' export interface ToolsDataStorage { /** Version */ diff --git a/src/modules/world-edit/lib/world-edit-tool.ts b/src/modules/world-edit/lib/world-edit-tool.ts index c5015d43..5baad0db 100644 --- a/src/modules/world-edit/lib/world-edit-tool.ts +++ b/src/modules/world-edit/lib/world-edit-tool.ts @@ -9,8 +9,10 @@ import { world, } from '@minecraft/server' +import { Command } from 'lib/command' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { noI18n, textUnitColorize } from 'lib/i18n/text' +import { inspect, isKeyof, noBoolean, stringify, util } from 'lib/util' import { BlocksSetRef, stringifyBlocksSetRef } from 'modules/world-edit/utils/blocks-set' import { worldEditPlayerSettings } from '../settings' diff --git a/src/modules/world-edit/lib/world-edit.ts b/src/modules/world-edit/lib/world-edit.ts index ec23d6b1..94fb0bc8 100644 --- a/src/modules/world-edit/lib/world-edit.ts +++ b/src/modules/world-edit/lib/world-edit.ts @@ -17,6 +17,10 @@ import { toPermutation, toReplaceTarget, } from '../utils/blocks-set' +import { isLocationError } from 'lib/utils/game' +import { ask } from 'lib/form/message' +import { getRole } from 'lib/roles' +import { Vec } from 'lib/vector' // TODO Add WorldEdit.runMultipleAsyncJobs diff --git a/src/modules/world-edit/tools/brush.ts b/src/modules/world-edit/tools/brush.ts index 77d540b5..ed65b21c 100644 --- a/src/modules/world-edit/tools/brush.ts +++ b/src/modules/world-edit/tools/brush.ts @@ -23,6 +23,11 @@ import { } from '../utils/blocks-set' import { shortenBlocksSetName } from '../utils/default-block-sets' import { SHAPES, ShapeFormula } from '../utils/shapes' +import { isLocationError } from 'lib/utils/game' +import { isKeyof } from 'lib/util' +import { is } from 'lib/roles' +import { ModalForm } from 'lib/form/modal' +import { Vec } from 'lib/vector' interface Storage { shape: string diff --git a/src/modules/world-edit/tools/create-region.ts b/src/modules/world-edit/tools/create-region.ts index 16a0f6f8..34bb07a8 100644 --- a/src/modules/world-edit/tools/create-region.ts +++ b/src/modules/world-edit/tools/create-region.ts @@ -6,6 +6,10 @@ import { noI18n } from 'lib/i18n/text' import { SphereArea } from 'lib/region/areas/sphere' import { WeBackup, WorldEdit } from '../lib/world-edit' import { WorldEditTool } from '../lib/world-edit-tool' +import { Vec } from 'lib/vector' +import { Region } from 'lib/region' +import { regionTypes } from 'lib/region' +import { ModalForm } from 'lib/form/modal' interface Storage { version: number From 463d7429240a45c2486b490b1aaaff638ff91453 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:57:38 +0300 Subject: [PATCH 04/14] fix: update to 1.21.124 --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b0388453..2fc40def 100644 --- a/package.json +++ b/package.json @@ -17,19 +17,19 @@ "@formatjs/intl-locale": "^4.2.11", "@formatjs/intl-numberformat": "^8.15.4", "@formatjs/intl-pluralrules": "^5.4.4", - "@minecraft/server": "2.1.0-beta.1.21.90-stable", - "@minecraft/server-gametest": "1.0.0-beta.1.21.90-stable", - "@minecraft/server-net": "1.0.0-beta.1.21.90-stable", - "@minecraft/server-ui": "2.1.0-beta.1.21.90-stable", - "@minecraft/vanilla-data": "1.21.90", + "@minecraft/server": "2.4.0-beta.1.21.120-stable", + "@minecraft/server-gametest": "1.0.0-beta.1.21.120-stable", + "@minecraft/server-net": "1.0.0-beta.1.21.120-stable", + "@minecraft/server-ui": "2.1.0-beta.1.21.120-stable", + "@minecraft/vanilla-data": "1.21.120", "async-mutex": "^0.5.0" }, "resolutions": { - "@minecraft/server": "2.1.0-beta.1.21.90-stable", - "@minecraft/server-gametest": "1.0.0-beta.1.21.90-stable", - "@minecraft/server-net": "1.0.0-beta.1.21.90-stable", - "@minecraft/server-ui": "2.1.0-beta.1.21.90-stable", - "@minecraft/vanilla-data": "1.21.90" + "@minecraft/server": "2.4.0-beta.1.21.120-stable", + "@minecraft/server-gametest": "1.0.0-beta.1.21.120-stable", + "@minecraft/server-net": "1.0.0-beta.1.21.120-stable", + "@minecraft/server-ui": "2.1.0-beta.1.21.120-stable", + "@minecraft/vanilla-data": "1.21.120" }, "devDependencies": { "@eslint/js": "^9.29.0", From e61a886ed9e3f244ef64fbf45d6bf79d545f8ff2 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:39:58 +0300 Subject: [PATCH 05/14] frmat --- src/lib/command/utils.test.ts | 1 - src/lib/i18n/text.ts | 446 ++++++++++++------------- src/lib/rpg/loot-table.ts | 2 +- src/lib/rpg/newbie.ts | 196 +++++------ src/lib/shop/buttons/item-modifier.ts | 2 +- src/lib/shop/cost.test.ts | 10 +- src/modules/world-edit/utils/shapes.ts | 3 +- 7 files changed, 308 insertions(+), 352 deletions(-) diff --git a/src/lib/command/utils.test.ts b/src/lib/command/utils.test.ts index 7d1bdbef..f5a656d4 100644 --- a/src/lib/command/utils.test.ts +++ b/src/lib/command/utils.test.ts @@ -31,4 +31,3 @@ describe('command utils', () => { `) }) }) - diff --git a/src/lib/i18n/text.ts b/src/lib/i18n/text.ts index 152b97ca..c5766c50 100644 --- a/src/lib/i18n/text.ts +++ b/src/lib/i18n/text.ts @@ -1,264 +1,234 @@ -import { Player, RawText } from "@minecraft/server"; -import { defaultLang, Language } from "lib/assets/lang"; -import { extractedTranslatedPlurals } from "lib/assets/lang-messages"; -import { Vec } from "lib/vector"; -import { separateNumberWithDots } from "../util"; -import { stringify } from "../utils/inspect"; -import { ms } from "../utils/ms"; -import { intlPlural, intlRemaining } from "./intl"; +import { Player, RawText } from '@minecraft/server' +import { defaultLang, Language } from 'lib/assets/lang' +import { extractedTranslatedPlurals } from 'lib/assets/lang-messages' +import { Vec } from 'lib/vector' +import { separateNumberWithDots } from '../util' +import { stringify } from '../utils/inspect' +import { ms } from '../utils/ms' +import { intlPlural, intlRemaining } from './intl' import { - I18nMessage, - Message, - RawTextArg, - ServerSideI18nMessage, - SharedI18nMessage, - SharedI18nMessageJoin, -} from "./message"; -export type MaybeRawText = string | RawText; + I18nMessage, + Message, + RawTextArg, + ServerSideI18nMessage, + SharedI18nMessage, + SharedI18nMessageJoin, +} from './message' +export type MaybeRawText = string | RawText declare global { - /** Text that can be displayed on player screen and should support translation */ - type Text = string | Message; - - type SharedText = import("lib/i18n/message").SharedI18nMessage; - - namespace Text { - export interface Colors { - /** Color of strings, objects and other messages */ - unit: string; - /** Color of numbers and bigints */ - num: string; - /** Color of regular template text i18n`Like this one` */ - text: string; - } - - export interface Static { - /** - * @example - * t.time(3000) -> "3 секунды" - */ - time(time: number): Message; - - /** - * @example - * t.time(3000) -> "00:00:03" - * t.time(ms.from('min', 32) + 1000) -> "00:32:01" - * t.time(ms.from('day', 1) + ms.from('min', 32) + 1000) -> "1 д. 00:32:01" - * t.time(ms.from('day', 10000) + ms.from('min', 32) + 1000) -> "10000 д. 00:32:01" - */ - hhmmss(time: number): SharedI18nMessage | string; - - restyle: (colors: Partial) => T; - - style: Text.Colors; - } - - /** "§7Some long text §fwith substring§7 and number §64§7" */ - export type Fn = (text: TemplateStringsArray, ...args: Arg[]) => T; - - export type FnWithJoin = Fn & { join: Fn }; - - interface Modifiers { - /** "§cSome long text §fwith substring§c and number §74§c" */ - error: T; - - /** "§eSome long text §fwith substring§e and number §64§e" */ - warn: T; - - /** "§aSome long text §fwith substring§a and number §64§a" */ - success: T; - - /** "§3Some long text §fwith substring§3 and number §64§3" */ - accent: T; - - /** "§8Some long text §7with substring§8 and number §74§8" */ - disabled: T; - - /** "§r§6Some long text §f§lwith substring§r§6 and number §f4§r§6" */ - header: T; - - /** "Some long text with substring and number 4" */ - nocolor: T; - } - export type Chained> = T & - Static> & - Modifiers>>; - - export type Table = readonly (string | readonly [Text, unknown])[]; - } + /** Text that can be displayed on player screen and should support translation */ + type Text = string | Message + + type SharedText = import('lib/i18n/message').SharedI18nMessage + + namespace Text { + export interface Colors { + /** Color of strings, objects and other messages */ + unit: string + /** Color of numbers and bigints */ + num: string + /** Color of regular template text i18n`Like this one` */ + text: string + } + + export interface Static { + /** + * @example + * t.time(3000) -> "3 секунды" + */ + time(time: number): Message + + /** + * @example + * t.time(3000) -> "00:00:03" + * t.time(ms.from('min', 32) + 1000) -> "00:32:01" + * t.time(ms.from('day', 1) + ms.from('min', 32) + 1000) -> "1 д. 00:32:01" + * t.time(ms.from('day', 10000) + ms.from('min', 32) + 1000) -> "10000 д. 00:32:01" + */ + hhmmss(time: number): SharedI18nMessage | string + + restyle: (colors: Partial) => T + + style: Text.Colors + } + + /** "§7Some long text §fwith substring§7 and number §64§7" */ + export type Fn = (text: TemplateStringsArray, ...args: Arg[]) => T + + export type FnWithJoin = Fn & { join: Fn } + + interface Modifiers { + /** "§cSome long text §fwith substring§c and number §74§c" */ + error: T + + /** "§eSome long text §fwith substring§e and number §64§e" */ + warn: T + + /** "§aSome long text §fwith substring§a and number §64§a" */ + success: T + + /** "§3Some long text §fwith substring§3 and number §64§3" */ + accent: T + + /** "§8Some long text §7with substring§8 and number §74§8" */ + disabled: T + + /** "§r§6Some long text §f§lwith substring§r§6 and number §f4§r§6" */ + header: T + + /** "Some long text with substring and number 4" */ + nocolor: T + } + export type Chained> = T & Static> & Modifiers>> + + export type Table = readonly (string | readonly [Text, unknown])[] + } } export function textTable(table: Text.Table): Message { - return new ServerSideI18nMessage(defaultColors(), (lang) => { - const long = table.length > 5; - return table - .map((v, i) => { - if (typeof v === "string") return ""; - - const [key, value] = v; - return `${i % 2 === 0 && long ? "§f" : "§7"}${key.to( - lang - )}: ${textUnitColorize(value, undefined, lang)}`; - }) - .join("\n"); - }); + return new ServerSideI18nMessage(defaultColors(), lang => { + const long = table.length > 5 + return table + .map((v, i) => { + if (typeof v === 'string') return '' + + const [key, value] = v + return `${i % 2 === 0 && long ? '§f' : '§7'}${key.to(lang)}: ${textUnitColorize(value, undefined, lang)}` + }) + .join('\n') + }) } function createStyle(colors: Text.Colors) { - return Object.freeze(colors); + return Object.freeze(colors) } const styles = { - nocolor: createStyle({ text: "", unit: "", num: "" }), - header: createStyle({ text: "§r§6", num: "§f", unit: "§f§l" }), - error: createStyle({ num: "§7", text: "§c", unit: "§f" }), - warn: createStyle({ num: "§6", text: "§e", unit: "§f" }), - accent: createStyle({ num: "§6", text: "§3", unit: "§f" }), - success: createStyle({ num: "§6", text: "§a", unit: "§f" }), - disabled: createStyle({ num: "§7", text: "§8", unit: "§7" }), -}; - -export const noI18n = createStatic(undefined, undefined, (colors) => { - return function simpleStr(template, ...args) { - return Message.concatTemplateStringsArray( - defaultLang, - template, - args, - colors - ); - } as Text.Chained>; -}); - -export const i18n = createStatic(undefined, undefined, (colors) => { - const i18n = ((template, ...args) => - new I18nMessage(template, args, colors)) as Text.FnWithJoin< - I18nMessage, - unknown - >; - - i18n.join = (template, ...args) => new Message(template, args, colors); - - return i18n as Text.Chained>; -}); - -export const i18nShared = createStatic(undefined, undefined, (colors) => { - const i18n = ((template, ...args) => - new SharedI18nMessage(template, args, colors)) as Text.FnWithJoin< - SharedI18nMessage, - RawTextArg - >; - - i18n.join = (template, ...args) => - new SharedI18nMessageJoin(template, args, colors); - - return i18n as Text.Chained>; -}); - -export const i18nPlural = createStatic(undefined, undefined, (colors) => { - return function i18nPlural(template, n) { - const id = ServerSideI18nMessage.templateToId(template); - return new ServerSideI18nMessage(colors, (l) => { - const translated = - extractedTranslatedPlurals[l]?.[id]?.[intlPlural(l, n)] ?? template; - return ServerSideI18nMessage.concatTemplateStringsArray( - l, - translated, - [n], - colors, - [] - ); - }); - } as Text.Chained< - (template: TemplateStringsArray, n: number) => ServerSideI18nMessage - >; -}); - -function defaultColors( - colors: Partial = {} -): Required { - return { - unit: colors.unit ?? "§f", - text: colors.text ?? "§7", - num: colors.num ?? "§6", - }; + nocolor: createStyle({ text: '', unit: '', num: '' }), + header: createStyle({ text: '§r§6', num: '§f', unit: '§f§l' }), + error: createStyle({ num: '§7', text: '§c', unit: '§f' }), + warn: createStyle({ num: '§6', text: '§e', unit: '§f' }), + accent: createStyle({ num: '§6', text: '§3', unit: '§f' }), + success: createStyle({ num: '§6', text: '§a', unit: '§f' }), + disabled: createStyle({ num: '§7', text: '§8', unit: '§7' }), +} + +export const noI18n = createStatic(undefined, undefined, colors => { + return function simpleStr(template, ...args) { + return Message.concatTemplateStringsArray(defaultLang, template, args, colors) + } as Text.Chained> +}) + +export const i18n = createStatic(undefined, undefined, colors => { + const i18n = ((template, ...args) => new I18nMessage(template, args, colors)) as Text.FnWithJoin + + i18n.join = (template, ...args) => new Message(template, args, colors) + + return i18n as Text.Chained> +}) + +export const i18nShared = createStatic(undefined, undefined, colors => { + const i18n = ((template, ...args) => new SharedI18nMessage(template, args, colors)) as Text.FnWithJoin< + SharedI18nMessage, + RawTextArg + > + + i18n.join = (template, ...args) => new SharedI18nMessageJoin(template, args, colors) + + return i18n as Text.Chained> +}) + +export const i18nPlural = createStatic(undefined, undefined, colors => { + return function i18nPlural(template, n) { + const id = ServerSideI18nMessage.templateToId(template) + return new ServerSideI18nMessage(colors, l => { + const translated = extractedTranslatedPlurals[l]?.[id]?.[intlPlural(l, n)] ?? template + return ServerSideI18nMessage.concatTemplateStringsArray(l, translated, [n], colors, []) + }) + } as Text.Chained<(template: TemplateStringsArray, n: number) => ServerSideI18nMessage> +}) + +function defaultColors(colors: Partial = {}): Required { + return { + unit: colors.unit ?? '§f', + text: colors.text ?? '§7', + num: colors.num ?? '§6', + } } function createStatic>>( - colors: Partial = {}, - modifier = false, - createFn: (colors: Text.Colors) => T + colors: Partial = {}, + modifier = false, + createFn: (colors: Text.Colors) => T, ): T { - const dcolors = defaultColors(colors); - const fn = createFn(dcolors); - fn.style = dcolors; - fn.time = createTime(dcolors); - fn.hhmmss = createTimeHHMMSS(dcolors); - fn.restyle = (colors) => createStatic(colors, false, createFn); - - if (!modifier) { - fn.nocolor = createStatic(styles.nocolor, true, createFn); - fn.header = createStatic(styles.header, true, createFn); - fn.error = createStatic(styles.error, true, createFn); - fn.warn = createStatic(styles.warn, true, createFn); - fn.accent = createStatic(styles.accent, true, createFn); - fn.success = createStatic(styles.success, true, createFn); - fn.disabled = createStatic(styles.disabled, true, createFn); - } - return fn; + const dcolors = defaultColors(colors) + const fn = createFn(dcolors) + fn.style = dcolors + fn.time = createTime(dcolors) + fn.hhmmss = createTimeHHMMSS(dcolors) + fn.restyle = colors => createStatic(colors, false, createFn) + + if (!modifier) { + fn.nocolor = createStatic(styles.nocolor, true, createFn) + fn.header = createStatic(styles.header, true, createFn) + fn.error = createStatic(styles.error, true, createFn) + fn.warn = createStatic(styles.warn, true, createFn) + fn.accent = createStatic(styles.accent, true, createFn) + fn.success = createStatic(styles.success, true, createFn) + fn.disabled = createStatic(styles.disabled, true, createFn) + } + return fn } -const dayMs = ms.from("day", 1); -function createTimeHHMMSS(colors: Text.Colors): Text.Static["hhmmss"] { - return (n) => { - const hhmmss = new Date(n).toHHMMSS(); - if (n <= dayMs) return hhmmss; +const dayMs = ms.from('day', 1) +function createTimeHHMMSS(colors: Text.Colors): Text.Static['hhmmss'] { + return n => { + const hhmmss = new Date(n).toHHMMSS() + if (n <= dayMs) return hhmmss - const days = ~~(n / dayMs); - return i18nShared.restyle(colors)`${days} д. ${hhmmss}`; - }; + const days = ~~(n / dayMs) + return i18nShared.restyle(colors)`${days} д. ${hhmmss}` + } } -function createTime(colors: Text.Colors): Text.Static["time"] { - return (ms) => new ServerSideI18nMessage(colors, (l) => intlRemaining(l, ms)); +function createTime(colors: Text.Colors): Text.Static['time'] { + return ms => new ServerSideI18nMessage(colors, l => intlRemaining(l, ms)) } export function textUnitColorize( - v: unknown, - { unit, num }: Text.Colors = defaultColors(), - lang: Language | false + v: unknown, + { unit, num }: Text.Colors = defaultColors(), + lang: Language | false, ): string { - switch (typeof v) { - case "string": - if (v.includes("§l")) return unit + v + "§r"; - return unit + v; - case "undefined": - return ""; - case "object": - if (v instanceof Message) { - if (!lang) { - throw new TypeError( - `Text unit colorize cannot translate Message '${v.id}' if no locale was given!` - ); - } - - const vstring = v.to(lang); - return vstring.startsWith("§") ? vstring : unit + vstring; - } - if (v instanceof Player) { - return unit + v.name; - } else if (Vec.isVec(v)) { - return Vec.string(v, true); - } else return stringify(v); - - case "number": - return `${num}${separateNumberWithDots(v)}`; - case "symbol": - case "function": - case "bigint": - return "§c<>"; - case "boolean": - return (v ? i18n.nocolor`§fДа` : i18n.nocolor`§cНет`).to( - lang || defaultLang - ); - } + switch (typeof v) { + case 'string': + if (v.includes('§l')) return unit + v + '§r' + return unit + v + case 'undefined': + return '' + case 'object': + if (v instanceof Message) { + if (!lang) { + throw new TypeError(`Text unit colorize cannot translate Message '${v.id}' if no locale was given!`) + } + + const vstring = v.to(lang) + return vstring.startsWith('§') ? vstring : unit + vstring + } + if (v instanceof Player) { + return unit + v.name + } else if (Vec.isVec(v)) { + return Vec.string(v, true) + } else return stringify(v) + + case 'number': + return `${num}${separateNumberWithDots(v)}` + case 'symbol': + case 'function': + case 'bigint': + return '§c<>' + case 'boolean': + return (v ? i18n.nocolor`§fДа` : i18n.nocolor`§cНет`).to(lang || defaultLang) + } } diff --git a/src/lib/rpg/loot-table.ts b/src/lib/rpg/loot-table.ts index dfe61977..5ce147ba 100644 --- a/src/lib/rpg/loot-table.ts +++ b/src/lib/rpg/loot-table.ts @@ -236,7 +236,7 @@ export class LootTable { let i = length return Array.from({ length }, () => { i-- - if (air > 0) return air--, undefined + if (air > 0) return (air--, undefined) air = Math.randomInt(0, i - (explictItems.length + randomizableItems.length)) diff --git a/src/lib/rpg/newbie.ts b/src/lib/rpg/newbie.ts index b1c63b31..b9ed3a5d 100644 --- a/src/lib/rpg/newbie.ts +++ b/src/lib/rpg/newbie.ts @@ -1,128 +1,114 @@ -import { EntityDamageCause, Player, system, world } from "@minecraft/server"; -import { PlayerProperties } from "lib/assets/player-json"; -import { Cooldown } from "lib/cooldown"; -import { ask } from "lib/form/message"; -import { i18n } from "lib/i18n/text"; -import { Join } from "lib/player-join"; -import { createLogger } from "lib/utils/logger"; -import { ms } from "lib/utils/ms"; +import { EntityDamageCause, Player, system, world } from '@minecraft/server' +import { PlayerProperties } from 'lib/assets/player-json' +import { Cooldown } from 'lib/cooldown' +import { ask } from 'lib/form/message' +import { i18n } from 'lib/i18n/text' +import { Join } from 'lib/player-join' +import { createLogger } from 'lib/utils/logger' +import { ms } from 'lib/utils/ms' -const newbieTime = ms.from("hour", 2); +const newbieTime = ms.from('hour', 2) -const property = PlayerProperties["lw:newbie"]; +const property = PlayerProperties['lw:newbie'] export function isNewbie(player: Player) { - return !!player.database.survival.newbie; + return !!player.database.survival.newbie } export function askForExitingNewbieMode( - player: Player, - reason: Text, - callback: VoidFunction, - back: VoidFunction = () => player.success(i18n`Успешно отменено`) + player: Player, + reason: Text, + callback: VoidFunction, + back: VoidFunction = () => player.success(i18n`Успешно отменено`), ) { - if (!isNewbie(player)) return callback(); + if (!isNewbie(player)) return callback() - ask( - player, - i18n`Если вы совершите это действие, вы потеряете статус новичка: + ask( + player, + i18n`Если вы совершите это действие, вы потеряете статус новичка: - Другие игроки смогут наносить вам урон - Другие игроки смогут забирать ваш лут после смерти`, - i18n.error`Я больше не новичок`, - () => { - exitNewbieMode(player, reason); - callback(); - }, - i18n`НЕТ, НАЗАД`, - back - ); + i18n.error`Я больше не новичок`, + () => { + exitNewbieMode(player, reason) + callback() + }, + i18n`НЕТ, НАЗАД`, + back, + ) } -const logger = createLogger("Newbie"); +const logger = createLogger('Newbie') function exitNewbieMode(player: Player, reason: Text) { - if (!isNewbie(player)) return; + if (!isNewbie(player)) return - player.warn(i18n.warn`Вы ${reason}, поэтому вышли из режима новичка.`); - delete player.database.survival.newbie; - player.setProperty(property, false); + player.warn(i18n.warn`Вы ${reason}, поэтому вышли из режима новичка.`) + delete player.database.survival.newbie + player.setProperty(property, false) - logger.player(player).info`Exited newbie mode because ${reason}`; + logger.player(player).info`Exited newbie mode because ${reason}` } export function enterNewbieMode(player: Player, resetAnarchyOnlineTime = true) { - player.database.survival.newbie = 1; - if (resetAnarchyOnlineTime) player.scores.anarchyOnlineTime = 0; - player.setProperty(property, true); + player.database.survival.newbie = 1 + if (resetAnarchyOnlineTime) player.scores.anarchyOnlineTime = 0 + player.setProperty(property, true) } -Join.onFirstTimeSpawn.subscribe(enterNewbieMode); +Join.onFirstTimeSpawn.subscribe(enterNewbieMode) Join.onMoveAfterJoin.subscribe(({ player }) => { - const value = isNewbie(player); - if (value !== player.getProperty(property)) - player.setProperty(property, value); -}); - -const damageCd = new Cooldown(ms.from("min", 1), false); - -world.afterEvents.entityHurt.subscribe( - ({ hurtEntity, damage, damageSource: { damagingEntity, cause } }) => { - if (!(hurtEntity instanceof Player)) return; - if (damage === -17179869184) return; - - const health = hurtEntity.getComponent("health"); - const denyDamage = () => { - logger.player(hurtEntity) - .info`Recieved damage ${damage}, health ${health?.currentValue}, with cause ${cause}`; - if (health) health.setCurrentValue(health.currentValue + damage); - hurtEntity.teleport(hurtEntity.location); - }; - - if ( - hurtEntity.database.survival.newbie && - cause === EntityDamageCause.fireTick - ) { - denyDamage(); - } else if ( - damagingEntity instanceof Player && - damagingEntity.database.survival.newbie - ) { - if (damageCd.isExpired(damagingEntity)) { - denyDamage(); - askForExitingNewbieMode( - damagingEntity, - i18n`ударили игрока`, - () => void 0, - () => damagingEntity.info(i18n`Будь осторожнее в следующий раз.`) - ); - } else { - exitNewbieMode(damagingEntity, i18n.warn`снова ударили игрока`); - } - } - } -); - -new Command("newbie") - .setPermissions("member") - .setDescription(i18n`Используйте, чтобы выйти из режима новичка`) - .executes((ctx) => { - if (isNewbie(ctx.player)) { - askForExitingNewbieMode( - ctx.player, - i18n`использовали команду`, - () => void 0 - ); - } else return ctx.error(i18n`Вы не находитесь в режиме новичка.`); - }) - .overload("set") - .setPermissions("techAdmin") - .setDescription(i18n`Вводит в режим новичка`) - .executes((ctx) => { - enterNewbieMode(ctx.player); - ctx.player.success(); - }); - -system.runPlayerInterval((player) => { - if (isNewbie(player) && player.scores.anarchyOnlineTime * 2.5 > newbieTime) - exitNewbieMode(player, i18n.warn`провели на анархии больше 2 часов`); -}, "newbie mode exit"); + const value = isNewbie(player) + if (value !== player.getProperty(property)) player.setProperty(property, value) +}) + +const damageCd = new Cooldown(ms.from('min', 1), false) + +world.afterEvents.entityHurt.subscribe(({ hurtEntity, damage, damageSource: { damagingEntity, cause } }) => { + if (!(hurtEntity instanceof Player)) return + if (damage === -17179869184) return + + const health = hurtEntity.getComponent('health') + const denyDamage = () => { + logger.player(hurtEntity).info`Recieved damage ${damage}, health ${health?.currentValue}, with cause ${cause}` + if (health) health.setCurrentValue(health.currentValue + damage) + hurtEntity.teleport(hurtEntity.location) + } + + if (hurtEntity.database.survival.newbie && cause === EntityDamageCause.fireTick) { + denyDamage() + } else if (damagingEntity instanceof Player && damagingEntity.database.survival.newbie) { + if (damageCd.isExpired(damagingEntity)) { + denyDamage() + askForExitingNewbieMode( + damagingEntity, + i18n`ударили игрока`, + () => void 0, + () => damagingEntity.info(i18n`Будь осторожнее в следующий раз.`), + ) + } else { + exitNewbieMode(damagingEntity, i18n.warn`снова ударили игрока`) + } + } +}) + +new Command('newbie') + .setPermissions('member') + .setDescription(i18n`Используйте, чтобы выйти из режима новичка`) + .executes(ctx => { + if (isNewbie(ctx.player)) { + askForExitingNewbieMode(ctx.player, i18n`использовали команду`, () => void 0) + } else return ctx.error(i18n`Вы не находитесь в режиме новичка.`) + }) + .overload('set') + .setPermissions('techAdmin') + .setDescription(i18n`Вводит в режим новичка`) + .executes(ctx => { + enterNewbieMode(ctx.player) + ctx.player.success() + }) + +system.runPlayerInterval(player => { + if (isNewbie(player) && player.scores.anarchyOnlineTime * 2.5 > newbieTime) + exitNewbieMode(player, i18n.warn`провели на анархии больше 2 часов`) +}, 'newbie mode exit') diff --git a/src/lib/shop/buttons/item-modifier.ts b/src/lib/shop/buttons/item-modifier.ts index 0914f63d..738cfc9d 100644 --- a/src/lib/shop/buttons/item-modifier.ts +++ b/src/lib/shop/buttons/item-modifier.ts @@ -2,7 +2,7 @@ import { ContainerSlot, ItemStack, Player } from '@minecraft/server' import { getAuxOrTexture } from 'lib/form/chest' import { ItemFilter, OnSelect, selectItemForm } from 'lib/form/select-item' import { translateEnchantment, translateTypeId } from 'lib/i18n/lang' -import { i18n } from 'lib/i18n/text' +import { i18n } from 'lib/i18n/text' import { Cost, MultiCost, ShouldHaveItemCost } from '../cost' import { ShopForm, ShopFormSection } from '../form' import { ProductName } from '../product' diff --git a/src/lib/shop/cost.test.ts b/src/lib/shop/cost.test.ts index 1a60e0e9..b5b9c3f9 100644 --- a/src/lib/shop/cost.test.ts +++ b/src/lib/shop/cost.test.ts @@ -81,11 +81,11 @@ describe('MultiCost', () => { `"§c1.000, §cЯблоко §r§f§cx1, §cНезеритовый топор §r§f§cx1, §4§c10§4lvl"`, ) - expect(cost.failed(player)).toMatchInlineSnapshot(` - "§7§60§7/§61.000§7§f§7 - §c§70§c/§71§c §f§cЯблоко§c - §c§70§c/§71§c §f§cНезеритовый топор§c - §cНужно уровней опыта: §710§c, §70§c/§710§c" + expect(cost.failed(player)).toMatchInlineSnapshot(` + "§7§60§7/§61.000§7§f§7 + §c§70§c/§71§c §f§cЯблоко§c + §c§70§c/§71§c §f§cНезеритовый топор§c + §cНужно уровней опыта: §710§c, §70§c/§710§c" `) }) diff --git a/src/modules/world-edit/utils/shapes.ts b/src/modules/world-edit/utils/shapes.ts index 21eff4f8..463376d5 100644 --- a/src/modules/world-edit/utils/shapes.ts +++ b/src/modules/world-edit/utils/shapes.ts @@ -22,7 +22,8 @@ export const SHAPES = { 'customMountain': ({ x, y, z }) => y <= 0.5 * Math.sin(x / 10) + 0.5 * Math.cos(z / 10), 'tetrahedron': ({ x, y, z, yMin, zMin }) => ( - Math.abs(-x) + Math.abs(x) + Math.abs(y) + Math.abs(z) - yMin, zMin === 0 + Math.abs(-x) + Math.abs(x) + Math.abs(y) + Math.abs(z) - yMin, + zMin === 0 ), 'triangle_prism': ({ x, y, z, yMin }) => From 8a73d54da2a670a7a7712cb5ce789d2bced06c90 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:22:14 +0300 Subject: [PATCH 06/14] refactor: finish moving new code from folkbe --- package.json | 4 +- src/lib/clan/clan.ts | 162 ++++++++-- src/lib/clan/create.ts | 116 ++++++++ src/lib/clan/menu.ts | 273 ++++++++--------- src/lib/command/argument-types.ts | 26 ++ src/lib/command/index.ts | 231 ++++++++++++++- src/lib/command/utils.ts | 11 +- src/lib/cooldown.ts | 14 +- src/lib/cooldownreset.ts | 61 ++++ src/lib/cutscene/edit.ts | 376 ++++++++++++------------ src/lib/cutscene/menu.ts | 61 ++-- src/lib/database/inventory.ts | 5 + src/lib/database/item-stack.ts | 2 - src/lib/database/migrations.ts | 8 +- src/lib/database/persistent-set.ts | 5 +- src/lib/database/player.ts | 6 +- src/lib/database/properties.ts | 24 +- src/lib/database/utils.ts | 41 ++- src/lib/extensions/core.ts | 46 +-- src/lib/extensions/enviroment.ts | 6 +- src/lib/extensions/on-screen-display.ts | 2 +- src/lib/extensions/system.ts | 6 +- src/lib/extensions/world.ts | 34 ++- src/lib/form/action.ts | 24 +- src/lib/form/message.ts | 4 +- src/lib/form/modal.ts | 4 + src/lib/form/new.ts | 63 ++-- src/lib/form/select-player.ts | 3 +- src/lib/form/utils.ts | 11 - src/lib/lib.d.ts | 2 +- src/lib/mail.ts | 13 +- src/lib/player-move.ts | 6 +- src/lib/region/areas/area.ts | 3 + src/lib/region/big-structure.ts | 5 +- src/lib/region/command.ts | 75 ++--- src/lib/region/config.ts | 20 +- src/lib/region/database.ts | 13 +- src/lib/region/form.ts | 39 ++- src/lib/region/index.ts | 58 ++-- src/lib/region/kinds/region.ts | 12 +- src/lib/rpg/leaderboard.ts | 3 +- src/lib/settings.ts | 24 +- src/lib/utils/big-structure.ts | 3 +- src/lib/utils/item-name-x-count.ts | 111 ++++--- src/lib/utils/ms-old.test.ts | 10 + src/lib/utils/ms-old.ts | 109 +++++++ src/lib/utils/ms.ts | 2 +- src/lib/utils/rewards.ts | 2 +- src/modules/test/enchant.ts | 4 +- src/modules/test/test.ts | 7 +- 50 files changed, 1481 insertions(+), 669 deletions(-) create mode 100644 src/lib/clan/create.ts create mode 100644 src/lib/cooldownreset.ts create mode 100644 src/lib/utils/ms-old.test.ts create mode 100644 src/lib/utils/ms-old.ts diff --git a/package.json b/package.json index 2fc40def..f0927baf 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "printWidth": 120, "endOfLine": "crlf", "quoteProps": "consistent", - "plugins": [], + "plugins": [ + "prettier-plugin-jsdoc" + ], "jsdocTagsOrder": "{\"template\": 24.5}" }, "packageManager": "yarn@4.9.1", diff --git a/src/lib/clan/clan.ts b/src/lib/clan/clan.ts index 2a928e30..a6614bac 100644 --- a/src/lib/clan/clan.ts +++ b/src/lib/clan/clan.ts @@ -1,10 +1,41 @@ import { Player, system, world } from '@minecraft/server' import { table } from 'lib/database/abstract' +import { I18nMessage } from 'lib/i18n/message' +import { i18n } from 'lib/i18n/text' import './command' -interface StoredClan { - members: string[] - owners: string[] +interface ClanTemporalMember { + until: number +} + +export enum ClanRole { + Member = 'member', + Helper = 'helper', + Owner = 'owner', +} + +const roleNames: Record = { + [ClanRole.Member]: i18n`Участник`, + [ClanRole.Helper]: i18n`Помошник`, + [ClanRole.Owner]: i18n`Владелец`, +} + +export interface ClanMember { + id: string + createdAt: number + updatedAt: number + role: ClanRole +} + +interface ClanJSON { + members2: ClanMember[] + + /** @deprecated Use {@link members2} instead */ + members?: string[] + /** @deprecated Use {@link members2} instead */ + owners?: string[] + + temporalMembers?: Record name: string shortname: string @@ -16,7 +47,11 @@ interface StoredClan { } export class Clan { - private static database = table('clan') + private static database = table('clan') + + static roleToString(role: ClanRole) { + return roleNames[role] + } static getPlayerClan(playerId: string) { for (const clan of this.instances.values()) { @@ -28,12 +63,15 @@ export class Clan { return this.instances.values() } + static get(id: string) { + return this.instances.get(id) + } + static create(player: Player, name: string, shortname: string) { while (this.database.has(name)) name += '-' this.database.set(name, { - members: [player.id], - owners: [player.id], + members2: [], name, shortname, @@ -45,7 +83,9 @@ export class Clan { }) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return new Clan(name, this.database.get(name)!) + const clan = new Clan(name, this.database.get(name)!) + clan.addMember(player.id, ClanRole.Owner) + return clan } static getInvites(playerId: string) { @@ -57,15 +97,31 @@ export class Clan { static { world.afterEvents.worldLoad.subscribe(() => system.run(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const [id, db] of this.database.entries()) new Clan(id, db!) + for (const [id, db] of this.database.entries()) { + if (!db) continue + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + db.members2 ??= [] + + const clan = new Clan(id, db) + + // Migrate old format + if (db.owners?.length) { + for (const m of db.owners) clan.addMember(m, ClanRole.Owner) + delete db.owners + } + if (db.members?.length) { + for (const m of db.members) clan.addMember(m) + delete db.members + } + } }), ) } constructor( - private readonly id: string, - public readonly db: StoredClan, + public readonly id: string, + public readonly db: ClanJSON, ) { const clan = Clan.instances.get(this.id) if (clan) return clan @@ -91,41 +147,54 @@ export class Clan { } get members() { - return this.db.members as Readonly + return this.db.members2 as readonly ClanMember[] + } + + get membersIds() { + return this.db.members2.map(e => e.id) as readonly string[] } get owners() { - return this.db.owners as Readonly + return this.db.members2.filter(e => e.role === ClanRole.Owner).map(e => e.id) as readonly string[] } get joinRequests() { - return this.db.joinRequests as Readonly + return this.db.joinRequests as Readonly } get invites() { - return this.db.invites as Readonly + return this.db.invites as Readonly } isMember(playerId: string) { - return this.db.members.includes(playerId) + return !!this.getMember(playerId) } isOwner(playerId: string) { - return this.db.owners.includes(playerId) + return this.getMember(playerId)?.role === ClanRole.Owner + } + + isHelper(playerId: string) { + return this.getMember(playerId)?.role === ClanRole.Helper } isInvited(playerId: string) { return this.db.invites.includes(playerId) } - setRole(playerId: string, role: 'member' | 'owner') { - if (role === 'member') this.db.owners = this.db.owners.filter(e => e !== playerId) - if (role === 'owner') this.db.owners.push(playerId) + getMember(playerId: string) { + return this.db.members2.find(e => e.id === playerId) + } + + setMemberRole(playerId: string, role: ClanRole) { + const member = this.getMember(playerId) + + if (!member) return + member.role = role } remove(playerId: string) { - this.db.members = this.db.members.filter(e => e !== playerId) - this.db.owners = this.db.owners.filter(e => e !== playerId) + this.db.members2 = this.db.members2.filter(e => e.id !== playerId) } sendInvite(playerId: string) { @@ -151,16 +220,61 @@ export class Clan { this.db.invites = this.db.invites.filter(e => e !== id) } - add(id: string) { + addMember(id: string, role: ClanRole = ClanRole.Member) { + if (this.isMember(id)) return + for (const clan of Clan.getAll()) { clan.db.joinRequests = clan.db.joinRequests.filter(e => e !== id) clan.db.invites = clan.db.invites.filter(e => e !== id) } - this.db.members.push(id) + this.db.members2.push({ id, role, createdAt: Date.now(), updatedAt: Date.now() }) } delete() { Clan.database.delete(this.id) Clan.instances.delete(this.id) } + + addTemporalMember(id: string, until: number) { + this.db.temporalMembers ??= {} + this.db.temporalMembers[id] = { until } + } + + removeTemporalMember(id: string) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + if (this.db.temporalMembers) delete this.db.temporalMembers[id] + } + + isTemporalMemberValid(id: string, member?: ClanTemporalMember) { + if (!member) return false + + // 0 means forever + if (member.until === 0) return true + + if (Date.now() > member.until) { + this.removeTemporalMember(id) + return false + } + + return true + } + + get temporalMembers() { + if (!this.db.temporalMembers) return [] + + const members: { id: string; until: number }[] = [] + for (const [id, member] of Object.entries(this.db.temporalMembers)) { + if (this.isTemporalMemberValid(id, member)) members.push({ id, until: member.until }) + } + return members + } + + isTemporalMember(id: string): boolean { + return this.isTemporalMemberValid(id, this.db.temporalMembers?.[id]) + } + + getTemporalMember(id: string) { + const member = this.db.temporalMembers?.[id] + if (this.isTemporalMemberValid(id, member)) return member + } } diff --git a/src/lib/clan/create.ts b/src/lib/clan/create.ts new file mode 100644 index 00000000..d6469388 --- /dev/null +++ b/src/lib/clan/create.ts @@ -0,0 +1,116 @@ +import { Player } from '@minecraft/server' +import { ArrayForm } from 'lib/form/array' +import { MessageForm } from 'lib/form/message' +import { ModalForm } from 'lib/form/modal' +import { i18n } from 'lib/i18n/text' +import { Mail } from 'lib/mail' +import { Clan } from './clan' +import { cd, clanInvites, clanMenu, inClanMenu } from './menu' + +export function selectOrCreateClanMenu(player: Player, back?: VoidFunction) { + new ArrayForm(i18n`Выбор клана`, [...Clan.getAll()].reverse()) + .description(i18n`Выберите клан, чтобы отправить заявку или создайте свой клан!`) + .addCustomButtonBeforeArray(form => { + const invitedTo = Clan.getInvites(player.id) + if (invitedTo.length) + form.button(i18n.accent`Приглашения`.badge(invitedTo.length).to(player.lang), () => { + new ArrayForm(i18n`Приглашения`, invitedTo) + .button(clan => [ + getClanButtonName(clan), + () => { + clan.addMember(player.id) + player.success(i18n`Вы приняли приглашение в клан '${clan.name}'`) + inClanMenu({ clan }).show(player) + }, + ]) + .back(() => selectOrCreateClanMenu(player, back)) + .show(player) + }) + form.button(i18n.accent`Создать свой клан`.to(player.lang), () => + promptClanNameShortname( + player, + i18n`Создать клан`, + (name, shortname) => { + const clan = Clan.create(player, name, shortname) + clanInvites(player, clan, () => clanMenu(player)[1]()) + }, + () => selectOrCreateClanMenu(player, back), + ), + ) + }) + .button(clan => [ + getClanButtonName(clan, clan.isInvited(player.id) ? i18n.disabled : i18n), + () => { + if (!clan.requestJoin(player)) { + return player.fail( + i18n.error`Вы уже отправили заявку в клан '${Clan.getPlayerClan(player.id)?.name ?? clan.name}'!`, + ) + } + + player.success(i18n`Заявка на вступление в клан '${clan.name}' отправлена!`) + Mail.sendMultiple( + clan.owners, + i18n.nocolor`Запрос на вступление в клан от '${player.name}'`, + i18n`Игрок хочет вступить в ваш клан, вы можете принять или отклонить его через меню кланов`, + ) + }, + ]) + .back(back) + .show(player) +} +export function getClanButtonName(clan: Clan, style: Text.Fn = i18n): Text { + return style`[${clan.shortname}] ${clan.name}\nУчастники: ${clan.members.length} ${clan.owners.map(id => Player.nameOrUnknown(id)).join(', ')}` +} +export function promptClanNameShortname( + player: Player, + title: Text, + onDone: (name: string, shortname: string) => void, + back: VoidFunction, + clan?: Clan, + defaultName?: string, + defaultShortname?: string, +) { + if (!cd.isExpired(player, false)) return + new ModalForm(title.to(player.lang)) + .addTextField( + i18n`Название клана`.to(player.lang), + i18n`Ну, давай, придумай чета оригинальное`.to(player.lang), + defaultName, + ) + .addTextField( + i18n`Тэг клана`.to(player.lang), + i18n`Чтобы блатными в чате выглядеть`.to(player.lang), + defaultShortname, + ) + .show(player, (_, name, shortname) => { + name = name.trim() + shortname = shortname.trim() + + function err(reason: Text) { + return new MessageForm(i18n`Ошибка`.to(player.lang), reason.to(player.lang)) + .setButton1(i18n`Щас исправлю`.to(player.lang), () => + promptClanNameShortname(player, title, onDone, back, clan, name, shortname), + ) + .setButton2(i18n`Та ну не надоело`.to(player.lang), back) + .show(player) + } + + if (name.includes('§')) return err(i18n`Имя '${name}' не может содержать параграф`) + if (shortname.includes('§')) return err(i18n`Короткое имя '${shortname}' не может содержать параграф`) + if (shortname.length > 5) return err(i18n`Короткое имя '${shortname}' должно быть КОРОТКИМ, меньше 5 символов`) + if (shortname.length < 2) + return err( + i18n.error`Короткое имя '${shortname}' не может быть СЛИШКОМ коротким, минимум 2 символа. А то как понять че это за клан '${shortname}'`, + ) + + for (const c of Clan.getAll()) { + if (c === clan) continue + if (c.name === name) return err(i18n.error`Клан с именем '${name}' уже существует.`) + if (c.shortname === shortname) return err(i18n.error`Короткое имя '${shortname}' уже занято.`) + } + + if (!cd.isExpired(player)) return + + onDone(name, shortname) + }) +} diff --git a/src/lib/clan/menu.ts b/src/lib/clan/menu.ts index d0415f50..5908263d 100644 --- a/src/lib/clan/menu.ts +++ b/src/lib/clan/menu.ts @@ -1,23 +1,25 @@ import { Player, world } from '@minecraft/server' import { Cooldown } from 'lib/cooldown' +import { registerResettableCooldown } from 'lib/cooldownreset' import { ArrayForm } from 'lib/form/array' -import { MessageForm, ask } from 'lib/form/message' +import { ask, MessageForm } from 'lib/form/message' import { ModalForm } from 'lib/form/modal' -import { form } from 'lib/form/new' +import { form, FormContext, NewFormCreator } from 'lib/form/new' import { selectPlayer } from 'lib/form/select-player' import { BUTTON } from 'lib/form/utils' import { getFullname } from 'lib/get-fullname' import { i18n, textTable } from 'lib/i18n/text' import { Mail } from 'lib/mail' +import { is } from 'lib/roles' import { ms } from 'lib/utils/ms' -// import { registerResettableCooldown } from 'modules/commands/cooldownreset' -import { Clan } from './clan' +import { Clan, ClanMember, ClanRole } from './clan' +import { getClanButtonName, promptClanNameShortname, selectOrCreateClanMenu } from './create' -let cd: Cooldown +export let cd: Cooldown world.afterEvents.worldLoad.subscribe(() => { cd = new Cooldown(ms.from('day', 1), true, Cooldown.defaultDb.get('clan')) - // registerResettableCooldown('Изменение/создание клана', cd) + registerResettableCooldown('Изменение/создание клана', cd) }) export function clanMenu(player: Player, back?: VoidFunction) { @@ -34,114 +36,24 @@ export function clanMenu(player: Player, back?: VoidFunction) { } } -function selectOrCreateClanMenu(player: Player, back?: VoidFunction) { - new ArrayForm(i18n`Выбор клана`, [...Clan.getAll()].reverse()) - .description(i18n`Выберите клан, чтобы отправить заявку или создайте свой клан!`) - .addCustomButtonBeforeArray(form => { - const invitedTo = Clan.getInvites(player.id) - if (invitedTo.length) - form.button(i18n.accent`Приглашения`.badge(invitedTo.length).to(player.lang), () => { - new ArrayForm(i18n`Приглашения`, invitedTo) - .button(clan => [ - getClanName(clan), - () => { - clan.add(player.id) - player.success(i18n`Вы приняли приглашение в клан '${clan.name}'`) - inClanMenu({ clan }).show(player) - }, - ]) - .back(() => selectOrCreateClanMenu(player, back)) - .show(player) - }) - form.button(i18n.accent`Создать свой клан`.to(player.lang), () => - promptClanNameShortname( - player, - i18n`Создать клан`, - (name, shortname) => { - const clan = Clan.create(player, name, shortname) - clanInvites(player, clan, () => clanMenu(player)[1]()) - }, - () => selectOrCreateClanMenu(player, back), - ), - ) - }) - .button(clan => [ - getClanName(clan, clan.isInvited(player.id) ? i18n.disabled : i18n), - () => { - if (!clan.requestJoin(player)) return - player.fail(i18n.error`Вы уже отправили заявку в клан '${Clan.getPlayerClan(player.id)?.name ?? clan.name}'!`) - - Mail.sendMultiple( - clan.owners, - i18n.nocolor`Запрос на вступление в клан от '${player.name}'`, - i18n`Игрок хочет вступить в ваш клан, вы можете принять или отклонить его через меню кланов`, - ) - player.success(i18n`Заявка на вступление в клан '${clan.name}' отправлена!`) - }, - ]) - .back(back) - .show(player) - - function getClanName(clan: Clan, style: Text.Fn = i18n): Text { - return style`[${clan.shortname}] ${clan.name}\nУчастники: ${clan.members.length}` - } +interface ClanButtonContext { + f: NewFormCreator + clan: Clan + formContext: FormContext<{ clan: Clan }> + isOwner: boolean + isHelper: boolean } - -function promptClanNameShortname( - player: Player, - title: Text, - onDone: (name: string, shortname: string) => void, - back: VoidFunction, - clan?: Clan, - defaultName?: string, - defaultShortname?: string, -) { - if (!cd.isExpired(player, false)) return - new ModalForm(title.to(player.lang)) - .addTextField( - i18n`Имя клана`.to(player.lang), - i18n`Ну, давай, придумай чета оригинальное`.to(player.lang), - defaultName, - ) - .addTextField( - i18n`Краткое имя клана`.to(player.lang), - i18n`Чтобы блатными в чате выглядеть`.to(player.lang), - defaultShortname, - ) - .show(player, (_, name, shortname) => { - name = name.trim() - shortname = shortname.trim() - - function err(reason: Text) { - return new MessageForm(i18n`Ошибка`.to(player.lang), reason.to(player.lang)) - .setButton1(i18n`Щас исправлю`.to(player.lang), () => - promptClanNameShortname(player, title, onDone, back, clan, name, shortname), - ) - .setButton2(i18n`Та ну не надоело`.to(player.lang), back) - .show(player) - } - - if (name.includes('§')) return err(i18n`Имя '${name}' не может содержать параграф`) - if (shortname.includes('§')) return err(i18n`Короткое имя '${shortname}' не может содержать параграф`) - if (shortname.length > 5) return err(i18n`Короткое имя '${shortname}' должно быть КОРОТКИМ, меньше 5 символов`) - if (shortname.length < 2) - return err( - i18n.error`Короткое имя '${shortname}' не может быть СЛИШКОМ коротким, минимум 2 символа. А то как понять че это за клан '${shortname}'`, - ) - - for (const c of Clan.getAll()) { - if (c === clan) continue - if (c.name === name) return err(i18n.error`Клан с именем '${name}' уже существует.`) - if (c.shortname === shortname) return err(i18n.error`Короткое имя '${shortname}' уже занято.`) - } - - if (!cd.isExpired(player)) return - - onDone(name, shortname) - }) +const clanAdditionalButtons: ((ctx: ClanButtonContext) => void)[] = [] +export function registerClanMenuButton(register: (ctx: ClanButtonContext) => void) { + clanAdditionalButtons.push(register) } -const inClanMenu = form.params<{ clan: Clan }>((f, { self, player, params: { clan } }) => { +export const inClanMenu = form.params<{ clan: Clan }>((f, formContext) => { + const { + self, + player, + params: { clan }, + } = formContext f.title(i18n`Меню клана`) f.body( textTable([ @@ -151,14 +63,21 @@ const inClanMenu = form.params<{ clan: Clan }>((f, { self, player, params: { cla ) const isOwner = clan.isOwner(player.id) + const isHelper = clan.isHelper(player.id) f.button(i18n`Участники`.size(clan.members.length), () => clanMembers(player, clan, self)) - if (isOwner) { + if (isOwner || isHelper) { f.button(i18n`Заявки на вступление`.badge(clan.joinRequests.length), () => clanJoinRequests(player, clan, self)) f.button(i18n`Приглашения`.badge(clan.invites.length), () => clanInvites(player, clan, self)) - f.button(i18n`Изменить имя/короткое имя`, () => + } + + const context: ClanButtonContext = { clan, f, formContext, isHelper, isOwner } + for (const button of clanAdditionalButtons) button(context) + + if (isOwner) { + f.button(i18n`Изменить название или тэг клана`, () => promptClanNameShortname( player, i18n`Изменить`, @@ -175,7 +94,7 @@ const inClanMenu = form.params<{ clan: Clan }>((f, { self, player, params: { cla ) f.ask(i18n.error`Удалить клан`, i18n.error`Удалить`, () => { Mail.sendMultiple( - clan.members, + clan.membersIds, i18n.nocolor`Клан '${clan.name}' распущен`, i18n`К сожалению, клан был распущен. Хз че создателю не понравилось, найдите клан получше или создайте новый, печалиться смысла нет. Ну базы еще можете залутать, врятли создатель успел вас удалить из всех клановых баз.`, ) @@ -193,20 +112,30 @@ const inClanMenu = form.params<{ clan: Clan }>((f, { self, player, params: { cla new ArrayForm('Кланы', [...Clan.getAll()].reverse()) .button((clan, _, __) => { return [ - i18n.join`[${clan.shortname}] ${clan.name}`.size(clan.members.length).to(player.lang), + getClanButtonName(clan), form((f, { self }) => { f.title(clan.name) f.body(`Короткое имя: ${clan.shortname}`) for (const o of clan.members) { - f.button(getFullname(o), self) + f.button(i18n`${getFullname(o.id)}\n${Clan.roleToString(o.role)}`, self) } }).show, ] }) .show(player) }) + + if (is(player.id, 'techAdmin')) { + f.button(i18n`Админ: добавить игрока`, () => + selectPlayer(player, 'добавить в клан', self).then(e => { + clan.addMember(e.id) + player.success() + }), + ) + } }) + function clanJoinRequests(player: Player, clan: Clan, back?: VoidFunction) { const self = () => { clanJoinRequests(player, clan, back) @@ -221,8 +150,7 @@ function clanJoinRequests(player: Player, clan: Clan, back?: VoidFunction) { .setButton1(i18n`Принять!`.to(player.lang), () => { const message = i18n.nocolor`Вы приняты в клан ${clan.name}` Mail.send(id, message, i18n`Откройте меню клана с помощью /clan`) - - clan.add(id) + clan.addMember(id) self() }) .setButton2(i18n`Нет, не заслужил`.to(player.lang), () => { @@ -236,7 +164,7 @@ function clanJoinRequests(player: Player, clan: Clan, back?: VoidFunction) { .back(back) .show(player) } -function clanInvites(player: Player, clan: Clan, back?: VoidFunction) { +export function clanInvites(player: Player, clan: Clan, back?: VoidFunction) { new ArrayForm(i18n`Приглашения в клан '${clan.name}'`, clan.invites) .addCustomButtonBeforeArray(form => form.button(i18n.accent`Новое приглашение`.to(player.lang), BUTTON['+'], () => @@ -271,53 +199,94 @@ function inviteToClan(player: Player, clan: Clan, back?: VoidFunction) { back?.() }) } - function clanMembers(player: Player, clan: Clan, back?: VoidFunction) { const self = () => clanMembers(player, clan, back) new ArrayForm(i18n`Участники клана`, clan.members) .back(back) - .button(id => { - const memberName = getFullname(id) - return [memberName, () => clanMember({ clan, member: id, memberName }).show(player, self)] + .button(member => { + const memberName = i18n`${getFullname(member.id)}\n${Clan.roleToString(member.role)}` + return [memberName, () => clanMember({ clan, member, memberName }).show(player, self)] }) .show(player) } -const clanMember = form.params<{ clan: Clan; member: string; memberName: string }>( + +const clanMember = form.params<{ clan: Clan; member: ClanMember; memberName: Text }>( (f, { player, self, params: { clan, member, memberName } }) => { f.title(i18n`Участник`) + f.body(memberName) const isOwner = clan.isOwner(player.id) - const isMemberOwner = clan.isOwner(member) + const isHelper = clan.isHelper(player.id) + const isSelf = member.id === player.id - if (isOwner) { - f.button(isMemberOwner ? i18n`Понизить до участника` : i18n`Повысить до владельца`, () => { - clan.setRole(member, isMemberOwner ? 'member' : 'owner') - player.success(i18n`Роль участника клана ${memberName} сменена успешно.`) - Mail.send( - member, - isMemberOwner ? i18n.nocolor`Вы были понижены до участника` : i18n.nocolor`Вы были повышены до владельца`, - i18n`В клане '${clan.name}'`, - ) - self() - }).button(i18n.error`Выгнать`, () => { - new ModalForm(i18n`Выгнать участника '${memberName}'`.to(player.lang)) - .addTextField( - i18n`Причина`.to(player.lang), - i18n`Ничего не произойдет, если вы не укажете причину`.to(player.lang), - ) - .show(player, (_, reason) => { - if (reason) { - clan.remove(member) + if (isSelf) { + if (isOwner) { + const otherOwners = clan.owners.length > 1 + f.ask((otherOwners ? i18n.error : i18n.disabled)`Отказаться от владения`, i18n`Да`, () => { + if (!otherOwners) + return player.fail( + i18n.error`Вы не можете отказаться от владения клана являясь единственным его владельцем`, + ) + clan.setMemberRole(player.id, ClanRole.Member) + }) + + f.button((otherOwners ? i18n.error : i18n.disabled)`Покинуть клан`, () => { + if (!otherOwners) + return player.fail(i18n.error`Вы единственный владелец. Кнопка удаления клана находится в меню клана снизу`) + clan.remove(player.id) + }) + } + } else { + if (isOwner) { + const prevRole = Clan.roleToString(member.role) + f.button( + i18n`Сменить роль`, + roleSelect({ + member, + onSelect(role) { + clan.setMemberRole(member.id, role) + + const changeString = i18n`${prevRole} -> ${Clan.roleToString(role)}` + player.success(i18n`Роль участника клана ${memberName} сменена успешно: ${changeString}.`) Mail.send( - member, - i18n.nocolor`Вы выгнаны из клана '${clan.name}'`, - i18n`Вы были выгнаны из клана игроком '${player.name}'. Причина: ${reason}`, + member.id, + i18n.nocolor`Роль в клане ${changeString}`, + i18n`В клане '${clan.name}', сменена игроком ${getFullname(player)}`, ) - player.success(i18n`Участник ${memberName} успешно выгнан из клана ${clan.name}`) - } else player.info(i18n`Причина не была указана, участник остался в клане`) - self() - }) - }) + self() + }, + }), + ) + } + if (member.role === ClanRole.Owner ? isOwner : isOwner || isHelper) { + f.button(i18n.error`Выгнать`, () => { + new ModalForm(i18n`Выгнать участника '${memberName}'`.to(player.lang)) + .addTextField(i18n`Причина`.to(player.lang), i18n`Причина обязательна`.to(player.lang)) + .show(player, (_, reason) => { + if (reason) { + clan.remove(member.id) + Mail.send( + member.id, + i18n.nocolor`Вы выгнаны из клана '${clan.name}'`, + i18n`Вы были выгнаны из клана игроком '${player.name}'. Причина: ${reason}`, + ) + player.success(i18n`Участник ${memberName} успешно выгнан из клана ${clan.name}`) + } else player.fail(i18n`Причина не была указана, участник остался в клане`) + self() + }) + }) + } + } + }, +) + +const roleSelect = form.params<{ member: ClanMember; onSelect: (role: ClanRole) => void }>( + (f, { params: { member, onSelect } }) => { + f.title(i18n`Сменить роль ${Player.nameOrUnknown(member.id)}`) + + for (const role of Object.values(ClanRole)) { + const color = member.role === role ? i18n.accent : i18n + f.button(color`${Clan.roleToString(role)}`, onSelect.bind(undefined, role)) } }, ) diff --git a/src/lib/command/argument-types.ts b/src/lib/command/argument-types.ts index 05ac66e5..92dadc08 100644 --- a/src/lib/command/argument-types.ts +++ b/src/lib/command/argument-types.ts @@ -1,3 +1,5 @@ +import { CustomCommandParamType, CustomCommandRegistry } from '@minecraft/server' + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters export abstract class IArgumentType { /** The return type */ @@ -20,6 +22,12 @@ export abstract class IArgumentType { */ abstract typeName: string + abstract ctype: CustomCommandParamType + + register(ctx: CustomCommandRegistry, namespace: string) { + return true + } + /** The name this argument is */ abstract name: string @@ -46,6 +54,8 @@ export class LiteralArgumentType extends IArgumentTyp super() } + ctype: CustomCommandParamType = CustomCommandParamType.String + type = null typeName = 'literal' @@ -69,6 +79,8 @@ export class StringArgumentType extends IArgumentType super() } + ctype = CustomCommandParamType.String + type = 'string' typeName = '§3string' @@ -89,6 +101,8 @@ export class IntegerArgumentType extends IArgumentTyp super() } + ctype: CustomCommandParamType = CustomCommandParamType.Integer + type = 1 typeName = 'int' @@ -110,6 +124,8 @@ export class LocationArgumentType extends IArgumentTy super() } + ctype: CustomCommandParamType = CustomCommandParamType.Location + type = { x: 0, y: 0, z: 0 } as Vector3 typeName = 'location' @@ -132,6 +148,8 @@ export class BooleanArgumentType extends IArgumentTyp super() } + ctype: CustomCommandParamType = CustomCommandParamType.Boolean + type = false as boolean typeName = 'boolean' @@ -154,6 +172,14 @@ export class ArrayArgumentType = Command< type CommandCallback = (ctx: CommandContext, ...args: any[]) => void export class Command void> { + static loaded = false + static prefixes = ['.', '-'] static isCommand(message: string) { @@ -50,7 +64,7 @@ export class Command if (!parsed) { this.logger.player(event.sender).error`Unable to parse: ${event.message}` return event.sender.fail(noI18n`Failed to parse command`) - } else this.logger.player(event.sender).info`Command ${event.message}` + } else if (event.sender.name !== 'server') this.logger.player(event.sender).info`Command ${event.message}` const { cmd, args, input } = parsed @@ -93,7 +107,7 @@ export class Command } [stringifySymbol]() { - return `§f${Command.prefixes[0]}${this.getFullName()}` + return `§f/${this.getFullName()}` } private getFullName(name = ''): string { @@ -115,6 +129,7 @@ export class Command private stack: string sys = { + admin: false, /** * The name of the command * @@ -140,14 +155,16 @@ export class Command * @param player This will return the player that uses this command * @returns If this player has permission to use this command */ - requires: (player: Player) => is(player.id, 'admin'), + requires: ((player: Player) => is(player.id, 'admin')) as ((player: Player) => boolean) & { + onFail?: PlayerCallback + }, /** * Minimal role required to run this command. * * This is an alias to `requires: (p) => is(player, role)` */ - role: 'admin' as Role, + role: 'admin' as Role | undefined, /** * Other names that can call this command * @@ -187,6 +204,10 @@ export class Command this.stack = stringifyError.stack.get(2) if (!parent && !__VITEST__) Command.checkIsUnique(name) + if (Command.loaded) { + Command.logger.warn('Commands are already loaded, tried registering ', name, new Error().stack) + } + this.sys.name = name if (type) this.sys.type = type @@ -237,7 +258,7 @@ export class Command * @example * "admin" */ - setPermissions(arg?: Role | ((player: Player) => boolean) | 'everybody'): this + setPermissions(arg?: Role | (((player: Player) => boolean) & { onFail?: PlayerCallback }) | 'everybody'): this /** * Sets minimal role that allows player to execute the command. Default allowed role is admin. @@ -270,11 +291,16 @@ export class Command if (arg === 'everybody') { // Everybody this.sys.requires = () => true + this.sys.role = 'member' + this.sys.admin = false } else if (typeof arg === 'function') { // Custom permissions function this.sys.requires = arg + this.sys.admin = false + delete this.sys.role } else { // Role + this.sys.admin = arg === 'creator' || arg === 'techAdmin' this.sys.role = arg this.sys.requires = p => is(p.id, arg) } @@ -420,9 +446,186 @@ declare global { globalThis.Command = Command -world.beforeEvents.chatSend.subscribe(event => { - event.cancel = true - system.delay(() => { - Command.chatListener(event) - }) +// world.beforeEvents.chatSend.subscribe(event => { +// event.cancel = true +// system.delay(() => { +// Command.chatListener(event) +// }) +// }) + +system.beforeEvents.startup.subscribe(load => { + const namespace = 'folkbe' + for (const command of Command.commands) { + if (command.sys.depth !== 0) continue + // Only simple commands are supported rn + + try { + const mandatoryParameters: CustomCommandParameter[] = [] + const optionalParameters: CustomCommandParameter[] = [] + + let callback = command.sys.callback + const locationIndexes: number[] = [] + let i = 0 + + let nowOptional = false + + function collectParams(command: Command) { + const child = command.sys.children[0] + if (!child || child instanceof LiteralArgumentType) return + + // Location is split + if (child.sys.type.name.endsWith('*')) return collectParams(child) + + child.sys.type.register(load.customCommandRegistry, namespace) + const param: CustomCommandParameter = { name: child.sys.type.name, type: child.sys.type.ctype } + if (child.sys.type.optional) { + nowOptional = true + optionalParameters.push(param) + } else { + if (nowOptional) throw new Error('Mandatory param cannot come after optional in command ' + command.sys.name) + mandatoryParameters.push(param) + } + + if (child.sys.type instanceof LocationArgumentType) locationIndexes.push(i) + i++ + + if (child.sys.callback) callback = child.sys.callback + + collectParams(child) + } + collectParams(command) + + load.customCommandRegistry.registerCommand( + { + name: namespace + ':' + command.sys.name, + permissionLevel: command.sys.admin ? CommandPermissionLevel.GameDirectors : CommandPermissionLevel.Any, + description: command.sys.description.to(defaultLang), + mandatoryParameters, + optionalParameters, + }, + (ctx, ...args) => { + if (!callback) { + return { + status: CustomCommandStatus.Failure, + message: 'Команда не готова', + } + } + + const isServer = !(ctx.sourceEntity instanceof Player) + const output: CommandOutputBuffer = { output: '', isSync: isServer } + const player: Player = + ctx.sourceEntity instanceof Player ? ctx.sourceEntity : createPlayerProxy(ctx, command, output) + + const allowed = command.sys.requires(player) + if (!allowed) { + if (command.sys.requires.onFail) { + command.sys.requires.onFail(player) + } else { + if (command.sys.role) { + player.fail( + i18n.error`Команда доступна только начиная с роли ${ROLES[command.sys.role]}. Ваша роль: ${ROLES[getRole(player.id)]}`, + ) + } else { + player.fail(i18n.error`Команда недоступна`) + } + } + + return { status: CustomCommandStatus.Failure, message: output.output || undefined } + } + + for (const i of locationIndexes) { + const arg: unknown = args[i] + if (Vec.isVec(arg)) args[i] = Vec.floor(arg) + } + + if (isServer) { + execCmd(player, command, callback, args, output) + } else { + system.delay(() => { + if (!callback) throw new Error('no callback') + execCmd(player, command, callback, args, output) + }) + } + return { status: CustomCommandStatus.Success, message: output.output || undefined } + }, + ) + } catch (e) { + Command.logger.error('Failed to load command', command.sys.name, e) + } + } + + Command.loaded = true }) + +interface CommandOutputBuffer { + output: string + isSync: boolean +} + +function createPlayerProxy( + ctx: CustomCommandOrigin, + command: Command<(ctx: CommandContext) => void>, + output: CommandOutputBuffer, +): Player { + const sendMessage = (...messages: unknown[]) => { + const translated = messages.map(e => (e instanceof Message ? e.to(consoleLang) : e)) + const message = util.format([...translated]) + if (output.isSync) { + output.output += `${message}\n` + } else { + Command.logger.info('server', 'async', `/${command.sys.name}`, message) + } + } + return new Proxy( + { + dimension: ctx.sourceEntity?.dimension ?? ctx.sourceBlock?.dimension ?? world.overworld, + + commandPermissionLevel: CommandPermissionLevel.Owner, + playerPermissionLevel: PlayerPermissionLevel.Custom, + isValid: true, + fail: sendMessage, + success: sendMessage, + info: sendMessage, + warn: sendMessage, + sendMessage: sendMessage, + tell: sendMessage, + playSound: () => void 0, + id: 'server', + name: 'server', + } as Partial, + { + get(target, p, receiver) { + if (!(p in target)) { + throw new Error( + `Command is not supported to be run by server, tried to use ${String(p)}, only ${Object.keys(target).join(' | ')} are supported`, + ) + } + + return Reflect.get(target, p, receiver) as unknown + }, + }, + ) as unknown as Player +} + +function execCmd( + player: Player, + command: Command<(ctx: CommandContext) => void>, + callback: (ctx: CommandContext, ...args: unknown[]) => void | Promise, + args: unknown[], + output: CommandOutputBuffer, +) { + try { + Command.logger.player(player).info(command.sys.name, ...args) + const result = callback(new CommandContext({ sender: player, message: '', targets: [] }, [], command, ''), ...args) + if (result && result instanceof Promise) { + output.isSync = false + result.catch((e: unknown) => { + Command.logger.player(player).error(command.sys.name, '[async]', ...args, e) + player.fail('Ошибка в асинхронной команде ' + String(e)) + }) + } + } catch (e) { + Command.logger.player(player).error(command.sys.name, ...args, e) + player.fail('Ошибка в команде ' + String(e)) + } +} diff --git a/src/lib/command/utils.ts b/src/lib/command/utils.ts index e4f93ced..6753707a 100644 --- a/src/lib/command/utils.ts +++ b/src/lib/command/utils.ts @@ -1,7 +1,6 @@ import { ChatSendAfterEvent, Player } from '@minecraft/server' import { Sounds } from 'lib/assets/custom-sounds' import { developersAreWarned } from 'lib/assets/text' -import { intlListFormat } from 'lib/i18n/intl' import { i18n, noI18n } from 'lib/i18n/text' import { ROLES } from 'lib/roles' import { inaccurateSearch } from '../utils/search' @@ -80,12 +79,10 @@ export function suggest(player: Pick, input: string, op if (!search[0] || search[0][1] < settings.minMatchTriggerValue) return player.tell( - i18n.error`Вы имели ввиду ${intlListFormat( - i18n.error.style, - player.lang, - 'or', - search.slice(0, settings.maxSuggestionsCount).map(e => noI18n.nocolor`${e[0]} (${~~(e[1] * 100)}%%)`), - )}?`, + i18n.error`Вы имели ввиду ${search + .slice(0, settings.maxSuggestionsCount) + .map(e => noI18n.nocolor`${e[0]} (${~~(e[1] * 100)}%%)`) + .join(', ')}?`, ) } diff --git a/src/lib/cooldown.ts b/src/lib/cooldown.ts index fa8f30ca..657cb477 100644 --- a/src/lib/cooldown.ts +++ b/src/lib/cooldown.ts @@ -9,6 +9,14 @@ export class Cooldown { static defaultDb = table>('cooldowns', () => ({})) + static getDb(cd: Cooldown) { + return cd.db + } + + static getTime(cd: Cooldown) { + return cd.time + } + /** * Create class for manage player cooldowns * @@ -50,8 +58,10 @@ export class Cooldown { const id = player instanceof Player ? player.id : player const elapsed = this.getElapsed(id) if (elapsed) { - if (this.tell && player instanceof Player) - player.fail(i18n.error`Не так быстро! Попробуй через ${i18n.time(this.time - elapsed)}`) + if (this.tell && player instanceof Player) { + const after = this.time - elapsed + player.fail(i18n.error`Не так быстро! Попробуй через ${after > 1000 ? i18n.hhmmss(after) : i18n`${after}мсек`}`) + } return false } else { diff --git a/src/lib/cooldownreset.ts b/src/lib/cooldownreset.ts new file mode 100644 index 00000000..07f4f7ed --- /dev/null +++ b/src/lib/cooldownreset.ts @@ -0,0 +1,61 @@ +import { Cooldown } from 'lib/cooldown' +import { form } from 'lib/form/new' +import { getFullname } from 'lib/get-fullname' +import { i18n } from 'lib/i18n/text' + +interface CooldownController { + list(): Record + reset(id: string): void +} + +// After compilation the initialization of this variable is placed lower then the hoisted call of the function below for some reason +let cds: { name: string; cd: CooldownController }[] | undefined + +/** Use cooldown controller when the cooldown IS NOT AN INSTANCE OF COOLDOWN, e.g. its some custom data structure */ +export function registerResettableCooldown(name: string, cd: CooldownController | Cooldown) { + cds ??= [] + + if (cd instanceof Cooldown) { + cds.push({ + name, + cd: { + list() { + return Object.map(Cooldown.getDb(cd) as Record, (k, v) => + Cooldown.getTime(cd) + v < Date.now() ? false : [k, Cooldown.getTime(cd) + v], + ) + }, + reset(id) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete Cooldown.getDb(cd)[id] + }, + }, + }) + } else { + cds.push({ name, cd }) + } +} + +const cdsform = form(f => { + f.title('Кулдауны') + for (const cd of cds ?? []) { + f.button(cdform(cd)) + } +}) + +const cdform = form.params<{ cd: CooldownController; name: string }>((f, { params, self }) => { + const list = params.cd.list() + f.title(i18n.join`${params.name}`.size(Object.keys(list).length)) + for (const [id, time] of Object.entries(list)) { + const elapsed = time - Date.now() + if (elapsed < 0) continue + f.button(i18n`${getFullname(id)}\n${i18n.hhmmss(elapsed)}`, () => { + params.cd.reset(id) + self() + }) + } +}) + +new Command('cooldownreset') + .setPermissions('techAdmin') + .setDescription('Сбрасывает разные кулдауны') + .executes(cdsform.command) diff --git a/src/lib/cutscene/edit.ts b/src/lib/cutscene/edit.ts index 6fa7ca7a..28496c37 100644 --- a/src/lib/cutscene/edit.ts +++ b/src/lib/cutscene/edit.ts @@ -1,6 +1,6 @@ /* i18n-ignore */ -import { Container, ItemStack, MolangVariableMap, Player } from '@minecraft/server' +import { Container, ItemStack, MolangVariableMap, Player, world } from '@minecraft/server' import { Vec } from 'lib/vector' import { MinecraftItemTypes } from '@minecraft/vanilla-data' @@ -13,215 +13,223 @@ import { isLocationError } from 'lib/utils/game' import { Cutscene } from './cutscene' import { cutscene as cusceneCommand } from './menu' -/** List of items that controls the editing process */ -const controls: Record< - string, - [slot: number, item: ItemStack, onUse: (player: Player, cutscene: Cutscene, temp: Temporary) => void] -> = { - create: [ - 3, - new ItemStack(Items.WeTool).setInfo('§r§6> §fСоздать точку', 'используй предмет, чтобы создать точку катсцены.'), - (player, cutscene) => { - if (!cutscene.sections[0]) cutscene.withNewSection(cutscene.sections, {}) - - cutscene.withNewPoint(player, { sections: cutscene.sections, warn: true }) - player.info( - `Точка добавлена. Точек в секции: §f${cutscene.sections[cutscene.sections.length - 1]?.points.length}`, - ) - }, - ], - createSection: [ - 4, - new ItemStack(MinecraftItemTypes.ChainCommandBlock).setInfo( - '§r§3> §fСоздать секцию', - 'используй предмет, чтобы создать секцию катсцены (множество точек).', - ), - (player, cutscene) => { - cutscene.withNewSection(cutscene.sections, {}) - player.info(`Секция добавлена. Секций всего: §f${cutscene.sections.length}`) - }, - ], - cancel: [ - 7, - new ItemStack(MinecraftItemTypes.Barrier).setInfo( - '§r§c> §fОтмена', - 'используйте предмет, чтобы отменить редактироание катсцены и вернуть все в исходное состояние.', - ), - (player, cutscene, temp) => { - // Restore bakcup - const backup = EditingCutscene.get(player.id) - if (backup) cutscene.sections = backup.cutsceneSectionsBackup - - temp.cleanup() - player.success('Успешно отменено!') - }, - ], - saveAndExit: [ - 8, - new ItemStack(MinecraftItemTypes.HoneyBottle).setInfo( - '§r§6> §fСохранить и выйти', - 'используй предмет, чтобы выйти из меню катсцены.', - ), - (player, cutscene, temp) => { - temp.cleanup() - player.success(i18n`Сохранено. Проверить: ${cusceneCommand}§f play ${cutscene.id}`) - }, - ], +export const cutsceneEdit = { + /** + * Checks if the cutscene location is valid, then teleports the player to that location and backs up player inventory + * and cutscene data. + * + * @param player - The player who is editing the cutscene. + * @param cutscene - The cutscene cene to be edited + */ + editCatcutscene: (player: Player, cutscene: Cutscene): void => { + throw new Error('Not loaded!') + }, } -/** - * Checks if the cutscene location is valid, then teleports the player to that location and backs up player inventory - * and cutscene data. - * - * @param player - The player who is editing the cutscene. - * @param cutscene - The cutscene cene to be edited - */ -export function editCatcutscene(player: Player, cutscene: Cutscene) { - backupPlayerInventoryAndCutscene(player, cutscene) - - new Temporary(({ world, system, temporary }) => { - system.runInterval( - () => { - for (const section of cutscene.sections) { - if (!section) continue - - for (const point of section.points) particle(point, blueParticle) - } - }, - 'cutscene section edges particles', - 30, - ) - - const controller = { cancel: false } - util.catch(async function visualize() { - while (!temporary.cleaned) { - await system.sleep(10) - - const sections = cutscene.withNewPoint(player) - if (!sections) return - - await cutscene.forEachPoint( - point => { - if (!Vec.isValid(point)) return - particle(point, whiteParticle) - }, - { controller, sections, intervalTime: 1 }, +world.afterEvents.worldLoad.subscribe(() => { + /** List of items that controls the editing process */ + const controls: Record< + string, + [slot: number, item: ItemStack, onUse: (player: Player, cutscene: Cutscene, temp: Temporary) => void] + > = { + create: [ + 3, + new ItemStack(Items.WeTool).setInfo('§r§6> §fСоздать точку', 'используй предмет, чтобы создать точку катсцены.'), + (player, cutscene) => { + if (!cutscene.sections[0]) cutscene.withNewSection(cutscene.sections, {}) + + cutscene.withNewPoint(player, { sections: cutscene.sections, warn: true }) + player.info( + `Точка добавлена. Точек в секции: §f${cutscene.sections[cutscene.sections.length - 1]?.points.length}`, ) - } - }) + }, + ], + createSection: [ + 4, + new ItemStack(MinecraftItemTypes.ChainCommandBlock).setInfo( + '§r§3> §fСоздать секцию', + 'используй предмет, чтобы создать секцию катсцены (множество точек).', + ), + (player, cutscene) => { + cutscene.withNewSection(cutscene.sections, {}) + player.info(`Секция добавлена. Секций всего: §f${cutscene.sections.length}`) + }, + ], + cancel: [ + 7, + new ItemStack(MinecraftItemTypes.Barrier).setInfo( + '§r§c> §fОтмена', + 'используйте предмет, чтобы отменить редактироание катсцены и вернуть все в исходное состояние.', + ), + (player, cutscene, temp) => { + // Restore bakcup + const backup = EditingCutscene.get(player.id) + if (backup) cutscene.sections = backup.cutsceneSectionsBackup + + temp.cleanup() + player.success('Успешно отменено!') + }, + ], + saveAndExit: [ + 8, + new ItemStack(MinecraftItemTypes.HoneyBottle).setInfo( + '§r§6> §fСохранить и выйти', + 'используй предмет, чтобы выйти из меню катсцены.', + ), + (player, cutscene, temp) => { + temp.cleanup() + player.success(i18n`Сохранено. Проверить: ${cusceneCommand}§f play ${cutscene.id}`) + }, + ], + } + + cutsceneEdit.editCatcutscene = (player, cutscene) => { + backupPlayerInventoryAndCutscene(player, cutscene) - const cooldown = new Cooldown(1000) + new Temporary(({ world, system, temporary }) => { + system.runInterval( + () => { + for (const section of cutscene.sections) { + if (!section) continue - world.beforeEvents.itemUse.subscribe(event => { - if (event.source.id !== player.id) return - if (!cooldown.isExpired(player)) return + for (const point of section.points) particle(point, blueParticle) + } + }, + 'cutscene section edges particles', + 30, + ) - for (const [, control, onUse] of Object.values(controls)) { - if (control.is(event.itemStack)) { - event.cancel = true - system.delay(() => onUse(player, cutscene, temporary)) + const controller = { cancel: false } + util.catch(async function visualize() { + while (!temporary.cleaned) { + await system.sleep(10) + + const sections = cutscene.withNewPoint(player) + if (!sections) return + + await cutscene.forEachPoint( + point => { + if (!Vec.isValid(point)) return + particle(point, whiteParticle) + }, + { controller, sections, intervalTime: 1 }, + ) } - } - }) + }) - world.beforeEvents.playerLeave.subscribe(event => { - if (event.player.id === player.id) system.delay(() => temporary.cleanup()) - }) + const cooldown = new Cooldown(1000) - function particle(point: Vector3 | undefined, vars: MolangVariableMap) { - if (!point) return - try { - player.dimension.spawnParticle('minecraft:wax_particle', point, vars) - } catch (e) { - if (isLocationError(e)) return - if (e instanceof TypeError && e.message.includes('Native optional type conversion')) return + world.beforeEvents.itemUse.subscribe(event => { + if (event.source.id !== player.id) return + if (!cooldown.isExpired(player)) return - console.error(e) - } - } + for (const [, control, onUse] of Object.values(controls)) { + if (control.is(event.itemStack)) { + event.cancel = true + system.delay(() => onUse(player, cutscene, temporary)) + } + } + }) - return { - cleanup() { - const editingPlayer = EditingCutscene.get(player.id) - if (!editingPlayer) return + world.beforeEvents.playerLeave.subscribe(event => { + if (event.player.id === player.id) system.delay(() => temporary.cleanup()) + }) - const { hotbarSlots, position } = editingPlayer + function particle(point: Vector3 | undefined, vars: MolangVariableMap) { + if (!point) return + try { + player.dimension.spawnParticle('minecraft:wax_particle', point, vars) + } catch (e) { + if (isLocationError(e)) return + if (e instanceof TypeError && e.message.includes('Native optional type conversion')) return - if (player.isValid) { - forEachHotbarSlot(player, (i, container) => container.setItem(i, hotbarSlots[i])) - player.teleport(position) + console.error(e) } + } - EditingCutscene.delete(player.id) - cutscene.save() - }, - } - }) -} + return { + cleanup() { + const editingPlayer = EditingCutscene.get(player.id) + if (!editingPlayer) return -const blueParticle = new MolangVariableMap() -blueParticle.setColorRGBA('color', { - red: 0.5, - green: 0.5, - blue: 1, - alpha: 0, -}) + const { hotbarSlots, position } = editingPlayer -const whiteParticle = new MolangVariableMap() -whiteParticle.setColorRGBA('color', { - red: 1, - green: 1, - blue: 1, - alpha: 0, -}) + if (player.isValid) { + forEachHotbarSlot(player, (i, container) => container.setItem(i, hotbarSlots[i])) + player.teleport(position) + } -interface EditingCutscenePlayer { - hotbarSlots: (ItemStack | undefined)[] - position: Vector3 - cutsceneSectionsBackup: Cutscene['sections'] -} + EditingCutscene.delete(player.id) + cutscene.save() + }, + } + }) + } -/** Map of player id to player editing cutscene */ -const EditingCutscene = new Map() + const blueParticle = new MolangVariableMap() + blueParticle.setColorRGBA('color', { + red: 0.5, + green: 0.5, + blue: 1, + alpha: 0, + }) -function backupPlayerInventoryAndCutscene(player: Player, cutscene: Cutscene) { - EditingCutscene.set(player.id, { - hotbarSlots: backupPlayerInventory(player), - position: Vec.floor(player.location), - cutsceneSectionsBackup: cutscene.sections.slice(), + const whiteParticle = new MolangVariableMap() + whiteParticle.setColorRGBA('color', { + red: 1, + green: 1, + blue: 1, + alpha: 0, }) - cutscene.sections = [] -} + interface EditingCutscenePlayer { + hotbarSlots: (ItemStack | undefined)[] + position: Vector3 + cutsceneSectionsBackup: Cutscene['sections'] + } -function backupPlayerInventory(player: Player) { - const hotbarSlots: EditingCutscenePlayer['hotbarSlots'] = [] - const container = forEachHotbarSlot(player, (i, container) => { - hotbarSlots[i] = container.getItem(i) - container.setItem(i, undefined) - }) + /** Map of player id to player editing cutscene */ + const EditingCutscene = new Map() - for (const [slot, item] of Object.values(controls)) { - container.setItem(slot, item) + function backupPlayerInventoryAndCutscene(player: Player, cutscene: Cutscene) { + EditingCutscene.set(player.id, { + hotbarSlots: backupPlayerInventory(player), + position: Vec.floor(player.location), + cutsceneSectionsBackup: cutscene.sections.slice(), + }) + + cutscene.sections = [] } - return hotbarSlots -} + function backupPlayerInventory(player: Player) { + const hotbarSlots: EditingCutscenePlayer['hotbarSlots'] = [] + const container = forEachHotbarSlot(player, (i, container) => { + hotbarSlots[i] = container.getItem(i) + container.setItem(i, undefined) + }) + + for (const [slot, item] of Object.values(controls)) { + container.setItem(slot, item) + } -/** - * Iterates over the player's hotbar slots and performs a specified action on each slot. - * - * @param player - Target player to get hotbar from - * @param callback - Callback function that will be called for each hotbar slot. It takes two arguments: the index of - * the current slot (from 0 to 8) and the container` object belonging to the player. - */ -function forEachHotbarSlot(player: Player, callback: (i: number, container: Container) => void) { - const { container } = player - if (!container) throw new ReferenceError('Player has no container!') - - for (let i = 0; i < 9; i++) { - callback(i, container) + return hotbarSlots } - return container -} + /** + * Iterates over the player's hotbar slots and performs a specified action on each slot. + * + * @param player - Target player to get hotbar from + * @param callback - Callback function that will be called for each hotbar slot. It takes two arguments: the index of + * the current slot (from 0 to 8) and the container` object belonging to the player. + */ + function forEachHotbarSlot(player: Player, callback: (i: number, container: Container) => void) { + const { container } = player + if (!container) throw new ReferenceError('Player has no container!') + + for (let i = 0; i < 9; i++) { + callback(i, container) + } + + return container + } +}) diff --git a/src/lib/cutscene/menu.ts b/src/lib/cutscene/menu.ts index d74b6b17..fbfa4fc3 100644 --- a/src/lib/cutscene/menu.ts +++ b/src/lib/cutscene/menu.ts @@ -1,45 +1,45 @@ -import { Player } from '@minecraft/server' +import { Player, world } from '@minecraft/server' +import { PersistentSet } from 'lib/database/persistent-set' import { ActionForm } from 'lib/form/action' import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' import { form } from 'lib/form/new' import { i18n, noI18n } from 'lib/i18n/text' -import { is } from 'lib/roles' import { Cutscene } from './cutscene' -import { editCatcutscene } from './edit' +import { cutsceneEdit } from './edit' new Cutscene('test', 'Test') export const cutscene = new Command('cutscene') .setDescription(i18n`Катсцена`) - .setPermissions('member') + .setPermissions('helper') .executes(ctx => { - if (is(ctx.player.id, 'curator')) selectCutsceneMenu(ctx.player) - else Command.getHelpForCommand(cutscene, ctx) + selectCutsceneMenu(ctx.player) }) -cutscene - .overload('exit') - .setDescription(i18n`Выход из катсцены`) - .executes(ctx => { - const cutscene = Cutscene.getCurrent(ctx.player) - if (!cutscene) return ctx.error(i18n.error`Вы не находитесь в катсцене!`) - - cutscene.exit(ctx.player) - }) - -cutscene - .overload('play') - .setPermissions('techAdmin') - .string('name', false) - .executes((ctx, name) => { - const cutscene = Cutscene.all.get(name) - if (!cutscene) return ctx.error([...Cutscene.all.keys()].join('\n')) +const cutscenes = new PersistentSet('cutscenesIds') - cutscene.play(ctx.player) - }) +world.afterEvents.worldLoad.subscribe(() => { + for (const c of cutscenes) new Cutscene(c, c) +}) function selectCutsceneMenu(player: Player) { new ArrayForm(noI18n`Катсцены`, [...Cutscene.all.values()]) + .addCustomButtonBeforeArray(f => { + const cutscene = Cutscene.getCurrent(player) + if (cutscene) { + f.button('Выйти из текущей сцены', () => cutscene.exit(player)) + } + + f.button('Добавить', () => { + new ModalForm('Добавить катсцену').addTextField('Название', '').show(player, (ctx, id) => { + if (cutscenes.has(id)) ctx.error('Имя занято') + cutscenes.add(id) + const cutscene = new Cutscene(id, id) + manageCutsceneMenu({ cutscene }).show(player) + }) + }) + }) .description(noI18n`Список доступных для редактирования катсцен:`) .button(cutscene => [cutscene.id, manageCutsceneMenu({ cutscene }).show]) .show(player) @@ -47,10 +47,19 @@ function selectCutsceneMenu(player: Player) { const manageCutsceneMenu = form.params<{ cutscene: Cutscene }>((f, { player, params: { cutscene } }) => { const dots = cutscene.sections.reduce((count, section) => (section ? count + section.points.length : count), 0) + const created = cutscenes.has(cutscene.id) f.title(cutscene.id) .body(noI18n`Секций: ${cutscene.sections.length}\nТочек: ${dots}`) .button(ActionForm.backText, () => selectCutsceneMenu(player)) - .button(noI18n`Редактировать`, () => editCatcutscene(player, cutscene)) + .button(noI18n`Редактировать`, () => cutsceneEdit.editCatcutscene(player, cutscene)) .button(noI18n`Воспроизвести`, () => cutscene.play(player)) + + if (created) { + f.ask(noI18n.error`Удалить`, noI18n.error`Удалить`, () => { + Cutscene.all.delete(cutscene.id) + cutscenes.delete(cutscene.id) + player.success() + }) + } }) diff --git a/src/lib/database/inventory.ts b/src/lib/database/inventory.ts index 02089890..2db643fe 100644 --- a/src/lib/database/inventory.ts +++ b/src/lib/database/inventory.ts @@ -382,6 +382,11 @@ export class InventoryStore { if (!keepInventory) entity.container?.clearAll() } + set(key: string, inventory: Inventory) { + this.inventories.set(key, inventory) + this.requestSave() + } + /** * Checks if key was saved into this store * diff --git a/src/lib/database/item-stack.ts b/src/lib/database/item-stack.ts index 27d5326a..9fc41565 100644 --- a/src/lib/database/item-stack.ts +++ b/src/lib/database/item-stack.ts @@ -97,8 +97,6 @@ export class ItemLoreSchema { static loreSchemaId = 'lsid' - public readonly aha!: T - constructor( private properties: Schema, private prepareItem: (lang: Language, itemStack: Item, storage: ParsedSchema) => void, diff --git a/src/lib/database/migrations.ts b/src/lib/database/migrations.ts index 4ec53e26..296c0b6e 100644 --- a/src/lib/database/migrations.ts +++ b/src/lib/database/migrations.ts @@ -1,14 +1,16 @@ -import { system } from '@minecraft/server' +import { system, world } from '@minecraft/server' import { table } from './abstract' const database = table('databaseMigrations') export function migration(name: string, migrateFN: VoidFunction) { - if (!database.get(name)) { + world.afterEvents.worldLoad.subscribe(() => { + if (database.has(name)) return + system.delay(() => { if (database.get(name)) return migrateFN() database.set(name, true) }) - } + }) } diff --git a/src/lib/database/persistent-set.ts b/src/lib/database/persistent-set.ts index 10e0c1f3..f155d6c9 100644 --- a/src/lib/database/persistent-set.ts +++ b/src/lib/database/persistent-set.ts @@ -1,3 +1,4 @@ +import { world } from '@minecraft/server' import { LongDynamicProperty } from './properties' export class LimitedSet extends Set { @@ -17,7 +18,9 @@ export class PersistentSet extends LimitedSet { protected limit = 1_000, ) { super() - this.load() + world.afterEvents.worldLoad.subscribe(() => { + this.load() + }) } private load() { diff --git a/src/lib/database/player.ts b/src/lib/database/player.ts index fff387df..65a26d46 100644 --- a/src/lib/database/player.ts +++ b/src/lib/database/player.ts @@ -48,7 +48,11 @@ declare module '@minecraft/server' { } expand(Player, { - database: table('player', () => ({ role: DEFAULT_ROLE, inv: 'spawn', survival: {} })), + database: table('player', () => ({ + role: DEFAULT_ROLE, + inv: 'spawn', + survival: {}, + })), name(id) { if (!id) return void 0 diff --git a/src/lib/database/properties.ts b/src/lib/database/properties.ts index fc492dbb..b1480b59 100644 --- a/src/lib/database/properties.ts +++ b/src/lib/database/properties.ts @@ -1,6 +1,6 @@ import { world } from '@minecraft/server' import { ProxyDatabase } from 'lib/database/proxy' -import { i18n, noI18n } from 'lib/i18n/text' +import { noI18n } from 'lib/i18n/text' import { DatabaseDefaultValue, DatabaseError, UnknownTable, configureDatabase } from './abstract' import { DatabaseUtils } from './utils' @@ -13,7 +13,10 @@ class DynamicPropertyDB extends Pr ) { super(id, defaultValue) if (id in DynamicPropertyDB.tables) throw new DatabaseError(`Table ${this.id} already initialized!`) - this.init() + + world.afterEvents.worldLoad.subscribe(() => { + this.init() + }) DynamicPropertyDB.tables[id] = this as UnknownTable } @@ -56,7 +59,22 @@ export class LongDynamicProperty { world.setDynamicProperty(propertyId, strings.length) for (const [i, string] of strings.entries()) { - world.setDynamicProperty(`${propertyId}${separator}${i}`, string) + try { + world.setDynamicProperty(`${propertyId}${separator}${i}`, string) + } catch (e) { + console.error( + 'DATABASE SAVE FAIL', + propertyId, + 'index', + i, + 'of', + strings.length, + 'SIZE', + string.length, + 'error:', + e, + ) + } } } diff --git a/src/lib/database/utils.ts b/src/lib/database/utils.ts index a7fc4f6e..8541216b 100644 --- a/src/lib/database/utils.ts +++ b/src/lib/database/utils.ts @@ -23,7 +23,7 @@ export class DatabaseUtils { static chunkRegexp = /.{1,50}/g - static propertyChunkRegexp = /.{1,32767}/g + static propertyChunkRegexp = /.{1,20000}/g private static allEntities: TableEntity[] @@ -54,31 +54,30 @@ export class DatabaseUtils { .filter(e => e.tableName !== 'NOTDB') } - private static readonly tablesDimension = world.overworld - private static tables(): TableEntity[] { if (typeof this.allEntities !== 'undefined') return this.allEntities this.allEntities = this.getEntities() if (this.allEntities.length < 1) { console.warn(noI18n`§6Не удалось найти базы данных. Попытка загрузить бэкап...`) - - world.overworld - .getEntities({ - location: DatabaseUtils.entityLocation, - type: DatabaseUtils.entityTypeId, - maxDistance: 2, - }) - - .forEach(e => e.remove()) - - world.structureManager.place(this.backupName, this.tablesDimension, this.entityLocation) - this.allEntities = this.getEntities() - - if (this.allEntities.length < 1) { - console.warn(noI18n`§cНе удалось загрузить базы данных из бэкапа.`) - return [] - } else console.warn(`Бэкап успешно загружен! Всего баз данных: ${this.allEntities.length}`) + if (world.structureManager.get(this.backupName)) { + world.overworld + .getEntities({ + location: DatabaseUtils.entityLocation, + type: DatabaseUtils.entityTypeId, + maxDistance: 2, + }) + + .forEach(e => e.remove()) + + world.structureManager.place(this.backupName, world.overworld, this.entityLocation) + this.allEntities = this.getEntities() + + if (this.allEntities.length < 1) { + console.warn(noI18n`§cНе удалось загрузить базы данных из бэкапа.`) + return [] + } else console.warn(`Бэкап успешно загружен! Всего баз данных: ${this.allEntities.length}`) + } else console.error('Backup does not exists, initializing empty tables...') } return this.allEntities @@ -121,7 +120,7 @@ export class DatabaseUtils { world.structureManager.delete(this.backupName) world.structureManager.createFromWorld( this.backupName, - this.tablesDimension, + world.overworld, this.entityLocation, this.entityLocation, { includeBlocks: false, includeEntities: true, saveMode: StructureSaveMode.World }, diff --git a/src/lib/extensions/core.ts b/src/lib/extensions/core.ts index 7d4a602b..84408e7e 100644 --- a/src/lib/extensions/core.ts +++ b/src/lib/extensions/core.ts @@ -16,30 +16,32 @@ export const Core = { } if (!__VITEST__) { - system.run(function waiter() { - const entities = world.overworld.getEntities() - if (entities.length < 1) { - // No entity found, re-run waiter - return system.run(waiter) - } - - try { - EventLoader.load(Core.afterEvents.worldLoad) - } catch (e) { - console.error(e) - } - }) + world.afterEvents.worldLoad.subscribe(() => { + system.run(function waiter() { + const entities = world.overworld.getEntities() + if (entities.length < 1) { + // No entity found, re-run waiter + return system.run(waiter) + } - system.afterEvents.scriptEventReceive.subscribe( - data => { - if (data.id === 'SERVER:SAY') { - world.say(decodeURI(data.message)) + try { + EventLoader.load(Core.afterEvents.worldLoad) + } catch (e) { + console.error(e) } - }, - { - namespaces: ['SERVER'], - }, - ) + }) + + system.afterEvents.scriptEventReceive.subscribe( + data => { + if (data.id === 'SERVER:SAY') { + world.say(decodeURI(data.message)) + } + }, + { + namespaces: ['SERVER'], + }, + ) + }) } else { EventLoader.load(Core.afterEvents.worldLoad) } diff --git a/src/lib/extensions/enviroment.ts b/src/lib/extensions/enviroment.ts index 67c5ee80..95c908df 100644 --- a/src/lib/extensions/enviroment.ts +++ b/src/lib/extensions/enviroment.ts @@ -205,8 +205,10 @@ function getTimezone(language?: Language) { switch (language) { case Language.ru_RU: return 3 - default: + case Language.bg_BG: return 0 + default: + return 3 } } @@ -217,7 +219,7 @@ Date.prototype.toYYYYMMDD = function (lang) { const year = date.getFullYear() const month = (date.getMonth() + 1).toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, '0') - return `${day}-${month}-${year}` + return `${day}.${month}.${year}` } Date.prototype.toHHMM = function (lang) { diff --git a/src/lib/extensions/on-screen-display.ts b/src/lib/extensions/on-screen-display.ts index 2021c2d3..bd613686 100644 --- a/src/lib/extensions/on-screen-display.ts +++ b/src/lib/extensions/on-screen-display.ts @@ -219,7 +219,7 @@ const actionbarLock = new WeakPlayerMap<{ priority: ActionbarPriority; expires: const defaultOptions = { fadeInDuration: 0, fadeOutDuration: 0, stayDuration: 0 } const defaultTitleOptions = { ...defaultOptions, stayDuration: -1 } -run() +world.afterEvents.worldLoad.subscribe(run) function run() { system.run(() => { diff --git a/src/lib/extensions/system.ts b/src/lib/extensions/system.ts index 238301ee..e61c8ba0 100644 --- a/src/lib/extensions/system.ts +++ b/src/lib/extensions/system.ts @@ -105,7 +105,11 @@ expand(System.prototype, { function jobInterval() { system.runJob( (function* job() { - for (const _ of callback()) yield + try { + for (const _ of callback()) yield + } catch (e) { + console.error('Error in job interval', e) + } if (stopped) return if (tickInterval === 0) system.delay(jobInterval) else system.runTimeout(jobInterval, 'jobInterval', tickInterval) diff --git a/src/lib/extensions/world.ts b/src/lib/extensions/world.ts index f66cc4aa..07702214 100644 --- a/src/lib/extensions/world.ts +++ b/src/lib/extensions/world.ts @@ -1,6 +1,5 @@ -import { World, world } from '@minecraft/server' +import { Dimension, World, world } from '@minecraft/server' import { MinecraftDimensionTypes } from '@minecraft/vanilla-data' -import { stringify } from '../util' import { expand } from './extend' declare module '@minecraft/server' { @@ -12,30 +11,41 @@ declare module '@minecraft/server' { * Logs given message once * * @param type Type of log - * @param messages Data to log using world.debug() + * @param messages Data to log */ logOnce(type: string, ...messages: unknown[]): void - /** Prints data using world.say() and parses any object to string using toStr method. */ - debug(...data: unknown[]): void overworld: Dimension end: Dimension nether: Dimension } } +world.afterEvents.worldLoad.subscribe(() => { + expand(World.prototype, { + overworld: world.getDimension(MinecraftDimensionTypes.Overworld), + nether: world.getDimension(MinecraftDimensionTypes.Nether), + end: world.getDimension(MinecraftDimensionTypes.TheEnd), + }) +}) + expand(World.prototype, { say: world.sendMessage.bind(world), - overworld: world.getDimension(MinecraftDimensionTypes.Overworld), - nether: world.getDimension(MinecraftDimensionTypes.Nether), - end: world.getDimension(MinecraftDimensionTypes.TheEnd), - debug(...data: unknown[]) { - this.say(data.map(stringify).join(' ')) + get overworld() { + // throw new Error('Dimensions are not available') + return undefined as unknown as Dimension + }, + get nether() { + // throw new Error('Dimensions are not available') + return undefined as unknown as Dimension + }, + get end() { + // throw new Error('Dimensions are not available') + return undefined as unknown as Dimension }, - logOnce(name, ...data: unknown[]) { if (logs.has(name)) return - world.debug(...data) + console.log(name, ...data) logs.add(name) }, }) diff --git a/src/lib/form/action.ts b/src/lib/form/action.ts index c34c4a50..d48e87b7 100644 --- a/src/lib/form/action.ts +++ b/src/lib/form/action.ts @@ -3,7 +3,6 @@ import { ActionFormData, ActionFormResponse } from '@minecraft/server-ui' import { Language } from 'lib/assets/lang' import { ask } from 'lib/form/message' import { i18n, noI18n } from 'lib/i18n/text' -import { util } from 'lib/util' import { NewFormCallback } from './new' import { BUTTON, showForm } from './utils' @@ -82,7 +81,7 @@ export class ActionForm { } /** - * Adds back button to the form. Alias to {@link ActionForm.button} + * Adds back button to the form. Alias to {@link button} * * @param backCallback - Callback function that will be called when back button is pressed. */ @@ -92,7 +91,7 @@ export class ActionForm { } /** - * Adds ask button to the form. Alias to {@link ActionForm.button} + * Adds ask button to the form. Alias to {@link button} * * Ask is alias to {@link ask} * @@ -133,12 +132,21 @@ export class ActionForm { if (response === false || !(response instanceof ActionFormResponse) || typeof response.selection === 'undefined') return false - const callback = this.buttons[response.selection]?.callback - if (typeof callback === 'function') { - util.catch(() => callback(player, () => this.show(player)) as void) + const button = this.buttons[response.selection] + if (__TEST__) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await button?.callback!(player, () => this.show(player)) return true + } else { + try { + if (typeof button?.callback !== 'function') throw new Error('Callback is undefined') + button.callback(player, () => this.show(player)) + return true + } catch (e) { + console.error('OLD FORM BUTTON ERROR', player.name, button?.text, button?.callback, e) + player.fail(noI18n.error`Old button error: ${button?.text}, erorr: ${e}. Сообщите администрации.`) + return false + } } - - return false } } diff --git a/src/lib/form/message.ts b/src/lib/form/message.ts index 55fb5d96..a524b58f 100644 --- a/src/lib/form/message.ts +++ b/src/lib/form/message.ts @@ -92,14 +92,14 @@ export class MessageForm { /** Shows MessageForm to the player */ export function ask( player: Player, - text: Text, + messageFormBody: Text, yesText: Text, yesAction?: VoidFunction, noText: Text = i18n`Отмена`, noAction?: VoidFunction, ) { return new Promise(resolve => { - new MessageForm(i18n`Вы уверены?`.to(player.lang), text.to(player.lang)) + new MessageForm(i18n`Вы уверены?`.to(player.lang), messageFormBody.to(player.lang)) .setButton1(yesText.to(player.lang), () => { yesAction?.() resolve(true) diff --git a/src/lib/form/modal.ts b/src/lib/form/modal.ts index 6e128ed3..6f6cb264 100644 --- a/src/lib/form/modal.ts +++ b/src/lib/form/modal.ts @@ -195,6 +195,10 @@ export class ModalForm v return this } + submitButton(text: string) { + this.form.submitButton(text) + } + /** * Shows this form to a player * diff --git a/src/lib/form/new.ts b/src/lib/form/new.ts index 71a992f1..b8f7836e 100644 --- a/src/lib/form/new.ts +++ b/src/lib/form/new.ts @@ -1,10 +1,10 @@ import { Player } from '@minecraft/server' import { ActionFormData, ActionFormResponse } from '@minecraft/server-ui' +import { defaultLang } from 'lib/assets/lang' import { ActionForm } from 'lib/form/action' import { ask } from 'lib/form/message' -import { FormCallback, showForm } from 'lib/form/utils' +import { showForm } from 'lib/form/utils' import { i18n, noI18n } from 'lib/i18n/text' -import { Quest } from 'lib/quest' import { doNothing } from 'lib/util' export type NewFormCallback = (player: Player, back?: NewFormCallback) => unknown @@ -43,6 +43,8 @@ class Form { private buttons: NewFormCallback[] = [] + private buttonText: string[] = [] + /** * Adds a button to this form * @@ -98,18 +100,12 @@ class Form { this.form.button(text.to(this.player.lang), icon ?? undefined) this.buttons.push(finalCallback) + this.buttonText.push(text.to(defaultLang)) return this } - quest(quest: Quest, textOverride?: Text, descriptionOverride?: Text) { - const rendered = quest.button.render(this.player, () => this.show(), descriptionOverride) - if (!rendered) return - - this.button(textOverride && rendered[0] === quest.name ? textOverride : rendered[0], rendered[1], rendered[2]) - } - /** - * Adds ask button to the form. Alias to {@link Form.button} + * Adds ask button to the form. Alias to {@link button} * * Ask is alias to {@link ask} * @@ -144,17 +140,17 @@ class Form { `Callback for ${response.selection} does not exists, only ${this.buttons.length} callbacks are available`, ) - if (typeof callback === 'function') { - if (__TEST__) { - // Call right here to throw error + if (__TEST__) { + await callback(this.player, this.show) + } else { + try { + if (typeof callback !== 'function') throw new Error('Callback is undefined') await callback(this.player, this.show) - } else { - try { - await callback(this.player, this.show) - } catch (e) { - new FormCallback(this.form, this.player, this.show).error(String(e)) - console.error('Form error', e) - } + } catch (e) { + console.error('FORM BUTTON ERROR', this.player.name, this.buttonText[response.selection], callback, e) + this.player.fail( + noI18n.error`Button error: ${this.buttonText[response.selection]}, erorr: ${e}. Сообщите администрации.`, + ) } } } @@ -187,8 +183,31 @@ export class ShowForm { title(player: Player) { const form = new Form(player) - this.create(form, { player, back: doNothing, params: this.params, self: doNothing }) - return form.currentTitle ?? 'No title' + const error = new Error('STOP FORM CREATION WE GOT TITLE') + let title: undefined | Text + + try { + this.create( + Object.setPrototypeOf( + { + title(f) { + title = f + throw error + }, + } satisfies Partial, + form, + ) as Form, + { + player, + back: doNothing, + params: this.params, + self: doNothing, + }, + ) + } catch (e) { + if (e !== error) throw e + } + return title ?? 'No title' } get command() { diff --git a/src/lib/form/select-player.ts b/src/lib/form/select-player.ts index ad617317..d9fe1051 100644 --- a/src/lib/form/select-player.ts +++ b/src/lib/form/select-player.ts @@ -1,6 +1,7 @@ import { Player, world } from '@minecraft/server' import { ArrayForm } from 'lib/form/array' import { BUTTON } from 'lib/form/utils' +import { getFullname } from 'lib/get-fullname' import { i18n } from 'lib/i18n/text' import { NewFormCallback } from './new' @@ -121,7 +122,7 @@ export function selectPlayer( return players }) .button(({ id, name, online }) => { - return [(online ? '§f' : '§8') + name, () => resolve({ id, name })] + return [getFullname(id, { nameColor: online ? '§f' : '§8' }), () => resolve({ id, name })] }) .back(back) .show(player) diff --git a/src/lib/form/utils.ts b/src/lib/form/utils.ts index 204b6324..b35c6c48 100644 --- a/src/lib/form/utils.ts +++ b/src/lib/form/utils.ts @@ -65,17 +65,6 @@ export async function showForm( if (response.cancelationReason === FormCancelationReason.UserClosed) return false if (response.cancelationReason === FormCancelationReason.UserBusy) { switch (i) { - case 1: - // First attempt failed, maybe chat closed... - player.closeChat() - continue - - case 2: - // Second attempt, tell player to manually close chat... - player.info(i18n`Закрой чат!`) - await system.sleep(10) - continue - default: await system.sleep(10) break diff --git a/src/lib/lib.d.ts b/src/lib/lib.d.ts index 3e1ea67d..a6320018 100644 --- a/src/lib/lib.d.ts +++ b/src/lib/lib.d.ts @@ -1,5 +1,5 @@ import * as mc from '@minecraft/server' -import '../../tools/defines' +import '../../tools/defines.d.ts' declare global { type VoidFunction = () => void diff --git a/src/lib/mail.ts b/src/lib/mail.ts index c1109caf..c26f26a0 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail.ts @@ -55,7 +55,7 @@ export class Mail { private static inform(playerId: string, title: Message) { const player = Player.getById(playerId) - if (player) player.info(i18n`${i18n.header`Почта`}: ${title}, просмотреть: .mail`) + if (player) player.info(i18n`${i18n.header`Почта`}: ${title}, просмотреть: /mail`) } /** @@ -158,6 +158,17 @@ export class Mail { letter.read = true } + static readAllAndClaimRewards(player: Player) { + for (const { index, letter } of this.getLetters(player.id)) { + try { + this.readMessage(player.id, index) + this.claimRewards(player, index) + } catch (e) { + console.error('Failed to read and claim:', player.name, index, letter, e) + } + } + } + /** * Deletes a message from a player's mailbox * diff --git a/src/lib/player-move.ts b/src/lib/player-move.ts index a4842023..330222a1 100644 --- a/src/lib/player-move.ts +++ b/src/lib/player-move.ts @@ -31,8 +31,10 @@ export function anyPlayerNearRegion(region: Region, radius: number) { return false } -// Do it sync on first run because some of the funcs above use it sync. It will start interval too -for (const _ of jobPlayerPosition()) void 0 +world.afterEvents.worldLoad.subscribe(() => { + // Do it sync on first run because some of the funcs above use it sync. It will start interval too + for (const _ of jobPlayerPosition()) void 0 +}) function jobInterval() { system.delay(() => system.runJob(jobPlayerPosition())) diff --git a/src/lib/region/areas/area.ts b/src/lib/region/areas/area.ts index d50182c3..edd48249 100644 --- a/src/lib/region/areas/area.ts +++ b/src/lib/region/areas/area.ts @@ -108,6 +108,7 @@ export abstract class Area { ) { const { edges, dimension } = this const isIn = (vector: Vector3) => this.isIn({ location: vector, dimensionType: this.dimensionType }) + const { max, min } = this.dimension.heightRange return new Promise((resolve, reject) => { system.runJob( @@ -115,6 +116,8 @@ export abstract class Area { try { let i = 0 for (const vector of Vec.forEach(...edges)) { + if (vector.y < min || vector.y > max) continue + callback(vector, isIn(vector), dimension) i++ if (i % yieldEach === 0) yield diff --git a/src/lib/region/big-structure.ts b/src/lib/region/big-structure.ts index 714c9a8f..fdf77d5a 100644 --- a/src/lib/region/big-structure.ts +++ b/src/lib/region/big-structure.ts @@ -26,11 +26,14 @@ export class BigRegionStructure extends RegionStructure { false, regionId, Array.isArray(saved) ? (saved as BigStructureSaved[]) : undefined, + false, // entities ) } get exists(): boolean { - return !!world.structureManager.get(`mystructure:${this.bigStructure.prefix}|0`) + return ( + !!world.structureManager.get(`mystructure:${this.bigStructure.prefix}|0`) && this.bigStructure.toJSON().length > 0 + ) } protected get bigStructurePos() { diff --git a/src/lib/region/command.ts b/src/lib/region/command.ts index df4d4eba..d910fb72 100644 --- a/src/lib/region/command.ts +++ b/src/lib/region/command.ts @@ -1,4 +1,4 @@ -import { GameMode, LocationInUnloadedChunkError, MolangVariableMap, Player, system, world } from '@minecraft/server' +import { GameMode, LocationInUnloadedChunkError, MolangVariableMap, Player, system } from '@minecraft/server' import 'lib/command' import { Cooldown } from 'lib/cooldown' import { table } from 'lib/database/abstract' @@ -13,18 +13,20 @@ import { regionForm } from './form' export const regionTypes: { name: string; region: typeof Region; creatable: boolean; displayName: boolean }[] = [] export function registerRegionType(name: string, region: typeof Region, creatable = true, displayName = !creatable) { + // Unique to each region type + if (region.regions === Region.regions) region.regions = [] + regionTypes.push({ name, region, creatable, displayName }) } -const command = new Command('region') +new Command('region') .setDescription(i18n`Управляет регионами`) - .setPermissions('techAdmin') + .setPermissions('admin') .setGroup('public') .executes(regionForm.command) -command - .overload('permdebug') - .setPermissions('everybody') +new Command('regionpermdebug') + .setPermissions('admin') .setGroup('test') .executes(ctx => { Region.permissionDebug = !Region.permissionDebug @@ -51,9 +53,8 @@ const tpdb = table<{ type: string; i: number; enabled: boolean }>('regionTpTest' i: 0, enabled: false, })) -command - .overload('tp') - .setPermissions('techAdmin') +new Command('regiontp') + .setPermissions('admin') .setDescription('Входит в режим телепортации по группе регионов. Полезно для поиска данжа') .setGroup('test') .executes(ctx => { @@ -125,9 +126,8 @@ function updateTpTitle(player: Player) { const db = table<{ enabled: boolean }>('regionBorders', () => ({ enabled: false })) -command - .overload('borders') - .executes(ctx => ctx.player.tell(noI18n`Borders enabled: ${db.get(ctx.player.id).enabled}`)) +new Command('regionborders') + .setPermissions('admin') .boolean('toggle', true) .executes((ctx, newValue = !db.get(ctx.player.id).enabled) => { ctx.player.tell(noI18n`${db.get(ctx.player.id).enabled} -> ${newValue}`) @@ -135,43 +135,44 @@ command db.get(ctx.player.id).enabled = newValue }) -const variables = new MolangVariableMap() -variables.setColorRGBA('color', { red: 0, green: 1, blue: 0, alpha: 0 }) - system.runInterval( () => { if (!db.values().some(e => e.enabled)) return - const players = world.getAllPlayers() - for (const region of Region.getAll()) { - if (!(region.area instanceof SphereArea)) continue + for (const [playerId, value] of db.entriesImmutable()) { + if (!value.enabled) continue + const player = Player.getById(playerId) + if (!player) continue + + const regions = Region.getNear(player, 30) + + const variables = new MolangVariableMap() + variables.setColorRGBA('color', { red: 0, green: 1, blue: 0, alpha: 0 }) - const playersNearRegion = players.filter(e => region.area.isNear(e, 30)) - if (!playersNearRegion.length) continue + for (const region of regions) { + if (!(region.area instanceof SphereArea)) continue - let skip = 0 - region.area.forEachVector((vector, isIn) => { - skip++ - if (skip % 2 === 0) return - if (!Region.getAll().includes(region)) return // deleted + let skip = 0 + region.area.forEachVector((vector, isIn) => { + skip++ + if (skip % 2 === 0) return + if (!Region.getAll().includes(region)) return // deleted - try { - const r = Vec.distance(region.area.center, vector) - if (isIn && r > region.area.radius - 1) { - for (const player of playersNearRegion) { - if (!player.isValid) continue - if (!db.get(player.id).enabled) continue + try { + const r = Vec.distance(region.area.center, vector) + if (isIn && r > region.area.radius - 1) { + if (!player.isValid) return player.spawnParticle('minecraft:wax_particle', vector, variables) } + } catch (e) { + if (e instanceof LocationInUnloadedChunkError) return + throw e } - } catch (e) { - if (e instanceof LocationInUnloadedChunkError) return - throw e - } - }, 100) + }, 200) + } } }, 'region borders', - 40, + 60, ) diff --git a/src/lib/region/config.ts b/src/lib/region/config.ts index a0599e6c..99b4ac55 100644 --- a/src/lib/region/config.ts +++ b/src/lib/region/config.ts @@ -1,4 +1,4 @@ -import { BlockTypes, Entity } from '@minecraft/server' +import { BlockTypes, Entity, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' @@ -14,16 +14,18 @@ export const SWITCHES: string[] = [] /** All gates in minecraft */ export const GATES: string[] = [] -const blocks = BlockTypes.getAll() +world.afterEvents.worldLoad.subscribe(() => { + const blocks = BlockTypes.getAll() -function fill(target: string[], filter: (params: { id: string }) => boolean) { - for (const value of blocks) if (filter(value)) target.push(value.id) -} + function fill(target: string[], filter: (params: { id: string }) => boolean) { + for (const value of blocks) if (filter(value)) target.push(value.id) + } -fill(DOORS, e => e.id.endsWith('door')) -fill(TRAPDOORS, e => e.id.endsWith('trapdoor')) -fill(SWITCHES, e => /button|lever$/.test(e.id)) -fill(GATES, e => e.id.includes('fence_gate')) + fill(DOORS, e => e.id.endsWith('door')) + fill(TRAPDOORS, e => e.id.endsWith('trapdoor')) + fill(SWITCHES, e => /button|lever$/.test(e.id)) + fill(GATES, e => e.id.includes('fence_gate')) +}) /** A list of all containers a item could be in */ export const BLOCK_CONTAINERS = [ diff --git a/src/lib/region/database.ts b/src/lib/region/database.ts index af7a977a..cde4ec30 100644 --- a/src/lib/region/database.ts +++ b/src/lib/region/database.ts @@ -7,7 +7,7 @@ import { Area } from './areas/area' import './areas/cut' import { SphereArea } from './areas/sphere' import { RegionEvents } from './events' -import { RegionIsSaveable, type Region, type RegionPermissions } from './kinds/region' +import { Region, RegionIsSaveable, type RegionPermissions } from './kinds/region' export type RLDB = JsonObject | undefined @@ -70,6 +70,9 @@ export function registerSaveableRegion(kind: string, region: typeof Region) { // @ts-expect-error Yes, we ARE breaking typescript region.prototype[RegionIsSaveable] = true + // Unique to each region type + if (region.regions === Region.regions) region.regions = [] + kinds.push(region) } @@ -96,10 +99,10 @@ export function restoreRegionFromJSON([key, regionImmutable]: [string, Immutable const area = Area.fromJson(region.a) if (!area) return - if (!area.isValid()) { - console.warn('[Region][Database] Area', area.toString(), 'is invalid') - return - } + // if (!area.isValid()) { + // console.warn('[Region][Database] Area', area.toString(), 'is invalid') + // return + // } return kind.create(area, region, key) } diff --git a/src/lib/region/form.ts b/src/lib/region/form.ts index 40c02bb8..3ee16aa6 100644 --- a/src/lib/region/form.ts +++ b/src/lib/region/form.ts @@ -1,7 +1,12 @@ import { Player, world } from '@minecraft/server' import { parseArguments, parseLocationArguments } from 'lib/command/utils' +import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' import { form, NewFormCallback, NewFormCreator } from 'lib/form/new' +import { BUTTON, FormCallback } from 'lib/form/utils' import { i18n, noI18n, textTable } from 'lib/i18n/text' +import { inspect } from 'lib/utils/inspect' +import { Vec } from 'lib/vector' import { Area } from './areas/area' import { ChunkCubeArea } from './areas/chunk-cube' import { CylinderArea } from './areas/cylinder' @@ -10,11 +15,6 @@ import { RectangleArea } from './areas/rectangle' import { SphereArea } from './areas/sphere' import { regionTypes } from './command' import { Region } from './kinds/region' -import { ArrayForm } from 'lib/form/array' -import { BUTTON, FormCallback } from 'lib/form/utils' -import { ModalForm } from 'lib/form/modal' -import { Vec } from 'lib/vector' -import { inspect } from 'lib/utils/inspect' export const regionForm = form((f, { player, self }) => { f.title(noI18n`Управление регионами`) @@ -77,20 +77,43 @@ function regionList( }) .show(player) } -const selectArea = form.params<{ onSelect: (area: Area) => NewFormCallback; title: Text }>( + +let getPlayerSelection = (player: Player): { min: Vector3; max: Vector3 } | undefined => { + return +} + +import('../../modules/world-edit/lib/world-edit').then(({ WorldEdit }) => { + getPlayerSelection = player => WorldEdit.forPlayer(player).selection +}) + +export const selectArea = form.params<{ onSelect: (area: Area) => NewFormCallback; title: Text }>( (f, { player, self, params: { onSelect: onS, title } }) => { function onSelect(area: Area) { onS(area)(player, self) } f.title(title) + + const selection = getPlayerSelection(player) + if (selection) { + f.button( + noI18n.accent`Выделенная зона (${Vec.size(selection.min, selection.max)})\n(куб без ограничения высоты)`, + () => onSelect(new ChunkCubeArea({ from: selection.min, to: selection.max }, player.dimension.type)), + ) + + f.button(noI18n.accent`Выделенная зона (${Vec.size(selection.min, selection.max)})\n(куб)`, () => + onSelect(new RectangleArea({ from: selection.min, to: selection.max }, player.dimension.type)), + ) + } + f.button(noI18n`Сфера`, BUTTON['+'], () => { new ModalForm(noI18n`Сфера`) .addTextField(noI18n`Центр`, '~~~', '~~~') - .addSlider(noI18n`Радиус`, 1, 100, 1) + .addSlider(noI18n`Радиус`, 1, 200, 1) .show(player, (ctx, rawCenter, radius) => { const center = parseLocationFromForm(ctx, rawCenter, player) if (!center) return + // TODO: Just ignore the underradius if (center.y - radius <= -64) return player.fail( i18n`Нельзя создать регион, область которого ниже -64 (y: ${center.y} radius: ${radius} result: ${center.y - radius})`, @@ -183,7 +206,7 @@ const regionStructureForm = form.params<{ region: Region; title: Text }>((f, { p }) if (exists) f.ask(noI18n`§cУдалить структуру`, noI18n`§cУдалить`, () => region.structure?.delete()) }) -const editRegion = form.params<{ region: Region; displayName: boolean }>( +export const editRegion = form.params<{ region: Region; displayName: boolean }>( (f, { player, back, self, params: { region, displayName } }) => { const title = displayName ? (region.displayName ?? region.creator.name) : region.name f.title(title) diff --git a/src/lib/region/index.ts b/src/lib/region/index.ts index b1f676d1..23e8b33e 100644 --- a/src/lib/region/index.ts +++ b/src/lib/region/index.ts @@ -8,15 +8,14 @@ import { world, } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { CustomEntityTypes } from 'lib/assets/custom-entity-types' -import { Items } from 'lib/assets/custom-items' -import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' +// import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +// import { Items } from 'lib/assets/custom-items' +// import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' -import { onPlayerMove } from 'lib/player-move' +// import { onPlayerMove } from 'lib/player-move' import { is } from 'lib/roles' import { isNotPlaying } from 'lib/utils/game' -import { createLogger } from 'lib/utils/logger' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { EventSignal } from '../event-signal' @@ -30,14 +29,17 @@ import { SWITCHES, TRAPDOORS, } from './config' +// import { RegionEvents } from './events' +import { onPlayerMove } from 'lib/player-move' +import { createLogger } from 'lib/utils/logger' import { RegionEvents } from './events' +import './explosion' import { Region } from './kinds/region' export * from './command' export * from './config' export * from './database' -export * from './kinds/boss-arena' export * from './kinds/region' export * from './kinds/road' export * from './kinds/safe-area' @@ -71,6 +73,7 @@ export enum ActionGuardOrder { // Limits Permission = 7, Lowest = 6, + DefaultAllowAll = 5, } export const regionTypesThatIgnoreIsBuildingGuard: (typeof Region)[] = [] @@ -109,11 +112,14 @@ actionGuard((player, region, context) => { if (typeId === MinecraftItemTypes.EnderPearl) return ent.includes(MinecraftEntityTypes.EnderPearl) if (typeId === MinecraftItemTypes.WindCharge) return ent.includes(MinecraftEntityTypes.WindChargeProjectile) if (typeId === MinecraftItemTypes.Snowball) return ent.includes(MinecraftEntityTypes.Snowball) - if (typeId === Items.Fireball) return ent.includes(CustomEntityTypes.Fireball) } } }, ActionGuardOrder.ProjectileUsePrevent) +actionGuard(() => { + return true +}, ActionGuardOrder.DefaultAllowAll) + const permdebugLogger = createLogger('region-perm') const allowed: InteractionAllowed = (player, region, context, regions) => { @@ -180,10 +186,14 @@ world.afterEvents.entitySpawn.subscribe(({ entity }) => { if ((NOT_MOB_ENTITIES.includes(typeId) && typeId !== 'minecraft:item') || !entity.isValid) return const region = Region.getAt(entity) + if (!region) return // Allow entity spawn outside of region by default + + const { allowedAllItem, allowedEntities, disallowedFamilies } = region.permissions - if (isForceSpawnInRegionAllowed(entity) || (typeId === 'minecraft:item' && region?.permissions.allowedAllItem)) return - if (!region || region.permissions.allowedEntities === 'all' || region.permissions.allowedEntities.includes(typeId)) - return + if (isForceSpawnInRegionAllowed(entity)) return + if (allowedAllItem && typeId === 'minecraft:item') return + if (allowedEntities === 'all' || allowedEntities.includes(typeId)) return + if (disallowedFamilies?.length && entity.matches({ excludeFamilies: disallowedFamilies })) return entity.remove() }) @@ -198,20 +208,20 @@ onPlayerMove.subscribe(({ player, location, dimensionType }) => { RegionEvents.playerInRegionsCache.set(player, newest) const currentRegion = newest[0] - const isPlaying = !isNotPlaying(player) - - const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) - - if (typeof currentRegion !== 'undefined' && isPlaying) { - if (currentRegion.permissions.pvp === false) { - player.triggerEvent( - player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], - ) - player.setProperty(PlayerProperties['lw:newbie'], true) - } else if (currentRegion.permissions.pvp === 'pve') { - player.setProperty(PlayerProperties['lw:newbie'], true) - } else resetNewbie() - } else resetNewbie() + // const isPlaying = !isNotPlaying(player) + + // const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) + + // if (typeof currentRegion !== 'undefined' && isPlaying) { + // if (currentRegion.permissions.pvp === false) { + // player.triggerEvent( + // player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], + // ) + // player.setProperty(PlayerProperties['lw:newbie'], true) + // } else if (currentRegion.permissions.pvp === 'pve') { + // player.setProperty(PlayerProperties['lw:newbie'], true) + // } else resetNewbie() + // } else resetNewbie() EventSignal.emit(RegionEvents.onInterval, { player, currentRegion }) }) diff --git a/src/lib/region/kinds/region.ts b/src/lib/region/kinds/region.ts index bcbbaada..8f7457b6 100644 --- a/src/lib/region/kinds/region.ts +++ b/src/lib/region/kinds/region.ts @@ -33,6 +33,8 @@ export interface RegionPermissions extends Record[1] = {}, key?: string, ): InstanceType { - // Make region list actually specific to class - if (this !== Region && this.regionsListType !== this.name) { - this.regions = [] - this.regionsListType = this.name - } + // // Make region list actually specific to class + // if (this !== Region && this.regionsListType !== this.name) { + // this.regions = [] + // this.regionsListType = this.name + // } // if (!area.isValid()) throw new Error('Area ' + area.toString() + 'is invalid') diff --git a/src/lib/rpg/leaderboard.ts b/src/lib/rpg/leaderboard.ts index afc9bcb5..faadb9e6 100644 --- a/src/lib/rpg/leaderboard.ts +++ b/src/lib/rpg/leaderboard.ts @@ -85,6 +85,7 @@ export class Leaderboard { } update() { + if (this.entity.isValid) this.entity.teleport(this.info.location, { dimension: world[this.info.dimension] }) Leaderboard.db.set(this.entity.id, this.info) } @@ -174,7 +175,7 @@ system.runInterval( } }, 'leaderboardsInterval', - 40, + 100, ) const types = ['', i18nShared`к`, i18nShared`млн`, i18nShared`млрд`, i18nShared`трлн`] diff --git a/src/lib/settings.ts b/src/lib/settings.ts index ef7948c5..e3338b66 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -24,9 +24,19 @@ interface ConfigMeta { [SETTINGS_GROUP_NAME]?: Text } +// TODO Create global PaidFeaturesProvider +type SettingsPayCheck = ((player: Player) => boolean) & { onFail: PlayerCallback } + export type SettingsConfig = Record< string, - { name: Text; description?: Text; value: T; onChange?: VoidFunction } + { + name: Text + description?: Text + value: T + onChange?: VoidFunction + paid?: SettingsPayCheck + whenNotPaidDefault?: NoInfer + } > & ConfigMeta @@ -172,7 +182,10 @@ export class Settings { configurable: false, enumerable: true, get() { + const paid = player ? (config[prop]?.paid?.(player) ?? true) : true const value = config[prop]?.value + if (!paid) return config[prop]?.whenNotPaidDefault ?? (typeof value === 'boolean' ? !value : value) + if (typeof value === 'undefined') throw new TypeError(`No config value for prop ${prop}`) return ( (database.getImmutable(groupId) as SettingsDatabaseValue | undefined)?.[key] ?? @@ -254,8 +267,9 @@ export function settingsGroupMenu( const store = Settings.parseConfig(storeSource, groupName, config, forRegularPlayer ? player : null) const buttons: [string, (input: string | boolean) => string][] = [] + const displayName = (config[SETTINGS_GROUP_NAME] ?? groupName).to(player.lang) const form = new ModalForm<(ctx: FormCallback, ...options: (string | boolean)[]) => void>( - (config[SETTINGS_GROUP_NAME] ?? groupName).to(player.lang), + `${displayName.split('\n')[0]}`, ) for (const key in config) { @@ -265,6 +279,8 @@ export function settingsGroupMenu( const value = saved ?? setting.value + const paid = setting.paid?.(player) ?? true + const isUnset = typeof saved === 'undefined' const isRequired = (Reflect.get(setting, 'requires') as boolean) && isUnset const isToggle = typeof value === 'boolean' @@ -273,6 +289,8 @@ export function settingsGroupMenu( label += hints[key] ? `${hints[key]}\n` : '' + if (!paid) label += `§cКУПИТЕ ЧТОБЫ ИСПОЛЬЗОВАТЬ\n` + if (isRequired) label += '§c(!) ' label += `§f§l${setting.name.to(player.lang)}§r§f` //§r @@ -337,6 +355,8 @@ export function settingsGroupMenu( ]) } + form.submitButton('Сохранить') + form.show(player, (_, ...settings) => { const hints: Record = {} diff --git a/src/lib/utils/big-structure.ts b/src/lib/utils/big-structure.ts index da6753bf..2218642a 100644 --- a/src/lib/utils/big-structure.ts +++ b/src/lib/utils/big-structure.ts @@ -27,6 +27,7 @@ export class BigStructure extends Cuboid { saveOnCreate = true, date = Date.now().toString(32), private structures: BigStructureSaved[] = [], + private entities = false, ) { super(pos1, pos2) this.prefix = `${prefix}|${date}` @@ -52,7 +53,7 @@ export class BigStructure extends Cuboid { } catch {} world.structureManager.createFromWorld(id, this.dimension, min, max, { - includeEntities: false, + includeEntities: this.entities, includeBlocks: true, saveMode: this.saveMode, }) diff --git a/src/lib/utils/item-name-x-count.ts b/src/lib/utils/item-name-x-count.ts index 9e46b378..f2397435 100644 --- a/src/lib/utils/item-name-x-count.ts +++ b/src/lib/utils/item-name-x-count.ts @@ -1,11 +1,10 @@ -import { ItemPotionComponent, ItemStack, Player } from '@minecraft/server' -import { - MinecraftPotionEffectTypes as PotionEffects, - MinecraftPotionModifierTypes as PotionModifiers, -} from '@minecraft/vanilla-data' +import { ItemStack, Player } from '@minecraft/server' +// import { +// MinecraftPotionEffectTypes as PotionEffects, +// MinecraftPotionDeliveryTypes as PotionDelivery, +// } from '@minecraft/vanilla-data' import { Language } from 'lib/assets/lang' import { langToken, translateToken } from 'lib/i18n/lang' -import { i18n } from 'lib/i18n/text' /** Returns \nx */ export function itemNameXCount( @@ -15,17 +14,17 @@ export function itemNameXCount( player: Player | Language, ): string { const locale = player instanceof Player ? player.lang : player - const potion = item instanceof ItemStack && item.getComponent(ItemPotionComponent.componentId) - if (potion) { - const { potionEffectType: effect, potionLiquidType: liquid, potionModifierType: modifier } = potion + // const potion = item instanceof ItemStack && item.getComponent(ItemPotionComponent.componentId) + // if (potion) { + // const { potionEffectType: effect } = potion - const token = langToken(`minecraft:${liquid.id}_${effect.id}_potion`) - const modifierIndex = modifier.id === PotionModifiers.Normal ? 0 : modifier.id === PotionModifiers.Long ? 1 : 2 - const time = potionModifierToTime[effect.id]?.[modifierIndex] - const modifierS = modifierIndexToS[modifierIndex]?.to(locale) ?? '' + // const token = langToken(`minecraft:${liquid.id}_${effect.id}_potion`) + // const modifierIndex = modifier.id === PotionModifiers.Normal ? 0 : modifier.id === PotionModifiers.Long ? 1 : 2 + // const time = potionModifierToTime[effect.id]?.[modifierIndex] + // const modifierS = modifierIndexToS[modifierIndex]?.to(locale) ?? '' - return `${c}${item.nameTag ?? translateToken(token, locale)}${modifierS}${time ? ` §7${time}` : ''}` - } + // return `${c}${item.nameTag ?? translateToken(token, locale)}${modifierS}${time ? ` §7${time}` : ''}` + // } return `${c}${item.nameTag ? (c ? uncolor(item.nameTag) : item.nameTag).replace(/\n.*/, '') : translateToken(langToken(item), locale)}${amount && item.amount ? ` §r§f${c}x${item.amount}` : ''}` } @@ -34,30 +33,58 @@ function uncolor(t: string) { return t.replaceAll(/§./g, '') } -const modifierIndexToS = ['', i18n` (долгое)`, ' II'] - -// TODO Ensure it works properly for all modifiers -const potionModifierToTime: Record = { - [PotionEffects.Healing]: ['0:45', '2:00', '0:22'], - [PotionEffects.Swiftness]: ['3:00', '8:00', '1:30'], - [PotionEffects.FireResistance]: ['3:00', '8:00', ''], - [PotionEffects.NightVision]: ['3:00', '8:00', ''], - [PotionEffects.Strength]: ['3:00', '8:00', '1:30'], - [PotionEffects.Leaping]: ['3:00', '8:00', '1:30'], - [PotionEffects.WaterBreath]: ['3:00', '8:00', ''], - [PotionEffects.Invisibility]: ['3:00', '8:00', ''], - [PotionEffects.SlowFalling]: ['1:30', '4:00', ''], - - [PotionEffects.Poison]: ['0:45', '2:00', '0:22'], - [PotionEffects.Weakness]: ['1:30', '4:00', ''], - [PotionEffects.Slowing]: ['1:30', '4:00', ''], - [PotionEffects.Harming]: ['', '', ''], - [PotionEffects.Wither]: ['0:40', '', ''], - [PotionEffects.Infested]: ['3:00', '', ''], - [PotionEffects.Weaving]: ['3:00', '', ''], - [PotionEffects.Oozing]: ['3:00', '', ''], - [PotionEffects.WindCharged]: ['3:00', '', ''], - - [PotionEffects.TurtleMaster]: ['0:20', '0:40', '0:20'], - [PotionEffects.None]: ['', '', ''], -} satisfies Record +// const modifierIndexToS = ['', i18n` (долгое)`, ' II'] + +// // TODO Ensure it works properly for all modifiers +// const potionModifierToTime: Record = { +// [PotionEffects.Healing]: ['0:45', '2:00', '0:22'], +// [PotionEffects.Swiftness]: ['3:00', '8:00', '1:30'], +// [PotionEffects.FireResistance]: ['3:00', '8:00', ''], +// [PotionEffects.LongFireResistance]: [], + +// [PotionEffects.Nightvision]: ['3:00', '8:00', ''], +// [PotionEffects.Strength]: ['3:00', '8:00', '1:30'], +// [PotionEffects.Leaping]: ['3:00', '8:00', '1:30'], +// [PotionEffects.WaterBreathing]: ['3:00', '8:00', ''], +// [PotionEffects.Invisibility]: ['3:00', '8:00', ''], +// [PotionEffects.SlowFalling]: ['1:30', '4:00', ''], + +// [PotionEffects.Poison]: ['0:45', '2:00', '0:22'], +// [PotionEffects.Weakness]: ['1:30', '4:00', ''], +// [PotionEffects.Slowness]: ['1:30', '4:00', ''], +// [PotionEffects.Harming]: ['', '', ''], +// [PotionEffects.Wither]: ['0:40', '', ''], +// [PotionEffects.Infested]: ['3:00', '', ''], +// [PotionEffects.Weaving]: ['3:00', '', ''], +// [PotionEffects.Oozing]: ['3:00', '', ''], +// [PotionEffects.WindCharged]: ['3:00', '', ''], + +// [PotionEffects.TurtleMaster]: ['0:20', '0:40', '0:20'], +// [PotionEffects.Awkward]: ['', '', ''], +// [PotionEffects.LongInvisibility]: [], +// [PotionEffects.LongLeaping]: [], +// [PotionEffects.LongMundane]: [], +// [PotionEffects.LongNightvision]: [], +// [PotionEffects.LongPoison]: [], +// [PotionEffects.LongRegeneration]: [], +// [PotionEffects.LongSlowFalling]: [], +// [PotionEffects.LongSlowness]: [], +// [PotionEffects.LongStrength]: [], +// [PotionEffects.LongSwiftness]: [], +// [PotionEffects.LongTurtleMaster]: [], +// [PotionEffects.LongWaterBreathing]: [], +// [PotionEffects.LongWeakness]: [], +// [PotionEffects.Mundane]: [], +// [PotionEffects.Regeneration]: [], +// [PotionEffects.StrongHarming]: [], +// [PotionEffects.StrongHealing]: [], +// [PotionEffects.StrongLeaping]: [], +// [PotionEffects.StrongPoison]: [], +// [PotionEffects.StrongRegeneration]: [], +// [PotionEffects.StrongSlowness]: [], +// [PotionEffects.StrongStrength]: [], +// [PotionEffects.StrongSwiftness]: [], +// [PotionEffects.StrongTurtleMaster]: [], +// [PotionEffects.Thick]: [], +// [PotionEffects.Water]: [] +// } satisfies Record diff --git a/src/lib/utils/ms-old.test.ts b/src/lib/utils/ms-old.test.ts new file mode 100644 index 00000000..b47f69c5 --- /dev/null +++ b/src/lib/utils/ms-old.test.ts @@ -0,0 +1,10 @@ +import { ms } from './ms' +import { msold, ngettext } from './ms-old' + +describe('uhh', () => { + it('works', () => { + expect(msold.remaining(ms.from('min', 5) - 1000).type).toMatchInlineSnapshot(`"минут"`) + expect(msold.remaining(ms.from('min', 5)).type).toMatchInlineSnapshot(`"минут"`) + expect(msold.remaining(ms.from('min', 5) + 1000).type).toMatchInlineSnapshot(`"минут"`) + }) +}) diff --git a/src/lib/utils/ms-old.ts b/src/lib/utils/ms-old.ts new file mode 100644 index 00000000..26772e0c --- /dev/null +++ b/src/lib/utils/ms-old.ts @@ -0,0 +1,109 @@ +type Plurals = [one: string, two: string, five: string] +/** + * Gets plural form based on provided number + * + * @param n - Number + * @param forms - Plurals forms in format `1 секунда 2 секунды 5 секунд` + * @returns Plural form. Currently only Russian supported + */ + +export function ngettext(n: number, [one, few, more]: Plurals): string { + if (!Number.isInteger(n)) return more + return [one, few, more][ + n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2 + ] as unknown as string +} + +type Time = 'year' | 'month' | 'day' | 'hour' | 'min' | 'sec' | 'ms' + +// eslint-disable-next-line @typescript-eslint/naming-convention +export class msold { + /** + * Parses the remaining time in milliseconds into a more human-readable format + * + * @example + * const {type, value} = ms.remaining(1000) + * console.log(value + ' ' + type) // 1 секунда + * + * @example + * const {type, value} = ms.remaining(1000 * 60 * 2) + * console.log(value + ' ' + type) // 2 минуты + * + * @example + * const {type, value} = ms.remaining(1000 * 60 * 2, { converters: ['sec' ]}) // only convert to sec + * console.log(value + ' ' + type) // 120 секунд + * + * @param ms - Milliseconds to parse from + * @param options - Convertion options + * @param options.converters - List of types to convert to. If some time was not specified, e.g. ms, the most closest + * type will be used + * @returns - An object containing the parsed time and the type of time (e.g. "days", "hours", etc.) + */ + static remaining( + ms: number, + { + converters: converterTypes = ['sec', 'min', 'hour', 'day'], + friction: frictionOverride, + }: { converters?: Time[]; friction?: number } = {}, + ): { value: string; type: string } { + const converters = converterTypes.map(type => this.converters[type]).sort((a, b) => b.time - a.time) + for (const { time, friction = 0, plurals } of converters) { + const value = ms / time + if (~~value >= 1) { + // Replace all 234.0 values to 234 + const parsedTime = value + .toFixed(frictionOverride ?? friction) + .replace(/(\.[1-9]*)0+$/m, '$1') + .replace(/\.$/m, '') + + return { + value: parsedTime, + type: ngettext(parseInt(parsedTime), plurals), + } + } + } + + return { value: ms.toString(), type: 'миллисекунд' } + } + + /** Converts provided time to ms depending on the type */ + static from(type: Time, num: number) { + return this.converters[type].time * num + } + + private static converters: Record = { + ms: { + time: 1, + plurals: ['миллисекунду', 'миллисекунды', 'миллисекунд'], + }, + sec: { + time: 1000, + plurals: ['секунда', 'секунды', 'секунд'], + }, + min: { + time: 1000 * 60, + plurals: ['минуту', 'минуты', 'минут'], + friction: 1, + }, + hour: { + time: 1000 * 60 * 60, + plurals: ['час', 'часа', 'часов'], + friction: 1, + }, + day: { + time: 1000 * 60 * 60 * 24, + plurals: ['день', 'дня', 'дней'], + friction: 2, + }, + month: { + time: 1000 * 60 * 60 * 24 * 30, + plurals: ['месяц', 'месяца', 'месяцев'], + friction: 2, + }, + year: { + time: 1000 * 60 * 60 * 24 * 30 * 12, + plurals: ['год', 'года', 'лет'], + friction: 3, + }, + } +} diff --git a/src/lib/utils/ms.ts b/src/lib/utils/ms.ts index 802a6b46..34507bd5 100644 --- a/src/lib/utils/ms.ts +++ b/src/lib/utils/ms.ts @@ -1,6 +1,6 @@ /* i18n-ignore */ -type Time = 'year' | 'month' | 'day' | 'hour' | 'min' | 'sec' | 'ms' +export type Time = 'year' | 'month' | 'day' | 'hour' | 'min' | 'sec' | 'ms' // eslint-disable-next-line @typescript-eslint/naming-convention export class ms { diff --git a/src/lib/utils/rewards.ts b/src/lib/utils/rewards.ts index 7eb6c39f..9be8d1c2 100644 --- a/src/lib/utils/rewards.ts +++ b/src/lib/utils/rewards.ts @@ -89,7 +89,7 @@ export class Rewards { */ give(player: Player, tell = true): Rewards { for (const reward of this.entries) Rewards.giveOne(player, reward) - if (tell) player.success(i18n`Вы получили награды!`) + if (tell && this.entries.length) player.success(i18n`Вы получили награды!`) return this } diff --git a/src/modules/test/enchant.ts b/src/modules/test/enchant.ts index fc8c4ae6..3f107c6f 100644 --- a/src/modules/test/enchant.ts +++ b/src/modules/test/enchant.ts @@ -1,6 +1,6 @@ /* i18n-ignore */ -import { world } from '@minecraft/server' import { Enchantments } from 'lib/enchantments' +import { stringify } from 'lib/util' new Command('enchant') .setDescription('Зачаровывает предмет') @@ -37,7 +37,7 @@ new Command('enchant') newitem.lockMode = item.lockMode for (const prop of item.getDynamicPropertyIds()) newitem.setDynamicProperty(prop, item.getDynamicProperty(prop)) - if (newitem.enchantable) world.debug('enchants', [...newitem.enchantable.getEnchantments()]) + if (newitem.enchantable) ctx.player.tell('enchants ' + stringify([...newitem.enchantable.getEnchantments()])) mainhand.setItem(newitem) }) diff --git a/src/modules/test/test.ts b/src/modules/test/test.ts index b49bd409..e1c4838e 100644 --- a/src/modules/test/test.ts +++ b/src/modules/test/test.ts @@ -1,13 +1,14 @@ /* i18n-ignore */ /* eslint-disable */ -import { ItemStack, MolangVariableMap, Player, ScriptEventSource, system, world } from '@minecraft/server' +import { MolangVariableMap, Player, Potions, ScriptEventSource, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftCameraPresetsTypes, MinecraftEnchantmentTypes, MinecraftEntityTypes, MinecraftItemTypes, + MinecraftPotionDeliveryTypes, MinecraftPotionEffectTypes, } from '@minecraft/vanilla-data' @@ -195,7 +196,7 @@ const tests: Record< }, potionAux(ctx) { for (const effect of Object.values(MinecraftPotionEffectTypes)) { - const item = ItemStack.createPotion({ effect }) + const item = Potions.resolve(effect, MinecraftPotionDeliveryTypes.ThrownSplash) getAuxTextureOrPotionAux(item) } }, @@ -396,7 +397,7 @@ const tests: Record< }, dbinspect(ctx) { - world.debug( + console.log( 'test41', { DatabaseUtils }, world.overworld.getEntities({ type: DatabaseUtils.entityTypeId }).map(e => { From aea278c7818748f8c349af80832bdd3291e8c697 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:34:29 +0300 Subject: [PATCH 07/14] feat: new chat --- src/lib/chat/chat.ts | 144 +++++++++++++++++++++++++ src/lib/chat/command.ts | 64 +++++++++++ src/lib/chat/mute.ts | 97 +++++++++++++++++ src/lib/command/index.ts | 205 +++++++++++++++++++----------------- src/modules/chat/chat.ts | 125 ---------------------- src/modules/chat/mute.ts | 64 ----------- src/modules/loader.ts | 2 +- src/modules/lushway/chat.ts | 34 ++++++ 8 files changed, 448 insertions(+), 287 deletions(-) create mode 100644 src/lib/chat/chat.ts create mode 100644 src/lib/chat/command.ts create mode 100644 src/lib/chat/mute.ts delete mode 100644 src/modules/chat/chat.ts delete mode 100644 src/modules/chat/mute.ts create mode 100644 src/modules/lushway/chat.ts diff --git a/src/lib/chat/chat.ts b/src/lib/chat/chat.ts new file mode 100644 index 00000000..d2da4dd9 --- /dev/null +++ b/src/lib/chat/chat.ts @@ -0,0 +1,144 @@ +import { ChatSendBeforeEvent, Player, system, world } from '@minecraft/server' +import { Cooldown } from 'lib/cooldown' +import { table } from 'lib/database/abstract' +import { i18n, noI18n } from 'lib/i18n/text' +import { Settings } from 'lib/settings' +import { msold } from 'lib/utils/ms-old' +import './command' + +export declare namespace Chat { + interface MuteInfo { + mutedUntil: number + reason?: string + } + + interface Context { + sender: Player + text: string + nearPlayers: Player[] + farPlayers: Player[] + } +} + +export abstract class Chat { + private static instance?: Chat + + static getInstance(): Chat { + if (!this.instance) throw new Error('Chat.getInstance: Chat is not configured!') + return this.instance + } + + muteDb = table('chatMute') + + settings = Settings.world(...Settings.worldCommon, { + cooldown: { + name: 'Задержка чата (миллисекунды)', + description: '0 что бы отключить', + value: 0, + onChange: () => this.updateCooldown(), + }, + range: { + name: 'Радиус чата', + description: 'Радиус для скрытия сообщений дальних игроков', + value: 30, + }, + capsLimit: { + name: 'Макс больших букв в сообщении', + description: 'Не разрешает отправлять сообщения где слишком много капса', + value: 5, + }, + role: { + name: 'Роли в чате', + value: true, + }, + }) + + playerSettings = Settings.player(i18n`Чат\n§7Звуки и внешний вид чата`, 'chat', { + sound: { + name: i18n`Звук`, + description: i18n`Звука сообщений от игроков поблизости`, + value: true, + }, + }) + + private cooldown!: Cooldown + + private updateCooldown() { + this.cooldown = new Cooldown(this.settings.cooldown, true, {}) + } + + informAboutMute(player: Player, mute: Chat.MuteInfo): void { + const timeText = msold.remaining(mute.mutedUntil - Date.now()) + + return player.fail( + noI18n.error`Вы замьючены в чате на ${timeText.value} ${timeText.type} по причине: ${mute.reason}`, + ) + } + + registerChatListener() { + world.beforeEvents.chatSend.subscribe(this.chatListener) + } + + chatListener: (arg0: ChatSendBeforeEvent) => void + + constructor() { + if (Chat.instance) throw new Error('Chat was already initialized!') + Chat.instance = this + + world.afterEvents.worldLoad.subscribe(() => { + this.updateCooldown() + }) + + this.chatListener = event => { + event.cancel = true + system.delay(() => { + try { + const player = event.sender + + if (!this.cooldown.isExpired(event.sender)) { + console.log('Spam chat', player.name, event.message) + return + } + + const mute = this.muteDb.getImmutable(event.sender.id) + if (mute) { + if (mute.mutedUntil > Date.now()) { + console.log('Muted chat', player.name, event.message) + return this.informAboutMute(player, mute) + } + } + + const text = event.message.replace(/\\n/g, '\n').replace(/§./g, '').replace(/%/g, '%%').trim() + + const caps = text.split('').reduce((p, c) => (c !== c.toLowerCase() ? p + 1 : p), 0) + if (caps > this.settings.capsLimit) { + return event.sender.fail(noI18n.error`В сообщении слишком много капса (${caps}/${this.settings.capsLimit})`) + } + + const allPlayers = world.getAllPlayers() + + // Players that are near message sender + const nearPlayers = event.sender.dimension + .getPlayers({ + location: event.sender.location, + maxDistance: this.settings.range, + }) + .filter(e => e.id !== event.sender.id && e.dimension.id === event.sender.dimension.id) + + // Array with ranged players (include sender id) + const nearIds = nearPlayers.map(e => e.id) + nearIds.push(event.sender.id) + + // Outranged players + const farPlayers = allPlayers.filter(e => !nearIds.includes(e.id)) + + this.onMessage({ sender: event.sender, text, farPlayers, nearPlayers }) + } catch (error) { + console.error('Chat error handler', error) + } + }) + } + } + + protected abstract onMessage(ctx: Chat.Context): void +} diff --git a/src/lib/chat/command.ts b/src/lib/chat/command.ts new file mode 100644 index 00000000..5665f757 --- /dev/null +++ b/src/lib/chat/command.ts @@ -0,0 +1,64 @@ +import { Player } from '@minecraft/server' +import { emoji } from 'lib/assets/emoji' +import { ModalForm } from 'lib/form/modal' +import { form } from 'lib/form/new' +import { selectPlayer } from 'lib/form/select-player' +import { noI18n } from 'lib/i18n/text' +import { ROLES } from 'lib/roles' + +new Command('chat') + .setDescription('Управление отображением игрока/ранга в чате/игре') + .setPermissions('techAdmin') + .executes(ctx => { + const player = ctx.player + chatForm(player) + }) + +function chatForm(player: Player) { + selectPlayer(player, 'настроить его отображение в чате/игре').then(e => { + chatPlayerEditForm({ target: e }).show(player) + }) +} + +const chatPlayerEditForm = form.params<{ target: { name: string; id: string } }>( + (f, { player, self, params: { target } }) => { + f.title(target.name) + const db = Player.database.getImmutable(target.id) + f.body( + noI18n`Видимый ранг: ${db.displayRole}\nРоль: ${ROLES[db.role].to(player.lang)}\nЭмоджи: ${db.emoji}\nЦвет сообщения в чате: ${`${db.chatTextColor ?? ''}сообщение`}`, + ) + f.button('Назад', () => { + chatForm(player) + }) + f.button( + `Изменить эмоджи`, + form(f => { + f.button(`Очистить выбор: ${db.emoji ?? 'Не выбрано'}`, () => { + const ddb = Player.database.get(target.id) + delete ddb.emoji + self() + }) + for (const [name, e] of Object.entries(emoji.nickname)) { + f.button(`${name} ${e}`, () => { + const ddb = Player.database.get(target.id) + ddb.emoji = e + self() + }) + } + }), + ) + f.button('Изменить', () => { + new ModalForm('Изменить') + .addTextField('Видимый ранг', 'очистит ее', db.displayRole) + .addTextField('Цвет сообщения в чате', 'очистит его', db.chatTextColor) + .show(player, (ctx, displayRole, textColor) => { + const ddb = Player.database.get(target.id) + if (!displayRole) delete ddb.displayRole + else ddb.displayRole = displayRole + if (!textColor) delete ddb.chatTextColor + else ddb.chatTextColor = textColor + self() + }) + }) + }, +) diff --git a/src/lib/chat/mute.ts b/src/lib/chat/mute.ts new file mode 100644 index 00000000..2ac9ac0b --- /dev/null +++ b/src/lib/chat/mute.ts @@ -0,0 +1,97 @@ +import { Player } from '@minecraft/server' +import { CommandContext } from 'lib/command/context' +import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' +import { form } from 'lib/form/new' +import { selectPlayer } from 'lib/form/select-player' +import { getFullname } from 'lib/get-fullname' +import { ms, Time } from 'lib/utils/ms' +import { msold } from 'lib/utils/ms-old' +import { Chat } from './chat' + +function mute(type: Time, time: number, reason = 'за поведение', id: string, ctx: CommandContext) { + const actualTime = ms.from(type, time) + const muteInfo: Chat.MuteInfo = { mutedUntil: Date.now() + actualTime, reason } + Chat.getInstance().muteDb.set(id, muteInfo) + const player = Player.getById(id) + if (player) Chat.getInstance().informAboutMute(player, muteInfo) + + const timeText = msold.remaining(actualTime) + ctx.player.success( + `Игрок ${Player.nameOrUnknown(id)} был замьючен на ${timeText.value} ${timeText.type} по причине: ${reason}`, + ) +} + +function findOfflinePlayer(nameArg: string, ctx: CommandContext) { + for (const [id, data] of Player.database.entriesImmutable()) { + if (data.name === nameArg) return id + } + ctx.error(`Игрок ${nameArg} не найден`) +} + +new Command('mute') + .setDescription('Заглушить игрока в чате') + .setPermissions('helper') + .string('name', true) + .int('time', true) + .array('timeType', ['min', 'hour', 'day', 'sec'], true) + .string('reason', true) + .executes((ctx, nameArg, timeArg = 5, typeArg = 'min', reasonArg) => { + if (nameArg) { + if (typeof ms.converters[typeArg] === 'undefined') return ctx.error('Неизвестный тип времени') + const id = findOfflinePlayer(nameArg, ctx) + if (id) mute(typeArg, timeArg, reasonArg, id, ctx) + return + } + + selectPlayer(ctx.player, 'замутить').then(e => { + new ModalForm('Мут ' + e.name) + .addTextField('Время', 'введи', '5') + .addDropdownFromObject('Тип времени', { + min: 'Минуты', + hour: 'Часы', + }) + .addTextField('Причина', 'за поведение') + .show(ctx.player, (formctx, timeRaw, type, reason) => { + const time = parseInt(timeRaw) + if (isNaN(time)) return formctx.error(`${timeRaw} это не число`) + + mute(type, time, reason || undefined, e.id, ctx) + }) + }) + }) + +new Command('unmute') + .setDescription('Вернуть обратно') + .setPermissions('helper') + .string('name', true) + .executes((ctx, name) => { + if (name) { + const id = findOfflinePlayer(name, ctx) + if (id) { + if (!Chat.getInstance().muteDb.has(id)) return ctx.error('Не был замьючен') + + Chat.getInstance().muteDb.delete(id) + return ctx.reply('Размьючен') + } + return + } + + new ArrayForm('Муты', Chat.getInstance().muteDb.entries()) + .button(([id, info]) => { + if (!info) return false + const until = `До: ${new Date(info.mutedUntil).toYYYYMMDD()} ${new Date(info.mutedUntil).toHHMM()}` + return [ + `${getFullname(id)} ${until}\n${info.reason}`, + form((f, { self }) => { + f.title(getFullname(id)) + f.body(`Причина: ${info.mutedUntil}\n${until}`) + f.button('Размутить', () => { + Chat.getInstance().muteDb.delete(id) + self() + }) + }).show, + ] + }) + .show(ctx.player) + }) diff --git a/src/lib/command/index.ts b/src/lib/command/index.ts index 0b9ce95e..1f469608 100644 --- a/src/lib/command/index.ts +++ b/src/lib/command/index.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/unified-signatures */ import { ChatSendAfterEvent, + ChatSendBeforeEvent, CommandPermissionLevel, CustomCommandOrigin, CustomCommandParameter, @@ -57,7 +58,7 @@ export class Command static logger = createLogger('Command') - static chatListener(event: ChatSendAfterEvent) { + private static chatListener(event: ChatSendAfterEvent) { if (!this.isCommand(event.message)) return this.chatSendListener(event) const parsed = parseCommand(event.message, 1) @@ -99,6 +100,21 @@ export class Command sendCallback(args, childs, event, command, input) } + // TODO Better registration, global event etc + static registerChatListener(chat: (arg0: ChatSendBeforeEvent) => void) { + this.chatSendListener = chat + world.beforeEvents.chatSend.subscribe(event => { + event.cancel = true + system.delay(() => { + Command.chatListener(event) + }) + }) + } + + static register(namespace: string) { + register(namespace) + } + /** An array of all active commands */ static commands: Command[] = [] @@ -446,116 +462,111 @@ declare global { globalThis.Command = Command -// world.beforeEvents.chatSend.subscribe(event => { -// event.cancel = true -// system.delay(() => { -// Command.chatListener(event) -// }) -// }) - -system.beforeEvents.startup.subscribe(load => { - const namespace = 'folkbe' - for (const command of Command.commands) { - if (command.sys.depth !== 0) continue - // Only simple commands are supported rn - - try { - const mandatoryParameters: CustomCommandParameter[] = [] - const optionalParameters: CustomCommandParameter[] = [] - - let callback = command.sys.callback - const locationIndexes: number[] = [] - let i = 0 - - let nowOptional = false - - function collectParams(command: Command) { - const child = command.sys.children[0] - if (!child || child instanceof LiteralArgumentType) return - - // Location is split - if (child.sys.type.name.endsWith('*')) return collectParams(child) - - child.sys.type.register(load.customCommandRegistry, namespace) - const param: CustomCommandParameter = { name: child.sys.type.name, type: child.sys.type.ctype } - if (child.sys.type.optional) { - nowOptional = true - optionalParameters.push(param) - } else { - if (nowOptional) throw new Error('Mandatory param cannot come after optional in command ' + command.sys.name) - mandatoryParameters.push(param) - } +function register(namespace: string) { + system.beforeEvents.startup.subscribe(load => { + for (const command of Command.commands) { + if (command.sys.depth !== 0) continue + // Only simple commands are supported rn - if (child.sys.type instanceof LocationArgumentType) locationIndexes.push(i) - i++ + try { + const mandatoryParameters: CustomCommandParameter[] = [] + const optionalParameters: CustomCommandParameter[] = [] - if (child.sys.callback) callback = child.sys.callback + let callback = command.sys.callback + const locationIndexes: number[] = [] + let i = 0 - collectParams(child) - } - collectParams(command) - - load.customCommandRegistry.registerCommand( - { - name: namespace + ':' + command.sys.name, - permissionLevel: command.sys.admin ? CommandPermissionLevel.GameDirectors : CommandPermissionLevel.Any, - description: command.sys.description.to(defaultLang), - mandatoryParameters, - optionalParameters, - }, - (ctx, ...args) => { - if (!callback) { - return { - status: CustomCommandStatus.Failure, - message: 'Команда не готова', - } + let nowOptional = false + + function collectParams(command: Command) { + const child = command.sys.children[0] + if (!child || child instanceof LiteralArgumentType) return + + // Location is split + if (child.sys.type.name.endsWith('*')) return collectParams(child) + + child.sys.type.register(load.customCommandRegistry, namespace) + const param: CustomCommandParameter = { name: child.sys.type.name, type: child.sys.type.ctype } + if (child.sys.type.optional) { + nowOptional = true + optionalParameters.push(param) + } else { + if (nowOptional) + throw new Error('Mandatory param cannot come after optional in command ' + command.sys.name) + mandatoryParameters.push(param) } - const isServer = !(ctx.sourceEntity instanceof Player) - const output: CommandOutputBuffer = { output: '', isSync: isServer } - const player: Player = - ctx.sourceEntity instanceof Player ? ctx.sourceEntity : createPlayerProxy(ctx, command, output) + if (child.sys.type instanceof LocationArgumentType) locationIndexes.push(i) + i++ - const allowed = command.sys.requires(player) - if (!allowed) { - if (command.sys.requires.onFail) { - command.sys.requires.onFail(player) - } else { - if (command.sys.role) { - player.fail( - i18n.error`Команда доступна только начиная с роли ${ROLES[command.sys.role]}. Ваша роль: ${ROLES[getRole(player.id)]}`, - ) - } else { - player.fail(i18n.error`Команда недоступна`) + if (child.sys.callback) callback = child.sys.callback + + collectParams(child) + } + collectParams(command) + + load.customCommandRegistry.registerCommand( + { + name: namespace + ':' + command.sys.name, + permissionLevel: command.sys.admin ? CommandPermissionLevel.GameDirectors : CommandPermissionLevel.Any, + description: command.sys.description.to(defaultLang), + mandatoryParameters, + optionalParameters, + }, + (ctx, ...args) => { + if (!callback) { + return { + status: CustomCommandStatus.Failure, + message: 'Команда не готова', } } - return { status: CustomCommandStatus.Failure, message: output.output || undefined } - } + const isServer = !(ctx.sourceEntity instanceof Player) + const output: CommandOutputBuffer = { output: '', isSync: isServer } + const player: Player = + ctx.sourceEntity instanceof Player ? ctx.sourceEntity : createPlayerProxy(ctx, command, output) - for (const i of locationIndexes) { - const arg: unknown = args[i] - if (Vec.isVec(arg)) args[i] = Vec.floor(arg) - } + const allowed = command.sys.requires(player) + if (!allowed) { + if (command.sys.requires.onFail) { + command.sys.requires.onFail(player) + } else { + if (command.sys.role) { + player.fail( + i18n.error`Команда доступна только начиная с роли ${ROLES[command.sys.role]}. Ваша роль: ${ROLES[getRole(player.id)]}`, + ) + } else { + player.fail(i18n.error`Команда недоступна`) + } + } - if (isServer) { - execCmd(player, command, callback, args, output) - } else { - system.delay(() => { - if (!callback) throw new Error('no callback') + return { status: CustomCommandStatus.Failure, message: output.output || undefined } + } + + for (const i of locationIndexes) { + const arg: unknown = args[i] + if (Vec.isVec(arg)) args[i] = Vec.floor(arg) + } + + if (isServer) { execCmd(player, command, callback, args, output) - }) - } - return { status: CustomCommandStatus.Success, message: output.output || undefined } - }, - ) - } catch (e) { - Command.logger.error('Failed to load command', command.sys.name, e) + } else { + system.delay(() => { + if (!callback) throw new Error('no callback') + execCmd(player, command, callback, args, output) + }) + } + return { status: CustomCommandStatus.Success, message: output.output || undefined } + }, + ) + } catch (e) { + Command.logger.error('Failed to load command', command.sys.name, e) + } } - } - Command.loaded = true -}) + Command.loaded = true + }) +} interface CommandOutputBuffer { output: string diff --git a/src/modules/chat/chat.ts b/src/modules/chat/chat.ts deleted file mode 100644 index 3d181dc6..00000000 --- a/src/modules/chat/chat.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { world } from '@minecraft/server' -import { Sounds } from 'lib/assets/custom-sounds' -import { sendPacketToStdout } from 'lib/bds/api' -import { Cooldown } from 'lib/cooldown' -import { table } from 'lib/database/abstract' -import { getFullname } from 'lib/get-fullname' -import { i18n, noI18n } from 'lib/i18n/text' -import { Settings } from 'lib/settings' -import { muteInfo } from './mute' - -export class Chat { - static muteDb = table<{ mutedUntil: number; reason?: string }>('chatMute') - - static settings = Settings.world(...Settings.worldCommon, { - cooldown: { - name: 'Задержка чата (миллисекунды)', - description: '0 что бы отключить', - value: 0, - onChange: () => this.updateCooldown(), - }, - range: { - name: 'Радиус чата', - description: 'Радиус для скрытия сообщений дальних игроков', - value: 30, - }, - capsLimit: { - name: 'Макс больших букв в сообщении', - description: 'Не разрешает отправлять сообщения где слишком много капса', - value: 5, - }, - role: { - name: 'Роли в чате', - value: true, - }, - }) - - static playerSettings = Settings.player(i18n`Чат\n§7Звуки и внешний вид чата`, 'chat', { - sound: { - name: i18n`Звук`, - description: i18n`Звука сообщений от игроков поблизости`, - value: true, - }, - }) - - private static cooldown: Cooldown - - private static updateCooldown() { - this.cooldown = new Cooldown(this.settings.cooldown, true, {}) - } - - static { - this.updateCooldown() - Command.chatSendListener = event => { - if (Command.isCommand(event.message)) return - - try { - if (!this.cooldown.isExpired(event.sender)) return - const player = event.sender - - if (!this.cooldown.isExpired(event.sender)) { - console.log('Spam chat', player.name, event.message) - return - } - - const mute = this.muteDb.getImmutable(event.sender.id) - if (mute) { - if (mute.mutedUntil > Date.now()) { - console.log('Muted chat', player.name, event.message) - return muteInfo(player, mute) - } - } - - const messageText = event.message.replace(/\\n/g, '\n').replace(/§./g, '').trim() - - const caps = messageText.split('').reduce((p, c) => (c !== c.toLowerCase() ? p + 1 : p), 0) - if (caps > this.settings.capsLimit) { - return event.sender.fail(noI18n.error`В сообщении слишком много капса (${caps}/${this.settings.capsLimit})`) - } - - const allPlayers = world.getAllPlayers() - - // Players that are near message sender - const nearPlayers = event.sender.dimension - .getPlayers({ - location: event.sender.location, - maxDistance: this.settings.range, - }) - .filter(e => e.id !== event.sender.id && e.dimension.id === event.sender.dimension.id) - - // Array with ranged players (include sender id) - const nID = nearPlayers.map(e => e.id) - nID.push(event.sender.id) - - // Outranged players - const otherPlayers = allPlayers.filter(e => !nID.includes(e.id)) - const message = `${getFullname(event.sender, { nameColor: '§7', equippment: true })}§r: ${messageText}` - const fullrole = getFullname(event.sender, { name: false }) - - if (__SERVER__) { - // This is handled/parsed by ServerCore - // Dont really want to do request each time here - sendPacketToStdout('chatMessage', { - name: event.sender.name, - role: fullrole, - print: message, - message: messageText, - }) - } - - for (const near of nearPlayers) { - near.tell(message) - if (this.playerSettings(near).sound) near.playSound(Sounds.Click) - } - - for (const outranged of otherPlayers) { - outranged.tell(`${getFullname(event.sender, { nameColor: '§8' })}§7: ${messageText}`) - } - - event.sender.tell(message) - } catch (error) { - console.error(error) - } - } - } -} diff --git a/src/modules/chat/mute.ts b/src/modules/chat/mute.ts deleted file mode 100644 index 3dd1f591..00000000 --- a/src/modules/chat/mute.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Player } from '@minecraft/server' -import { ArrayForm } from 'lib/form/array' -import { ModalForm } from 'lib/form/modal' -import { form } from 'lib/form/new' -import { selectPlayer } from 'lib/form/select-player' -import { getFullname } from 'lib/get-fullname' -import { noI18n } from 'lib/i18n/text' -import { ms } from 'lib/utils/ms' -import { Chat } from './chat' - -export function muteInfo( - player: Player, - mute: { readonly mutedUntil: number; readonly reason?: string | undefined }, -): void { - return player.fail( - noI18n.error`Вы замьючены в чате до ${new Date(mute.mutedUntil).toYYYYMMDD()} ${new Date(mute.mutedUntil).toHHMM()}${mute.reason ? noI18n.error` по причине: ${mute.reason}` : ''}`, - ) -} -new Command('mute') - .setDescription('Заглушить игрока в чате') - .setPermissions('helper') - .executes(ctx => { - selectPlayer(ctx.player, 'замутить').then(e => { - new ModalForm('Мут ' + e.name) - .addTextField('Время', 'введи', '5') - .addDropdownFromObject('Тип времени', { - min: 'Минуты', - hour: 'Часы', - }) - .addTextField('Причина', 'опиши чтобы знал что делать нельзя') - .show(ctx.player, (formctx, timeRaw, type, reason) => { - const time = parseInt(timeRaw) - if (isNaN(time)) return formctx.error(`${timeRaw} это не число`) - - const actualTime = ms.from(type, time) - Chat.muteDb.set(e.id, { mutedUntil: Date.now() + actualTime, reason }) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (e.player) muteInfo(e.player, Chat.muteDb.get(e.id)!) - ctx.player.success() - }) - }) - }) -new Command('unmute') - .setDescription('Вернуть обратно') - .setPermissions('helper') - .executes(ctx => { - new ArrayForm('Муты', Chat.muteDb.entries()) - .button(([id, info]) => { - if (!info) return false - const until = `До: ${new Date(info.mutedUntil).toYYYYMMDD()} ${new Date(info.mutedUntil).toHHMM()}` - return [ - `${getFullname(id)} ${until}\n${info.reason}`, - form((f, { self }) => { - f.title(getFullname(id)) - f.body(`Причина: ${info.mutedUntil}\n${until}`) - f.button('Размутить', () => { - Chat.muteDb.delete(id) - self() - }) - }).show, - ] - }) - .show(ctx.player) - }) diff --git a/src/modules/loader.ts b/src/modules/loader.ts index c2e37567..09c990d2 100644 --- a/src/modules/loader.ts +++ b/src/modules/loader.ts @@ -3,7 +3,7 @@ import 'lib' import './anticheat/index' import './survival/import' -import './chat/chat' +import './lushway/chat' import './test/test' import './wiki/wiki' import './world-edit/builder' diff --git a/src/modules/lushway/chat.ts b/src/modules/lushway/chat.ts new file mode 100644 index 00000000..12580a69 --- /dev/null +++ b/src/modules/lushway/chat.ts @@ -0,0 +1,34 @@ +import { Sounds } from 'lib/assets/custom-sounds' +import { sendPacketToStdout } from 'lib/bds/api' +import { Chat } from 'lib/chat/chat' +import { getFullname } from 'lib/get-fullname' + +class LushWayChat extends Chat { + protected onMessage(ctx: Chat.Context): void { + const message = `${getFullname(ctx.sender, { nameColor: '§7', equippment: true })}§r: ${ctx.text}` + const fullrole = getFullname(ctx.sender, { name: false }) + if (__SERVER__) { + // This is handled/parsed by ServerCore + // Dont really want to do request each time here + sendPacketToStdout('chatMessage', { + name: ctx.sender.name, + role: fullrole, + print: message, + message: ctx.text, + }) + } + + for (const near of ctx.nearPlayers) { + near.tell(message) + if (this.playerSettings(near).sound) near.playSound(Sounds.Click) + } + + for (const outranged of ctx.farPlayers) { + outranged.tell(`${getFullname(ctx.sender, { nameColor: '§8' })}§7: ${ctx.text}`) + } + + ctx.sender.tell(message) + } +} + +Command.registerChatListener(new LushWayChat().chatListener) From da5cba71dc5dcc7be064e0fe986e7871eb22ddff Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:39:55 +0300 Subject: [PATCH 08/14] fix: enable disabled things in region --- src/lib/region/index.ts | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/lib/region/index.ts b/src/lib/region/index.ts index 23e8b33e..843f9ecc 100644 --- a/src/lib/region/index.ts +++ b/src/lib/region/index.ts @@ -8,14 +8,13 @@ import { world, } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -// import { CustomEntityTypes } from 'lib/assets/custom-entity-types' -// import { Items } from 'lib/assets/custom-items' -// import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' +import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' -// import { onPlayerMove } from 'lib/player-move' +import { onPlayerMove } from 'lib/player-move' import { is } from 'lib/roles' import { isNotPlaying } from 'lib/utils/game' +import { createLogger } from 'lib/utils/logger' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { EventSignal } from '../event-signal' @@ -29,11 +28,7 @@ import { SWITCHES, TRAPDOORS, } from './config' -// import { RegionEvents } from './events' -import { onPlayerMove } from 'lib/player-move' -import { createLogger } from 'lib/utils/logger' import { RegionEvents } from './events' -import './explosion' import { Region } from './kinds/region' export * from './command' @@ -208,20 +203,21 @@ onPlayerMove.subscribe(({ player, location, dimensionType }) => { RegionEvents.playerInRegionsCache.set(player, newest) const currentRegion = newest[0] - // const isPlaying = !isNotPlaying(player) - - // const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) - - // if (typeof currentRegion !== 'undefined' && isPlaying) { - // if (currentRegion.permissions.pvp === false) { - // player.triggerEvent( - // player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], - // ) - // player.setProperty(PlayerProperties['lw:newbie'], true) - // } else if (currentRegion.permissions.pvp === 'pve') { - // player.setProperty(PlayerProperties['lw:newbie'], true) - // } else resetNewbie() - // } else resetNewbie() + // TODO Replace with proper damage cancel on beforeEntityHurt once we update + const isPlaying = !isNotPlaying(player) + + const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) + + if (typeof currentRegion !== 'undefined' && isPlaying) { + if (currentRegion.permissions.pvp === false) { + player.triggerEvent( + player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], + ) + player.setProperty(PlayerProperties['lw:newbie'], true) + } else if (currentRegion.permissions.pvp === 'pve') { + player.setProperty(PlayerProperties['lw:newbie'], true) + } else resetNewbie() + } else resetNewbie() EventSignal.emit(RegionEvents.onInterval, { player, currentRegion }) }) From 8a6b26b5f4fe45934f70477593f8e01e7baca26e Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:42:39 +0300 Subject: [PATCH 09/14] fix --- src/lib/region/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib/region/index.ts b/src/lib/region/index.ts index 843f9ecc..c683ed20 100644 --- a/src/lib/region/index.ts +++ b/src/lib/region/index.ts @@ -8,6 +8,8 @@ import { world, } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' +import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +import { Items } from 'lib/assets/custom-items' import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' @@ -68,7 +70,6 @@ export enum ActionGuardOrder { // Limits Permission = 7, Lowest = 6, - DefaultAllowAll = 5, } export const regionTypesThatIgnoreIsBuildingGuard: (typeof Region)[] = [] @@ -107,14 +108,11 @@ actionGuard((player, region, context) => { if (typeId === MinecraftItemTypes.EnderPearl) return ent.includes(MinecraftEntityTypes.EnderPearl) if (typeId === MinecraftItemTypes.WindCharge) return ent.includes(MinecraftEntityTypes.WindChargeProjectile) if (typeId === MinecraftItemTypes.Snowball) return ent.includes(MinecraftEntityTypes.Snowball) + if (typeId === Items.Fireball) return ent.includes(CustomEntityTypes.Fireball) } } }, ActionGuardOrder.ProjectileUsePrevent) -actionGuard(() => { - return true -}, ActionGuardOrder.DefaultAllowAll) - const permdebugLogger = createLogger('region-perm') const allowed: InteractionAllowed = (player, region, context, regions) => { From 4fcd2a812111fc66dca9d2808db14854acc23af4 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:28:45 +0300 Subject: [PATCH 10/14] a --- src/lib/chat/chat.ts | 13 +- src/lib/lib.d.ts | 4 - .../commands/mail.ts => lib/mail/command.ts} | 76 +++-- src/lib/{mail.test.ts => mail/index.test.ts} | 2 +- src/lib/{mail.ts => mail/index.ts} | 17 +- src/lib/player-join.ts | 271 +++++++++--------- src/lib/rpg/newbie.ts | 1 - src/lib/utils/singleton.ts | 17 ++ src/modules/commands/ban.ts | 1 - src/modules/commands/db.ts | 187 ------------ src/modules/commands/index.ts | 6 +- src/modules/commands/leaderboard.ts | 143 --------- src/modules/commands/mute.ts | 1 - src/modules/commands/settings.ts | 17 -- .../commands/stringifyBenchmarkReult.ts | 57 ---- src/modules/commands/wipe.ts | 2 +- src/modules/loader.ts | 2 +- src/modules/lushway/{ => config}/chat.ts | 0 src/modules/lushway/config/core.ts | 3 + src/modules/lushway/config/join.ts | 20 ++ src/modules/lushway/loader.ts | 4 + src/modules/places/spawn.ts | 10 +- src/modules/survival/menu.ts | 6 +- 23 files changed, 271 insertions(+), 589 deletions(-) rename src/{modules/commands/mail.ts => lib/mail/command.ts} (66%) rename src/lib/{mail.test.ts => mail/index.test.ts} (96%) rename src/lib/{mail.ts => mail/index.ts} (93%) create mode 100644 src/lib/utils/singleton.ts delete mode 100644 src/modules/commands/ban.ts delete mode 100644 src/modules/commands/db.ts delete mode 100644 src/modules/commands/leaderboard.ts delete mode 100644 src/modules/commands/mute.ts delete mode 100644 src/modules/commands/settings.ts delete mode 100644 src/modules/commands/stringifyBenchmarkReult.ts rename src/modules/lushway/{ => config}/chat.ts (100%) create mode 100644 src/modules/lushway/config/core.ts create mode 100644 src/modules/lushway/config/join.ts create mode 100644 src/modules/lushway/loader.ts diff --git a/src/lib/chat/chat.ts b/src/lib/chat/chat.ts index d2da4dd9..f92747e9 100644 --- a/src/lib/chat/chat.ts +++ b/src/lib/chat/chat.ts @@ -4,6 +4,7 @@ import { table } from 'lib/database/abstract' import { i18n, noI18n } from 'lib/i18n/text' import { Settings } from 'lib/settings' import { msold } from 'lib/utils/ms-old' +import { Singleton } from 'lib/utils/singleton' import './command' export declare namespace Chat { @@ -20,14 +21,7 @@ export declare namespace Chat { } } -export abstract class Chat { - private static instance?: Chat - - static getInstance(): Chat { - if (!this.instance) throw new Error('Chat.getInstance: Chat is not configured!') - return this.instance - } - +export abstract class Chat extends Singleton { muteDb = table('chatMute') settings = Settings.world(...Settings.worldCommon, { @@ -82,8 +76,7 @@ export abstract class Chat { chatListener: (arg0: ChatSendBeforeEvent) => void constructor() { - if (Chat.instance) throw new Error('Chat was already initialized!') - Chat.instance = this + super() world.afterEvents.worldLoad.subscribe(() => { this.updateCooldown() diff --git a/src/lib/lib.d.ts b/src/lib/lib.d.ts index a6320018..b573e261 100644 --- a/src/lib/lib.d.ts +++ b/src/lib/lib.d.ts @@ -98,10 +98,6 @@ declare module '@minecraft/server' { prevRole?: Role quests?: import('./quest/quest').Quest.DB achivs?: import('./achievements/achievement').Achievement.DB - join?: { - position?: number[] - stage?: number - } unlockedPortals?: string[] } } diff --git a/src/modules/commands/mail.ts b/src/lib/mail/command.ts similarity index 66% rename from src/modules/commands/mail.ts rename to src/lib/mail/command.ts index 30479e24..afabdcd6 100644 --- a/src/modules/commands/mail.ts +++ b/src/lib/mail/command.ts @@ -1,11 +1,14 @@ -import { Player } from '@minecraft/server' +import { Player, world } from '@minecraft/server' +import { MinecraftItemTypes } from '@minecraft/vanilla-data' import { ActionForm } from 'lib/form/action' import { ArrayForm } from 'lib/form/array' import { ask } from 'lib/form/message' -import { i18n, i18nPlural } from 'lib/i18n/text' +import { ModalForm } from 'lib/form/modal' +import { BUTTON } from 'lib/form/utils' +import { i18n } from 'lib/i18n/text' import { Mail } from 'lib/mail' import { Join } from 'lib/player-join' -import { Menu } from 'lib/rpg/menu' +import { is } from 'lib/roles' import { Settings } from 'lib/settings' import { Rewards } from 'lib/utils/rewards' @@ -14,7 +17,9 @@ const command = new Command('mail') .setPermissions('member') .executes(ctx => mailMenu(ctx.player)) -const getSettings = Settings.player(...Menu.settings, { +const mailGroup = [i18n`Почта\n§7Прочтение сообщения, инфо при входе`, 'mail'] as const + +const getSettings = Settings.player(...mailGroup, { mailReadOnOpen: { name: i18n`Читать письмо при открытии`, description: i18n`Помечать ли письмо прочитанным при открытии`, @@ -27,7 +32,7 @@ const getSettings = Settings.player(...Menu.settings, { }, }) -const getJoinSettings = Settings.player(...Join.settings.extend, { +const getJoinSettings = Settings.player(...Join.getPlayerSettings.extend, { unreadMails: { name: i18n`Почта`, description: i18n`Показывать ли при входе сообщение с кол-вом непрочитанных`, @@ -35,8 +40,18 @@ const getJoinSettings = Settings.player(...Join.settings.extend, { }, }) +world.afterEvents.playerSpawn.subscribe(({ player }) => { + if (!getJoinSettings(player).unreadMails) return + + const unreadCount = Mail.getUnreadMessagesCount(player.id) + if (unreadCount === 0) return + + player.info(i18n.join`${i18n.header`Почта:`} ${i18n`У вас ${unreadCount} непрочитанных сообщений!`} ${command}`) +}) + export function mailMenu(player: Player, back?: VoidFunction) { - new ArrayForm(i18n`Почта`.badge(Mail.getUnreadMessagesCount(player.id)), Mail.getLetters(player.id)) + const letters = Mail.getLetters(player.id) + new ArrayForm(i18n`Почта`.badge(Mail.getUnreadMessagesCount(player.id)), letters) .filters({ unread: { name: i18n`Непрочитанные`, @@ -49,15 +64,51 @@ export function mailMenu(player: Player, back?: VoidFunction) { value: false, }, sort: { - name: i18n`Соритровать по`, + name: i18n`Сортировать по`, value: [ ['date', i18n`Дате`], ['name', i18n`Имени`], ], }, }) + .addCustomButtonBeforeArray((f, _, back) => { + if (letters.length) { + f.button('Прочитать все\n§7(и собрать награды если есть)', () => { + Mail.readAllAndClaimRewards(player) + player.success() + mailMenu(player) + }) + } else { + f.button('Все прочитано', back) + } + + if (is(player.id, 'moderator')) { + f.button('Объявление', BUTTON['+'], () => { + new ModalForm('Объявление для всего сервера') + .addTextField('Заголовок', 'вы крутые там д0а') + .addTextField('Строка 1', 'мы вас поздравляем') + .addTextField('Строка2', 'вы теперь можете') + .addTextField('Строка3', 'читать все сообщения в почте') + .addTextField('Строка 4', 'да') + .addTextField('Строка5', 'вот.') + .addSlider('Алмазов за прочтение', 0, 100, 1, 5) + .show(player, (ctx, ...args) => { + const diamonds = args.pop() as number + const rewards = new Rewards() + if (diamonds) rewards.item(MinecraftItemTypes.Diamond, diamonds) + Mail.sendMultiple( + [...Player.database.keys()], + i18n`${args[0]}`, + i18n`${args.slice(1).filter(Boolean).join('\n')}`, + rewards, + ) + mailMenu(player) + }) + }) + } + }) .button(({ letter, index }) => { - const name = `${letter.read ? '§7' : '§f'}${letter.title}${letter.read ? '\n§8' : '§c*\n§7'}${letter.content}` + const name = `${letter.read ? '§7' : '§f'}${letter.title.replace(/§./g, '')}${letter.read ? '\n§8' : '§c*\n§7'}${letter.content.replace(/§./g, '')}` return [ name, () => { @@ -144,12 +195,3 @@ function letterDetailsMenu( form.show(player) } - -Join.onMoveAfterJoin.subscribe(({ player }) => { - if (!getJoinSettings(player).unreadMails) return - - const unreadCount = Mail.getUnreadMessagesCount(player.id) - if (unreadCount === 0) return - - player.info(i18n.join`${i18n.header`Почта:`} ${i18nPlural`У вас ${unreadCount} непрочитанных сообщений!`} ${command}`) -}) diff --git a/src/lib/mail.test.ts b/src/lib/mail/index.test.ts similarity index 96% rename from src/lib/mail.test.ts rename to src/lib/mail/index.test.ts index bdce007c..c97392d5 100644 --- a/src/lib/mail.test.ts +++ b/src/lib/mail/index.test.ts @@ -3,7 +3,7 @@ import 'lib/extensions/player' import { Mail } from 'lib/mail' import { Rewards } from 'lib/utils/rewards' import { TEST_clearDatabase } from 'test/utils' -import { i18n } from './i18n/text' +import { i18n } from '../i18n/text' describe('mail', () => { beforeEach(() => { diff --git a/src/lib/mail.ts b/src/lib/mail/index.ts similarity index 93% rename from src/lib/mail.ts rename to src/lib/mail/index.ts index c26f26a0..884d490b 100644 --- a/src/lib/mail.ts +++ b/src/lib/mail/index.ts @@ -1,10 +1,10 @@ import { Player } from '@minecraft/server' import { Rewards } from 'lib/utils/rewards' -import { defaultLang } from './assets/lang' -import { table } from './database/abstract' -import { Message } from './i18n/message' -import { i18n, noI18n } from './i18n/text' +import { defaultLang } from '../assets/lang' +import { table } from '../database/abstract' +import { Message } from '../i18n/message' +import { i18n, noI18n } from '../i18n/text' /** A global letter is a letter sent to multiple players */ interface GlobalLetter { @@ -44,6 +44,7 @@ export class Mail { this.dbPlayers .get(playerId) // TODO Use player offline lang once added + .push({ read: false, title: title.to(defaultLang), @@ -61,10 +62,10 @@ export class Mail { /** * Sends a mail to multiple players * - * @param playerIds The recievers - * @param title The letter title - * @param content The letter content - * @param rewards The attached rewards + * @param {string[]} playerIds The recievers + * @param {string} title The letter title + * @param {string} content The letter content + * @param {Rewards} rewards The attached rewards */ static sendMultiple(playerIds: readonly string[], title: Message, content: Message, rewards = new Rewards()) { let id = new Date().toISOString() diff --git a/src/lib/player-join.ts b/src/lib/player-join.ts index 9a0fbda7..664569fe 100644 --- a/src/lib/player-join.ts +++ b/src/lib/player-join.ts @@ -1,43 +1,36 @@ import { Player, system, world } from '@minecraft/server' -import { sendPacketToStdout } from 'lib/bds/api' import { EventSignal } from 'lib/event-signal' -import { i18n, noI18n } from 'lib/i18n/text' +import { i18n } from 'lib/i18n/text' import { Settings } from 'lib/settings' import { util } from 'lib/util' import { Core } from './extensions/core' import { ActionbarPriority } from './extensions/on-screen-display' -import { getFullname } from './get-fullname' +import { Singleton } from './utils/singleton' +import { WeakPlayerMap } from './weak-player-storage' -class JoinBuilder { - config = { - /** Array with strings to show on join. They will change every second. You can use $ from animation.vars */ - title_animation: { - stages: ['» $title «', '» $title «'], - /** @type {Record} */ - vars: { title: `${Core.name}§r§f` }, - }, - actionBar: '', // Optional - subtitle: i18n.nocolor`Добро пожаловать!`, // Optional - messages: { - air: i18n.nocolor`§8Очнулся в воздухе`, - ground: i18n.nocolor`§8Проснулся`, - sound: 'break.amethyst_cluster', - }, +export declare namespace Join { + interface Database { + position?: number[] + stage?: number } - onMoveAfterJoin = new EventSignal<{ player: Player; joinTimes: number; firstJoin: boolean }>() + type Where = 'air' | 'ground' +} - onFirstTimeSpawn = new EventSignal() +export abstract class Join extends Singleton { + static onMoveAfterJoin = new EventSignal<{ player: Player; joinTimes: number; firstJoin: boolean }>() + + constructor() { + super() + system.runPlayerInterval(player => this.onInterval(player), 'joinInterval', 20) - eventsDefaultSubscribers = { - time: this.onMoveAfterJoin.subscribe(({ player, firstJoin }) => { - if (!firstJoin) player.tell(i18n.nocolor`${timeNow()}, ${player.name}!\n§r§3Время §b• §3${shortTime()}`) - }, -1), - playerSpawn: world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { - if (!initialSpawn) return - this.setPlayerJoinPosition(player) - EventSignal.emit(this.onFirstTimeSpawn, player) - }), + new Command('join') + .setDescription(i18n`Имитирует первый вход`) + .setPermissions('techAdmin') + .executes(ctx => { + const player = ctx.player + this.emitFirstJoin(player) + }) } private playerAt(player: Player) { @@ -46,130 +39,150 @@ class JoinBuilder { return [location.x, location.y, location.z, rotation.x, rotation.y].map(Math.floor) } - setPlayerJoinPosition(player: Player) { - player.database.join ??= {} + /** Used when you need to e.g. teleport users when they join */ + playerSpawnEventSubscriber = world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { + if (!initialSpawn) return + this.setPlayerJoinPosition(player) + }) + /** Used when you need to e.g. teleport users when they join */ + setPlayerJoinPosition(player: Player) { if (!player.isValid) return - player.database.join.position = this.playerAt(player) + this.joinPositions.set(player, { position: this.playerAt(player) }) } - constructor() { - system.runPlayerInterval( - player => { - if (!player.isValid) return - const db = player.database.join - - if (Array.isArray(db?.position)) { - const time = util.benchmark('joinInterval', 'join') - const notMoved = Array.equals(db.position, this.playerAt(player)) - - if (notMoved) { - // Player still stays at joined position... - if (player.isOnGround || player.isFlying) { - // Player will not move, show animation - db.stage = db.stage ?? -1 - db.stage++ - if (isNaN(db.stage) || db.stage >= Join.config.title_animation.stages.length) db.stage = 0 - - // Creating title - let title = Join.config.title_animation.stages[db.stage] ?? '' - for (const [key, value] of Object.entries(Join.config.title_animation.vars)) { - title = title.replace('$' + key, value) - } - - // Show actionBar - if (Join.config.actionBar) { - player.onScreenDisplay.setActionBar(Join.config.actionBar, ActionbarPriority.Highest) - } - - player.onScreenDisplay.setHudTitle(title, { - fadeInDuration: 0, - fadeOutDuration: 20, - stayDuration: 40, - subtitle: Join.config.subtitle.to(player.lang), - }) - } else { - // Player joined in air - this.join(player, 'air') - } - } else { - // Player moved on ground - this.join(player, 'ground') - } - - time() - } - }, - 'joinInterval', - 20, - ) + protected joinPositions = new WeakPlayerMap({ removeOnLeave: true }) - new Command('join') - .setDescription(i18n`Имитирует первый вход`) - .setPermissions('member') - .executes(ctx => { - const player = ctx.player - this.emitFirstJoin(player) - }) - } + private onInterval(player: Player) { + if (!player.isValid) return + const db = this.joinPositions.get(player) - private join(player: Player, where: 'air' | 'ground') { - delete player.database.join - player.scores.joinTimes++ + if (Array.isArray(db?.position)) { + const time = util.benchmark('joinInterval', 'join') + const notMoved = Array.equals(db.position, this.playerAt(player)) - const message = Join.config.messages[where] + if (notMoved) { + if (player.isOnGround || player.isFlying) this.notMovingInterval?.(player, db) + else this.joinedAt(player, 'air') + } else this.joinedAt(player, 'ground') - __SERVER__ && - sendPacketToStdout('joinOrLeave', { - name: player.name, - role: getFullname(player, { name: false }), - status: 'move', - where, - print: noI18n.nocolor`${'§l§f' + player.name} ${getFullname(player, { name: false })}: ${message}`, - }) + time() + } + } - for (const other of world.getPlayers()) { - if (other.id === player.id) continue + private joinedAt(player: Player, where: Join.Where) { + this.joinPositions.delete(player) + player.scores.joinTimes++ - const settings = this.settings(other) - if (settings.sound) other.playSound(Join.config.messages.sound) - if (settings.message) other.tell(i18n.nocolor.join`§7${player.name} ${message}`) - } + this.onJoinMove(where, player) - EventSignal.emit(this.onMoveAfterJoin, { + EventSignal.emit(Join.onMoveAfterJoin, { player, joinTimes: player.scores.joinTimes, firstJoin: player.scores.joinTimes === 1, }) } - settings = Settings.player(i18n`Вход\n§7Все действия, связанные со входом`, 'join', { - message: { name: i18n`Сообщение`, description: i18n`о входе других игроков`, value: true }, - sound: { name: i18n`Звук`, description: i18n`при входе игроков`, value: true }, + protected notMovingInterval?(player: Player, db: Join.Database): void + + protected abstract onJoinMove(where: Join.Where, player: Player): void + + /** Used when you test how join looks or when you add /wipe like command */ + emitFirstJoin(player: Player) { + EventSignal.emit(Join.onMoveAfterJoin, { player, joinTimes: 1, firstJoin: true }) + } + + static getPlayerSettings = Settings.player(i18n`Вход\n§7Все действия, связанные со входом`, 'join', { time: { name: i18n`Время`, description: i18n`при входе`, value: true }, }) - emitFirstJoin(player: Player) { - EventSignal.emit(this.onMoveAfterJoin, { player, joinTimes: 1, firstJoin: true }) + protected timeListener = Join.onMoveAfterJoin.subscribe(({ player, firstJoin }) => { + if (!firstJoin && Join.getPlayerSettings(player).time) + player.tell(i18n.nocolor`${this.timeNow()}, ${player.name}!\n§r§3Время §b• §3${this.shortTime()}`) + }, -1) + + /** Выводит строку времени */ + private timeNow(): Text { + const time = new Date(Date()).getHours() + 3 + if (time < 6) return i18n`§9Доброй ночи` + if (time < 12) return i18n`§6Доброе утро` + if (time < 18) return i18n`§bДобрый день` + return i18n`§3Добрый вечер` + } + + // TODO Use date.toHHMMSS + /** Выводит время в формате 00:00 */ + private shortTime(): string { + const time = new Date(Date()) + time.setHours(time.getHours() + 3) + return `${time.getHours()}:${String(time.getMinutes()).padStart(2, '0')}` } } -export const Join = new JoinBuilder() +export abstract class JoinWithTitle extends Join { + config = { + /** Array with strings to show on join. They will change every second. You can use $ from animation.vars */ + titleAnimation: { + stages: ['» $title «', '» $title «'], + vars: { title: `${Core.name}§r§f` } as Record, + }, + actionBar: '', // Optional + subtitle: i18n.nocolor`Добро пожаловать!`, // Optional + } + + protected notMovingInterval(player: Player, db: Join.Database): void { + if (this.config.titleAnimation.stages.length) { + db.stage = db.stage ?? -1 + db.stage++ + if (isNaN(db.stage) || db.stage >= this.config.titleAnimation.stages.length) db.stage = 0 + + // Creating title + let title = this.config.titleAnimation.stages[db.stage] ?? '' + for (const [key, value] of Object.entries(this.config.titleAnimation.vars)) { + title = title.replace('$' + key, value) + } + + player.onScreenDisplay.setHudTitle(title, { + fadeInDuration: 0, + fadeOutDuration: 20, + stayDuration: 40, + subtitle: this.config.subtitle.to(player.lang), + }) + } -/** Выводит строку времени */ -function timeNow(): Text { - const time = new Date(Date()).getHours() + 3 - if (time < 6) return i18n`§9Доброй ночи` - if (time < 12) return i18n`§6Доброе утро` - if (time < 18) return i18n`§bДобрый день` - return i18n`§3Добрый вечер` + // Show actionBar + if (this.config.actionBar) { + player.onScreenDisplay.setActionBar(this.config.actionBar, ActionbarPriority.Highest) + } + } } -// TODO Use date.toHHMMSS -/** Выводит время в формате 00:00 */ -function shortTime(): string { - const time = new Date(Date()) - time.setHours(time.getHours() + 3) - const min = String(time.getMinutes()) - return `${time.getHours()}:${min.length == 2 ? min : '0' + min}` +export abstract class JoinWithMessage extends JoinWithTitle { + protected messages = { + air: i18n.nocolor`§8Очнулся в воздухе`, + ground: i18n.nocolor`§8Проснулся`, + } + + protected sound = 'break.amethyst_cluster' + + protected onJoinMove(where: Join.Where, player: Player) { + const message = this.messages[where] + + this.onJoinMoveMessage(player, where, message) + + for (const other of world.getPlayers()) { + if (other.id === player.id) continue + + const settings = this.getPlayerSettingsWithMessage(other) + if (settings.sound) other.playSound(this.sound) + if (settings.message) other.tell(i18n.nocolor.join`§7${player.name} ${message}`) + } + } + + abstract onJoinMoveMessage(player: Player, where: Join.Where, message: Text): void + + getPlayerSettingsWithMessage = Settings.player(...Join.getPlayerSettings.extend, { + message: { name: i18n`Сообщение`, description: i18n`о входе других игроков`, value: true }, + sound: { name: i18n`Звук`, description: i18n`при входе игроков`, value: true }, + }) } diff --git a/src/lib/rpg/newbie.ts b/src/lib/rpg/newbie.ts index b9ed3a5d..08ecbb6a 100644 --- a/src/lib/rpg/newbie.ts +++ b/src/lib/rpg/newbie.ts @@ -56,7 +56,6 @@ export function enterNewbieMode(player: Player, resetAnarchyOnlineTime = true) { player.setProperty(property, true) } -Join.onFirstTimeSpawn.subscribe(enterNewbieMode) Join.onMoveAfterJoin.subscribe(({ player }) => { const value = isNewbie(player) if (value !== player.getProperty(property)) player.setProperty(property, value) diff --git a/src/lib/utils/singleton.ts b/src/lib/utils/singleton.ts new file mode 100644 index 00000000..d4970790 --- /dev/null +++ b/src/lib/utils/singleton.ts @@ -0,0 +1,17 @@ +export class Singleton { + private static instance?: Singleton + + static getInstance(this: abstract new (...args: any) => T) { + const self = this as unknown as typeof Singleton + if (!self.instance) throw new Error('getInstance: ' + self.name + 'is not initialized!') + return self.instance as T + } + + constructor() { + if ((this.constructor as typeof Singleton).instance) { + throw new Error(this.constructor.name + ' is already initialized!') + } + + ;(this.constructor as typeof Singleton).instance = this + } +} diff --git a/src/modules/commands/ban.ts b/src/modules/commands/ban.ts deleted file mode 100644 index 5cec3b70..00000000 --- a/src/modules/commands/ban.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement ban diff --git a/src/modules/commands/db.ts b/src/modules/commands/db.ts deleted file mode 100644 index 47145ec4..00000000 --- a/src/modules/commands/db.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* i18n-ignore */ - -import { Player, system, world } from '@minecraft/server' - -import { UnknownTable, getProvider } from 'lib/database/abstract' -import { ActionForm } from 'lib/form/action' -import { ModalForm } from 'lib/form/modal' -import { i18n, noI18n } from 'lib/i18n/text' -import { stringifyBenchmarkResult } from './stringifyBenchmarkReult' -import { util } from 'lib/util' -import { inspect } from 'lib/util' -import { getRole } from 'lib/roles' -import { ROLES } from 'lib/roles' -import { ArrayForm } from 'lib/form/array' - -new Command('db') - .setDescription('Просматривает базу данных') - .setPermissions('techAdmin') - .executes(ctx => selectTable(ctx.player, true)) - -function selectTable(player: Player, firstCall?: true) { - const form = new ActionForm('Таблицы данных') - for (const [tableId, table] of Object.entries(getProvider().tables)) { - const name = noI18n`${tableId} ${`§7${table.size}`} ${getProvider().getRawTableData(tableId).length / (256 * 1024)}§r` - - form.button(name, () => showTable(player, tableId, table)) - } - form.show(player) - if (firstCall) player.info('Закрой чат!') -} - -function showTable(player: Player, tableId: string, table: UnknownTable) { - const selfback = () => showTable(player, tableId, table) - const keys = [...table.keys()] - new ArrayForm(`${tableId} ${keys.length}`, keys) - .addCustomButtonBeforeArray(form => { - form - .button('§3Новое значение§r', () => { - changeValue(player, null, (newVal, key) => table.set(key, newVal), selfback) - }) - .ask('§cОчистить таблицу', 'ДААА УДАЛИТЬ ВСЕ НАФИГ', () => { - for (const key of table.keys()) table.delete(key) - }) - }) - .back(() => selectTable(player)) - .button(key => { - let name = key - if (tableId === 'player') { - const playerDatabase = table as typeof Player.database - name = `${playerDatabase.get(key).name} ${(ROLES[getRole(key)] as Text | undefined)?.to(player.lang) ?? '§7Без роли'}\n§8(${key})` - } else { - name += `\n§7${JSON.stringify(table.get(key)).slice(0, 200).replace(/"/g, '')}` - } - - return [name, () => tableProperty(key, table, player, selfback)] as const - }) - .show(player) -} - -function tableProperty(key: string, table: UnknownTable, player: Player, back: VoidFunction) { - key = key + '' - let value: unknown - let failedToLoad = false - - try { - value = table.get(key) - } catch (e) { - console.error(e) - value = 'a' - failedToLoad = true - } - - new ActionForm( - '§3Ключ ' + key, - `§7Тип: §f${typeof value}\n ${failedToLoad ? '\n§cОшибка при получении данных из таблицы!§r\n\n' : ''}\n${inspect(value)}\n `, - ) - .button('Изменить', () => - changeValue( - player, - value, - newValue => { - table.set(key, newValue) - player.tell(inspect(value) + '§r -> ' + inspect(newValue)) - }, - () => tableProperty(key, table, player, back), - key, - ), - ) - .button('Переименовать', () => { - new ModalForm('Переименовать').addTextField('Ключ', 'останется прежним', key).show(player, (ctx, newKey) => { - if (newKey) { - player.success(i18n`Renamed ${key} -> ${newKey}`) - table.set(newKey, table.get(key)) - } else player.info(i18n`Key ${key} left as is`) - }) - }) - .button(noI18n.error`Удалить`, () => { - table.delete(key) - system.delay(back) - }) - .addButtonBack(back, player.lang) - .show(player) -} - -function changeValue( - player: Player, - value: unknown, - onChange: (value: unknown, key: string) => void, - back: VoidFunction, - key?: string, -) { - let valueType = typeof value - const typeDropdown = ['string', 'number', 'boolean', 'object'] - if (value) typeDropdown.unshift('Оставить прежний §7(' + valueType + ')') - const stringifiedValue = value ? JSON.stringify(value) : '' - - new ModalForm('§3+Значение ') - .addTextField('Ключ', ' ', key) - .addTextField('Значение', 'оставь пустым для отмены', stringifiedValue) - .addDropdown('Тип', typeDropdown) - .show(player, (ctx, key, input: string, type: string) => { - if (!input) back() - let newValue: unknown = input - - if ( - !type.includes(valueType) && - (type === 'string' || type === 'object' || type === 'boolean' || type === 'number') - ) { - valueType = type - } - switch (valueType) { - case 'number': - newValue = Number(input) - break - - case 'boolean': - newValue = input === 'true' - break - - case 'object': - try { - newValue = JSON.parse(input) - } catch (e: unknown) { - world.say(`§4DB §cJSON.parse error: ${(e as Error).message}`) - return - } - - break - } - onChange(newValue, key) - }) -} - -const cmd = new Command('benchmark') - .setAliases('bench') - .setDescription('Показывает время работы серверных систем') - .setPermissions('techAdmin') - -cmd - .string('type', true) - .boolean('pathes', true) - .boolean('sort', true) - .array('output', ['form', 'chat', 'log'], true) - .executes((ctx, type = 'timers', timerPathes = false, sort = true, output = 'form') => { - if (!(type in util.benchmark.results)) - return ctx.error( - 'Неизвестный тип бенчмарка! Доступные типы: \n §f' + Object.keys(util.benchmark.results).join('\n '), - ) - - const result = stringifyBenchmarkResult({ type: type, timerPathes, sort }) - - switch (output) { - case 'form': { - const show = () => { - new ActionForm('Benchmark', result) - .button('Refresh', null, show) - .button('Exit', null, () => void 0) - .show(ctx.player) - } - return show() - } - case 'chat': - return ctx.reply(result) - case 'log': - return console.log(result) - } - }) diff --git a/src/modules/commands/index.ts b/src/modules/commands/index.ts index c1af2255..b57a7254 100644 --- a/src/modules/commands/index.ts +++ b/src/modules/commands/index.ts @@ -1,20 +1,17 @@ import './camera' -import './db' import './gamemode' import './help' import './items' import './kill' -import './leaderboard' -import './mail' import './name' import './pid' import './ping' +import './player' import './role' import './rtp' import './rules' import './scores' import './send' -import './settings' import './shell' import './sit' import './socials' @@ -22,4 +19,3 @@ import './stats' import './tp' import './version' import './wipe' -import './player' diff --git a/src/modules/commands/leaderboard.ts b/src/modules/commands/leaderboard.ts deleted file mode 100644 index 28a66b0b..00000000 --- a/src/modules/commands/leaderboard.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* i18n-ignore */ - -import { Player, world } from '@minecraft/server' -import { ActionForm } from 'lib/form/action' -import { ModalForm } from 'lib/form/modal' -import { BUTTON } from 'lib/form/utils' -import { Leaderboard, LeaderboardInfo } from 'lib/rpg/leaderboard' -import { Vec } from 'lib/vector' - -new Command('leaderboard') - .setAliases('leaderboards', 'lb') - .setDescription('Управляет таблицами лидеров') - .setPermissions('techAdmin') - .executes(ctx => { - leaderboardMenu(ctx.player) - }) - -function leaderboardMenu(player: Player) { - const form = new ActionForm('Таблицы лидеров') - - form.button('§3Добавить', BUTTON['+'], p => editLeaderboard(p)) - - for (const lb of Leaderboard.all.values()) { - form.button(info(lb), () => { - editLeaderboard(player, lb) - }) - } - - form.show(player) -} - -function info(lb: Leaderboard) { - return lb.info.displayName + '\n' + Vec.string(Vec.floor(lb.info.location)) -} - -function editLeaderboard(player: Player, lb?: Leaderboard, data: Partial = lb?.info ?? {}) { - const action = lb ? 'Изменить ' : 'Выбрать ' - function update() { - if (!lb && isRequired(data)) { - lb = Leaderboard.createLeaderboard(data) - } - if (lb) { - lb.update() - lb.updateLeaderboard() - } - - editLeaderboard(player, lb, lb ? void 0 : data) - } - - function warn(...keys: (keyof typeof data)[]) { - if (keys.find(k => typeof data[k] === 'undefined')) return ' §e(!)' - return '' - } - - const form = new ActionForm('Таблица лидеров', lb ? info(lb) : '') - .addButtonBack(() => leaderboardMenu(player), player.lang) - .button(action + 'целевую таблицу' + warn('displayName', 'objective'), () => { - new ModalForm('Изменение целевой таблицы') - .addDropdownFromObject( - 'Выбрать из списка', - Object.fromEntries(world.scoreboard.getObjectives().map(e => [e.id, e.displayName])), - { defaultValue: data.displayName }, - ) - // .addTextField( - // 'Отображаемое имя', - // data.displayName ? 'Не изменится' : 'Имя счета по умолчанию', - // data.displayName - // ) - .show( - player, - ( - ctx, - id, - - // displayName - ) => { - const scoreboard = world.scoreboard.getObjective(id) - if (!scoreboard) return ctx.error('Скора не существует! Его удалили пока ты редактировал(а) форму хахаха') - - data.objective = id - data.displayName = scoreboard.displayName - if (lb) lb.scoreboard = scoreboard - // if (!data.displayName) displayName = scoreboard.displayName - // if (displayName) data.displayName = displayName - update() - }, - ) - }) - .button(action + 'позицию' + warn('location', 'dimension'), () => { - const dimensions: Record = { - overworld: 'Верхний мир', - nether: 'Нижний мир', - end: 'Край', - } - new ModalForm('Позиция') - .addTextField('Позиция', 'Не изменится', Vec.string(data.location ?? Vec.floor(player.location))) - .addDropdownFromObject('Измерение', dimensions, { - defaultValueIndex: Object.keys(dimensions).findIndex(e => e === player.dimension.type), - }) - .show(player, (ctx, location, dimension) => { - if (location) { - const l = location.split(' ').map(Number) - if (l.length !== 3 || l.find(isNaN)) - return ctx.error("Неверная локация '" + location + "', ожидался формат 'x y z' с числами") - - const [x, y, z] = l as [number, number, number] - data.location = { x, y, z } - } - data.dimension = dimension - update() - }) - }) - .button(action + 'стиль' + warn('style'), () => { - const styles: Record = { - gray: '§7Серый', - white: '§fБелый', - green: '§2Зеленый', - } - new ModalForm('Стиль') - .addDropdownFromObject('Стиль', styles, { - defaultValueIndex: data.style ? Object.keys(styles).findIndex(v => v === data.style) : void 0, - }) - .show(player, (ctx, style) => { - data.style = style - update() - }) - }) - - if (lb) { - form.button('Переместить к себе', () => { - data.location = Vec.floor(player.location).add(Vec.one.multiply(0.5)) - data.dimension = player.dimension.type - update() - }) - form.ask('§cУдалить таблицу лидеров', '§cУдалить', () => lb && lb.remove(), 'Отмена') - } - - form.show(player) -} - -function isRequired(data: Partial): data is LeaderboardInfo { - return !!data.dimension && !!data.displayName && !!data.location && !!data.objective && !!data.style -} diff --git a/src/modules/commands/mute.ts b/src/modules/commands/mute.ts deleted file mode 100644 index f694668b..00000000 --- a/src/modules/commands/mute.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement mute diff --git a/src/modules/commands/settings.ts b/src/modules/commands/settings.ts deleted file mode 100644 index 610051e8..00000000 --- a/src/modules/commands/settings.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { i18n } from 'lib/i18n/text' -import { playerSettingsMenu, worldSettingsMenu } from 'lib/settings' - -new Command('settings') - .setAliases('options') - .setPermissions('member') - .setDescription(i18n`Настройки`) - .executes(ctx => { - playerSettingsMenu(ctx.player) - }) - -new Command('wsettings') - .setPermissions('techAdmin') - .setDescription(i18n`Настройки мира`) - .executes(ctx => { - worldSettingsMenu(ctx.player) - }) diff --git a/src/modules/commands/stringifyBenchmarkReult.ts b/src/modules/commands/stringifyBenchmarkReult.ts deleted file mode 100644 index 853a1738..00000000 --- a/src/modules/commands/stringifyBenchmarkReult.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { TIMERS_PATHES } from 'lib/extensions/system' -import { util } from 'lib/util' - -/** - * It takes the benchmark results and sorts them by average time, then it prints them out in a nice format - * - * @returns A string. - */ -export function stringifyBenchmarkResult({ type = 'test', timerPathes = false, sort = true } = {}) { - const results = util.benchmark.results[type] - if (!results) return `No results for type ${type}` - - let output = '' - let res = Object.entries(results) - - if (sort) res = res.sort((a, b) => a[1] - b[1]) - - const max = Math.max(...res.map(e => e[1])) - - for (const [key, average] of res) { - const color = colors.find(e => e[0] > average)?.[1] ?? '§4' - const isPath = timerPathes && key in TIMERS_PATHES - - output += `§3Label §f${key}§r\n` - output += `§3| §7average: ${color}${formatDecimal(average)}ms\n` - // output += `§3| §7total time: §f${totalTime}ms\n` - // output += `§3| §7call count: §f${totalCount}\n` - const percent = average / max - if (percent !== 1) output += `§3| §7faster: §f${~~(100 - percent * 100)}%%\n` - if (isPath) output += `§3| §7path: §f${getPath(key)}\n` - output += '\n\n' - } - return output -} - -function formatDecimal(num: number): string { - if (num === 0) return '0' - - if (num > 0.01) return num.toFixed(2) - - const str = num.toFixed(20) // Ensure we have enough decimal places - const match = /^0\.0*[1-9]{0,3}/.exec(str) - - return match?.[0] ?? str -} - -const colors: [number, string][] = [ - [0.1, '§a'], - [0.3, '§2'], - [0.5, '§g'], - [0.65, '§6'], - [0.8, '§c'], -] - -export function getPath(key: string) { - return `\n${TIMERS_PATHES[key]}`.replace(/\n/g, '\n§3| §r') -} diff --git a/src/modules/commands/wipe.ts b/src/modules/commands/wipe.ts index 2aa323e8..e0741956 100644 --- a/src/modules/commands/wipe.ts +++ b/src/modules/commands/wipe.ts @@ -238,7 +238,7 @@ function wipe(player: Player) { for (let i = 0; i <= 26; i++) player.runCommand(`replaceitem entity @s slot.enderchest ${i} air`) - system.runTimeout(() => Join.emitFirstJoin(player), 'clear', 30) + system.runTimeout(() => Join.getInstance().emitFirstJoin(player), 'clear', 30) } function exitFromAllQuests(player: Player) { diff --git a/src/modules/loader.ts b/src/modules/loader.ts index 09c990d2..69408a89 100644 --- a/src/modules/loader.ts +++ b/src/modules/loader.ts @@ -3,7 +3,7 @@ import 'lib' import './anticheat/index' import './survival/import' -import './lushway/chat' +import './lushway/loader' import './test/test' import './wiki/wiki' import './world-edit/builder' diff --git a/src/modules/lushway/chat.ts b/src/modules/lushway/config/chat.ts similarity index 100% rename from src/modules/lushway/chat.ts rename to src/modules/lushway/config/chat.ts diff --git a/src/modules/lushway/config/core.ts b/src/modules/lushway/config/core.ts new file mode 100644 index 00000000..9dc3c9af --- /dev/null +++ b/src/modules/lushway/config/core.ts @@ -0,0 +1,3 @@ +import { Core } from 'lib/extensions/core' + +Core.name = '§aLush§fWay' diff --git a/src/modules/lushway/config/join.ts b/src/modules/lushway/config/join.ts new file mode 100644 index 00000000..2dc452b4 --- /dev/null +++ b/src/modules/lushway/config/join.ts @@ -0,0 +1,20 @@ +import { Player } from '@minecraft/server' +import { sendPacketToStdout } from 'lib/bds/api' +import { getFullname } from 'lib/get-fullname' +import { noI18n } from 'lib/i18n/text' +import { JoinWithMessage } from 'lib/player-join' + +export class LushWayJoin extends JoinWithMessage { + onJoinMoveMessage(player: Player, where: 'air' | 'ground', message: Text): void { + __SERVER__ && + sendPacketToStdout('joinOrLeave', { + name: player.name, + role: getFullname(player, { name: false }), + status: 'move', + where, + print: noI18n.nocolor`${'§l§f' + player.name} ${getFullname(player, { name: false })}: ${message}`, + }) + } +} + +new LushWayJoin() diff --git a/src/modules/lushway/loader.ts b/src/modules/lushway/loader.ts new file mode 100644 index 00000000..431fcfca --- /dev/null +++ b/src/modules/lushway/loader.ts @@ -0,0 +1,4 @@ +import './config/core' + +import './config/chat' +import './config/join' diff --git a/src/modules/places/spawn.ts b/src/modules/places/spawn.ts index c22d54b2..3c179798 100644 --- a/src/modules/places/spawn.ts +++ b/src/modules/places/spawn.ts @@ -71,12 +71,12 @@ class SpawnBuilder extends AreaWithInventory { .setPermissions('everybody') .setDescription(i18n.nocolor`§r§bПеремещает на спавн`) - world.afterEvents.playerSpawn.unsubscribe(Join.eventsDefaultSubscribers.playerSpawn) + world.afterEvents.playerSpawn.unsubscribe(Join.getInstance().playerSpawnEventSubscriber) world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { // Skip after death respawns if (!initialSpawn) return if (player.isSimulated()) return - if (isNotPlaying(player)) return Join.setPlayerJoinPosition(player) + if (isNotPlaying(player)) return Join.getInstance().setPlayerJoinPosition(player) // Check settings if (!this.settings(player).teleportToSpawnOnJoin) @@ -86,7 +86,11 @@ class SpawnBuilder extends AreaWithInventory { util.catch(() => { this.logger.player(player).info`Teleporting player to spawn on join` this.portal?.teleport(player) - system.runTimeout(() => Join.setPlayerJoinPosition(player), 'Spawn set player position after join', 10) + system.runTimeout( + () => Join.getInstance().setPlayerJoinPosition(player), + 'Spawn set player position after join', + 10, + ) }) }) diff --git a/src/modules/survival/menu.ts b/src/modules/survival/menu.ts index cbee4223..4d41028f 100644 --- a/src/modules/survival/menu.ts +++ b/src/modules/survival/menu.ts @@ -3,13 +3,15 @@ import { achievementsForm, achievementsFormName } from 'lib/achievements/command import { clanMenu } from 'lib/clan/menu' import { Core } from 'lib/extensions/core' import { form } from 'lib/form/new' +import { BUTTON } from 'lib/form/utils' import { i18n } from 'lib/i18n/text' import { Mail } from 'lib/mail' +import { mailMenu } from 'lib/mail/command' import { Join } from 'lib/player-join' import { questsMenu } from 'lib/quest/menu' import { Menu } from 'lib/rpg/menu' import { playerSettingsMenu } from 'lib/settings' -import { mailMenu } from 'modules/commands/mail' +import { doNothing } from 'lib/util' import { statsForm } from 'modules/commands/stats' import { baseMenu } from 'modules/places/base/base-menu' import { wiki } from 'modules/wiki/wiki' @@ -17,8 +19,6 @@ import { Anarchy } from '../places/anarchy/anarchy' import { Spawn } from '../places/spawn' import { recurForm } from './recurring-events' import { speedrunForm } from './speedrun/target' -import { BUTTON } from 'lib/form/utils' -import { doNothing } from 'lib/util' function tp( player: Player, From a832c05d5721d4a99f24db3ac37db9ecab9842c2 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:59:46 +0300 Subject: [PATCH 11/14] refactor: make it start --- src/lib/anticheat/anti-piston-abuse.ts | 29 +++ src/lib/anticheat/anti-wither-bedrock-kill.ts | 22 ++ src/lib/anticheat/ban.ts | 15 ++ src/lib/anticheat/freeze.ts | 53 +++++ src/lib/anticheat/log-provider.ts | 16 ++ src/lib/cooldownreset.ts | 4 +- src/lib/database/command.ts | 147 ++++++++++++++ src/lib/extensions/world.ts | 2 +- src/lib/mail/index.ts | 1 + src/lib/roles.test.ts | 53 ----- src/lib/roles.ts | 175 ---------------- src/lib/rpg/leaderboard/command.ts | 143 +++++++++++++ src/lib/rpg/leaderboard/index.ts | 192 ++++++++++++++++++ src/lib/util.ts | 2 + src/lib/utils/benchmark.ts | 91 +++++++++ 15 files changed, 715 insertions(+), 230 deletions(-) create mode 100644 src/lib/anticheat/anti-piston-abuse.ts create mode 100644 src/lib/anticheat/anti-wither-bedrock-kill.ts create mode 100644 src/lib/anticheat/ban.ts create mode 100644 src/lib/anticheat/freeze.ts create mode 100644 src/lib/anticheat/log-provider.ts create mode 100644 src/lib/database/command.ts delete mode 100644 src/lib/roles.test.ts delete mode 100644 src/lib/roles.ts create mode 100644 src/lib/rpg/leaderboard/command.ts create mode 100644 src/lib/rpg/leaderboard/index.ts create mode 100644 src/lib/utils/benchmark.ts diff --git a/src/lib/anticheat/anti-piston-abuse.ts b/src/lib/anticheat/anti-piston-abuse.ts new file mode 100644 index 00000000..ab5e4967 --- /dev/null +++ b/src/lib/anticheat/anti-piston-abuse.ts @@ -0,0 +1,29 @@ +import { system, world } from '@minecraft/server' +import { MinecraftBlockTypes } from '@minecraft/vanilla-data' +import { Vec } from 'lib/vector' +import { antiCheatLog } from './log-provider' + +world.afterEvents.pistonActivate.subscribe(event => { + const locations = event.piston.getAttachedBlocksLocations() + + system.runTimeout( + () => { + if (!event.block.isValid) return + + for (const location of locations) { + const block = event.block.dimension.getBlock(location) + if (block?.typeId !== MinecraftBlockTypes.Hopper) continue + + const nearbyPlayers = event.block.dimension.getPlayers({ location: event.block.location, maxDistance: 20 }) + const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') + + antiCheatLog(`ПОРШЕНЬ ДЮП ${Vec.string(event.block.location)}\n${nearbyPlayersNames}`) + + event.block.dimension.createExplosion(event.block.location, 5, { breaksBlocks: true }) + return + } + }, + 'piston dupe prevent', + 2, + ) +}) diff --git a/src/lib/anticheat/anti-wither-bedrock-kill.ts b/src/lib/anticheat/anti-wither-bedrock-kill.ts new file mode 100644 index 00000000..401bdce0 --- /dev/null +++ b/src/lib/anticheat/anti-wither-bedrock-kill.ts @@ -0,0 +1,22 @@ +import { world } from '@minecraft/server' +import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' +import { Vec } from 'lib/vector' +import { antiCheatLog } from './log-provider' + +world.afterEvents.entitySpawn.subscribe(event => { + const { entity } = event + + if (entity.typeId !== MinecraftEntityTypes.Wither) return + + const { location } = entity + const block = entity.dimension.getBlock(location) + + if (block?.typeId !== MinecraftBlockTypes.Bedrock) return + + const nearbyPlayers = event.entity.dimension.getPlayers({ location, maxDistance: 20 }) + const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') + + antiCheatLog(`ОБНАРУЖЕН АБУЗ ВИЗЕРА ${Vec.string(location)}\n${nearbyPlayersNames}`) + + entity.remove() +}) diff --git a/src/lib/anticheat/ban.ts b/src/lib/anticheat/ban.ts new file mode 100644 index 00000000..cdef101a --- /dev/null +++ b/src/lib/anticheat/ban.ts @@ -0,0 +1,15 @@ +import { system, world } from '@minecraft/server' + +new Command('ban') + .setDescription('Кикает и убирает игрока из вайтлиста') + .setPermissions('helper') + .string('playerName') + .executes((ctx, name) => { + system.delay(() => { + world.overworld.runCommand(`allowlist remove ${name}`) + world.overworld.runCommand( + `kick ${name} "Вы были забанены\nОбжаловать можно через бот техподдержки: @FolkLore_Support_bot"`, + ) + }) + ctx.player.success() + }) diff --git a/src/lib/anticheat/freeze.ts b/src/lib/anticheat/freeze.ts new file mode 100644 index 00000000..059fb656 --- /dev/null +++ b/src/lib/anticheat/freeze.ts @@ -0,0 +1,53 @@ +import { InputPermissionCategory, Player, system } from '@minecraft/server' +import { ActionbarPriority } from 'lib/extensions/on-screen-display' +import { selectPlayer } from 'lib/form/select-player' + +new Command('freeze') + .setDescription('Останавливает движение игрока до unfreeze') + .setPermissions('helper') + .string('playerName') + .executes((ctx, name) => { + if (name) { + const player = Player.getByName(name) + if (!player) return ctx.error('Player not found') + + system.delay(() => { + player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false) + player.onScreenDisplay.setActionBar('§cВы были заморожены', ActionbarPriority.Highest) + }) + return ctx.reply('Успешно') + } + selectPlayer(ctx.player, 'заморозить').then(({ player }) => { + if (!player) return ctx.player.fail('Выберите онлайн игрока') + + player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false) + player.onScreenDisplay.setActionBar('§cВы были заморожены', ActionbarPriority.Highest) + ctx.player.success() + }) + }) + +new Command('unfreeze') + .setDescription('Возвращает движение игроку') + .setPermissions('helper') + + .string('playerName') + .executes((ctx, name) => { + if (name) { + const player = Player.getByName(name) + if (!player) return ctx.error('Player not found') + + system.delay(() => { + player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true) + player.onScreenDisplay.setActionBar('§aВы были разморожены', ActionbarPriority.Highest) + }) + return ctx.reply('Успешно') + } + selectPlayer(ctx.player, 'заморозить').then(({ id }) => { + const player = Player.getById(id) + if (!player) return ctx.player.fail('Выберите онлайн игрока') + + player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true) + player.onScreenDisplay.setActionBar('§aВы были разморожены', ActionbarPriority.Highest) + ctx.player.success() + }) + }) diff --git a/src/lib/anticheat/log-provider.ts b/src/lib/anticheat/log-provider.ts new file mode 100644 index 00000000..4ae0bbb3 --- /dev/null +++ b/src/lib/anticheat/log-provider.ts @@ -0,0 +1,16 @@ +import { createLogger } from 'lib/utils/logger' + +export const antiCheatLogger = createLogger('anticheat') + +export function antiCheatLog(text: string) { + if (!log) return antiCheatLogger.warn('No provider: ', text) + + antiCheatLogger.warn(text) + log(text) +} + +let log: null | ((text: string) => void) = null + +export function registerAntiCheatLogProvider(provider: typeof log) { + log = provider +} diff --git a/src/lib/cooldownreset.ts b/src/lib/cooldownreset.ts index 07f4f7ed..ba1d18fa 100644 --- a/src/lib/cooldownreset.ts +++ b/src/lib/cooldownreset.ts @@ -11,7 +11,9 @@ interface CooldownController { // After compilation the initialization of this variable is placed lower then the hoisted call of the function below for some reason let cds: { name: string; cd: CooldownController }[] | undefined -/** Use cooldown controller when the cooldown IS NOT AN INSTANCE OF COOLDOWN, e.g. its some custom data structure */ +/** + * Use cooldown controller when the cooldown IS NOT AN INSTANCE OF COOLDOWN, e.g. its some custom data structure + */ export function registerResettableCooldown(name: string, cd: CooldownController | Cooldown) { cds ??= [] diff --git a/src/lib/database/command.ts b/src/lib/database/command.ts new file mode 100644 index 00000000..ed40b0c5 --- /dev/null +++ b/src/lib/database/command.ts @@ -0,0 +1,147 @@ +/* i18n-ignore */ +import { Player, system, world } from '@minecraft/server' +import { UnknownTable, getProvider } from 'lib/database/abstract' +import { ActionForm } from 'lib/form/action' +import { ArrayForm } from 'lib/form/array' +import { ModalForm } from 'lib/form/modal' +import { i18n, noI18n } from 'lib/i18n/text' +import { ROLES, getRole } from 'lib/roles' +import { inspect } from 'lib/util' + +new Command('db') + .setDescription('Просматривает базу данных') + .setPermissions('techAdmin') + .executes(ctx => selectTable(ctx.player, true)) + +function selectTable(player: Player, firstCall?: true) { + const form = new ActionForm('Таблицы данных') + for (const [tableId, table] of Object.entries(getProvider().tables)) { + const name = noI18n`${tableId} ${`§7${table.size}`} ${getProvider().getRawTableData(tableId).length / (256 * 1024)}§r` + + form.button(name, () => showTable(player, tableId, table)) + } + form.show(player) + if (firstCall) player.info('Закрой чат!') +} + +function showTable(player: Player, tableId: string, table: UnknownTable) { + const selfback = () => showTable(player, tableId, table) + const keys = [...table.keys()] + new ArrayForm(`${tableId} ${keys.length}`, keys) + .addCustomButtonBeforeArray(form => { + form + .button('§3Новое значение§r', () => { + changeValue(player, null, (newVal, key) => table.set(key, newVal), selfback) + }) + .ask('§cОчистить таблицу', 'ДААА УДАЛИТЬ ВСЕ НАФИГ', () => { + for (const key of table.keys()) table.delete(key) + }) + }) + .back(() => selectTable(player)) + .button(key => { + let name = key + if (tableId === 'player') { + const playerDatabase = table as typeof Player.database + name = `${playerDatabase.get(key).name} ${(ROLES[getRole(key)] as Text | undefined)?.to(player.lang) ?? '§7Без роли'}\n§8(${key})` + } else { + name += `\n§7${JSON.stringify(table.get(key)).slice(0, 200).replace(/"/g, '')}` + } + + return [name, () => tableProperty(key, table, player, selfback)] as const + }) + .show(player) +} + +function tableProperty(key: string, table: UnknownTable, player: Player, back: VoidFunction) { + key = key + '' + let value: unknown + let failedToLoad = false + + try { + value = table.get(key) + } catch (e) { + console.error(e) + value = 'a' + failedToLoad = true + } + + new ActionForm( + '§3Ключ ' + key, + `§7Тип: §f${typeof value}\n ${failedToLoad ? '\n§cОшибка при получении данных из таблицы!§r\n\n' : ''}\n${inspect(value)}\n `, + ) + .button('Изменить', () => + changeValue( + player, + value, + newValue => { + table.set(key, newValue) + player.tell(inspect(value) + '§r -> ' + inspect(newValue)) + }, + () => tableProperty(key, table, player, back), + key, + ), + ) + .button('Переименовать', () => { + new ModalForm('Переименовать').addTextField('Ключ', 'останется прежним', key).show(player, (ctx, newKey) => { + if (newKey) { + player.success(i18n`Renamed ${key} -> ${newKey}`) + table.set(newKey, table.get(key)) + } else player.info(i18n`Key ${key} left as is`) + }) + }) + .button(noI18n.error`Удалить`, () => { + table.delete(key) + system.delay(back) + }) + .addButtonBack(back, player.lang) + .show(player) +} + +function changeValue( + player: Player, + value: unknown, + onChange: (value: unknown, key: string) => void, + back: VoidFunction, + key?: string, +) { + let valueType = typeof value + const typeDropdown = ['string', 'number', 'boolean', 'object'] + if (value) typeDropdown.unshift('Оставить прежний §7(' + valueType + ')') + const stringifiedValue = value ? JSON.stringify(value) : '' + + new ModalForm('§3+Значение ') + .addTextField('Ключ', ' ', key) + .addTextField('Значение', 'оставь пустым для отмены', stringifiedValue) + .addDropdown('Тип', typeDropdown) + .show(player, (ctx, key, input: string, type: string) => { + if (!input) back() + let newValue: unknown = input + + if ( + !type.includes(valueType) && + (type === 'string' || type === 'object' || type === 'boolean' || type === 'number') + ) { + valueType = type + } + switch (valueType) { + case 'number': + newValue = Number(input) + break + + case 'boolean': + newValue = input === 'true' + break + + case 'object': + try { + newValue = JSON.parse(input) + } catch (e: unknown) { + world.say(`§4DB §cJSON.parse error: ${(e as Error).message}`) + return + } + + break + } + onChange(newValue, key) + }) +} diff --git a/src/lib/extensions/world.ts b/src/lib/extensions/world.ts index 07702214..1e719667 100644 --- a/src/lib/extensions/world.ts +++ b/src/lib/extensions/world.ts @@ -11,7 +11,7 @@ declare module '@minecraft/server' { * Logs given message once * * @param type Type of log - * @param messages Data to log + * @param messages Data to log using world.debug() */ logOnce(type: string, ...messages: unknown[]): void diff --git a/src/lib/mail/index.ts b/src/lib/mail/index.ts index 884d490b..f5f9748c 100644 --- a/src/lib/mail/index.ts +++ b/src/lib/mail/index.ts @@ -5,6 +5,7 @@ import { defaultLang } from '../assets/lang' import { table } from '../database/abstract' import { Message } from '../i18n/message' import { i18n, noI18n } from '../i18n/text' +import './command' /** A global letter is a letter sent to multiple players */ interface GlobalLetter { diff --git a/src/lib/roles.test.ts b/src/lib/roles.test.ts deleted file mode 100644 index 014b4e23..00000000 --- a/src/lib/roles.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { GameMode, Player } from '@minecraft/server' -import { TEST_createPlayer } from 'test/utils' -import { getRole, is, setRole } from './roles' - -describe('roles auto switch gamemode', () => { - it('should switch gamemode', () => { - const player = TEST_createPlayer() - - expect(player.getGameMode()).toBe(GameMode.Survival) - - expect(getRole(player)).toBe('member') - expect(getRole(player.id)).toBe(getRole(player)) - - setRole(player, 'admin') - expect(getRole(player.id)).toBe('admin') - expect(player.getGameMode()).toBe(GameMode.Survival) - - setRole(player, 'spectator') - expect(player.getGameMode()).toBe(GameMode.Spectator) - - setRole(player, 'member') - expect(player.getGameMode()).toBe(GameMode.Survival) - }) - - it('should return valid role', () => { - // @ts-expect-error - const player = new Player() as Player - - // @ts-expect-error - player.database.role = 'oldrole' - - expect(getRole(player)).toBe('member') - }) - - it('should not throw for unknown player', () => { - setRole('unknown', 'tester') - }) -}) - -describe('roles is', () => { - it('should test is', () => { - const player = TEST_createPlayer() - - expect(is(player.id, 'admin')).toBe(false) - expect(is(player.id, 'member')).toBe(true) - expect(is(player.id, 'spectator')).toBe(true) - - setRole(player, 'admin') - expect(is(player.id, 'admin')).toBe(true) - expect(is(player.id, 'builder')).toBe(true) - expect(is(player.id, 'chefAdmin')).toBe(false) - }) -}) diff --git a/src/lib/roles.ts b/src/lib/roles.ts deleted file mode 100644 index 77b41221..00000000 --- a/src/lib/roles.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { GameMode, Player, ScriptEventSource, system, world } from '@minecraft/server' -import { EventSignal } from 'lib/event-signal' -import { isKeyof } from 'lib/util' -import { Core } from './extensions/core' -import { i18n, noI18n } from './i18n/text' - -declare global { - /** Any known role */ - type Role = keyof typeof ROLES -} - -/** The roles that are in this server */ -export const ROLES = { - creator: i18n.nocolor`§aРуководство`, - curator: i18n.nocolor`§6Куратор`, - techAdmin: i18n.nocolor`§cТех. Админ`, - chefAdmin: i18n.nocolor`§dГл. Админ`, - admin: i18n.nocolor`§5Админ`, - moderator: i18n.nocolor`§6Модератор`, - helper: i18n.nocolor`§eПомошник`, - grandBuilder: i18n.nocolor`§bГл. Строитель`, - builder: i18n.nocolor`§3Строитель`, - member: i18n.nocolor`§fУчастник`, - spectator: i18n.nocolor`§9Наблюдатель`, - tester: i18n.nocolor`§9Тестер`, -} - -export const DEFAULT_ROLE: Role = 'member' - -/** List of role permissions */ -const PERMISSIONS: Record = { - creator: ['creator'], - curator: ['creator', 'curator'], - techAdmin: ['creator', 'curator', 'techAdmin'], - - chefAdmin: ['creator', 'curator', 'chefAdmin'], - admin: ['creator', 'curator', 'chefAdmin', 'admin'], - moderator: ['creator', 'curator', 'chefAdmin', 'admin', 'moderator'], - helper: ['creator', 'curator', 'chefAdmin', 'admin', 'moderator', 'helper'], - - grandBuilder: ['creator', 'curator', 'techAdmin', 'chefAdmin', 'admin', 'grandBuilder'], - builder: ['creator', 'curator', 'techAdmin', 'chefAdmin', 'admin', 'builder', 'grandBuilder'], - member: Object.keys(ROLES).filter(e => e !== 'spectator'), - spectator: [], // Any - tester: Object.keys(ROLES).filter(e => e !== 'spectator' && e !== 'member'), -} - -/** - * List of roles who can change role that goes after their position - * - * Also known as role hierarchy - */ -export const WHO_CAN_CHANGE: Role[] = ['creator', 'curator', 'techAdmin', 'chefAdmin', 'admin', 'grandBuilder'] - -/** - * Checks if player has permissions for performing role actions. (e.g. if player role is above or equal) - * - * @example - * is(player.id, 'admin') // Player is admin, grandAdmin or any role above - * - * @example - * is(player.id, 'grandBuilder') // Player is grandBuilder, chefAdmin, techAdmin or any role above - * - * @param playerID ID of the player to get role from - * @param role Role to check - */ -export function is(playerID: string, role: Role) { - if (!PERMISSIONS[role].length) return true - - return PERMISSIONS[role].includes(getRole(playerID)) -} - -/** - * Gets the role of this player - * - * @example - * getRole(player.id) - * - * @example - * getRole(player) - * - * @param playerID Player or his id to get role from - * @returns Player role - */ -export function getRole(playerID: Player | string): Role { - if (playerID instanceof Player) playerID = playerID.id - - const role = Player.database.getImmutable(playerID).role - - if (!Object.keys(ROLES).includes(role)) return 'member' - return role -} - -/** - * Sets the role of this player - * - * @example - * setRole(player.id, 'admin') - * - * @example - * setRole(player, 'member') - * - * @param player - Player to set role one - * @param role - Role to set - */ -export function setRole(player: Player | string, role: Role): void { - const id = player instanceof Player ? player.id : player - const database = Player.database.get(id) - if (typeof database !== 'undefined') { - EventSignal.emit(Core.beforeEvents.roleChange, { - id, - player: player instanceof Player ? player : Player.getById(player), - newRole: role, - oldRole: database.role, - }) - - // @ts-expect-error settings role in setRole function is allowed - // role property is marked readonly so no other functions will change that - database.role = role - } -} - -// Set spectator gamemode to the spectator role -Core.beforeEvents.roleChange.subscribe(({ newRole, oldRole, player }) => { - if (!player) return - if (newRole === 'spectator') { - player.setGameMode(GameMode.Spectator) - } else if (oldRole === 'spectator') { - player.setGameMode(GameMode.Survival) - } -}) - -// Set spectator gamemode on join with spectator role -world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { - if (player.isSimulated()) return - if (initialSpawn) { - if (player.database.role === 'spectator') { - player.setGameMode(GameMode.Spectator) - } - } -}) - -/* istanbul ignore next */ -if (!__VITEST__) { - // Allow recieving roles from scriptevent function run by console - system.afterEvents.scriptEventReceive.subscribe( - event => { - if (event.id.toLowerCase().startsWith('role:')) { - if (event.sourceType === ScriptEventSource.Server) { - // Allow - } else { - if (Player.database.values().find(e => WHO_CAN_CHANGE.includes(e.role))) { - return console.error(`(SCRIPTEVENT::${event.id}) Admin already set.`) - } - } - - const role = event.id.toLowerCase().replace('role:', '') - if (!isKeyof(role, ROLES)) { - return console.error( - `(SCRIPTEVENT::${event.id}) Unkown role: ${role}, allowed roles:\n${Object.entries(ROLES) - .map(e => noI18n`${e[0]}: ${e[1]}`) - .join('\n')}`, - ) - } - - const player = [...Player.database.entriesImmutable()].find(e => e[1].name === event.message)?.[0] - if (!player) return console.error(`(SCRIPTEVENT::${event.id}) PLAYER NOT FOUND`) - - setRole(player, role) - console.warn(`(SCRIPTEVENT::${event.id}) ROLE HAS BEEN SET`) - } - }, - { namespaces: ['role'] }, - ) -} diff --git a/src/lib/rpg/leaderboard/command.ts b/src/lib/rpg/leaderboard/command.ts new file mode 100644 index 00000000..45725eb3 --- /dev/null +++ b/src/lib/rpg/leaderboard/command.ts @@ -0,0 +1,143 @@ +/* i18n-ignore */ + +import { Player, world } from '@minecraft/server' +import { ActionForm } from 'lib/form/action' +import { ModalForm } from 'lib/form/modal' +import { BUTTON } from 'lib/form/utils' +import { Vec } from 'lib/vector' +import { Leaderboard, LeaderboardInfo } from './index' + +new Command('leaderboard') + .setAliases('leaderboards', 'lb') + .setDescription('Управляет таблицами лидеров') + .setPermissions('techAdmin') + .executes(ctx => { + leaderboardMenu(ctx.player) + }) + +function leaderboardMenu(player: Player) { + const form = new ActionForm('Таблицы лидеров') + + form.button('§3Добавить', BUTTON['+'], p => editLeaderboard(p)) + + for (const lb of Leaderboard.all.values()) { + form.button(info(lb), () => { + editLeaderboard(player, lb) + }) + } + + form.show(player) +} + +function info(lb: Leaderboard) { + return lb.info.displayName + '\n' + Vec.string(Vec.floor(lb.info.location)) +} + +function editLeaderboard(player: Player, lb?: Leaderboard, data: Partial = lb?.info ?? {}) { + const action = lb ? 'Изменить ' : 'Выбрать ' + function update() { + if (!lb && isRequired(data)) { + lb = Leaderboard.createLeaderboard(data) + } + if (lb) { + lb.update() + lb.updateLeaderboard() + } + + editLeaderboard(player, lb, lb ? void 0 : data) + } + + function warn(...keys: (keyof typeof data)[]) { + if (keys.find(k => typeof data[k] === 'undefined')) return ' §e(!)' + return '' + } + + const form = new ActionForm('Таблица лидеров', lb ? info(lb) : '') + .addButtonBack(() => leaderboardMenu(player), player.lang) + .button(action + 'целевую таблицу' + warn('displayName', 'objective'), () => { + new ModalForm('Изменение целевой таблицы') + .addDropdownFromObject( + 'Выбрать из списка', + Object.fromEntries(world.scoreboard.getObjectives().map(e => [e.id, e.displayName])), + { defaultValue: data.displayName }, + ) + // .addTextField( + // 'Отображаемое имя', + // data.displayName ? 'Не изменится' : 'Имя счета по умолчанию', + // data.displayName + // ) + .show( + player, + ( + ctx, + id, + + // displayName + ) => { + const scoreboard = world.scoreboard.getObjective(id) + if (!scoreboard) return ctx.error('Скора не существует! Его удалили пока ты редактировал(а) форму хахаха') + + data.objective = id + data.displayName = scoreboard.displayName + if (lb) lb.scoreboard = scoreboard + // if (!data.displayName) displayName = scoreboard.displayName + // if (displayName) data.displayName = displayName + update() + }, + ) + }) + .button(action + 'позицию' + warn('location', 'dimension'), () => { + const dimensions: Record = { + overworld: 'Верхний мир', + nether: 'Нижний мир', + end: 'Край', + } + new ModalForm('Позиция') + .addTextField('Позиция', 'Не изменится', Vec.string(data.location ?? Vec.floor(player.location))) + .addDropdownFromObject('Измерение', dimensions, { + defaultValueIndex: Object.keys(dimensions).findIndex(e => e === player.dimension.type), + }) + .show(player, (ctx, location, dimension) => { + if (location) { + const l = location.split(' ').map(Number) + if (l.length !== 3 || l.find(isNaN)) + return ctx.error("Неверная локация '" + location + "', ожидался формат 'x y z' с числами") + + const [x, y, z] = l as [number, number, number] + data.location = { x, y, z } + } + data.dimension = dimension + update() + }) + }) + .button(action + 'стиль' + warn('style'), () => { + const styles: Record = { + gray: '§7Серый', + white: '§fБелый', + green: '§2Зеленый', + } + new ModalForm('Стиль') + .addDropdownFromObject('Стиль', styles, { + defaultValueIndex: data.style ? Object.keys(styles).findIndex(v => v === data.style) : void 0, + }) + .show(player, (ctx, style) => { + data.style = style + update() + }) + }) + + if (lb) { + form.button('Переместить к себе', () => { + data.location = Vec.floor(player.location).add(Vec.one.multiply(0.5)) + data.dimension = player.dimension.type + update() + }) + form.ask('§cУдалить таблицу лидеров', '§cУдалить', () => lb && lb.remove(), 'Отмена') + } + + form.show(player) +} + +function isRequired(data: Partial): data is LeaderboardInfo { + return !!data.dimension && !!data.displayName && !!data.location && !!data.objective && !!data.style +} diff --git a/src/lib/rpg/leaderboard/index.ts b/src/lib/rpg/leaderboard/index.ts new file mode 100644 index 00000000..b6391594 --- /dev/null +++ b/src/lib/rpg/leaderboard/index.ts @@ -0,0 +1,192 @@ +import { + Entity, + Player, + RawMessage, + RawText, + ScoreboardObjective, + ScoreboardScoreInfo, + system, + world, +} from '@minecraft/server' +import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +import { defaultLang } from 'lib/assets/lang' +import { table } from 'lib/database/abstract' +import { scoreboardDisplayNames } from 'lib/database/scoreboard' +import { i18n, i18nShared } from 'lib/i18n/text' +import { isKeyof } from 'lib/util' +import { Vec } from 'lib/vector' +import './command' + +export interface LeaderboardInfo { + style: keyof typeof Leaderboard.styles + objective: string + displayName: string + location: Vector3 + dimension: DimensionType +} + +const biggest = (a: ScoreboardScoreInfo, b: ScoreboardScoreInfo) => b.score - a.score +const smallest = (a: ScoreboardScoreInfo, b: ScoreboardScoreInfo) => a.score - b.score + +export class Leaderboard { + static db = table('leaderboard') + + static tag = 'LEADERBOARD' + + static entityId = CustomEntityTypes.FloatingText + + static formatScore(objectiveId: string, score: number, convertToMetricNumbers = false) { + if (objectiveId.endsWith('SpeedRun')) return i18n.hhmmss(score) + if (objectiveId.endsWith('Time')) return i18n.hhmmss(score * 2.5) + if (objectiveId.endsWith('Date')) return new Date(score * 1000).format() + if (convertToMetricNumbers) return toMetricNumbers(score) + else return score + } + + static styles = { + gray: { objName: '7', fill1: '7', fill2: 'f', pos: '7', nick: 'f', score: '7' }, + white: { objName: 'f', fill1: 'f', fill2: 'f', pos: 'f', nick: 'f', score: 'f' }, + green: { objName: 'a', fill1: '2', fill2: '3', pos: 'a', nick: 'f', score: 'a' }, + } + + private static untypedStyles = this.styles as Record> + + static all = new Map() + + static createLeaderboard({ + objective, + location, + dimension = 'overworld', + style = 'green', + displayName = objective, + }: LeaderboardInfo) { + const entity = world[dimension].spawnEntity(Leaderboard.entityId, Vec.floor(location)) + entity.nameTag = 'updating...' + entity.addTag(Leaderboard.tag) + + return new Leaderboard(entity, { style, objective, location, dimension, displayName }) + } + + /** Creates manager of Leaderboard */ + constructor( + public entity: Entity, + public info: LeaderboardInfo, + ) { + const previous = Leaderboard.all.get(entity.id) + if (previous) return previous + + this.update() + Leaderboard.all.set(entity.id, this) + } + + remove() { + Reflect.deleteProperty(Leaderboard.db, this.entity.id) + Leaderboard.all.delete(this.entity.id) + this.entity.remove() + } + + update() { + if (this.entity.isValid) this.entity.teleport(this.info.location, { dimension: world[this.info.dimension] }) + Leaderboard.db.set(this.entity.id, this.info) + } + + private objective?: ScoreboardObjective + + set scoreboard(v) { + this.objective = v + } + + get scoreboard() { + return ( + this.objective ?? + (this.objective = + world.scoreboard.getObjective(this.info.objective) ?? + world.scoreboard.addObjective(this.info.objective, this.info.displayName)) + ) + } + + get name(): string { + const id = this.objective?.id + if (!id) return 'noname' + if (isKeyof(id, scoreboardDisplayNames)) return scoreboardDisplayNames[id].to(defaultLang) + return this.scoreboard.displayName.toString() + } + + get nameRawText(): RawText | RawMessage { + const id = this.objective?.id + if (!id) return { text: 'noname' } + if (isKeyof(id, scoreboardDisplayNames)) return scoreboardDisplayNames[id].toRawText() + return { text: this.scoreboard.displayName.toString() } + } + + updateLeaderboard() { + if (!this.entity.isValid) return + + // const npc = this.entity.getComponent(EntityComponentTypes.Npc) + // if (!npc) return + + const scoreboard = this.scoreboard + const id = this.scoreboard.id + const name = this.name + const style = Leaderboard.untypedStyles[this.info.style] ?? Leaderboard.styles.gray + const filler = `§${style.fill1}-§${style.fill2}-`.repeat(10) + + // const rawtext: RawMessage[] = [{ text: `§l${style.objName}` }, name, { text: `\n§l${filler}§r\n` }] + let leaderboard = `§l§${style.objName}${name}\n§l${filler}§r\n` + for (const [i, scoreInfo] of scoreboard + .getScores() + .sort(id.endsWith('SpeedRun') ? smallest : biggest) + .slice(0, 10) + .entries()) { + const { pos: t, nick: n, score: s } = style + + const name = Player.nameOrUnknown(scoreInfo.participant.displayName) + + // rawtext.push({ text: `§${t}#${i + 1}§r §${n}${name}§r §${s}` }) + const score = Leaderboard.formatScore(id, scoreInfo.score, true) + leaderboard += `§${t}#${i + 1}§r §${n}${name}§r §${s}${typeof score === 'number' ? score : score.to(defaultLang)}§r\n` + // rawtext.push( + // typeof score === 'string' || typeof score === 'number' ? { text: score.toString() } : score.toRawText(), + // ) + // rawtext.push({ text: '§r\n' }) + } + + this.entity.nameTag = leaderboard + // npc.name = '' + // npc.name = JSON.stringify({ rawtext }) + } +} + +system.runInterval( + () => { + for (const [id, leaderboard] of Leaderboard.db.entriesImmutable()) { + if (typeof leaderboard === 'undefined') continue + const info = Leaderboard.all.get(id) + + if (info?.entity) { + if (info.entity.isValid) info.updateLeaderboard() + } else { + const entity = world[leaderboard.dimension] + .getEntities({ location: leaderboard.location, tags: [Leaderboard.tag], type: Leaderboard.entityId }) + .find(e => e.id === id) + + if (!entity || !entity.isValid || typeof leaderboard === 'undefined') continue + new Leaderboard(entity, leaderboard).updateLeaderboard() + } + } + }, + 'leaderboardsInterval', + 100, +) + +const types = ['', i18nShared`к`, i18nShared`млн`, i18nShared`млрд`, i18nShared`трлн`] + +/** This will display in text in thousands, millions and etc... For ex: "1400 -> "1.4k", "1000000" -> "1M", etc... */ +function toMetricNumbers(value: number) { + const exp = (Math.log10(value) / 3) | 0 + + if (exp === 0) return value.toString() + + const scaled = value / Math.pow(10, exp * 3) + return i18nShared.nocolor.join`${scaled.toFixed(1)}${exp > 5 ? `E${exp}` : types[exp]}` +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 46aa81de..16e3d397 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -3,6 +3,8 @@ import { TerminalColors } from './assets/terminal-colors' import stringifyError from './utils/error' import { inspect, stringify } from './utils/inspect' +import './utils/benchmark' + export { inspect, stringify, stringifyError } export const util = { diff --git a/src/lib/utils/benchmark.ts b/src/lib/utils/benchmark.ts new file mode 100644 index 00000000..34ec0ecc --- /dev/null +++ b/src/lib/utils/benchmark.ts @@ -0,0 +1,91 @@ +import { TIMERS_PATHES } from 'lib/extensions/system' +import { ActionForm } from 'lib/form/action' +import { util } from 'lib/util' + +new Command('benchmark') + .setAliases('bench') + .setDescription('Показывает время работы интервалов скриптов') + .setPermissions('techAdmin') + .string('type', true) + .boolean('pathes', true) + .boolean('sort', true) + .array('output', ['form', 'chat', 'log'], true) + .executes((ctx, type = 'timers', timerPathes = false, sort = true, output = 'form') => { + if (!(type in util.benchmark.results)) + return ctx.error( + 'Неизвестный тип бенчмарка! Доступные типы: \n §f' + Object.keys(util.benchmark.results).join('\n '), + ) + + const result = stringifyBenchmarkResult({ type: type, timerPathes, sort }) + + switch (output) { + case 'form': { + const show = () => { + new ActionForm('Benchmark', result) + .button('Refresh', null, show) + .button('Exit', null, () => void 0) + .show(ctx.player) + } + return show() + } + case 'chat': + return ctx.reply(result) + case 'log': + return console.log(result) + } + }) + +/** + * It takes the benchmark results and sorts them by average time, then it prints them out in a nice format + * + * @returns A string. + */ +export function stringifyBenchmarkResult({ type = 'test', timerPathes = false, sort = true } = {}) { + const results = util.benchmark.results[type] + if (!results) return `No results for type ${type}` + + let output = '' + let res = Object.entries(results) + + if (sort) res = res.sort((a, b) => a[1] - b[1]) + + const max = Math.max(...res.map(e => e[1])) + + for (const [key, average] of res) { + const color = colors.find(e => e[0] > average)?.[1] ?? '§4' + const isPath = timerPathes && key in TIMERS_PATHES + + output += `§3Label §f${key}§r\n` + output += `§3| §7average: ${color}${formatDecimal(average)}ms\n` + // output += `§3| §7total time: §f${totalTime}ms\n` + // output += `§3| §7call count: §f${totalCount}\n` + const percent = average / max + if (percent !== 1) output += `§3| §7faster: §f${~~(100 - percent * 100)}%%\n` + if (isPath) output += `§3| §7path: §f${getPath(key)}\n` + output += '\n\n' + } + return output +} + +function formatDecimal(num: number): string { + if (num === 0) return '0' + + if (num > 0.01) return num.toFixed(2) + + const str = num.toFixed(20) // Ensure we have enough decimal places + const match = /^0\.0*[1-9]{0,3}/.exec(str) + + return match?.[0] ?? str +} + +const colors: [number, string][] = [ + [0.1, '§a'], + [0.3, '§2'], + [0.5, '§g'], + [0.65, '§6'], + [0.8, '§c'], +] + +export function getPath(key: string) { + return `\n${TIMERS_PATHES[key]}`.replace(/\n/g, '\n§3| §r') +} From 5363616089842abd65e21f8a5db1b13dad849ce5 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:59:54 +0300 Subject: [PATCH 12/14] refactor: make it start --- src/index.ts | 18 +- src/lib.ts | 10 +- src/lib/achievements/achievement.ts | 6 + .../anticheat/forbidden-items.ts | 0 src/{modules => lib}/anticheat/whitelist.ts | 7 +- src/lib/assets/intl-global-object.ts | 3 + src/lib/chat/chat.ts | 3 +- src/lib/clan/clan.ts | 5 +- src/lib/clan/create.ts | 17 +- src/lib/clan/menu.ts | 13 +- src/lib/command/help.ts | 74 +++ src/lib/command/index.ts | 4 +- src/lib/command/utils.test.ts | 1 + src/lib/cutscene/cutscene.ts | 8 +- src/lib/cutscene/edit.ts | 8 +- src/lib/cutscene/menu.ts | 4 +- src/lib/database/abstract.ts | 19 +- src/lib/database/item-stack.test.ts | 24 +- src/lib/database/migrations.ts | 5 +- src/lib/database/persistent-set.ts | 22 +- src/lib/database/player.ts | 2 +- src/lib/database/properties.ts | 9 +- src/lib/database/proxy.ts | 15 +- src/lib/database/scoreboard.ts | 7 +- src/lib/database/utils.ts | 3 +- src/lib/enchantments.ts | 3 +- src/lib/extensions/core.ts | 3 +- src/lib/extensions/on-screen-display.ts | 3 +- src/lib/extensions/system.ts | 25 +- src/lib/extensions/world.ts | 9 +- src/lib/form/chest.ts | 7 +- src/lib/form/lore.ts | 10 +- src/lib/form/quest.ts | 18 + src/lib/lib.d.ts | 5 - src/lib/load/message1.ts | 4 +- src/lib/load/message2.ts | 6 +- src/lib/load/watchdog.ts | 4 - src/lib/location.ts | 71 +-- src/lib/player-join.ts | 4 + src/lib/player-move.ts | 3 +- src/lib/portals.ts | 1 + src/lib/quest/quest.ts | 6 + src/lib/recurring-event.ts | 17 +- src/lib/region/areas/area.ts | 10 +- src/lib/region/config.ts | 5 +- src/lib/region/explosion.ts | 14 + src/lib/region/index.ts | 43 +- src/lib/region/kinds/minearea.ts | 1 + src/lib/region/kinds/region.ts | 4 +- src/lib/region/structure.ts | 2 +- .../commands/role.ts => lib/roles/command.ts} | 9 +- src/lib/roles/index.ts | 174 ++++++++ src/lib/roles/r.ts | 1 + src/lib/rpg/boss.ts | 4 +- src/lib/rpg/custom-item.ts | 15 +- src/lib/rpg/loot-table.ts | 9 +- src/lib/rpg/menu.ts | 26 +- src/lib/rpg/minimap.ts | 5 +- src/lib/scheduled-block-place.ts | 2 +- src/lib/settings/command.ts | 17 + src/lib/settings/index.test.ts | 24 + src/lib/settings/index.ts | 422 ++++++++++++++++++ src/lib/shop/cost/item-cost.ts | 10 +- src/lib/shop/form.ts | 10 +- src/lib/sidebar.ts | 9 +- src/lib/util.ts | 9 +- src/lib/utils/benchmark.ts | 1 + src/lib/utils/error.ts | 4 +- src/lib/utils/game.ts | 2 + src/lib/utils/load-ref.ts | 77 ++++ src/lib/utils/singleton.test.ts | 43 ++ src/lib/utils/singleton.ts | 25 +- src/modules/anticheat/anti-piston-abuse.ts | 29 -- .../anticheat/anti-wither-bedrock-kill.ts | 22 - src/modules/anticheat/index.ts | 4 - src/modules/anticheat/log-provider.ts | 16 - src/modules/commands/index.ts | 1 - src/modules/indicator/pvp.ts | 4 +- src/modules/loader.ts | 18 +- src/modules/lushway/loader.ts | 5 + src/modules/places/anarchy/airdrop.ts | 16 +- src/modules/places/base/actions/rotting.ts | 30 +- src/modules/places/dungeons/command.ts | 22 +- src/modules/places/dungeons/loot.ts | 11 +- src/modules/places/lib/npc/guide.ts | 2 +- src/modules/places/lib/safe-place.ts | 12 +- src/modules/places/spawn.ts | 106 ++--- src/modules/places/stone-quarry/barman.ts | 34 +- src/modules/places/tech-city/engineer.ts | 21 +- src/modules/places/tech-city/tech-city.ts | 7 +- .../places/village-of-explorers/items.ts | 12 +- .../places/village-of-explorers/mage.ts | 10 +- .../village-of-explorers.ts | 2 +- .../village-of-miners/village-of-miners.ts | 2 +- src/modules/pvp/fireball.ts | 13 +- src/modules/pvp/ice-bomb.ts | 14 +- src/modules/pvp/raid.ts | 9 +- src/modules/quests/daily/index.ts | 7 +- src/modules/quests/learning/learning.ts | 28 +- src/modules/survival/random-teleport.ts | 18 +- src/modules/survival/sidebar.ts | 7 +- src/modules/survival/speedrun/target.ts | 3 +- .../commands/region/set/block-is-avaible.ts | 7 +- src/modules/world-edit/config.ts | 15 +- src/modules/world-edit/menu.ts | 5 +- src/modules/world-edit/tools/brush.ts | 24 +- src/modules/world-edit/tools/tool.ts | 5 +- src/modules/world-edit/utils/blocks-set.ts | 2 +- .../world-edit/utils/default-block-sets.ts | 29 +- src/test/vitest.d.ts | 7 +- yarn.lock | 87 ++-- 111 files changed, 1513 insertions(+), 616 deletions(-) rename src/{modules => lib}/anticheat/forbidden-items.ts (100%) rename src/{modules => lib}/anticheat/whitelist.ts (93%) create mode 100644 src/lib/assets/intl-global-object.ts create mode 100644 src/lib/command/help.ts create mode 100644 src/lib/form/quest.ts create mode 100644 src/lib/region/explosion.ts rename src/{modules/commands/role.ts => lib/roles/command.ts} (95%) create mode 100644 src/lib/roles/index.ts create mode 100644 src/lib/roles/r.ts create mode 100644 src/lib/settings/command.ts create mode 100644 src/lib/settings/index.test.ts create mode 100644 src/lib/settings/index.ts create mode 100644 src/lib/utils/load-ref.ts create mode 100644 src/lib/utils/singleton.test.ts delete mode 100644 src/modules/anticheat/anti-piston-abuse.ts delete mode 100644 src/modules/anticheat/anti-wither-bedrock-kill.ts delete mode 100644 src/modules/anticheat/index.ts delete mode 100644 src/modules/anticheat/log-provider.ts diff --git a/src/index.ts b/src/index.ts index 38458904..e56de347 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,14 @@ -import { system } from '@minecraft/server' +// Takes too much to load dynamically which results in interrupted error +import 'lib/assets/intl-global-object' +import 'lib/assets/intl' -system.beforeEvents.startup.subscribe(() => { - system.run(() => { - if (__TEST__) import('./test/loader') - else import('./modules/loader') - }) +import('./modules/loader').catch(e => { + console.error('Loading error', e) }) + +// system.beforeEvents.startup.subscribe(() => { +// system.run(() => { +// if (__TEST__) import('./test/loader') +// else import('./modules/loader') +// }) +// }) diff --git a/src/lib.ts b/src/lib.ts index 02540c45..ccd281c6 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -5,13 +5,13 @@ import 'lib/load/message1' import 'lib/database/properties' // Database -// export * from 'lib/database/inventory' -// export * from 'lib/database/player' -// export * from 'lib/database/scoreboard' -// export * from 'lib/database/utils' +import 'lib/database/inventory' +import 'lib/database/player' +import 'lib/database/scoreboard' +import 'lib/database/utils' // // Command -// export * from 'lib/command/index' +import 'lib/command/index' // // Lib // export * from 'lib/roles' diff --git a/src/lib/achievements/achievement.ts b/src/lib/achievements/achievement.ts index 6eed9521..e6505fa7 100644 --- a/src/lib/achievements/achievement.ts +++ b/src/lib/achievements/achievement.ts @@ -4,6 +4,12 @@ import { i18n } from 'lib/i18n/text' import { isNotPlaying } from 'lib/utils/game' import { Rewards } from 'lib/utils/rewards' +declare module '@minecraft/server' { + interface PlayerDatabase { + achivs?: Achievement.DB + } +} + export namespace Achievement { export interface DBSingle { id: string diff --git a/src/modules/anticheat/forbidden-items.ts b/src/lib/anticheat/forbidden-items.ts similarity index 100% rename from src/modules/anticheat/forbidden-items.ts rename to src/lib/anticheat/forbidden-items.ts diff --git a/src/modules/anticheat/whitelist.ts b/src/lib/anticheat/whitelist.ts similarity index 93% rename from src/modules/anticheat/whitelist.ts rename to src/lib/anticheat/whitelist.ts index 5a510746..6413ba8c 100644 --- a/src/modules/anticheat/whitelist.ts +++ b/src/lib/anticheat/whitelist.ts @@ -2,10 +2,9 @@ import { system, world } from '@minecraft/server' import { defaultLang } from 'lib/assets/lang' import { noI18n } from 'lib/i18n/text' -import { is } from 'lib/roles' -import { DEFAULT_ROLE } from 'lib/roles' -import { ROLES } from 'lib/roles' +import { DEFAULT_ROLE, is, ROLES } from 'lib/roles' import { Settings } from 'lib/settings' +import { onLoad } from 'lib/utils/load-ref' import { createLogger } from 'lib/utils/logger' // Delay execution to move whitelist settings to the end of the settings menu @@ -41,7 +40,7 @@ system.delay(() => { } }) - system.delay(() => { + onLoad(() => { if (whitelist.enabled) { logger.info('To disable, use /scriptevent whitelist:disable') } diff --git a/src/lib/assets/intl-global-object.ts b/src/lib/assets/intl-global-object.ts new file mode 100644 index 00000000..bff4140d --- /dev/null +++ b/src/lib/assets/intl-global-object.ts @@ -0,0 +1,3 @@ +//@ts-expect-error Define global intl if not defined +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +globalThis.Intl ??= {} \ No newline at end of file diff --git a/src/lib/chat/chat.ts b/src/lib/chat/chat.ts index f92747e9..d2c59dae 100644 --- a/src/lib/chat/chat.ts +++ b/src/lib/chat/chat.ts @@ -3,6 +3,7 @@ import { Cooldown } from 'lib/cooldown' import { table } from 'lib/database/abstract' import { i18n, noI18n } from 'lib/i18n/text' import { Settings } from 'lib/settings' +import { onLoad } from 'lib/utils/load-ref' import { msold } from 'lib/utils/ms-old' import { Singleton } from 'lib/utils/singleton' import './command' @@ -78,7 +79,7 @@ export abstract class Chat extends Singleton { constructor() { super() - world.afterEvents.worldLoad.subscribe(() => { + onLoad(() => { this.updateCooldown() }) diff --git a/src/lib/clan/clan.ts b/src/lib/clan/clan.ts index a6614bac..d14ebbf5 100644 --- a/src/lib/clan/clan.ts +++ b/src/lib/clan/clan.ts @@ -1,7 +1,8 @@ -import { Player, system, world } from '@minecraft/server' +import { Player, system } from '@minecraft/server' import { table } from 'lib/database/abstract' import { I18nMessage } from 'lib/i18n/message' import { i18n } from 'lib/i18n/text' +import { onLoad } from 'lib/utils/load-ref' import './command' interface ClanTemporalMember { @@ -95,7 +96,7 @@ export class Clan { private static instances = new Map() static { - world.afterEvents.worldLoad.subscribe(() => + onLoad(() => system.run(() => { for (const [id, db] of this.database.entries()) { if (!db) continue diff --git a/src/lib/clan/create.ts b/src/lib/clan/create.ts index d6469388..71d73637 100644 --- a/src/lib/clan/create.ts +++ b/src/lib/clan/create.ts @@ -1,11 +1,15 @@ import { Player } from '@minecraft/server' +import { Cooldown } from 'lib/cooldown' +import { registerResettableCooldown } from 'lib/cooldownreset' import { ArrayForm } from 'lib/form/array' import { MessageForm } from 'lib/form/message' import { ModalForm } from 'lib/form/modal' import { i18n } from 'lib/i18n/text' import { Mail } from 'lib/mail' +import { onLoad } from 'lib/utils/load-ref' +import { ms } from 'lib/utils/ms' import { Clan } from './clan' -import { cd, clanInvites, clanMenu, inClanMenu } from './menu' +import { clanInvites, clanMenu, inClanMenu } from './menu' export function selectOrCreateClanMenu(player: Player, back?: VoidFunction) { new ArrayForm(i18n`Выбор клана`, [...Clan.getAll()].reverse()) @@ -61,6 +65,13 @@ export function selectOrCreateClanMenu(player: Player, back?: VoidFunction) { export function getClanButtonName(clan: Clan, style: Text.Fn = i18n): Text { return style`[${clan.shortname}] ${clan.name}\nУчастники: ${clan.members.length} ${clan.owners.map(id => Player.nameOrUnknown(id)).join(', ')}` } + +const cooldown = onLoad(() => { + const cd = new Cooldown(ms.from('day', 1), true, Cooldown.defaultDb.get('clan')) + registerResettableCooldown('Создание/изменение названия клана', cd) + return cd +}) + export function promptClanNameShortname( player: Player, title: Text, @@ -70,7 +81,7 @@ export function promptClanNameShortname( defaultName?: string, defaultShortname?: string, ) { - if (!cd.isExpired(player, false)) return + if (!cooldown.value.isExpired(player, false)) return new ModalForm(title.to(player.lang)) .addTextField( i18n`Название клана`.to(player.lang), @@ -109,7 +120,7 @@ export function promptClanNameShortname( if (c.shortname === shortname) return err(i18n.error`Короткое имя '${shortname}' уже занято.`) } - if (!cd.isExpired(player)) return + if (!cooldown.value.isExpired(player)) return onDone(name, shortname) }) diff --git a/src/lib/clan/menu.ts b/src/lib/clan/menu.ts index 5908263d..37cf14b7 100644 --- a/src/lib/clan/menu.ts +++ b/src/lib/clan/menu.ts @@ -1,6 +1,4 @@ -import { Player, world } from '@minecraft/server' -import { Cooldown } from 'lib/cooldown' -import { registerResettableCooldown } from 'lib/cooldownreset' +import { Player } from '@minecraft/server' import { ArrayForm } from 'lib/form/array' import { ask, MessageForm } from 'lib/form/message' import { ModalForm } from 'lib/form/modal' @@ -11,17 +9,9 @@ import { getFullname } from 'lib/get-fullname' import { i18n, textTable } from 'lib/i18n/text' import { Mail } from 'lib/mail' import { is } from 'lib/roles' -import { ms } from 'lib/utils/ms' import { Clan, ClanMember, ClanRole } from './clan' import { getClanButtonName, promptClanNameShortname, selectOrCreateClanMenu } from './create' -export let cd: Cooldown - -world.afterEvents.worldLoad.subscribe(() => { - cd = new Cooldown(ms.from('day', 1), true, Cooldown.defaultDb.get('clan')) - registerResettableCooldown('Изменение/создание клана', cd) -}) - export function clanMenu(player: Player, back?: VoidFunction) { const clan = Clan.getPlayerClan(player.id) @@ -69,7 +59,6 @@ export const inClanMenu = form.params<{ clan: Clan }>((f, formContext) => { if (isOwner || isHelper) { f.button(i18n`Заявки на вступление`.badge(clan.joinRequests.length), () => clanJoinRequests(player, clan, self)) - f.button(i18n`Приглашения`.badge(clan.invites.length), () => clanInvites(player, clan, self)) } diff --git a/src/lib/command/help.ts b/src/lib/command/help.ts new file mode 100644 index 00000000..5756a02d --- /dev/null +++ b/src/lib/command/help.ts @@ -0,0 +1,74 @@ +import { Player } from '@minecraft/server' +import { defaultLang } from 'lib/assets/lang' +import { CmdLet } from 'lib/command/cmdlet' +import { Command } from 'lib/command/index' +import { commandNoPermissions, commandNotFound } from 'lib/command/utils' +import { i18n, noI18n } from 'lib/i18n/text' +import { getRole, ROLES } from 'lib/roles' + +const help = new Command('fbhelp') + .setDescription(i18n`Справка по скриптовым командам`) + .setAliases('?', 'h') + .setPermissions('techAdmin') + +help + .int('page', true) + .int('commandsOnPage', true) + .executes((ctx, inputPage, commandsOnPage) => { + const avaibleCommands = Command.commands.filter(e => e.sys.requires(ctx.player)) + const cmds = Math.max(1, commandsOnPage ?? 15) + const maxPages = Math.ceil(avaibleCommands.length / cmds) + const page = Math.min(Math.max(inputPage ?? 1, 1), maxPages) + const path = avaibleCommands.slice(page * cmds - cmds, page * cmds) + + const cv = colors[getRole(ctx.player.id)] + + ctx.reply(noI18n.nocolor`${cv}─═─═─═─═─═ §r${page}/${maxPages} ${cv}═─═─═─═─═─═─`) + + for (const command of path) { + const q = '§f.' + + const c = i18n.nocolor`${cv}§r ${q}${command.sys.name} §o§7- ${ + command.sys.description ? command.sys.description.to(ctx.player.lang) : i18n`Пусто` //§r + }`.to(ctx.player.lang) + + ctx.reply(c) + } + ctx.reply(i18n.nocolor`${cv}─═─═─═§f Доступно: ${avaibleCommands.length}/${Command.commands.length} ${cv}═─═─═─═─`) + }) + +function helpForCommand(player: Player, commandName: string) { + const cmd = Command.commands.find(e => e.sys.name == commandName || e.sys.aliases.includes(commandName)) + + if (!cmd) return commandNotFound(player, commandName) + + if (!cmd.sys.requires(player)) return commandNoPermissions(player, cmd) + + const d = cmd.sys + const aliases = d.aliases.length > 0 ? i18n` (также ${d.aliases.join(', ')})` : '' + const overview = i18n.nocolor` §fКоманда §6.${d.name}${aliases}§7§o - ${d.description}` + + player.tell(' ') + player.tell(overview) + player.tell(' ') + + let child = false + for (const subcommand of Command.getHelp(player.lang, cmd)) { + child = true + player.tell(`§7 §f.${subcommand}`) + } + if (child) player.tell(' ') + return +} + +Command.getHelpForCommand = (command, ctx) => helpForCommand(ctx.player, command.sys.name) +help.string('commandName').executes((ctx, command) => helpForCommand(ctx.player, command)) + +new CmdLet('help').setDescription(i18n`Выводит справку о команде`).executes(ctx => { + helpForCommand(ctx.player, ctx.command.sys.name) + return 'stop' +}) + +const colors: Record = Object.fromEntries( + Object.entriesStringKeys(ROLES).map(([role, display]) => [role, display.to(defaultLang).slice(0, 2)]), +) diff --git a/src/lib/command/index.ts b/src/lib/command/index.ts index 1f469608..b3cba9e1 100644 --- a/src/lib/command/index.ts +++ b/src/lib/command/index.ts @@ -136,7 +136,7 @@ export class Command for (const command of this.commands) { if (!command.sys.parent && command.sys.name === name) { Command.logger - .warn`Duplicate command name: ${name} at\n${stringifyError.stack.get(2)}${command.stack ? i18n.warn`And:\n${command.stack}` : ''}` + .warn`Duplicate command name: ${name} at\n${stringifyError.stack.get(0)}${command.stack ? i18n.warn`And:\n${command.stack}` : ''}` return } } @@ -217,7 +217,7 @@ export class Command * @param {string} name - Name of the new command */ constructor(name: string, type?: IArgumentType, depth = 0, parent: Command | null = null) { - this.stack = stringifyError.stack.get(2) + this.stack = stringifyError.stack.get(0) if (!parent && !__VITEST__) Command.checkIsUnique(name) if (Command.loaded) { diff --git a/src/lib/command/utils.test.ts b/src/lib/command/utils.test.ts index f5a656d4..7d1bdbef 100644 --- a/src/lib/command/utils.test.ts +++ b/src/lib/command/utils.test.ts @@ -31,3 +31,4 @@ describe('command utils', () => { `) }) }) + diff --git a/src/lib/cutscene/cutscene.ts b/src/lib/cutscene/cutscene.ts index c68edeb8..f74d5e13 100644 --- a/src/lib/cutscene/cutscene.ts +++ b/src/lib/cutscene/cutscene.ts @@ -6,7 +6,7 @@ import { table } from 'lib/database/abstract' import { noI18n } from 'lib/i18n/text' import { Compass } from 'lib/rpg/menu' import { Sidebar } from 'lib/sidebar' -import { restorePlayerCamera } from 'lib/utils/game' +import { onLoad, restorePlayerCamera } from 'lib/utils/game' import { WeakPlayerMap } from 'lib/weak-player-storage' /** @@ -64,7 +64,9 @@ export class Cutscene { ) { Cutscene.all.set(id, this) - this.sections = Cutscene.db.get(this.id).slice() + onLoad(() => { + this.sections = Cutscene.db.get(this.id).slice() + }) } private get defaultSection() { @@ -295,6 +297,6 @@ function bezier>(vectors: [T, T, T, T], axis: k function getVector5(player: Player): Vector5 { const { x: rx, y: ry } = player.getRotation() - const { x, y, z } = Vec.floor(player.getHeadLocation()) + const { x, y, z } = player.getHeadLocation() return { x, y, z, rx: Math.floor(rx), ry: Math.floor(ry) } } diff --git a/src/lib/cutscene/edit.ts b/src/lib/cutscene/edit.ts index 28496c37..110e580d 100644 --- a/src/lib/cutscene/edit.ts +++ b/src/lib/cutscene/edit.ts @@ -1,6 +1,6 @@ /* i18n-ignore */ -import { Container, ItemStack, MolangVariableMap, Player, world } from '@minecraft/server' +import { Container, ItemStack, MolangVariableMap, Player } from '@minecraft/server' import { Vec } from 'lib/vector' import { MinecraftItemTypes } from '@minecraft/vanilla-data' @@ -9,7 +9,7 @@ import { Cooldown } from 'lib/cooldown' import { i18n } from 'lib/i18n/text' import { Temporary } from 'lib/temporary' import { util } from 'lib/util' -import { isLocationError } from 'lib/utils/game' +import { isLocationError, onLoad } from 'lib/utils/game' import { Cutscene } from './cutscene' import { cutscene as cusceneCommand } from './menu' @@ -26,7 +26,7 @@ export const cutsceneEdit = { }, } -world.afterEvents.worldLoad.subscribe(() => { +onLoad(() => { /** List of items that controls the editing process */ const controls: Record< string, @@ -52,7 +52,7 @@ world.afterEvents.worldLoad.subscribe(() => { ), (player, cutscene) => { cutscene.withNewSection(cutscene.sections, {}) - player.info(`Секция добавлена. Секций всего: §f${cutscene.sections.length}`) + player.info(`Секция добавлена. Создайте точку внутри секции. Секций всего: §f${cutscene.sections.length}`) }, ], cancel: [ diff --git a/src/lib/cutscene/menu.ts b/src/lib/cutscene/menu.ts index fbfa4fc3..f2baf4c8 100644 --- a/src/lib/cutscene/menu.ts +++ b/src/lib/cutscene/menu.ts @@ -1,4 +1,4 @@ -import { Player, world } from '@minecraft/server' +import { Player } from '@minecraft/server' import { PersistentSet } from 'lib/database/persistent-set' import { ActionForm } from 'lib/form/action' import { ArrayForm } from 'lib/form/array' @@ -19,7 +19,7 @@ export const cutscene = new Command('cutscene') const cutscenes = new PersistentSet('cutscenesIds') -world.afterEvents.worldLoad.subscribe(() => { +cutscenes.onLoad(() => { for (const c of cutscenes) new Cutscene(c, c) }) diff --git a/src/lib/database/abstract.ts b/src/lib/database/abstract.ts index ce0bbf60..bf3b1e7d 100644 --- a/src/lib/database/abstract.ts +++ b/src/lib/database/abstract.ts @@ -10,10 +10,11 @@ export interface Table { delete(key: Key): boolean size: number keys(): MapIterator - values(): Value[] - valuesImmutable(): MapIterator> + values(): Immutable[] + valuesIterator(): MapIterator> entries(): [Key, Value][] entriesImmutable(): MapIterator<[Key, Immutable]> + onLoad(waiter: (value: void) => void): void } export function table(name: string): Table @@ -72,10 +73,22 @@ export class MemoryTable extends ProxyDataba this.value = new Map(Object.entries(tableData)) as Map } } + + protected loaded = true + + onLoad(waiter: (value: void) => void): void { + waiter() + } } if (__TEST__) { - class TestDatabase extends ProxyDatabase {} + class TestDatabase extends ProxyDatabase implements Table { + protected loaded = true + + onLoad(waiter: (value: void) => void): void { + waiter() + } + } configureDatabase({ tables: TestDatabase.tables, diff --git a/src/lib/database/item-stack.test.ts b/src/lib/database/item-stack.test.ts index 955008ce..ab2fc8ce 100644 --- a/src/lib/database/item-stack.test.ts +++ b/src/lib/database/item-stack.test.ts @@ -1,7 +1,7 @@ import 'lib/extensions/enviroment' -import { defaultLang } from 'lib/assets/lang' import { ItemLoreSchema } from './item-stack' +import { defaultLang } from 'lib/assets/lang' describe('item stack', () => { it('should create item', () => { @@ -67,26 +67,4 @@ describe('item stack', () => { ] `) }) - - it('should have right types', () => { - const schema = new ItemLoreSchema('test 3') - .property('test', String) - .property('owned', Boolean) - .property('key', String) - - .build() - - const { storage } = schema.create(defaultLang, { - test: '', - owned: true, - key: '', - }) - - // @ts-expect-error Expect this to not allow arbitrary keys - storage.lol - - expectTypeOf(storage.key).toBeString() - expectTypeOf(storage.owned).toBeBoolean() - expectTypeOf(storage.key).toBeString() - }) }) diff --git a/src/lib/database/migrations.ts b/src/lib/database/migrations.ts index 296c0b6e..e57957be 100644 --- a/src/lib/database/migrations.ts +++ b/src/lib/database/migrations.ts @@ -1,10 +1,11 @@ -import { system, world } from '@minecraft/server' +import { system } from '@minecraft/server' +import { onLoad } from 'lib/utils/load-ref' import { table } from './abstract' const database = table('databaseMigrations') export function migration(name: string, migrateFN: VoidFunction) { - world.afterEvents.worldLoad.subscribe(() => { + onLoad(() => { if (database.has(name)) return system.delay(() => { diff --git a/src/lib/database/persistent-set.ts b/src/lib/database/persistent-set.ts index f155d6c9..ee8238a3 100644 --- a/src/lib/database/persistent-set.ts +++ b/src/lib/database/persistent-set.ts @@ -1,4 +1,4 @@ -import { world } from '@minecraft/server' +import { onLoad } from 'lib/utils/load-ref' import { LongDynamicProperty } from './properties' export class LimitedSet extends Set { @@ -18,11 +18,15 @@ export class PersistentSet extends LimitedSet { protected limit = 1_000, ) { super() - world.afterEvents.worldLoad.subscribe(() => { - this.load() - }) + for (const key in LimitedSet.prototype) { + ;(this as Record)[key] = () => { + throw new Error(`PersistentSet<${id}> is not yet loaded!`) + } + } } + onLoad = onLoad(() => this.load()).onLoad + private load() { const id = `PersistentSet<${this.id}>:` try { @@ -31,12 +35,20 @@ export class PersistentSet extends LimitedSet { if (!Array.isArray(values)) return console.warn(`${id} Dynamic property is not array, it is:`, values) values.forEach(e => this.add(e as T)) + + for (const [key, value] of Object.entries(LimitedSet.prototype)) (this as Record)[key] = value } catch (error) { console.error(`${id} Failed to load:`, error) + + for (const key in LimitedSet.prototype) { + ;(this as Record)[key] = () => { + throw new Error(`PersistentSet<${id}> Failed to load: ${error}`) + } + } } } - save() { + protected save() { LongDynamicProperty.set(this.id, JSON.stringify([...this])) return this } diff --git a/src/lib/database/player.ts b/src/lib/database/player.ts index 65a26d46..6025db0e 100644 --- a/src/lib/database/player.ts +++ b/src/lib/database/player.ts @@ -1,7 +1,7 @@ import { Player, world, type PlayerDatabase } from '@minecraft/server' import { expand } from 'lib/extensions/extend' import { i18n } from 'lib/i18n/text' -import { DEFAULT_ROLE } from 'lib/roles' +import { DEFAULT_ROLE } from '../roles/index' import { Table, table } from './abstract' declare module '@minecraft/server' { diff --git a/src/lib/database/properties.ts b/src/lib/database/properties.ts index b1480b59..d539e1d3 100644 --- a/src/lib/database/properties.ts +++ b/src/lib/database/properties.ts @@ -1,6 +1,7 @@ import { world } from '@minecraft/server' import { ProxyDatabase } from 'lib/database/proxy' import { noI18n } from 'lib/i18n/text' +import { onLoad } from 'lib/utils/load-ref' import { DatabaseDefaultValue, DatabaseError, UnknownTable, configureDatabase } from './abstract' import { DatabaseUtils } from './utils' @@ -14,16 +15,16 @@ class DynamicPropertyDB extends Pr super(id, defaultValue) if (id in DynamicPropertyDB.tables) throw new DatabaseError(`Table ${this.id} already initialized!`) - world.afterEvents.worldLoad.subscribe(() => { - this.init() - }) DynamicPropertyDB.tables[id] = this as UnknownTable } - private init() { + onLoad = onLoad(() => this.load()).onLoad + + private load() { // Init try { this.value = new Map(this.restore(LongDynamicProperty.get(this.id) as Record)) + this.loaded = true } catch (error) { console.error(new DatabaseError(noI18n`Failed to init table '${this.id}': ${error}`)) } diff --git a/src/lib/database/proxy.ts b/src/lib/database/proxy.ts index 65b3b850..195e8096 100644 --- a/src/lib/database/proxy.ts +++ b/src/lib/database/proxy.ts @@ -8,7 +8,7 @@ const PROXY_TARGET = Symbol('proxy_target') type DynamicObject = Record type ProxiedDynamicObject = DynamicObject & { [IS_PROXIED]?: boolean } -export class ProxyDatabase implements Table { +export abstract class ProxyDatabase implements Table { static tables: Record = {} constructor( @@ -18,6 +18,8 @@ export class ProxyDatabase impleme ProxyDatabase.tables[id] = this as UnknownTable } + abstract onLoad(waiter: (value: void) => void): void + get size(): number { return this.value.size } @@ -35,6 +37,7 @@ export class ProxyDatabase impleme } getImmutable(key: Key): Immutable { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) const value = this.value.get(key) if (this.defaultValue && typeof value === 'undefined') { this.value.set(key, this.defaultValue(key)) @@ -59,13 +62,11 @@ export class ProxyDatabase impleme return this.value.keys() } - values(): Value[] { - const values: Value[] = [] - for (const value of this.value.values()) values.push(value) - return values + values() { + return [...this.value.values()] as Immutable[] } - valuesImmutable() { + valuesIterator() { return this.value.values() as MapIterator> } @@ -140,6 +141,8 @@ export class ProxyDatabase impleme protected value = new Map() + protected loaded = false + private proxyCache = new WeakMap() protected restore(from: Record) { diff --git a/src/lib/database/scoreboard.ts b/src/lib/database/scoreboard.ts index 5348d4f6..7583af4c 100644 --- a/src/lib/database/scoreboard.ts +++ b/src/lib/database/scoreboard.ts @@ -3,6 +3,7 @@ import { defaultLang } from 'lib/assets/lang' import { expand } from 'lib/extensions/extend' import { i18nShared } from 'lib/i18n/text' import { capitalize } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' declare module '@minecraft/server' { namespace ScoreNames { @@ -165,13 +166,15 @@ export class ScoreboardDB { return objective } - scoreboard + scoreboard!: ScoreboardObjective constructor( public id: string, displayName: string = id, ) { - this.scoreboard = ScoreboardDB.objective(id, displayName) + onLoad(() => { + this.scoreboard = ScoreboardDB.objective(id, displayName) + }) } set(id: Entity | string, value: number) { diff --git a/src/lib/database/utils.ts b/src/lib/database/utils.ts index 8541216b..0892d369 100644 --- a/src/lib/database/utils.ts +++ b/src/lib/database/utils.ts @@ -2,6 +2,7 @@ import { Entity, StructureSaveMode, system, world } from '@minecraft/server' import { noI18n } from 'lib/i18n/text' +import { onLoad } from 'lib/utils/load-ref' import { Vec } from 'lib/vector' interface TableEntity { @@ -133,6 +134,6 @@ export class DatabaseUtils { } } -world.afterEvents.worldLoad.subscribe(() => { +onLoad(() => { world.overworld.runCommand('tickingarea add 0 -64 0 0 200 0 database true') }) diff --git a/src/lib/enchantments.ts b/src/lib/enchantments.ts index 587608b3..8e069384 100644 --- a/src/lib/enchantments.ts +++ b/src/lib/enchantments.ts @@ -6,7 +6,6 @@ import { enchantmentsJson } from './assets/enchantments' import { Core } from './extensions/core' const location = { x: 0, y: -10, z: 0 } -const dimension = world.overworld export const Enchantments = { custom: {} as Record>>, @@ -15,6 +14,8 @@ export const Enchantments = { } function load() { + const dimension = world.overworld + let expecting = enchantmentsJson.items as number for (let i = 1; i <= enchantmentsJson.files; i++) { const structure = `mystructure:generated/${i}` diff --git a/src/lib/extensions/core.ts b/src/lib/extensions/core.ts index 84408e7e..e650dcda 100644 --- a/src/lib/extensions/core.ts +++ b/src/lib/extensions/core.ts @@ -1,4 +1,5 @@ import { Player, system, world } from '@minecraft/server' +import { onLoad } from 'lib/utils/load-ref' import { EventLoader, EventSignal } from '../event-signal' /** Core server features */ @@ -16,7 +17,7 @@ export const Core = { } if (!__VITEST__) { - world.afterEvents.worldLoad.subscribe(() => { + onLoad(() => { system.run(function waiter() { const entities = world.overworld.getEntities() if (entities.length < 1) { diff --git a/src/lib/extensions/on-screen-display.ts b/src/lib/extensions/on-screen-display.ts index bd613686..fb9a194a 100644 --- a/src/lib/extensions/on-screen-display.ts +++ b/src/lib/extensions/on-screen-display.ts @@ -1,5 +1,6 @@ import { Player, RawMessage, ScreenDisplay, system, world } from '@minecraft/server' import { ScreenDisplaySymbol } from 'lib/extensions/player' +import { onLoad } from 'lib/utils/load-ref' import { fromMsToTicks, fromTicksToMs } from 'lib/utils/ms' import { WeakPlayerMap } from 'lib/weak-player-storage' @@ -219,7 +220,7 @@ const actionbarLock = new WeakPlayerMap<{ priority: ActionbarPriority; expires: const defaultOptions = { fadeInDuration: 0, fadeOutDuration: 0, stayDuration: 0 } const defaultTitleOptions = { ...defaultOptions, stayDuration: -1 } -world.afterEvents.worldLoad.subscribe(run) +onLoad(run) function run() { system.run(() => { diff --git a/src/lib/extensions/system.ts b/src/lib/extensions/system.ts index e61c8ba0..71f5aa91 100644 --- a/src/lib/extensions/system.ts +++ b/src/lib/extensions/system.ts @@ -1,5 +1,6 @@ import { system, System, world } from '@minecraft/server' import stringifyError from 'lib/utils/error' +import { LoadRef } from 'lib/utils/load-ref' import { capitalize, util } from '../util' import { expand } from './extend' @@ -64,15 +65,24 @@ expand(System.prototype, { runJob(generator, name) { const id = name ?? stringifyError.parent() + const source = stringifyError.stack.get() return super.runJob( (function* runJobWrapper() { - let v - do { - const end = util.benchmark(id, 'job') - v = generator.next() - end() - yield - } while (!v.done) + try { + let v + do { + const end = util.benchmark(id, 'job') + v = generator.next() + if ((v.value as unknown) instanceof Promise) + (v.value as unknown as Promise).catch((e: unknown) => + console.error('Error in async job', name, e, source), + ) + end() + yield + } while (!v.done) + } catch (e) { + console.error('Error in job', name, e, source) + } })(), ) }, @@ -156,6 +166,7 @@ function Timer( TIMERS_PATHES[visualId] = path function timer() { + if (type !== 'timeout' && !LoadRef.loadFinished) return util.catch(fn, capitalize(type)) } diff --git a/src/lib/extensions/world.ts b/src/lib/extensions/world.ts index 1e719667..f9620847 100644 --- a/src/lib/extensions/world.ts +++ b/src/lib/extensions/world.ts @@ -1,5 +1,6 @@ import { Dimension, World, world } from '@minecraft/server' import { MinecraftDimensionTypes } from '@minecraft/vanilla-data' +import { onLoad } from 'lib/utils/load-ref' import { expand } from './extend' declare module '@minecraft/server' { @@ -21,7 +22,7 @@ declare module '@minecraft/server' { } } -world.afterEvents.worldLoad.subscribe(() => { +onLoad(() => { expand(World.prototype, { overworld: world.getDimension(MinecraftDimensionTypes.Overworld), nether: world.getDimension(MinecraftDimensionTypes.Nether), @@ -32,15 +33,15 @@ world.afterEvents.worldLoad.subscribe(() => { expand(World.prototype, { say: world.sendMessage.bind(world), get overworld() { - // throw new Error('Dimensions are not available') + throw new Error('Dimensions are not available') return undefined as unknown as Dimension }, get nether() { - // throw new Error('Dimensions are not available') + throw new Error('Dimensions are not available') return undefined as unknown as Dimension }, get end() { - // throw new Error('Dimensions are not available') + throw new Error('Dimensions are not available') return undefined as unknown as Dimension }, logOnce(name, ...data: unknown[]) { diff --git a/src/lib/form/chest.ts b/src/lib/form/chest.ts index 723f20fe..3526091f 100644 --- a/src/lib/form/chest.ts +++ b/src/lib/form/chest.ts @@ -1,7 +1,7 @@ import { BlockPermutation, ItemPotionComponent, ItemStack, Player } from '@minecraft/server' import { ActionFormData, ActionFormResponse } from '@minecraft/server-ui' -import { MinecraftItemTypes, MinecraftPotionLiquidTypes } from '@minecraft/vanilla-data' +import { MinecraftItemTypes, MinecraftPotionDeliveryTypes } from '@minecraft/vanilla-data' import { Items, totalCustomItems } from 'lib/assets/custom-items' import { textureData } from 'lib/assets/texture-data' import { translateTypeId } from 'lib/i18n/lang' @@ -44,8 +44,9 @@ export function getAuxTextureOrPotionAux(itemStack: ItemStack) { const potion = itemStack.getComponent(ItemPotionComponent.componentId) if (!potion) return getAuxOrTexture(MinecraftItemTypes.Potion) - const { potionEffectType: effect, potionLiquidType: liquid } = potion - const type = liquid.id !== MinecraftPotionLiquidTypes.Regular ? '_' + liquid.id.toLowerCase() : '' + // TODO ENSure it works correctly + const { potionEffectType: effect, potionDeliveryType: delivery } = potion + const type = delivery.id !== MinecraftPotionDeliveryTypes.Consume ? '_' + delivery.id.toLowerCase() : '' const effectId = (effect.id[0] ?? '').toLowerCase() + effect.id diff --git a/src/lib/form/lore.ts b/src/lib/form/lore.ts index e1c2584c..1d98f050 100644 --- a/src/lib/form/lore.ts +++ b/src/lib/form/lore.ts @@ -2,6 +2,7 @@ import { Player } from '@minecraft/server' import { table } from 'lib/database/abstract' import { i18n } from 'lib/i18n/text' import { form, NewFormCallback, NewFormCreator } from './new' +import { QuestForm } from './quest' interface LoreFormDb { seen: string[] @@ -11,7 +12,7 @@ type AddFn = (f: NewFormCreator) => void export type LF = Omit -export class LoreForm { +export class LoreForm extends QuestForm { static db = table('loreForm', () => ({ seen: [] })) static list: LoreForm[] = [] @@ -22,10 +23,11 @@ export class LoreForm { constructor( protected id: string, - protected form: NewFormCreator, - protected player: Player, - protected back: NewFormCallback, + form: NewFormCreator, + player: Player, + back: NewFormCallback, ) { + super(form, player, back) this.db = LoreForm.db.get(`${id} ${player.id}`) LoreForm.list.push(this) } diff --git a/src/lib/form/quest.ts b/src/lib/form/quest.ts new file mode 100644 index 00000000..df93f5db --- /dev/null +++ b/src/lib/form/quest.ts @@ -0,0 +1,18 @@ +import { Quest } from 'lib/quest' +import { FormContext, NewFormCallback, NewFormCreator } from './new' +import { Player } from '@minecraft/server' + +export class QuestForm { + constructor( + protected form: NewFormCreator, + protected player: Player, + protected back: NewFormCallback, + ) {} + + quest(quest: Quest, textOverride?: Text, descriptionOverride?: Text) { + const rendered = quest.button.render(this.player, () => this.back, descriptionOverride) + if (!rendered) return + + this.form.button(textOverride && rendered[0] === quest.name ? textOverride : rendered[0], rendered[1], rendered[2]) + } +} diff --git a/src/lib/lib.d.ts b/src/lib/lib.d.ts index b573e261..8555176c 100644 --- a/src/lib/lib.d.ts +++ b/src/lib/lib.d.ts @@ -94,11 +94,6 @@ type Narrowable = string | number | bigint | boolean declare module '@minecraft/server' { interface PlayerDatabase { name?: string | undefined - readonly role: Role - prevRole?: Role - quests?: import('./quest/quest').Quest.DB - achivs?: import('./achievements/achievement').Achievement.DB - unlockedPortals?: string[] } } diff --git a/src/lib/load/message1.ts b/src/lib/load/message1.ts index ba464668..a5de1efd 100644 --- a/src/lib/load/message1.ts +++ b/src/lib/load/message1.ts @@ -1,7 +1,5 @@ -import { world } from '@minecraft/server' - if (__GIT__) console.info('§7' + __GIT__) const message = '§9> §fReloading script...' if (!__VITEST__) console.info(message) -if (!__RELEASE__) world.say(message) +// if (!__RELEASE__) world.say(message) diff --git a/src/lib/load/message2.ts b/src/lib/load/message2.ts index 81cd160f..8a552dd1 100644 --- a/src/lib/load/message2.ts +++ b/src/lib/load/message2.ts @@ -8,8 +8,10 @@ system.delay(() => { if (__GIT__) import('lib/roles').then(({ is }) => { - for (const player of world.getAllPlayers()) - if (is(player.id, 'techAdmin')) player.tell(`§sCommit: §f${__GIT__.replace(/^Commit: /, '')}`) + world.afterEvents.worldLoad.subscribe(() => { + for (const player of world.getAllPlayers()) + if (is(player.id, 'techAdmin')) player.tell(`§sCommit: §f${__GIT__.replace(/^Commit: /, '')}`) + }) }) globalThis.loaded = 0 diff --git a/src/lib/load/watchdog.ts b/src/lib/load/watchdog.ts index e02a4786..45f6e81c 100644 --- a/src/lib/load/watchdog.ts +++ b/src/lib/load/watchdog.ts @@ -7,10 +7,6 @@ declare global { globalThis.loaded = Date.now() -//@ts-expect-error Define global intl if not defined -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -globalThis.Intl ??= {} - const reasons: Record = { Hang: 'Скрипт завис', StackOverflow: 'Стэк переполнен', diff --git a/src/lib/location.ts b/src/lib/location.ts index 8b26f6a4..b9869f89 100644 --- a/src/lib/location.ts +++ b/src/lib/location.ts @@ -2,9 +2,10 @@ import { Player, TeleportOptions, Vector3, system, world } from '@minecraft/serv import { isEmpty } from 'lib/util' import { Vec, VecSymbol } from 'lib/vector' import { EventLoaderWithArg } from './event-signal' -import { i18n, noI18n } from './i18n/text' +import { noI18n } from './i18n/text' import { Place } from './rpg/place' import { Settings } from './settings' +import { onLoad } from './utils/load-ref' import { VectorInDimension } from './utils/point' interface LocationCommon { @@ -46,7 +47,7 @@ class Location { onChange: () => location.load(true), } - location.load() + onLoad(() => location.load()) location.firstLoad = true // Set floored value on reload @@ -166,42 +167,48 @@ export const locationWithRotation = LocationWithRotation.creator< /** Creates reference to a location that can be changed via settings command */ export const locationWithRadius = LocationWithRadius.creator() -system.delay(() => { - for (const [k, d] of Settings.worldDatabase.entries()) { - if (!Object.keys(d).length) { - Settings.worldDatabase.delete(k) +onLoad(() => { + system.delay(() => { + for (const [k, d] of Settings.worldDatabase.entries()) { + if (!Object.keys(d).length) { + Settings.worldDatabase.delete(k) + } } - } + }) }) /** Migration helper */ export function migrateLocationName(oldGroup: string, oldName: string, newGroup: string, newName: string) { - const group = Settings.worldDatabase.get(oldGroup) - const location = group[oldName] - if (typeof location !== 'undefined') { - console.debug(i18n`Migrating location ${oldGroup}:${oldName} to ${newGroup}:${newName}`) - - Settings.worldDatabase.get(newGroup)[newName] = location - - Reflect.deleteProperty(Settings.worldDatabase.get(oldGroup), oldName) - } else if (!Settings.worldDatabase.get(newGroup)[newName]) { - console.warn( - i18n.error`No location found at ${oldGroup}:${oldName}. Group: ${isEmpty(group) ? [...Settings.worldDatabase.keys()] : Object.keys(group)}`, - ) - } + onLoad(() => { + const group = Settings.worldDatabase.get(oldGroup) + const location = group[oldName] + if (typeof location !== 'undefined') { + console.debug(`Migrating location ${oldGroup}:${oldName} to ${newGroup}:${newName}`) + + Settings.worldDatabase.get(newGroup)[newName] = location + + Reflect.deleteProperty(Settings.worldDatabase.get(oldGroup), oldName) + } else if (!Settings.worldDatabase.get(newGroup)[newName]) { + console.warn( + noI18n.warn`No location found at ${oldGroup}:${oldName}. Group: ${isEmpty(group) ? [...Settings.worldDatabase.keys()] : Object.keys(group)}`, + ) + } + }) } export function migrateLocationGroup(from: string, to: string) { - const group = Settings.worldDatabase.get(from) - if (typeof group !== 'undefined') { - console.debug(i18n`Migrating group ${from} to ${to}`) - - Settings.worldDatabase.set(to, { ...Settings.worldDatabase.get(to), ...group }) - - Settings.worldDatabase.delete(from) - } else { - console.warn( - i18n.error`No group found for migration: ${from} -> ${to}. Groups: ${[...Settings.worldDatabase.keys()]}`, - ) - } + onLoad(() => { + const group = Settings.worldDatabase.get(from) + if (typeof group !== 'undefined') { + console.debug(noI18n`Migrating group ${from} to ${to}`) + + Settings.worldDatabase.set(to, { ...Settings.worldDatabase.get(to), ...group }) + + Settings.worldDatabase.delete(from) + } else { + console.warn( + noI18n.warn`No group found for migration: ${from} -> ${to}. Groups: ${[...Settings.worldDatabase.keys()]}`, + ) + } + }) } diff --git a/src/lib/player-join.ts b/src/lib/player-join.ts index 664569fe..1fe76f6a 100644 --- a/src/lib/player-join.ts +++ b/src/lib/player-join.ts @@ -51,6 +51,10 @@ export abstract class Join extends Singleton { this.joinPositions.set(player, { position: this.playerAt(player) }) } + isJoining(player: Player) { + return this.joinPositions.has(player) + } + protected joinPositions = new WeakPlayerMap({ removeOnLeave: true }) private onInterval(player: Player) { diff --git a/src/lib/player-move.ts b/src/lib/player-move.ts index 330222a1..c0581a2a 100644 --- a/src/lib/player-move.ts +++ b/src/lib/player-move.ts @@ -2,6 +2,7 @@ import { Player, ShortcutDimensions, system, world } from '@minecraft/server' import type { Region } from 'lib/region' import { Vec } from 'lib/vector' import { EventSignal } from './event-signal' +import { onLoad } from './utils/game' import { VectorInDimension } from './utils/point' import { WeakPlayerMap } from './weak-player-storage' @@ -31,7 +32,7 @@ export function anyPlayerNearRegion(region: Region, radius: number) { return false } -world.afterEvents.worldLoad.subscribe(() => { +onLoad(() => { // Do it sync on first run because some of the funcs above use it sync. It will start interval too for (const _ of jobPlayerPosition()) void 0 }) diff --git a/src/lib/portals.ts b/src/lib/portals.ts index 38649b96..edae1844 100644 --- a/src/lib/portals.ts +++ b/src/lib/portals.ts @@ -41,6 +41,7 @@ export class Portal { }: { lockAction?: LockActionCheckOptions; fadeScreen?: boolean; title?: string } = {}, updateHud?: VoidFunction, ) { + console.log('Portal teleport') if (!this.canTeleport(player, lockAction)) return if (fadeScreen) this.fadeScreen(player) diff --git a/src/lib/quest/quest.ts b/src/lib/quest/quest.ts index b52150a0..963064b4 100644 --- a/src/lib/quest/quest.ts +++ b/src/lib/quest/quest.ts @@ -12,6 +12,12 @@ import { QuestButton } from './button' import { PlayerQuest } from './player' import { QS } from './step' +declare module '@minecraft/server' { + interface PlayerDatabase { + quests?: Quest.DB + } +} + export declare namespace Quest { interface DB { active: { id: string; i: number; db?: unknown }[] diff --git a/src/lib/recurring-event.ts b/src/lib/recurring-event.ts index bfdae62d..0ddca771 100644 --- a/src/lib/recurring-event.ts +++ b/src/lib/recurring-event.ts @@ -4,6 +4,7 @@ import { fromMsToTicks } from 'lib/utils/ms' import { table } from './database/abstract' import { setDefaults } from './database/defaults' import later, { Later } from './utils/later' +import { onLoad } from './utils/load-ref' later.runtime = { setTimeout: (fn, delayMs) => system.runTimeout(fn, 'laterSetTimeout', fromMsToTicks(delayMs)), @@ -31,11 +32,11 @@ type RecurringEventCallback = (storage: T, ct export class RecurringEvent { static db = table('recurringEvents', () => ({ lastRun: '', storage: {} })) - protected db: DB - protected schedule: Later.Schedule - protected interval: Later.Timer + protected db!: DB + + protected interval!: Later.Timer stop() { this.interval.clear() @@ -57,12 +58,14 @@ export class RecurringEvent { { runAfterOffline = false }: RecurringOptions = {}, ) { this.schedule = later.schedule(scheduleData) - this.db = RecurringEvent.db.get(id) as DB - this.db.storage = setDefaults(this.db.storage, this.createStorage()) + onLoad(() => { + this.db = RecurringEvent.db.get(id) as DB + this.db.storage = setDefaults(this.db.storage, this.createStorage()) - this.interval = later.setInterval(this.run.bind(this), scheduleData) + this.interval = later.setInterval(this.run.bind(this), scheduleData) - if (runAfterOffline) this.run(this.db.lastRun === this.getLastRunDate().toString()) + if (runAfterOffline) this.run(this.db.lastRun === this.getLastRunDate().toString()) + }) } protected run(restoreAfterOffline = false) { diff --git a/src/lib/region/areas/area.ts b/src/lib/region/areas/area.ts index edd48249..65f3074b 100644 --- a/src/lib/region/areas/area.ts +++ b/src/lib/region/areas/area.ts @@ -1,6 +1,7 @@ import { Dimension, system, world } from '@minecraft/server' import { i18n, noI18n } from 'lib/i18n/text' -import { stringifyError } from 'lib/util' +import { stringifyError, util } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' @@ -20,7 +21,7 @@ export abstract class Area { static asSaveableArea(this: T) { const b = this as AreaWithType - world.afterEvents.worldLoad.subscribe(() => { + onLoad(() => { b.type = new (this as unknown as AreaCreator)({}).type if ((this as unknown as typeof Area).loaded) { @@ -103,12 +104,13 @@ export abstract class Area { } forEachVector( - callback: (vector: Vector3, isIn: boolean, dimension: Dimension) => void | Promise | symbol, + callback: (vector: Vector3, isIn: boolean, dimension: Dimension) => void | Promise, yieldEach = 10, ) { const { edges, dimension } = this const isIn = (vector: Vector3) => this.isIn({ location: vector, dimensionType: this.dimensionType }) const { max, min } = this.dimension.heightRange + const stack = new Error().stack return new Promise((resolve, reject) => { system.runJob( @@ -118,7 +120,7 @@ export abstract class Area { for (const vector of Vec.forEach(...edges)) { if (vector.y < min || vector.y > max) continue - callback(vector, isIn(vector), dimension) + util.catch(() => callback(vector, isIn(vector), dimension), 'Area.forEachVector', stack) i++ if (i % yieldEach === 0) yield } diff --git a/src/lib/region/config.ts b/src/lib/region/config.ts index 99b4ac55..9648f0f1 100644 --- a/src/lib/region/config.ts +++ b/src/lib/region/config.ts @@ -1,6 +1,7 @@ -import { BlockTypes, Entity, world } from '@minecraft/server' +import { BlockTypes, Entity } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +import { onLoad } from 'lib/utils/load-ref' /** All doors and switches in minecraft */ export const DOORS: string[] = [] @@ -14,7 +15,7 @@ export const SWITCHES: string[] = [] /** All gates in minecraft */ export const GATES: string[] = [] -world.afterEvents.worldLoad.subscribe(() => { +onLoad(() => { const blocks = BlockTypes.getAll() function fill(target: string[], filter: (params: { id: string }) => boolean) { diff --git a/src/lib/region/explosion.ts b/src/lib/region/explosion.ts new file mode 100644 index 00000000..0c1752bc --- /dev/null +++ b/src/lib/region/explosion.ts @@ -0,0 +1,14 @@ +import { Block, world } from '@minecraft/server' +import { Region } from './kinds/region' +import { SafeAreaRegion } from './kinds/safe-area' + +world.beforeEvents.explosion.subscribe(event => { + event.setImpactedBlocks(event.getImpactedBlocks().filter(canBlockExplode)) +}) + +function canBlockExplode(block: Block) { + const region = Region.getAt(block) + if (region instanceof SafeAreaRegion) return false + + return true +} diff --git a/src/lib/region/index.ts b/src/lib/region/index.ts index c683ed20..2f1b0382 100644 --- a/src/lib/region/index.ts +++ b/src/lib/region/index.ts @@ -8,15 +8,14 @@ import { world, } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -import { CustomEntityTypes } from 'lib/assets/custom-entity-types' -import { Items } from 'lib/assets/custom-items' -import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' +// import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +// import { Items } from 'lib/assets/custom-items' +// import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' -import { onPlayerMove } from 'lib/player-move' +// import { onPlayerMove } from 'lib/player-move' import { is } from 'lib/roles' import { isNotPlaying } from 'lib/utils/game' -import { createLogger } from 'lib/utils/logger' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { EventSignal } from '../event-signal' @@ -30,7 +29,11 @@ import { SWITCHES, TRAPDOORS, } from './config' +// import { RegionEvents } from './events' +import { onPlayerMove } from 'lib/player-move' +import { createLogger } from 'lib/utils/logger' import { RegionEvents } from './events' +import './explosion' import { Region } from './kinds/region' export * from './command' @@ -108,7 +111,6 @@ actionGuard((player, region, context) => { if (typeId === MinecraftItemTypes.EnderPearl) return ent.includes(MinecraftEntityTypes.EnderPearl) if (typeId === MinecraftItemTypes.WindCharge) return ent.includes(MinecraftEntityTypes.WindChargeProjectile) if (typeId === MinecraftItemTypes.Snowball) return ent.includes(MinecraftEntityTypes.Snowball) - if (typeId === Items.Fireball) return ent.includes(CustomEntityTypes.Fireball) } } }, ActionGuardOrder.ProjectileUsePrevent) @@ -201,21 +203,20 @@ onPlayerMove.subscribe(({ player, location, dimensionType }) => { RegionEvents.playerInRegionsCache.set(player, newest) const currentRegion = newest[0] - // TODO Replace with proper damage cancel on beforeEntityHurt once we update - const isPlaying = !isNotPlaying(player) - - const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) - - if (typeof currentRegion !== 'undefined' && isPlaying) { - if (currentRegion.permissions.pvp === false) { - player.triggerEvent( - player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], - ) - player.setProperty(PlayerProperties['lw:newbie'], true) - } else if (currentRegion.permissions.pvp === 'pve') { - player.setProperty(PlayerProperties['lw:newbie'], true) - } else resetNewbie() - } else resetNewbie() + // const isPlaying = !isNotPlaying(player) + + // const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) + + // if (typeof currentRegion !== 'undefined' && isPlaying) { + // if (currentRegion.permissions.pvp === false) { + // player.triggerEvent( + // player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], + // ) + // player.setProperty(PlayerProperties['lw:newbie'], true) + // } else if (currentRegion.permissions.pvp === 'pve') { + // player.setProperty(PlayerProperties['lw:newbie'], true) + // } else resetNewbie() + // } else resetNewbie() EventSignal.emit(RegionEvents.onInterval, { player, currentRegion }) }) diff --git a/src/lib/region/kinds/minearea.ts b/src/lib/region/kinds/minearea.ts index e40c28c4..5c2e4ee1 100644 --- a/src/lib/region/kinds/minearea.ts +++ b/src/lib/region/kinds/minearea.ts @@ -64,6 +64,7 @@ export class MineareaRegion extends RegionWithStructure { if (this.restoringStructurePromise) return this.restoringStructurePromise this.restoringStructurePromise = this.internalRestoreStructure(eachVectorCallback) + this.restoringStructurePromise.catch((e: unknown) => console.error('MineareaRegion.restoreStructure', e)) const result = await this.restoringStructurePromise delete this.restoringStructurePromise return result diff --git a/src/lib/region/kinds/region.ts b/src/lib/region/kinds/region.ts index 8f7457b6..304672cd 100644 --- a/src/lib/region/kinds/region.ts +++ b/src/lib/region/kinds/region.ts @@ -98,10 +98,10 @@ export class Region { if (!key) { // We are creating new region and should save it region.save() - region.onCreate() + util.catch(() => region.onCreate(), `${this.name}.onCreate`) } else { // Restoring region with existing key - region.onRestore() + util.catch(() => region.onRestore(), `${this.name}.onCreate`) } if (area.radius) this.chunkQuery.add(region) diff --git a/src/lib/region/structure.ts b/src/lib/region/structure.ts index 569f1405..41a7dd00 100644 --- a/src/lib/region/structure.ts +++ b/src/lib/region/structure.ts @@ -95,7 +95,7 @@ export class RegionStructure { yieldEach?: number, ) { const structure = world.structureManager.get(this.id) - if (!structure) throw new ReferenceError('No structure found!') + if (!structure) throw new ReferenceError(`No structure found! ${this.id}`) const [from] = this.region.area.edges const offset = this.offset ? { x: this.offset, y: this.offset, z: this.offset } : undefined diff --git a/src/modules/commands/role.ts b/src/lib/roles/command.ts similarity index 95% rename from src/modules/commands/role.ts rename to src/lib/roles/command.ts index 7bea5e37..996a4641 100644 --- a/src/modules/commands/role.ts +++ b/src/lib/roles/command.ts @@ -3,13 +3,13 @@ import { ArrayForm } from 'lib/form/array' import { ModalForm } from 'lib/form/modal' import { FormCallback } from 'lib/form/utils' import { i18n } from 'lib/i18n/text' -import { getRole, setRole, ROLES, WHO_CAN_CHANGE } from 'lib/roles' +import { getRole, ROLES, setRole, WHO_CAN_CHANGE } from 'lib/roles' const FULL_HIERARCHY = Object.keys(ROLES) function canChange(who: Role, target: Role, allowSame = false) { if (allowSame && who === target) return true - if (who === 'creator') return true + if (who === 'creator' || who === 'techAdmin') return true return FULL_HIERARCHY.indexOf(who) < FULL_HIERARCHY.indexOf(target) } @@ -19,8 +19,7 @@ const command = new Command('role') .setPermissions('everybody') .executes(ctx => roleMenu(ctx.player)) -const restoreRole = command - .overload('restore') +const restoreRole = new Command('rolerestore') .setDescription(i18n`Восстанавливает вашу роль`) .setPermissions(p => !!p.database.prevRole) .executes(ctx => { @@ -70,7 +69,7 @@ function roleMenu(player: Player) { const button = this.button?.([player.id, player.database], { sort: 'role' }, form, back) if (button) - form.button(i18n`§3Сменить мою роль\n§7(Восстановить потом: §f.role restore§7)`.to(player.lang), button[1]) + form.button(i18n`§3Сменить мою роль\n§7(Восстановить потом: §f/rolerestore§7)`.to(player.lang), button[1]) }) .button(([id, { role, name: dbname }], _, form) => { const target = players.find(e => e.id === id) ?? id diff --git a/src/lib/roles/index.ts b/src/lib/roles/index.ts new file mode 100644 index 00000000..c921baa4 --- /dev/null +++ b/src/lib/roles/index.ts @@ -0,0 +1,174 @@ +import { GameMode, Player, ScriptEventSource, system } from '@minecraft/server' +import { EventSignal } from 'lib/event-signal' +import { Core } from '../extensions/core' +import { i18n, noI18n } from '../i18n/text' +import { isKeyof } from '../util' +import('./command') + +declare global { + /** Any known role */ + type Role = keyof typeof ROLES +} + +declare module '@minecraft/server' { + interface PlayerDatabase { + readonly role: Role + prevRole?: Role + } +} + +/** The roles that are in this server */ +export const ROLES = { + creator: i18n.nocolor`§aРуководство`, + curator: i18n.nocolor`§6Куратор`, + techAdmin: i18n.nocolor`§cТех. Админ`, + chefAdmin: i18n.nocolor`§dГл. Админ`, + admin: i18n.nocolor`§5Админ`, + moderator: i18n.nocolor`§6Модератор`, + helper: i18n.nocolor`§eПомошник`, + grandBuilder: i18n.nocolor`§bГл. Строитель`, + builder: i18n.nocolor`§3Строитель`, + member: i18n.nocolor`§fУчастник`, + spectator: i18n.nocolor`§9Наблюдатель`, + tester: i18n.nocolor`§9Тестер`, +} + +export const DEFAULT_ROLE: Role = 'member' + +/** List of role permissions */ +const PERMISSIONS: Record = { + creator: ['creator'], + curator: ['creator', 'curator'], + techAdmin: ['creator', 'curator', 'techAdmin'], + + chefAdmin: ['creator', 'curator', 'chefAdmin'], + admin: ['creator', 'curator', 'chefAdmin', 'admin'], + moderator: ['creator', 'curator', 'chefAdmin', 'admin', 'moderator'], + helper: ['creator', 'curator', 'chefAdmin', 'admin', 'moderator', 'helper'], + + grandBuilder: ['creator', 'curator', 'techAdmin', 'chefAdmin', 'admin', 'grandBuilder'], + builder: ['creator', 'curator', 'techAdmin', 'chefAdmin', 'admin', 'builder', 'grandBuilder'], + member: Object.keys(ROLES).filter(e => e !== 'spectator'), + spectator: [], // Any + tester: Object.keys(ROLES).filter(e => e !== 'spectator' && e !== 'member'), +} + +/** + * List of roles who can change role that goes after their position + * + * Also known as role hierarchy + */ +export const WHO_CAN_CHANGE: Role[] = ['creator', 'techAdmin', 'admin', 'moderator', 'helper'] + +/** + * Checks if player has permissions for performing role actions. (e.g. if player role is above or equal) + * + * @example + * is(player.id, 'admin') // Player is admin, grandAdmin or any role above + * + * @example + * is(player.id, 'grandBuilder') // Player is grandBuilder, chefAdmin, techAdmin or any role above + * + * @param playerID ID of the player to get role from + * @param role Role to check + */ +export function is(playerID: string, role: Role) { + if (!PERMISSIONS[role].length) return true + + return PERMISSIONS[role].includes(getRole(playerID)) +} + +/** + * Gets the role of this player + * + * @example + * getRole(player.id) + * + * @example + * getRole(player) + * + * @param playerID Player or his id to get role from + * @returns Player role + */ +export function getRole(playerID: Player | string): Role { + if (playerID === 'server') return 'creator' + + const id = playerID instanceof Player ? playerID.id : playerID + const role = Player.database.getImmutable(id).role + + if (!Object.keys(ROLES).includes(role)) return 'member' + return role +} + +/** + * Sets the role of this player + * + * @example + * setRole(player.id, 'admin') + * + * @example + * setRole(player, 'member') + * + * @param player - Player to set role one + * @param role - Role to set + */ +export function setRole(player: Player | string, role: Role): void { + const id = player instanceof Player ? player.id : player + const database = Player.database.get(id) + if (typeof database !== 'undefined') { + EventSignal.emit(Core.beforeEvents.roleChange, { + id, + player: player instanceof Player ? player : Player.getById(player), + newRole: role, + oldRole: database.role, + }) + + // @ts-expect-error settings role in setRole function is allowed + // role property is marked readonly so no other functions will change that + database.role = role + } +} + +// Set spectator gamemode to the spectator role +Core.beforeEvents.roleChange.subscribe(({ newRole, oldRole, player }) => { + if (!player) return + if (newRole === 'spectator') { + player.setGameMode(GameMode.Spectator) + } else if (oldRole === 'spectator') { + player.setGameMode(GameMode.Survival) + } +}) + +/* istanbul ignore next */ +if (!__VITEST__) { + // Allow recieving roles from scriptevent function run by console + system.afterEvents.scriptEventReceive.subscribe( + event => { + if (event.id.toLowerCase().startsWith('role:')) { + if (event.sourceType === ScriptEventSource.Server) { + // Allow + } else { + if (Player.database.values().find(e => WHO_CAN_CHANGE.includes(e.role))) { + return console.error(`(SCRIPTEVENT::${event.id}) Admin already set.`) + } + } + + const role = event.id.toLowerCase().replace('role:', '') + if (!isKeyof(role, ROLES)) { + return console.error( + `(SCRIPTEVENT::${event.id}) Unkown role: ${role}, allowed roles:\n${Object.entries(ROLES) + .map(e => noI18n`${e[0]}: ${e[1]}`) + .join('\n')}`, + ) + } + + const player = [...Player.database.entriesImmutable()].find(e => e[1].name === event.message)?.[0] + if (!player) return console.error(`(SCRIPTEVENT::${event.id}) PLAYER NOT FOUND`) + + setRole(player, role) + console.warn(`(SCRIPTEVENT::${event.id}) ROLE HAS BEEN SET`) + } + }, + { namespaces: ['role'] }, + ) +} diff --git a/src/lib/roles/r.ts b/src/lib/roles/r.ts new file mode 100644 index 00000000..d4649695 --- /dev/null +++ b/src/lib/roles/r.ts @@ -0,0 +1 @@ +import './index' diff --git a/src/lib/rpg/boss.ts b/src/lib/rpg/boss.ts index 36bcd761..2660e5b5 100644 --- a/src/lib/rpg/boss.ts +++ b/src/lib/rpg/boss.ts @@ -126,10 +126,10 @@ export class Boss { if (Array.isArray(this.options.allowedEntities)) this.options.allowedEntities.push(options.typeId, MinecraftEntityTypes.Player) - const areadb = Boss.arenaDb.get(this.options.place.id) - this.location = location(options.place) this.location.onLoad.subscribe(center => { + const areadb = Boss.arenaDb.get(this.options.place.id) + this.check() const area = (areadb?.area ? Area.fromJson(areadb.area) : undefined) ?? diff --git a/src/lib/rpg/custom-item.ts b/src/lib/rpg/custom-item.ts index 58302cc9..91bbb2bd 100644 --- a/src/lib/rpg/custom-item.ts +++ b/src/lib/rpg/custom-item.ts @@ -1,14 +1,15 @@ -import { ItemStack, system } from '@minecraft/server' +import { ItemStack } from '@minecraft/server' import { Items } from 'lib/assets/custom-items' import { defaultLang } from 'lib/assets/lang' import { translateTypeId } from 'lib/i18n/lang' import { i18n } from 'lib/i18n/text' +import { MaybeRef, onLoad } from 'lib/utils/game' -export const customItems: ItemStack[] = [] +export const customItems: MaybeRef[] = [] -class CustomItem { - constructor(public id: string) { - system.run(() => this.onBuild()) +export class CustomItem { + constructor(protected _typeId?: string) { + onLoad(() => this.onBuild()) } protected onBuild() { @@ -16,8 +17,6 @@ class CustomItem { customItems.push(this.cache) } - protected _typeId: string | undefined - typeId(typeId: string) { this._typeId = typeId return this @@ -38,7 +37,7 @@ class CustomItem { } get itemStack() { - if (!this._typeId) throw new TypeError('No type id specified for custom item ' + this.id) + if (!this._typeId) throw new TypeError('No type id specified for custom item') const item = new ItemStack(this._typeId).setInfo( this._nameTag && `§6${this._nameTag}`, diff --git a/src/lib/rpg/loot-table.ts b/src/lib/rpg/loot-table.ts index 5ce147ba..efd284be 100644 --- a/src/lib/rpg/loot-table.ts +++ b/src/lib/rpg/loot-table.ts @@ -6,6 +6,7 @@ import { EventSignal } from 'lib/event-signal' import { inspect, isKeyof, pick } from 'lib/util' import { copyAllItemPropertiesExceptEnchants } from 'lib/utils/game' import { selectByChance } from './random' +import { CustomItem } from './custom-item' type RandomCostMap = Record<`${number}...${number}` | number, Percent> type Percent = `${number}%` @@ -54,18 +55,18 @@ export class Loot { * @param type Keyof MinecraftItemTypes */ item(type: Exclude | Items) { - this.create(new ItemStack(isKeyof(type, MinecraftItemTypes) ? MinecraftItemTypes[type] : type)) + this.create(() => new ItemStack(isKeyof(type, MinecraftItemTypes) ? MinecraftItemTypes[type] : type)) return this } - itemStack(item: ItemStack | (() => ItemStack)) { - this.create(item) + itemStack(item: CustomItem | (() => ItemStack)) { + this.create(item instanceof CustomItem ? () => item.itemStack : item) return this } - private create(itemStack: ItemStack | (() => ItemStack)) { + private create(itemStack: () => ItemStack) { if (this.current) this.items.push(this.current) this.current = { itemStack, weight: 100, amount: [1], damage: [0], enchantments: {}, custom: [] } } diff --git a/src/lib/rpg/menu.ts b/src/lib/rpg/menu.ts index ebad2b3c..f5f07620 100644 --- a/src/lib/rpg/menu.ts +++ b/src/lib/rpg/menu.ts @@ -1,13 +1,13 @@ -import { ContainerSlot, EquipmentSlot, ItemLockMode, ItemStack, ItemTypes, Player, world } from '@minecraft/server' +import { ContainerSlot, EquipmentSlot, ItemLockMode, ItemStack, Player, world } from '@minecraft/server' import { InventoryInterval } from 'lib/action' import { Items } from 'lib/assets/custom-items' import { form } from 'lib/form/new' -import { i18n, i18nShared, noI18n } from 'lib/i18n/text' +import { i18n, noI18n } from 'lib/i18n/text' import { util } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' import { Vec } from 'lib/vector' import { WeakPlayerMap, WeakPlayerSet } from 'lib/weak-player-storage' import { MinimapNpc, resetMinimapNpcPosition, setMinimapEnabled, setMinimapNpcPosition } from './minimap' -import { Language } from 'lib/assets/lang' export class Menu { static settings: [Text, string] = [i18n`Меню\n§7Разные настройки интерфейсов и меню в игре`, 'menu'] @@ -23,9 +23,11 @@ export class Menu { return item.clone() } - static itemStack = this.createItem() + static itemStack = onLoad(() => this.createItem()) - static item = createPublicGiveItemCommand('menu', this.itemStack, another => this.isMenu(another), i18n`меню`, false) + static item = onLoad(() => + createPublicGiveItemCommand('menu', this.itemStack.value, another => this.isMenu(another), i18n`меню`, false), + ) static { world.afterEvents.itemUse.subscribe(({ source: player, itemStack }) => { @@ -45,7 +47,7 @@ export class Menu { } static isMenu(slot: Pick) { - return this.isCompass(slot) || slot.typeId === this.itemStack.typeId + return this.isCompass(slot) || slot.typeId === this.itemStack.value.typeId } } @@ -66,9 +68,11 @@ export class Compass { } } - private static items = new Array(32).fill(null).map((_, i) => { - return Menu.createItem(`${Items.CompassPrefix}${i}`) - }) + private static items = onLoad(() => + new Array(32).fill(null).map((_, i) => { + return Menu.createItem(`${Items.CompassPrefix}${i}`) + }), + ) /** Map of player as key and compass target as value */ private static players = new WeakPlayerMap() @@ -106,7 +110,7 @@ export class Compass { const target = this.players.get(player) if (!target || player.database.inv === 'spawn') { - if (Menu.isCompass(slot)) slot.setItem(Menu.itemStack) + if (Menu.isCompass(slot)) slot.setItem(Menu.itemStack.value) return } @@ -129,7 +133,7 @@ export class Compass { const angle = Math.atan2(sin, cos) const i = Math.floor((16 * angle) / Math.PI + 16) || 0 - if (typeof i === 'number') return this.items[i] + if (typeof i === 'number') return this.items.value[i] } } diff --git a/src/lib/rpg/minimap.ts b/src/lib/rpg/minimap.ts index 71a8bd69..b8b535cb 100644 --- a/src/lib/rpg/minimap.ts +++ b/src/lib/rpg/minimap.ts @@ -1,5 +1,6 @@ import { Player, world } from '@minecraft/server' import { playerJson, PlayerProperties } from 'lib/assets/player-json' +import { onLoad } from 'lib/utils/load-ref' export function setMinimapEnabled(player: Player, status: boolean) { player.setProperty(PlayerProperties['lw:minimap'], status) @@ -32,7 +33,9 @@ world.afterEvents.playerSpawn.subscribe(event => { resetAllMinimaps(event.player) }) -world.getAllPlayers().forEach(resetAllMinimaps) +onLoad(() => { + world.getAllPlayers().forEach(resetAllMinimaps) +}) function resetAllMinimaps(player: Player) { resetMinimapNpcPosition(player, MinimapNpc.Airdrop) diff --git a/src/lib/scheduled-block-place.ts b/src/lib/scheduled-block-place.ts index 9aceb7d2..f4cc6a3d 100644 --- a/src/lib/scheduled-block-place.ts +++ b/src/lib/scheduled-block-place.ts @@ -240,7 +240,7 @@ const scheduledDimensionForm = ( system.runJob( (function* placeNow() { let i = 0 - for (const immutableSchedule of schedules.valuesImmutable()) { + for (const immutableSchedule of schedules.valuesIterator()) { if (!immutableSchedule) continue i++ if (i % 100 === 0) yield diff --git a/src/lib/settings/command.ts b/src/lib/settings/command.ts new file mode 100644 index 00000000..610051e8 --- /dev/null +++ b/src/lib/settings/command.ts @@ -0,0 +1,17 @@ +import { i18n } from 'lib/i18n/text' +import { playerSettingsMenu, worldSettingsMenu } from 'lib/settings' + +new Command('settings') + .setAliases('options') + .setPermissions('member') + .setDescription(i18n`Настройки`) + .executes(ctx => { + playerSettingsMenu(ctx.player) + }) + +new Command('wsettings') + .setPermissions('techAdmin') + .setDescription(i18n`Настройки мира`) + .executes(ctx => { + worldSettingsMenu(ctx.player) + }) diff --git a/src/lib/settings/index.test.ts b/src/lib/settings/index.test.ts new file mode 100644 index 00000000..ded2efc6 --- /dev/null +++ b/src/lib/settings/index.test.ts @@ -0,0 +1,24 @@ +import { Settings } from '.' + +describe('setting change events', () => { + it('should emit events on change', () => { + const onChange = vi.fn() + const settings = Settings.world('groupName', 'group1', { + name1: { + name: 'name', + description: 'description', + value: true, + onChange, + }, + }) + + settings.name1 = false + expect(onChange).toHaveBeenCalledOnce() + + settings.name1 = true + expect(onChange).toHaveBeenCalledTimes(2) + + settings.name1 = true + expect(onChange).toHaveBeenCalledTimes(3) + }) +}) diff --git a/src/lib/settings/index.ts b/src/lib/settings/index.ts new file mode 100644 index 00000000..d04adaea --- /dev/null +++ b/src/lib/settings/index.ts @@ -0,0 +1,422 @@ +import { Player } from '@minecraft/server' +import { ActionForm } from 'lib/form/action' +import { ModalForm } from 'lib/form/modal' +import { FormCallback } from 'lib/form/utils' +import { stringify } from 'lib/util' +import { createLogger } from 'lib/utils/logger' +import { WeakPlayerMap } from 'lib/weak-player-storage' +import { MemoryTable, Table, table } from '../database/abstract' +import { Message } from '../i18n/message' +import { i18n, noI18n } from '../i18n/text' +import stringifyError from '../utils/error' +import './command' + +// TODO refactor(leaftail1880): Move all types under the Settings namespace +// TODO refactor(leaftail1880): Move everything into the lib/settings/ folder + +type DropdownSetting = [value: string, displayText: Text] + +/** Any setting value type */ +type SettingValue = string | boolean | number | DropdownSetting[] + +export const SETTINGS_GROUP_NAME = Symbol('SettingGroupName') + +interface ConfigMeta { + [SETTINGS_GROUP_NAME]?: Text +} + +// TODO Create global PaidFeaturesProvider +type SettingsPayCheck = ((player: Player) => boolean) & { onFail: PlayerCallback } + +export type SettingsConfig = Record< + string, + { + name: Text + description?: Text + value: T + onChange?: VoidFunction + paid?: SettingsPayCheck + whenNotPaidDefault?: NoInfer + } +> & + ConfigMeta + +/** Сonverting true and false to boolean and string[] to string and string literal to plain string */ +/* eslint-disable @typescript-eslint/naming-convention */ +type toPlain = T extends true | false + ? boolean + : T extends string + ? string + : T extends DropdownSetting[] + ? T[number][0] + : T extends number + ? number + : T + +export type SettingsConfigParsed = { -readonly [K in keyof T]: toPlain } + +export type SettingsDatabaseValue = Record +export type SettingsDatabase = Table + +export type PlayerSettingValues = boolean | string | number | DropdownSetting[] + +type WorldSettingsConfig = SettingsConfig & Record + +export class Settings { + /** Creates typical settings database */ + private static createDatabase(name: string) { + return table(name, () => ({})) + } + + static playerDatabase = this.createDatabase('playerOptions') + + static playerConfigs: Record> = {} + + /** + * It creates a proxy object that has the same properties as the `CONFIG` object, but the values are stored in a + * database + * + * @template Config + * @param groupName - The name that shows to players + * @param groupId - The id for the database. + * @param config - This is an object that contains the default values for each option. + * @returns An function that returns object with properties that are getters and setters. + */ + static player>( + groupName: Text, + groupId: string, + config: Config, + ) { + this.insertGroup('playerConfigs', groupName, groupId, config) + + const cache = new WeakPlayerMap() + + const fn = (player: Player): SettingsConfigParsed => { + const cached = cache.get(player) + if (cached) { + return cached as SettingsConfigParsed + } else { + const settings = this.parseConfig( + Settings.playerDatabase, + groupId, + this.playerConfigs[groupId] as Config, + player, + ) + cache.set(player, settings) + return settings + } + } + + fn.groupId = groupId + fn.groupName = groupName + fn.extend = [groupName, groupId] as const + fn.override = (setting: keyof Config, value: Partial[string]>) => { + for (const [k, v] of Object.entries(value)) { + if (config[setting]) (config[setting] as unknown as Record)[k] = v + } + } + + return fn + } + + static worldDatabase = this.createDatabase('worldOptions') + + static worldConfigs: Record = {} + + /** + * It takes a prefix and a configuration object, and returns a proxy that uses the prefix to store the configuration + * object's properties in localStorage + * + * @template Config + * @param groupId - The id for the database. + * @param config - The default values for the options. + * @returns An object with properties that are getters and setters. + */ + static world( + groupName: Text, + groupId: string, + config: Config, + ): SettingsConfigParsed { + this.insertGroup('worldConfigs', groupName, groupId, config) + return this.parseConfig(Settings.worldDatabase, groupId, this.worldConfigs[groupId] as Config) + } + + static worldCommon = [i18n`Общие настройки мира\n§7Чат, спавн и тд`, 'common'] as const + + private static insertGroup( + to: 'worldConfigs' | 'playerConfigs', + groupName: Text, + groupId: string, + config: SettingsConfig, + ) { + if (!(groupId in this[to])) { + this[to][groupId] = config + } else { + this[to][groupId] = { ...config, ...this[to][groupId] } + } + + this[to][groupId][SETTINGS_GROUP_NAME] = groupName + } + + /** + * It creates a proxy object that allows you to access and modify the values of a given object, but the values are + * stored in a database + * + * @param database - The database. + * @param groupId - The group id of the settings + * @param config - This is the default configuration object. It's an object with the keys being the option names and + * the values being the default values. + * @param player - The player object. + * @returns An object with getters and setters + */ + static parseConfig( + database: SettingsDatabase, + groupId: string, + config: Config, + player: Player | null = null, + ) { + const settings = {} + + for (const prop in config) { + const key = player ? `${player.id}:${prop}` : prop + Object.defineProperty(settings, prop, { + configurable: false, + enumerable: true, + get() { + const paid = player ? (config[prop]?.paid?.(player) ?? true) : true + const value = config[prop]?.value + if (!paid) return config[prop]?.whenNotPaidDefault ?? (typeof value === 'boolean' ? !value : value) + + if (typeof value === 'undefined') throw new TypeError(`No config value for prop ${prop}`) + return ( + (database.getImmutable(groupId) as SettingsDatabaseValue | undefined)?.[key] ?? + (Settings.isDropdown(value) ? value[0]?.[0] : value) + ) + }, + set(v: toPlain) { + Settings.set(database, groupId, key, v, config[prop]) + }, + }) + } + + return settings as SettingsConfigParsed + } + + static set( + database: SettingsDatabase, + groupId: string, + key: string, + v: SettingValue, + configProp = Settings.worldConfigs[groupId]?.[key], + ) { + let value = database.get(groupId) + if (typeof value === 'undefined') { + database.set(groupId, {}) + value = database.get(groupId) + } + value[key] = v + configProp?.onChange?.() + database.set(groupId, value) + } + + static isDropdown(v: SettingValue): v is DropdownSetting[] { + return ( + Array.isArray(v) && + v.length > 0 && + v.every( + e => Array.isArray(e) && (typeof e[1] === 'string' || e[1] instanceof Message) && typeof e[0] === 'string', + ) + ) + } +} + +export function settingsModal>( + player: Player, + config: Config, + settingsStorage: SettingsConfigParsed, + back: VoidFunction, +) { + const propertyName = 'modal' + settingsGroupMenu( + player, + propertyName, + false, + {}, + new MemoryTable({ [propertyName]: settingsStorage }, () => ({})), + { [propertyName]: config }, + back, + false, + ) +} + +const logger = createLogger('Settings') + +// TODO ref(leatail1880): Clenup settingsGroupMenu parameters +export function settingsGroupMenu( + player: Player, + groupName: string, + forRegularPlayer: boolean, + hints: Record = {}, + storeSource = forRegularPlayer ? Settings.playerDatabase : Settings.worldDatabase, + configSource = forRegularPlayer ? Settings.playerConfigs : Settings.worldConfigs, + back = forRegularPlayer ? playerSettingsMenu : worldSettingsMenu, + showHintAboutSavedStatus = true, +) { + const displayType = forRegularPlayer ? 'own' : 'world' + const config = configSource[groupName] + if (!config) throw new TypeError(`No config for groupName ${groupName}`) + + const store = Settings.parseConfig(storeSource, groupName, config, forRegularPlayer ? player : null) + const buttons: [string, (input: string | boolean) => string][] = [] + const displayName = (config[SETTINGS_GROUP_NAME] ?? groupName).to(player.lang) + const form = new ModalForm<(ctx: FormCallback, ...options: (string | boolean)[]) => void>( + `${displayName.split('\n')[0]}`, + ) + + for (const key in config) { + const saved = store[key] as string | number | boolean | undefined + const setting = config[key] + if (!setting) throw new TypeError(`No setting for key ${key}`) + + const value = saved ?? setting.value + + const paid = setting.paid?.(player) ?? true + + const isUnset = typeof saved === 'undefined' + const isRequired = (Reflect.get(setting, 'requires') as boolean) && isUnset + const isToggle = typeof value === 'boolean' + + let label = '' + + label += hints[key] ? `${hints[key]}\n` : '' + + if (!paid) label += `§cКУПИТЕ ЧТОБЫ ИСПОЛЬЗОВАТЬ\n` + + if (isRequired) label += '§c(!) ' + label += `§f§l${setting.name.to(player.lang)}§r§f` //§r + + if (setting.description) label += `§i - ${setting.description.to(player.lang)}` + if (isUnset) label += i18n.nocolor`§8(По умолчанию)\n`.to(player.lang) + + if (isToggle) { + form.addToggle(label, value) + } else if (Settings.isDropdown(setting.value)) { + form.addDropdownFromObject(label, Object.fromEntries(setting.value.map(e => [e[0], e[1].to(player.lang)])), { + defaultValueIndex: Settings.isDropdown(value) ? undefined : value, + }) + } else { + const isString = typeof value === 'string' + + if (!isString) { + label += i18n.nocolor`\n§7§lЗначение:§r ${stringify(value)}`.to(player.lang) + label += i18n.nocolor`\n§7§lТип: §r§f${settingTypes[typeof value] ?? typeof value}`.to(player.lang) + } + + form.addTextField(label, i18n`Настройка не изменится`.to(player.lang), isString ? value : JSON.stringify(value)) + } + + buttons.push([ + key, + input => { + try { + if (typeof input === 'undefined' || input === '') return '' + + let result + if (typeof input === 'boolean' || Settings.isDropdown(setting.value)) { + result = input + } else { + switch (typeof setting.value) { + case 'string': + result = input + break + case 'number': + result = Number(input) + if (isNaN(result)) return i18n.error`Введите число!`.to(player.lang) + break + case 'object': + result = JSON.parse(input) as typeof result + + break + } + } + + if (stringify(store[key]) === stringify(result)) return '' + if (typeof result !== 'undefined') { + logger.player(player).info`Changed ${displayType} setting '${groupName} > ${key}' to '${result}'` + store[key] = result + } + + return showHintAboutSavedStatus ? i18n.success`Сохранено!`.to(player.lang) : '' + } catch (error: unknown) { + logger.player(player).info`Changing ${displayType} setting '${groupName} > ${key}' error: ${error}` + + return stringifyError.isError(error) ? `§c${error.message}` : stringify(error) + } + }, + ]) + } + + form.submitButton('Сохранить') + + form.show(player, (_, ...settings) => { + const hints: Record = {} + + for (const [i, setting] of settings.entries()) { + const button = buttons[i] + if (!button) continue + + const [key, callback] = button + const hint = callback(setting) + + if (hint) hints[key] = hint + } + + if (Object.keys(hints).length) { + // Show current menu with hints + self() + } else { + // No hints, go back to previous menu + back(player) + } + + function self() { + settingsGroupMenu(player, groupName, forRegularPlayer, hints, storeSource, configSource, back) + } + }) +} + +const settingTypes: Partial< + Record<'string' | 'number' | 'object' | 'boolean' | 'symbol' | 'bigint' | 'undefined' | 'function', Text> +> = { string: i18n`Строка`, number: i18n`Число`, object: i18n`JSON-Объект`, boolean: i18n`Переключатель` } + +/** Opens player settings menu */ +export function playerSettingsMenu(player: Player, back?: VoidFunction) { + const form = new ActionForm(i18n`§dНастройки`.to(player.lang)) + if (back) form.addButtonBack(back, player.lang) + + for (const groupName in Settings.playerConfigs) { + const name = Settings.playerConfigs[groupName]?.[SETTINGS_GROUP_NAME] + if (name) form.button(name.to(player.lang), () => settingsGroupMenu(player, groupName, true)) + } + + form.show(player) +} + +export function worldSettingsMenu(player: Player) { + const form = new ActionForm(noI18n`§dНастройки мира`) + + for (const [groupId, group] of Object.entries(Settings.worldConfigs)) { + const database = Settings.worldDatabase.get(groupId) + + let unsetCount = 0 + for (const [key, option] of Object.entries(group)) { + if (option.required && typeof database[key] === 'undefined') unsetCount++ + } + + form.button(i18n.nocolor.join`${group[SETTINGS_GROUP_NAME] ?? groupId}`.badge(unsetCount).to(player.lang), () => { + settingsGroupMenu(player, groupId, false) + }) + } + + form.show(player) +} diff --git a/src/lib/shop/cost/item-cost.ts b/src/lib/shop/cost/item-cost.ts index 06ead61e..3820286a 100644 --- a/src/lib/shop/cost/item-cost.ts +++ b/src/lib/shop/cost/item-cost.ts @@ -1,6 +1,5 @@ import { ContainerSlot, EntityComponentTypes, EquipmentSlot, ItemStack, Player } from '@minecraft/server' import { eqSlots } from 'lib/form/select-item' -import { Message } from 'lib/i18n/message' import { i18n, noI18n } from 'lib/i18n/text' import { itemNameXCount } from '../../utils/item-name-x-count' import { Cost } from '../cost' @@ -19,10 +18,11 @@ export class ItemCost extends Cost { * @param amount - Amount of items to search for. */ constructor( - private readonly item: string | ItemStack, + private readonly item: string | ItemStack | (() => ItemStack), private readonly amount = item instanceof ItemStack ? item.amount : 1, protected is = (itemStack: ItemStack) => { if (typeof this.item === 'string') return itemStack.typeId === this.item + if (typeof this.item === 'function') return this.item().is(itemStack) return this.item.is(itemStack) }, ) { @@ -79,7 +79,11 @@ export class ItemCost extends Cost { toString(player: Player, canBuy?: boolean, amount = true): string { return itemNameXCount( - this.item instanceof ItemStack ? this.item : { typeId: this.item, amount: this.amount }, + this.item instanceof ItemStack + ? this.item + : typeof this.item === 'function' + ? this.item() + : { typeId: this.item, amount: this.amount }, canBuy ? '§7' : '§c', amount, player.lang, diff --git a/src/lib/shop/form.ts b/src/lib/shop/form.ts index 03a8e969..8d85e56f 100644 --- a/src/lib/shop/form.ts +++ b/src/lib/shop/form.ts @@ -1,15 +1,13 @@ -import { ContainerSlot, ItemStack, Player } from '@minecraft/server' +import { ContainerSlot, ItemStack, Player, Potions } from '@minecraft/server' import { MinecraftItemTypes, + MinecraftPotionDeliveryTypes as PotionDelivery, MinecraftPotionEffectTypes as PotionEffects, - MinecraftPotionLiquidTypes as PotionLiquids, - MinecraftPotionModifierTypes as PotionModifiers, } from '@minecraft/vanilla-data' import { shopFormula } from 'lib/assets/shop' import { table } from 'lib/database/abstract' import { ActionForm } from 'lib/form/action' import { getAuxOrTexture, getAuxTextureOrPotionAux } from 'lib/form/chest' -import { Message } from 'lib/i18n/message' import { i18n } from 'lib/i18n/text' import { Cost } from 'lib/shop/cost' import { isKeyof } from 'lib/util' @@ -177,8 +175,8 @@ export class ShopForm { return this } - potion(cost: Cost, effect: PotionEffects, modifier = PotionModifiers.Normal, liquid = PotionLiquids.Regular) { - const item = ItemStack.createPotion({ effect, modifier, liquid }) + potion(cost: Cost, effect: PotionEffects, delivery = PotionDelivery.Consume) { + const item = Potions.resolve(effect, delivery) this.itemStack(item, cost, getAuxTextureOrPotionAux(item)) } diff --git a/src/lib/sidebar.ts b/src/lib/sidebar.ts index dcff2907..192750ed 100644 --- a/src/lib/sidebar.ts +++ b/src/lib/sidebar.ts @@ -1,6 +1,7 @@ import { Player } from '@minecraft/server' -import { util, wrap } from 'lib/util' +import { wrap } from 'lib/util' import { ActionbarPriority } from './extensions/on-screen-display' +import { onLoad } from './utils/game' import { WeakPlayerSet } from './weak-player-storage' type Format = @@ -25,7 +26,7 @@ export class Sidebar { static forceHide = new WeakPlayerSet() - content + content!: SidebarVariables getExtra @@ -54,7 +55,9 @@ export class Sidebar { this.name = name this.getExtra = getExtra this.getOptions = getOptions - this.content = this.init(content) + onLoad(() => { + this.content = this.init(content) + }) Sidebar.instances.push(this) } diff --git a/src/lib/util.ts b/src/lib/util.ts index 16e3d397..50ad931c 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -3,25 +3,22 @@ import { TerminalColors } from './assets/terminal-colors' import stringifyError from './utils/error' import { inspect, stringify } from './utils/inspect' -import './utils/benchmark' - export { inspect, stringify, stringifyError } export const util = { /** Runs the given callback safly. If it throws any error it will be handled */ catch(this: void, fn: () => void | Promise, subtype = 'Handled', originalStack?: string) { const prefix = `§6${subtype}: ` + const add = originalStack ? '\n\n' + stringifyError.stack.get(0, originalStack) : '' try { const promise = fn() if (promise instanceof Promise) { promise.catch((e: unknown) => { - console.error(prefix + stringifyError(e as Error, { omitStackLines: 1 })) + console.error(prefix + stringifyError(e as Error, { omitStackLines: 1 }) + add) }) } } catch (e: unknown) { - console.error( - prefix + stringifyError(e as Error, { omitStackLines: 1 }) + (originalStack ? '\n\n' + originalStack : ''), - ) + console.error(prefix + stringifyError(e as Error, { omitStackLines: 1 }) + add) } }, diff --git a/src/lib/utils/benchmark.ts b/src/lib/utils/benchmark.ts index 34ec0ecc..bfc3736a 100644 --- a/src/lib/utils/benchmark.ts +++ b/src/lib/utils/benchmark.ts @@ -1,3 +1,4 @@ +import { Command } from 'lib/command' import { TIMERS_PATHES } from 'lib/extensions/system' import { ActionForm } from 'lib/form/action' import { util } from 'lib/util' diff --git a/src/lib/utils/error.ts b/src/lib/utils/error.ts index 9539d84b..f6063a4b 100644 --- a/src/lib/utils/error.ts +++ b/src/lib/utils/error.ts @@ -57,7 +57,7 @@ const stringifyError = Object.assign( [/(.*)\(native\)(.*)/, '§8$1(native)$2§f'], [s => (s.includes('lib') ? `§7${s.replace(/§./g, '')}§f` : s)], // [s => (s.startsWith('§7') ? s : s.replace(/:(\d+)/, ':§6$1§f'))], - [/__init \(index\.js:4\)/, ''], + [/__init \(index\.js:8\)/, ''], ] as [RegExp | ((s: string) => string), string?][], /** Parses stack */ @@ -71,6 +71,8 @@ const stringifyError = Object.assign( .join('\n') } + stack = stack.slice(0, 1000) + const stackArray = stack.split('\n') const mappedStack = stackArray diff --git a/src/lib/utils/game.ts b/src/lib/utils/game.ts index 18929a94..a63ea948 100644 --- a/src/lib/utils/game.ts +++ b/src/lib/utils/game.ts @@ -1,3 +1,5 @@ +export * from './load-ref' + import { Block, GameMode, diff --git a/src/lib/utils/load-ref.ts b/src/lib/utils/load-ref.ts new file mode 100644 index 00000000..5482eeb9 --- /dev/null +++ b/src/lib/utils/load-ref.ts @@ -0,0 +1,77 @@ +import { system, world } from '@minecraft/server' +import { util } from 'lib/util' +import stringifyError from './error' + +export class LoadRef { + static loadStarted = false + + static loadFinished = false + + static unwrap(v: MaybeRef): T { + return v instanceof LoadRef ? v.value : v + } + + protected static loaders: (() => void)[] = [] + + static { + world.afterEvents.worldLoad.subscribe(() => { + LoadRef.loadStarted = true + system.runJob( + (function* loadRefJob() { + for (const loader of LoadRef.loaders) { + loader() + yield + } + LoadRef.loadFinished = true + })(), + ) + }) + } + + get value(): T { + throw new Error('Value is not yet loaded! Value defined at: \n' + this.stack) + } + + private stack: string + + constructor(loader: () => T) { + this.stack = stringifyError.stack.get() + LoadRef.loaders.push(() => { + util.catch( + () => { + const value = loader() + Object.defineProperty(this, 'value', { value }) + util.catch( + () => { + for (const waiter of this.waiters) waiter(value) + }, + 'LoadRefWaiterError', + this.stack, + ) + }, + 'LoadRefError', + this.stack, + ) + }) + } + + protected waiters: ((value: T) => void)[] = [] + + onLoad = (waiter: (value: T) => void) => { + this.waiters.push(waiter) + } +} + +export type MaybeRef = T | LoadRef + +export function onLoad(loader: () => T) { + if (LoadRef.loadStarted) + return { + value: loader(), + onLoad(v: (v: T) => void) { + v(this.value) + }, + } + + return new LoadRef(loader) +} diff --git a/src/lib/utils/singleton.test.ts b/src/lib/utils/singleton.test.ts new file mode 100644 index 00000000..16e6e41b --- /dev/null +++ b/src/lib/utils/singleton.test.ts @@ -0,0 +1,43 @@ +import { Singleton } from './singleton' + +describe('Singleton', () => { + it('should create singleton', () => { + class test extends Singleton {} + + new test() + + test.getInstance() + + class b extends Singleton {} + + expect(() => b.getInstance()).toThrow() + + new b() + }) + + it('should use singleton from subclass', () => { + class Parent extends Singleton {} + + class Sub extends Parent {} + + const instance = new Sub() + + expect(() => Parent.getInstance()).not.toThrow() + expect(Parent.getInstance()).toBe(instance) + }) + + it('should use singleton from subclass', () => { + class Parent extends Singleton {} + + class Sub extends Parent {} + + class Sub2 extends Sub {} + + const instance = new Sub2() + + expect(() => Sub.getInstance()).not.toThrow() + expect(Sub.getInstance()).toBe(instance) + expect(() => Parent.getInstance()).not.toThrow() + expect(Parent.getInstance()).toBe(instance) + }) +}) diff --git a/src/lib/utils/singleton.ts b/src/lib/utils/singleton.ts index d4970790..958e7d66 100644 --- a/src/lib/utils/singleton.ts +++ b/src/lib/utils/singleton.ts @@ -1,17 +1,34 @@ export class Singleton { private static instance?: Singleton + private static where?: string + static getInstance(this: abstract new (...args: any) => T) { const self = this as unknown as typeof Singleton - if (!self.instance) throw new Error('getInstance: ' + self.name + 'is not initialized!') + if (!self.instance) throw new Error('getInstance: ' + self.name + ' is not initialized!') return self.instance as T } constructor() { - if ((this.constructor as typeof Singleton).instance) { - throw new Error(this.constructor.name + ' is already initialized!') + const singleton = this.constructor as typeof Singleton + if (singleton.instance) { + throw new Error(`${singleton.name} is already initialized! ${singleton.where}`) } - ;(this.constructor as typeof Singleton).instance = this + const stack = new Error().stack + + singleton.instance = this + singleton.where = stack + + let prototype + let limit = 10 + do { + prototype = Object.getPrototypeOf(prototype ?? singleton) as typeof Singleton + if (prototype === Singleton) break + + prototype.instance ??= this + prototype.where = stack + limit-- + } while (limit) } } diff --git a/src/modules/anticheat/anti-piston-abuse.ts b/src/modules/anticheat/anti-piston-abuse.ts deleted file mode 100644 index ab5e4967..00000000 --- a/src/modules/anticheat/anti-piston-abuse.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { system, world } from '@minecraft/server' -import { MinecraftBlockTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib/vector' -import { antiCheatLog } from './log-provider' - -world.afterEvents.pistonActivate.subscribe(event => { - const locations = event.piston.getAttachedBlocksLocations() - - system.runTimeout( - () => { - if (!event.block.isValid) return - - for (const location of locations) { - const block = event.block.dimension.getBlock(location) - if (block?.typeId !== MinecraftBlockTypes.Hopper) continue - - const nearbyPlayers = event.block.dimension.getPlayers({ location: event.block.location, maxDistance: 20 }) - const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') - - antiCheatLog(`ПОРШЕНЬ ДЮП ${Vec.string(event.block.location)}\n${nearbyPlayersNames}`) - - event.block.dimension.createExplosion(event.block.location, 5, { breaksBlocks: true }) - return - } - }, - 'piston dupe prevent', - 2, - ) -}) diff --git a/src/modules/anticheat/anti-wither-bedrock-kill.ts b/src/modules/anticheat/anti-wither-bedrock-kill.ts deleted file mode 100644 index 401bdce0..00000000 --- a/src/modules/anticheat/anti-wither-bedrock-kill.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { world } from '@minecraft/server' -import { MinecraftBlockTypes, MinecraftEntityTypes } from '@minecraft/vanilla-data' -import { Vec } from 'lib/vector' -import { antiCheatLog } from './log-provider' - -world.afterEvents.entitySpawn.subscribe(event => { - const { entity } = event - - if (entity.typeId !== MinecraftEntityTypes.Wither) return - - const { location } = entity - const block = entity.dimension.getBlock(location) - - if (block?.typeId !== MinecraftBlockTypes.Bedrock) return - - const nearbyPlayers = event.entity.dimension.getPlayers({ location, maxDistance: 20 }) - const nearbyPlayersNames = nearbyPlayers.map(e => e.name).join('\n') - - antiCheatLog(`ОБНАРУЖЕН АБУЗ ВИЗЕРА ${Vec.string(location)}\n${nearbyPlayersNames}`) - - entity.remove() -}) diff --git a/src/modules/anticheat/index.ts b/src/modules/anticheat/index.ts deleted file mode 100644 index 7db9223a..00000000 --- a/src/modules/anticheat/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './anti-piston-abuse' -import './anti-wither-bedrock-kill' -import './forbidden-items' -import './whitelist' diff --git a/src/modules/anticheat/log-provider.ts b/src/modules/anticheat/log-provider.ts deleted file mode 100644 index 4ae0bbb3..00000000 --- a/src/modules/anticheat/log-provider.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createLogger } from 'lib/utils/logger' - -export const antiCheatLogger = createLogger('anticheat') - -export function antiCheatLog(text: string) { - if (!log) return antiCheatLogger.warn('No provider: ', text) - - antiCheatLogger.warn(text) - log(text) -} - -let log: null | ((text: string) => void) = null - -export function registerAntiCheatLogProvider(provider: typeof log) { - log = provider -} diff --git a/src/modules/commands/index.ts b/src/modules/commands/index.ts index b57a7254..82b917ff 100644 --- a/src/modules/commands/index.ts +++ b/src/modules/commands/index.ts @@ -7,7 +7,6 @@ import './name' import './pid' import './ping' import './player' -import './role' import './rtp' import './rules' import './scores' diff --git a/src/modules/indicator/pvp.ts b/src/modules/indicator/pvp.ts index 25c2468e..e2a76e12 100644 --- a/src/modules/indicator/pvp.ts +++ b/src/modules/indicator/pvp.ts @@ -5,11 +5,11 @@ import { emoji } from 'lib/assets/emoji' import { Core } from 'lib/extensions/core' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n } from 'lib/i18n/text' -import { BossArenaRegion } from 'lib/region' import { RegionEvents } from 'lib/region/events' +import { BossArenaRegion } from 'lib/region/kinds/boss-arena' import { Boss } from 'lib/rpg/boss' -import { ms } from 'lib/utils/ms' import { Settings } from 'lib/settings' +import { ms } from 'lib/utils/ms' import { WeakPlayerMap } from 'lib/weak-player-storage' import { Anarchy } from 'modules/places/anarchy/anarchy' diff --git a/src/modules/loader.ts b/src/modules/loader.ts index 69408a89..27d664ee 100644 --- a/src/modules/loader.ts +++ b/src/modules/loader.ts @@ -1,9 +1,21 @@ -import 'lib' +import 'lib/load/enviroment' +import 'lib/load/message1' -import './anticheat/index' -import './survival/import' +// Database provider +import 'lib/database/properties' + +// Database +import 'lib/database/inventory' +import 'lib/database/player' +import 'lib/database/scoreboard' +import 'lib/database/utils' + +// Command +import 'lib/command/index' import './lushway/loader' + +import './survival/import' import './test/test' import './wiki/wiki' import './world-edit/builder' diff --git a/src/modules/lushway/loader.ts b/src/modules/lushway/loader.ts index 431fcfca..4d9723ce 100644 --- a/src/modules/lushway/loader.ts +++ b/src/modules/lushway/loader.ts @@ -1,3 +1,8 @@ +import 'lib/anticheat/ban' +import 'lib/anticheat/forbidden-items' +import 'lib/anticheat/freeze' +import 'lib/anticheat/whitelist' + import './config/core' import './config/chat' diff --git a/src/modules/places/anarchy/airdrop.ts b/src/modules/places/anarchy/airdrop.ts index 381748ad..c3d2d599 100644 --- a/src/modules/places/anarchy/airdrop.ts +++ b/src/modules/places/anarchy/airdrop.ts @@ -2,13 +2,13 @@ import { LocationInUnloadedChunkError, system, world } from '@minecraft/server' import { Items } from 'lib/assets/custom-items' import { i18n } from 'lib/i18n/text' +import { Airdrop } from 'lib/rpg/airdrop' +import { Loot } from 'lib/rpg/loot-table' +import { isNotPlaying } from 'lib/utils/game' +import { Vec } from 'lib/vector' import { Anarchy } from 'modules/places/anarchy/anarchy' import { CannonItem, CannonShellItem } from '../../pvp/cannon' import { randomLocationInAnarchy } from './random-location-in-anarchy' -import { Vec } from 'lib/vector' -import { isNotPlaying } from 'lib/utils/game' -import { Airdrop } from 'lib/rpg/airdrop' -import { Loot } from 'lib/rpg/loot-table' const base = new Loot('base_airdrop') .item('Gunpowder') @@ -19,10 +19,10 @@ const base = new Loot('base_airdrop') .amount({ '25...50': '40%', '51...90': '2%' }) .weight('20%') - .itemStack(CannonShellItem.blueprint) + .itemStack(() => CannonShellItem.blueprint) .weight('10%') - .itemStack(CannonItem.blueprint) + .itemStack(() => CannonItem.blueprint) .weight('5%') .item(Items.Money) @@ -39,10 +39,10 @@ const powerfull = new Loot('powerfull_airdrop') .amount({ '30...64': '40%', '65...128': '2%' }) .weight('20%') - .itemStack(CannonShellItem.itemStack) + .itemStack(CannonShellItem) .weight('10%') - .itemStack(CannonItem.itemStack) + .itemStack(CannonItem) .weight('5%') .item(Items.Money) diff --git a/src/modules/places/base/actions/rotting.ts b/src/modules/places/base/actions/rotting.ts index 6122178d..abdb7057 100644 --- a/src/modules/places/base/actions/rotting.ts +++ b/src/modules/places/base/actions/rotting.ts @@ -1,22 +1,22 @@ import { Block, BlockPermutation, ContainerSlot, Player, system } from '@minecraft/server' import { MinecraftBlockTypes } from '@minecraft/vanilla-data' +import { Cooldown } from 'lib/cooldown' import { table } from 'lib/database/abstract' import { form } from 'lib/form/new' import { Message } from 'lib/i18n/message' import { i18n } from 'lib/i18n/text' -import { anyPlayerNearRegion } from 'lib/player-move' -import { ScheduleBlockPlace } from 'lib/scheduled-block-place' -import { itemNameXCount } from 'lib/utils/item-name-x-count' -import { spawnParticlesInArea } from 'modules/world-edit/config' -import { BaseRegion, RottingState } from '../region' -import { Cooldown } from 'lib/cooldown' import { Mail } from 'lib/mail' +import { anyPlayerNearRegion } from 'lib/player-move' import { actionGuard, ActionGuardOrder } from 'lib/region' +import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { isEmpty } from 'lib/util' -import { getBlockStatus, isLocationError, isNotPlaying } from 'lib/utils/game' +import { getBlockStatus, isLocationError, isNotPlaying, onLoad } from 'lib/utils/game' +import { itemNameXCount } from 'lib/utils/item-name-x-count' import { ms } from 'lib/utils/ms' import { Vec } from 'lib/vector' +import { spawnParticlesInArea } from 'modules/world-edit/config' +import { BaseRegion, RottingState } from '../region' const takeMaterialsTime = __DEV__ ? ms.from('day', 1) : ms.from('day', 1) const blocksReviseTime = __DEV__ ? ms.from('min', 1) : ms.from('min', 2) @@ -24,9 +24,11 @@ const materialsReviseTime = __DEV__ ? ms.from('min', 1) : ms.from('min', 1) const cooldowns = table>('baseCoooldowns', () => ({})) -const blocksToMaterialsCooldown = new Cooldown(blocksReviseTime, false, cooldowns.get('blocksToMaterials')) -const reviseMaterialsCooldown = new Cooldown(materialsReviseTime, false, cooldowns.get('revise')) -const takeMaterialsCooldown = new Cooldown(takeMaterialsTime, false, cooldowns.get('takeMaterials')) +const blocksToMaterialsCooldown = onLoad( + () => new Cooldown(blocksReviseTime, false, cooldowns.get('blocksToMaterials')), +) +const reviseMaterialsCooldown = onLoad(() => new Cooldown(materialsReviseTime, false, cooldowns.get('revise'))) +const takeMaterialsCooldown = onLoad(() => new Cooldown(takeMaterialsTime, false, cooldowns.get('takeMaterials'))) system.runInterval( () => { @@ -40,9 +42,9 @@ system.runInterval( spawnParticlesInArea(base.area.center, Vec.add(base.area.center, Vec.one)) if (block.typeId === MinecraftBlockTypes.Barrel) { - if (blocksToMaterialsCooldown.isExpired(base.id)) blocksToMaterials(base) - if (reviseMaterialsCooldown.isExpired(base.id)) reviseMaterials(base, block) - if (takeMaterialsCooldown.isExpired(base.id)) takeMaterials(base, block) + if (blocksToMaterialsCooldown.value.isExpired(base.id)) blocksToMaterials(base) + if (reviseMaterialsCooldown.value.isExpired(base.id)) reviseMaterials(base, block) + if (takeMaterialsCooldown.value.isExpired(base.id)) takeMaterials(base, block) } else startRotting(base, RottingState.Destroyed) } }, @@ -81,7 +83,7 @@ const baseRottingMenu = form.params<{ base: BaseRegion }>((f, { params: { base } f.title(i18n`Гниение базы`) f.body( - i18n`Чтобы база не гнила, в бочке ежедневно должны быть следующие ресурсы:\n${materials}\nМатериалы в бочке:\n${barrelMaterials}\n${missingMaterialsText}\nДо следующего сбора ресурсов: ${i18n.hhmmss(takeMaterialsCooldown.getRemainingTime(base.id))}`, + i18n`Чтобы база не гнила, в бочке ежедневно должны быть следующие ресурсы:\n${materials}\nМатериалы в бочке:\n${barrelMaterials}\n${missingMaterialsText}\nДо следующего сбора ресурсов: ${i18n.hhmmss(takeMaterialsCooldown.value.getRemainingTime(base.id))}`, ) }) diff --git a/src/modules/places/dungeons/command.ts b/src/modules/places/dungeons/command.ts index 627a3575..bb713c40 100644 --- a/src/modules/places/dungeons/command.ts +++ b/src/modules/places/dungeons/command.ts @@ -6,14 +6,15 @@ import { Items } from 'lib/assets/custom-items' import { StructureDungeonsId } from 'lib/assets/structures' import { ItemLoreSchema } from 'lib/database/item-stack' import { ActionbarPriority } from 'lib/extensions/on-screen-display' +import { ArrayForm } from 'lib/form/array' import { i18n, noI18n } from 'lib/i18n/text' import { SphereArea } from 'lib/region/areas/sphere' +import { isKeyof } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' +import { Vec } from 'lib/vector' import { DungeonRegion } from 'modules/places/dungeons/dungeon' import { CustomDungeonRegion } from './custom-dungeon' import { Dungeon } from './loot' -import { Vec } from 'lib/vector' -import { ArrayForm } from 'lib/form/array' -import { isKeyof } from 'lib/util' const toolSchema = new ItemLoreSchema('dungeonCreationTool', Items.WeTool) .property('type', String) @@ -116,7 +117,7 @@ system.runPlayerInterval( for (const l of Vec.forEach(from, to)) { if (!Vec.isEdge(from, to, l)) continue - player.spawnParticle('minecraft:balloon_gas_particle', l, particle) + player.spawnParticle('minecraft:balloon_gas_particle', l, particle.value) } player.onScreenDisplay.setActionBar( @@ -128,10 +129,13 @@ system.runPlayerInterval( 15, ) -const particle = new MolangVariableMap() +const particle = onLoad(() => { + const vars = new MolangVariableMap() -particle.setVector3('direction', { - x: 0, - y: 0, - z: 0, + vars.setVector3('direction', { + x: 0, + y: 0, + z: 0, + }) + return vars }) diff --git a/src/modules/places/dungeons/loot.ts b/src/modules/places/dungeons/loot.ts index 00b4b2e0..0755dda3 100644 --- a/src/modules/places/dungeons/loot.ts +++ b/src/modules/places/dungeons/loot.ts @@ -1,15 +1,14 @@ import { Items } from 'lib/assets/custom-items' import { StructureDungeonsId } from 'lib/assets/structures' import { i18n } from 'lib/i18n/text' +import { Loot, LootTable } from 'lib/rpg/loot-table' import { CannonItem, CannonShellItem } from 'modules/pvp/cannon' import { FireBallItem } from 'modules/pvp/fireball' import { IceBombItem } from 'modules/pvp/ice-bomb' import { BaseItem } from '../base/base' -import { LootTable } from 'lib/rpg/loot-table' -import { Loot } from 'lib/rpg/loot-table' const defaultLoot = new Loot('dungeon_default_loot') - .itemStack(CannonShellItem.blueprint) + .itemStack(() => CannonShellItem.blueprint) .weight('5%') .item('Apple') @@ -178,18 +177,18 @@ const customLoot: Record = { Sharpness: { '1...3': '1%', '4...5': '10%' }, }) - .itemStack(CannonItem.itemStack) + .itemStack(CannonItem) .weight('40%') .amount({ '1...2': '1%' }) - .itemStack(CannonShellItem.itemStack) + .itemStack(CannonShellItem) .weight('60%') .amount({ '1...9': '10%', '10...16': '1%', }) - .itemStack(BaseItem.itemStack) + .itemStack(BaseItem) .weight('5%') .amount({ '0...1': '1%' }) diff --git a/src/modules/places/lib/npc/guide.ts b/src/modules/places/lib/npc/guide.ts index f6149077..64b2b533 100644 --- a/src/modules/places/lib/npc/guide.ts +++ b/src/modules/places/lib/npc/guide.ts @@ -15,7 +15,7 @@ export class GuideNpc extends NpcForm { for (const quest of Quest.quests.values()) { if (quest.guideIgnore) continue - if (quest.place.group === group) f.quest(quest) + if (quest.place.group === group) ctx.lf.quest(quest) } }) } diff --git a/src/modules/places/lib/safe-place.ts b/src/modules/places/lib/safe-place.ts index 158e98ab..125cc0b5 100644 --- a/src/modules/places/lib/safe-place.ts +++ b/src/modules/places/lib/safe-place.ts @@ -7,9 +7,9 @@ import { ArrayForm } from 'lib/form/array' import { debounceMenu } from 'lib/form/utils' import { SharedI18nMessage } from 'lib/i18n/message' import { i18n, noI18n } from 'lib/i18n/text' -import { locationWithRadius, locationWithRotation, location, Vector3Radius } from 'lib/location' +import { location, locationWithRadius, locationWithRotation, Vector3Radius } from 'lib/location' import { Portal } from 'lib/portals' -import { SafeAreaRegion, actionGuard, ActionGuardOrder } from 'lib/region' +import { actionGuard, ActionGuardOrder, SafeAreaRegion } from 'lib/region' import { SphereArea } from 'lib/region/areas/sphere' import { RegionEvents } from 'lib/region/events' import { Group } from 'lib/rpg/place' @@ -18,6 +18,14 @@ import { ErrorCost } from 'lib/shop/cost/cost' import { Product } from 'lib/shop/product' import { Vec } from 'lib/vector' +declare module '@minecraft/server' { + interface PlayerDatabase { + unlockedPortals?: string[] + } +} + +export {} + export class SafePlace { static places: SafePlace[] = [] diff --git a/src/modules/places/spawn.ts b/src/modules/places/spawn.ts index 3c179798..5529d9e5 100644 --- a/src/modules/places/spawn.ts +++ b/src/modules/places/spawn.ts @@ -2,22 +2,22 @@ import { GameMode, Player, system, world } from '@minecraft/server' import { MinecraftEffectTypes } from '@minecraft/vanilla-data' +import { InventoryStore } from 'lib/database/inventory' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' +import { locationWithRotation } from 'lib/location' import { Join } from 'lib/player-join' +import { Portal } from 'lib/portals' import { SphereArea } from 'lib/region/areas/sphere' import { RegionEvents } from 'lib/region/events' import { SafeAreaRegion } from 'lib/region/kinds/safe-area' import { Menu } from 'lib/rpg/menu' import { Group } from 'lib/rpg/place' -import { isNotPlaying } from 'lib/utils/game' +import { Settings } from 'lib/settings' +import { util } from 'lib/util' +import { isNotPlaying, onLoad } from 'lib/utils/game' import { createLogger } from 'lib/utils/logger' import { showSurvivalHud } from 'modules/survival/sidebar' import { AreaWithInventory } from './lib/area-with-inventory' -import { InventoryStore } from 'lib/database/inventory' -import { util } from 'lib/util' -import { Settings } from 'lib/settings' -import { locationWithRotation } from 'lib/location' -import { Portal } from 'lib/portals' class SpawnBuilder extends AreaWithInventory { group = new Group('common', i18nShared`Общее`) @@ -49,53 +49,55 @@ class SpawnBuilder extends AreaWithInventory { constructor() { super() this.onRegionInterval() - if (this.location.valid) { - const spawnLocation = this.location - world.setDefaultSpawnLocation(spawnLocation) - - this.portal = new Portal('spawn', null, null, player => { - if (!Portal.canTeleport(player)) return - Portal.fadeScreen(player) - - this.switchInventory(player) - spawnLocation.teleport(player) - - showSurvivalHud(player) - - // Need to happen last because showSurvivalHud will reset title show time - Portal.showHudTitle(player, '§9> §bSpawn §9<') - }) - - this.portal - .createCommand() - .setPermissions('everybody') - .setDescription(i18n.nocolor`§r§bПеремещает на спавн`) - - world.afterEvents.playerSpawn.unsubscribe(Join.getInstance().playerSpawnEventSubscriber) - world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { - // Skip after death respawns - if (!initialSpawn) return - if (player.isSimulated()) return - if (isNotPlaying(player)) return Join.getInstance().setPlayerJoinPosition(player) - - // Check settings - if (!this.settings(player).teleportToSpawnOnJoin) - return this.logger.player(player) - .info`Not teleporting to spawn on join because player disabled it via settings` - - util.catch(() => { - this.logger.player(player).info`Teleporting player to spawn on join` - this.portal?.teleport(player) - system.runTimeout( - () => Join.getInstance().setPlayerJoinPosition(player), - 'Spawn set player position after join', - 10, - ) + onLoad(() => { + if (this.location.valid) { + const spawnLocation = this.location + world.setDefaultSpawnLocation(spawnLocation) + + this.portal = new Portal('spawn', null, null, player => { + if (!Portal.canTeleport(player)) return + Portal.fadeScreen(player) + + this.switchInventory(player) + spawnLocation.teleport(player) + + showSurvivalHud(player) + + // Need to happen last because showSurvivalHud will reset title show time + Portal.showHudTitle(player, '§9> §bSpawn §9<') + }) + + this.portal + .createCommand() + .setPermissions('everybody') + .setDescription(i18n.nocolor`§r§bПеремещает на спавн`) + + world.afterEvents.playerSpawn.unsubscribe(Join.getInstance().playerSpawnEventSubscriber) + world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => { + // Skip after death respawns + if (!initialSpawn) return + if (player.isSimulated()) return + if (isNotPlaying(player)) return Join.getInstance().setPlayerJoinPosition(player) + + // Check settings + if (!this.settings(player).teleportToSpawnOnJoin) + return this.logger.player(player) + .info`Not teleporting to spawn on join because player disabled it via settings` + + util.catch(() => { + this.logger.player(player).info`Teleporting player to spawn on join` + this.portal?.teleport(player) + system.runTimeout( + () => Join.getInstance().setPlayerJoinPosition(player), + 'Spawn set player position after join', + 10, + ) + }) }) - }) - this.region = SafeAreaRegion.create(new SphereArea({ center: spawnLocation, radius: 30 }, 'overworld')) - } + this.region = SafeAreaRegion.create(new SphereArea({ center: spawnLocation, radius: 30 }, 'overworld')) + } + }) } loadInventory(player: Player): void { @@ -105,7 +107,7 @@ class SpawnBuilder extends AreaWithInventory { xp: 0, health: 20, equipment: {}, - slots: { 0: Menu.itemStack }, + slots: { 0: Menu.itemStack.value }, }, clearAll: true, }) diff --git a/src/modules/places/stone-quarry/barman.ts b/src/modules/places/stone-quarry/barman.ts index 2401f6a0..f5f9ed0e 100644 --- a/src/modules/places/stone-quarry/barman.ts +++ b/src/modules/places/stone-quarry/barman.ts @@ -1,9 +1,8 @@ -import { ItemStack } from '@minecraft/server' +import { ItemStack, Potions } from '@minecraft/server' import { MinecraftPotionEffectTypes as e, MinecraftItemTypes as i, - MinecraftPotionLiquidTypes as lt, - MinecraftPotionModifierTypes as mt, + MinecraftPotionDeliveryTypes as lt, } from '@minecraft/vanilla-data' import { i18n, i18nShared } from 'lib/i18n/text' import { Group } from 'lib/rpg/place' @@ -19,40 +18,23 @@ export class Barman extends ShopNpc { form.itemStack(new ItemStack(i.MilkBucket), new MoneyCost(10)) form.itemStack(new ItemStack(i.HoneyBottle), new MoneyCost(20)) - form.itemStack( - ItemStack.createPotion({ effect: e.FireResistance, liquid: lt.Lingering }).setInfo(i18n`Квас`, undefined), - new MoneyCost(40), - ) + form.itemStack(Potions.resolve(e.FireResistance, lt.Consume).setInfo(i18n`Квас`, undefined), new MoneyCost(40)) form.itemStack( - ItemStack.createPotion({ effect: e.FireResistance, liquid: lt.Lingering, modifier: mt.Long }).setInfo( - i18n`Пиво`, - undefined, - ), + Potions.resolve(e.LongFireResistance, lt.Consume).setInfo(i18n`Пиво`, undefined), new MoneyCost(50), ) - form.itemStack( - ItemStack.createPotion({ effect: e.Invisibility, liquid: lt.Lingering, modifier: mt.Long }).setInfo( - i18n`Сидр`, - undefined, - ), - new MoneyCost(500), - ) + form.itemStack(Potions.resolve(e.LongInvisibility, lt.Consume).setInfo(i18n`Сидр`, undefined), new MoneyCost(500)) form.itemStack( - ItemStack.createPotion({ effect: e.WaterBreath, liquid: lt.Lingering, modifier: mt.Long }).setInfo( - i18n`Настойка из шпината`, - undefined, - ), + Potions.resolve(e.LongWaterBreathing, lt.Consume).setInfo(i18n`Настойка из шпината`, undefined), new MoneyCost(300), ) + form.potion form.itemStack( - ItemStack.createPotion({ effect: e.TurtleMaster, liquid: lt.Lingering, modifier: mt.Long }).setInfo( - i18n`Вино`, - undefined, - ), + Potions.resolve(e.LongTurtleMaster, lt.Consume).setInfo(i18n`Вино`, undefined), new MoneyCost(1000), ) }) diff --git a/src/modules/places/tech-city/engineer.ts b/src/modules/places/tech-city/engineer.ts index 074c2a3e..8fd54ee7 100644 --- a/src/modules/places/tech-city/engineer.ts +++ b/src/modules/places/tech-city/engineer.ts @@ -2,7 +2,7 @@ import { ItemStack, Player } from '@minecraft/server' import { MinecraftItemTypes as i, MinecraftItemTypes } from '@minecraft/vanilla-data' import { Items } from 'lib/assets/custom-items' import { i18n, i18nShared } from 'lib/i18n/text' -import { customItems, CustomItemWithBlueprint } from 'lib/rpg/custom-item' +import { CustomItem, CustomItemWithBlueprint } from 'lib/rpg/custom-item' import { isNewbie } from 'lib/rpg/newbie' import { Group } from 'lib/rpg/place' import { Cost, ItemCost, MultiCost } from 'lib/shop/cost' @@ -11,16 +11,11 @@ import { CannonItem, CannonShellItem } from 'modules/pvp/cannon' import { BaseItem } from '../base/base' import { MagicSlimeBall } from '../village-of-explorers/items' -export const CircuitBoard = new ItemStack(Items.CircuitBoard).setInfo( - undefined, +export const CircuitBoard = new CustomItem(Items.CircuitBoard).lore( i18n`Используется для создания базы у Инжинера в Технограде\n\nМожно получить из усиленного сундука и робота`, ) -export const Chip = new ItemStack(Items.Chip).setInfo( - undefined, - i18n`Используется для создания платы у Инжинера в Технограде`, -) -customItems.push(CircuitBoard, Chip) +export const Chip = new CustomItem(Items.Chip).lore(i18n`Используется для создания платы у Инжинера в Технограде`) export const NotNewbieCost = new (class NotNewbieCost extends Cost { toString(player: Player, canBuy?: boolean): string { @@ -48,25 +43,25 @@ export class Engineer extends ShopNpc { menu.itemStack( BaseItem.itemStack, new MultiCost(NotNewbieCost) - .item(CircuitBoard) + .item(CircuitBoard.itemStack) .item(MinecraftItemTypes.NetherStar) .item(BaseItem.blueprint) .item(MinecraftItemTypes.EnderPearl, 5) - .item(MagicSlimeBall, 30) + .item(MagicSlimeBall.itemStack, 30) .money(4_000), ) for (const [item, cost] of [ - [CannonItem, new MultiCost().item(Chip).money(200)], + [CannonItem, new MultiCost().item(Chip.itemStack).money(200)], [CannonShellItem, new MultiCost().item(MinecraftItemTypes.Gunpowder, 20).money(100)], ] as [CustomItemWithBlueprint, Cost][]) { menu.itemStack(item.itemStack, new MultiCost(new ItemCost(item.blueprint), cost)) } menu.itemStack( - CircuitBoard, + CircuitBoard.itemStack, new MultiCost() - .item(Chip) + .item(Chip.itemStack) .item(MinecraftItemTypes.IronIngot, 20) .item(MinecraftItemTypes.GoldIngot, 10) .item(MinecraftItemTypes.Quartz, 10) diff --git a/src/modules/places/tech-city/tech-city.ts b/src/modules/places/tech-city/tech-city.ts index 53b66704..293a5282 100644 --- a/src/modules/places/tech-city/tech-city.ts +++ b/src/modules/places/tech-city/tech-city.ts @@ -1,5 +1,7 @@ import { i18n, i18nShared } from 'lib/i18n/text' import { CutArea } from 'lib/region/areas/cut' +import { Loot } from 'lib/rpg/loot-table' +import { onLoad } from 'lib/utils/load-ref' import { CannonItem, CannonShellItem } from 'modules/pvp/cannon' import { QuartzMineRegion } from '../anarchy/quartz' import { BaseItem } from '../base/base' @@ -10,12 +12,13 @@ import { Stoner } from '../lib/npc/stoner' import { Woodman } from '../lib/npc/woodman' import { Engineer } from './engineer' import { createBossGolem } from './golem.boss' -import { Loot } from 'lib/rpg/loot-table' class TechCityBuilder extends City { constructor() { super('TechCity', i18nShared`Техноград`) - this.create() + onLoad(() => { + this.create() + }) } engineer = new Engineer(this.group) diff --git a/src/modules/places/village-of-explorers/items.ts b/src/modules/places/village-of-explorers/items.ts index d1917d6e..4baf92ec 100644 --- a/src/modules/places/village-of-explorers/items.ts +++ b/src/modules/places/village-of-explorers/items.ts @@ -1,10 +1,8 @@ -import { ItemStack } from '@minecraft/server' import { MinecraftItemTypes } from '@minecraft/vanilla-data' import { i18n } from 'lib/i18n/text' -import { customItems } from 'lib/rpg/custom-item' +import { CustomItem } from 'lib/rpg/custom-item' -export const MagicSlimeBall = new ItemStack(MinecraftItemTypes.SlimeBall).setInfo( - i18n`§aМагическая слизь`, - i18n`Используется у Инженера`, -) -customItems.push(MagicSlimeBall) +export const MagicSlimeBall = new CustomItem('magicSlimeBall') + .typeId(MinecraftItemTypes.SlimeBall) + .nameTag(i18n`§aМагическая слизь`) + .lore(i18n`Используется у Инженера`) diff --git a/src/modules/places/village-of-explorers/mage.ts b/src/modules/places/village-of-explorers/mage.ts index cbfd96b8..6393d215 100644 --- a/src/modules/places/village-of-explorers/mage.ts +++ b/src/modules/places/village-of-explorers/mage.ts @@ -6,7 +6,6 @@ import { MinecraftEnchantmentTypes, MinecraftItemTypes, MinecraftPotionEffectTypes, - MinecraftPotionModifierTypes, } from '@minecraft/vanilla-data' import { Sounds } from 'lib/assets/custom-sounds' @@ -19,8 +18,7 @@ import { Cost, MoneyCost, MultiCost } from 'lib/shop/cost' import { ErrorCost, FreeCost } from 'lib/shop/cost/cost' import { ShopFormSection } from 'lib/shop/form' import { ShopNpc } from 'lib/shop/npc' -import { addNamespace } from 'lib/util' -import { doNothing } from 'lib/util' +import { addNamespace, doNothing } from 'lib/util' import { copyAllItemPropertiesExceptEnchants } from 'lib/utils/game' import { FireBallItem } from 'modules/pvp/fireball' import { IceBombItem } from 'modules/pvp/ice-bomb' @@ -180,10 +178,10 @@ export class Mage extends ShopNpc { form.potion(new MoneyCost(100), MinecraftPotionEffectTypes.Strength) form.potion(new MoneyCost(100), MinecraftPotionEffectTypes.Healing) form.potion(new MoneyCost(100), MinecraftPotionEffectTypes.Swiftness) - form.potion(new MoneyCost(10), MinecraftPotionEffectTypes.NightVision, MinecraftPotionModifierTypes.Long) + form.potion(new MoneyCost(10), MinecraftPotionEffectTypes.LongNightvision) }) - .itemStack(IceBombItem, new MoneyCost(100)) - .itemStack(FireBallItem, new MoneyCost(100)) + .itemStack(IceBombItem.itemStack, new MoneyCost(100)) + .itemStack(FireBallItem.itemStack, new MoneyCost(100)) .itemStack(new ItemStack(i.TotemOfUndying), new MultiCost().money(6_000).item(i.Emerald, 1)) .itemStack(new ItemStack(i.EnchantedGoldenApple), new MultiCost().item(i.GoldenApple).money(10_000)), ) diff --git a/src/modules/places/village-of-explorers/village-of-explorers.ts b/src/modules/places/village-of-explorers/village-of-explorers.ts index 4e33ffa7..7fc05f6b 100644 --- a/src/modules/places/village-of-explorers/village-of-explorers.ts +++ b/src/modules/places/village-of-explorers/village-of-explorers.ts @@ -37,7 +37,7 @@ class VillageOfExporersBuilder extends City { i18n`Исследователи тип, не понял что ли, глупик, путешествуй смотри наслаждайся, ИССЛЕДУЙ`, ) - f.quest(techCityInvestigating.goToCityQuest, i18n`А где мне базу сделать-то?`) + lf.quest(techCityInvestigating.goToCityQuest, i18n`А где мне базу сделать-то?`) }) } diff --git a/src/modules/places/village-of-miners/village-of-miners.ts b/src/modules/places/village-of-miners/village-of-miners.ts index 89b44f77..700d1c60 100644 --- a/src/modules/places/village-of-miners/village-of-miners.ts +++ b/src/modules/places/village-of-miners/village-of-miners.ts @@ -76,7 +76,7 @@ class VillageOfMinersBuilder extends City { i18n`Они есть... просто они сидят дома и смотрят стрим @shp1natqp`, ) - f.quest( + lf.quest( stoneQuarryInvestigating.goToCityQuest, i18n`Как мне переплавить руду?`, i18n`Возьми у меня задание и отправляйся в другое поселение следуя компасу.`, diff --git a/src/modules/pvp/fireball.ts b/src/modules/pvp/fireball.ts index 66019118..2fe728cf 100644 --- a/src/modules/pvp/fireball.ts +++ b/src/modules/pvp/fireball.ts @@ -1,19 +1,14 @@ -import { ItemStack, system, world } from '@minecraft/server' +import { system, world } from '@minecraft/server' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { Items } from 'lib/assets/custom-items' import { i18n } from 'lib/i18n/text' -import { customItems } from 'lib/rpg/custom-item' +import { CustomItem } from 'lib/rpg/custom-item' import { Vec } from 'lib/vector' import { explosibleEntities, ExplosibleEntityOptions } from './explosible-entities' import { decreaseMainhandItemCount } from './throwable-tnt' -export const FireBallItem = new ItemStack(Items.Fireball).setInfo( - undefined, - i18n`Используйте, чтобы отправить все в огненный ад`, -) - -customItems.push(FireBallItem) +export const FireBallItem = new CustomItem(Items.Fireball).lore(i18n`Используйте, чтобы отправить все в огненный ад`) const fireballExplosion: ExplosibleEntityOptions = { damage: 3, @@ -23,7 +18,7 @@ const fireballExplosion: ExplosibleEntityOptions = { } world.afterEvents.itemUse.subscribe(event => { - if (!FireBallItem.is(event.itemStack)) return + if (!FireBallItem.isItem(event.itemStack)) return decreaseMainhandItemCount(event.source) diff --git a/src/modules/pvp/ice-bomb.ts b/src/modules/pvp/ice-bomb.ts index 3e8dd092..47a4f8b9 100644 --- a/src/modules/pvp/ice-bomb.ts +++ b/src/modules/pvp/ice-bomb.ts @@ -1,8 +1,8 @@ -import { Entity, EntityComponentTypes, ItemStack, Player, system, world } from '@minecraft/server' +import { Entity, EntityComponentTypes, Player, system, world } from '@minecraft/server' import { MinecraftBlockTypes, MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' import { i18n } from 'lib/i18n/text' -import { customItems } from 'lib/rpg/custom-item' +import { CustomItem } from 'lib/rpg/custom-item' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' import { ms } from 'lib/utils/ms' import { toPoint } from 'lib/utils/point' @@ -11,11 +11,9 @@ import { WeakPlayerSet } from 'lib/weak-player-storage' import { BaseRegion } from 'modules/places/base/region' import { getEdgeBlocksOf } from 'modules/places/mineshaft/get-edge-blocks-of' -export const IceBombItem = new ItemStack(MinecraftItemTypes.Snowball).setInfo( - i18n`§3Снежная бомба`, - i18n`Используйте, чтобы отправить все к снежной королеве подо льдину`, -) -customItems.push(IceBombItem) +export const IceBombItem = new CustomItem(MinecraftItemTypes.Snowball) + .nameTag(i18n`§3Снежная бомба`) + .lore(i18n`Используйте, чтобы отправить все к снежной королеве подо льдину`) const ICE_BOMB_TRANSOFORM: Record = { [MinecraftBlockTypes.Water]: MinecraftBlockTypes.FrostedIce, @@ -28,7 +26,7 @@ const iceBombs = new Set() const usedIceBombs = new WeakPlayerSet() world.afterEvents.itemUse.subscribe(event => { - if (!event.itemStack.is(IceBombItem)) return + if (!IceBombItem.isItem(event.itemStack)) return usedIceBombs.add(event.source) }) diff --git a/src/modules/pvp/raid.ts b/src/modules/pvp/raid.ts index 125918ea..7aa7d64c 100644 --- a/src/modules/pvp/raid.ts +++ b/src/modules/pvp/raid.ts @@ -6,13 +6,14 @@ import { i18n } from 'lib/i18n/text' import { Region } from 'lib/region' import { MineareaRegion } from 'lib/region/kinds/minearea' import { ScheduleBlockPlace } from 'lib/scheduled-block-place' +import { onLoad } from 'lib/utils/load-ref' import { ms } from 'lib/utils/ms' import { BaseRegion } from 'modules/places/base/region' const notify = new Map() const targetLockTime = ms.from('min', 8) const raiderLockTime = ms.from('min', 10) -const objective = ScoreboardDB.objective('raid') +const objective = onLoad(() => ScoreboardDB.objective('raid')) world.beforeEvents.explosion.subscribe(event => { const checker = createBlockExplosionChecker() @@ -81,11 +82,11 @@ system.runInterval( } else notify.set(id, { time: time - 1, reason }) } - for (const { participant, score } of objective.getScores()) { + for (const { participant, score } of objective.value.getScores()) { if (score > 1) { - objective.addScore(participant, -1) + objective.value.addScore(participant, -1) } else { - objective.removeParticipant(participant) + objective.value.removeParticipant(participant) } } }, diff --git a/src/modules/quests/daily/index.ts b/src/modules/quests/daily/index.ts index dad779dd..ba35388a 100644 --- a/src/modules/quests/daily/index.ts +++ b/src/modules/quests/daily/index.ts @@ -2,6 +2,7 @@ import { Player } from '@minecraft/server' import { table } from 'lib/database/abstract' import { form } from 'lib/form/new' +import { QuestForm } from 'lib/form/quest' import { intlListFormat } from 'lib/i18n/intl' import { i18n, textTable } from 'lib/i18n/text' import { questMenuCustomButtons } from 'lib/quest/menu' @@ -68,7 +69,7 @@ new RecurringEvent( currentDailyQuestCity = mostPopular storage.cityId = mostPopular?.group.id ?? '' - for (const value of db.values()) { + for (const [, value] of db.entries()) { if (!value.takenToday) value.streak = 0 value.today = 0 value.takenToday = false @@ -122,7 +123,7 @@ questMenuCustomButtons.subscribe(({ player, form }) => { } }) -export const dailyQuestsForm = form((f, { player }) => { +export const dailyQuestsForm = form((f, { player, self }) => { const playerDb = db.get(player.id) f.title(i18n`Ежедневные задания`) f.body( @@ -154,6 +155,6 @@ export const dailyQuestsForm = form((f, { player }) => { } for (const quest of currentDailyQuests) { - f.quest(quest) + new QuestForm(f, player, self).quest(quest) } }) diff --git a/src/modules/quests/learning/learning.ts b/src/modules/quests/learning/learning.ts index c813918d..4c317941 100644 --- a/src/modules/quests/learning/learning.ts +++ b/src/modules/quests/learning/learning.ts @@ -10,25 +10,25 @@ import { createPublicGiveItemCommand, Menu } from 'lib/rpg/menu' import { Items } from 'lib/assets/custom-items' import { ActionbarPriority } from 'lib/extensions/on-screen-display' +import { ActionForm } from 'lib/form/action' import { i18n, i18nShared, noI18n } from 'lib/i18n/text' +import { location } from 'lib/location' +import { actionGuard, ActionGuardOrder } from 'lib/region' import { RegionEvents } from 'lib/region/events' import { MineareaRegion } from 'lib/region/kinds/minearea' import { enterNewbieMode } from 'lib/rpg/newbie' import { noGroup } from 'lib/rpg/place' +import { Temporary } from 'lib/temporary' +import { onLoad } from 'lib/utils/load-ref' import { createLogger } from 'lib/utils/logger' import { createPointVec } from 'lib/utils/point' +import { Vec } from 'lib/vector' import { WeakPlayerMap, WeakPlayerSet } from 'lib/weak-player-storage' import { Anarchy } from 'modules/places/anarchy/anarchy' import { OrePlace, ores } from 'modules/places/mineshaft/algo' import { Spawn } from 'modules/places/spawn' import { VillageOfMiners } from 'modules/places/village-of-miners/village-of-miners' import airdropTable from './airdrop' -import { ActionForm } from 'lib/form/action' -import { Temporary } from 'lib/temporary' -import { ActionGuardOrder } from 'lib/region' -import { actionGuard } from 'lib/region' -import { location } from 'lib/location' -import { Vec } from 'lib/vector' const logger = createLogger('Learning Quest') @@ -69,8 +69,8 @@ class Learning { // in spawn inventory that will be replaced with // anarchy system.delay(() => { - this.startAxeGiveCommand.ensure(player) - player.getComponent('equippable')?.setEquipment(EquipmentSlot.Offhand, Menu.itemStack) + this.startAxeGiveCommand.value.ensure(player) + player.getComponent('equippable')?.setEquipment(EquipmentSlot.Offhand, Menu.itemStack.value) }) } @@ -276,11 +276,13 @@ class Learning { craftingTableLocation = location(this.quest.group.place('crafting table').name(noI18n`Верстак`)) - startAxeGiveCommand = createPublicGiveItemCommand( - 'startwand', - new ItemStack(MinecraftItemTypes.WoodenAxe), - s => s.typeId === MinecraftItemTypes.WoodenAxe && s.getDynamicProperty('startwand') === true, - i18n`§r§6Начальный топор`, + startAxeGiveCommand = onLoad(() => + createPublicGiveItemCommand( + 'startwand', + new ItemStack(MinecraftItemTypes.WoodenAxe), + s => s.typeId === MinecraftItemTypes.WoodenAxe && s.getDynamicProperty('startwand') === true, + i18n`§r§6Начальный топор`, + ), ) blockedOre = new WeakPlayerMap() diff --git a/src/modules/survival/random-teleport.ts b/src/modules/survival/random-teleport.ts index 50173534..310df09d 100644 --- a/src/modules/survival/random-teleport.ts +++ b/src/modules/survival/random-teleport.ts @@ -3,7 +3,6 @@ import { EquipmentSlot, ItemLockMode, - ItemStack, LocationInUnloadedChunkError, LocationOutOfWorldBoundariesError, Player, @@ -12,15 +11,14 @@ import { } from '@minecraft/server' import { MinecraftEffectTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' +import { LockAction } from 'lib/action' +import { CustomItem } from 'lib/rpg/custom-item' import { util } from 'lib/util' import { Vec } from 'lib/vector' -import { LockAction } from 'lib/action' -const RTP_ELYTRA = new ItemStack(MinecraftItemTypes.Elytra, 1).setInfo( - '§6Элитра перемещения', - 'Элитра перелета, пропадает на земле', -) -RTP_ELYTRA.lockMode = ItemLockMode.slot +const RTP_ELYTRA = new CustomItem(MinecraftItemTypes.Elytra) + .nameTag('§6Элитра перемещения') + .lore('Элитра перелета, пропадает на земле') const IN_SKY = new Set() new LockAction(player => IN_SKY.has(player.id), '§cВ начале коснитесь земли!') @@ -158,7 +156,9 @@ function giveElytra(player: Player, c = 5) { } } - slot.setItem(RTP_ELYTRA) + const clone = RTP_ELYTRA.itemStack.clone() + clone.lockMode = ItemLockMode.slot + slot.setItem(clone) player.database.survival.rtpElytra = 1 } @@ -184,6 +184,6 @@ function clearElytra(player: Player) { if (!equippable) return const slot = equippable.getEquipmentSlot(EquipmentSlot.Chest) const item = slot.getItem() - if (item && RTP_ELYTRA.is(item)) slot.setItem(undefined) + if (item && RTP_ELYTRA.isItem(item)) slot.setItem(undefined) delete player.database.survival.rtpElytra } diff --git a/src/modules/survival/sidebar.ts b/src/modules/survival/sidebar.ts index 9eb4e227..da5917a8 100644 --- a/src/modules/survival/sidebar.ts +++ b/src/modules/survival/sidebar.ts @@ -2,12 +2,13 @@ import { Player, system, TicksPerSecond, world } from '@minecraft/server' import { emoji } from 'lib/assets/emoji' import { i18n } from 'lib/i18n/text' +import { Join } from 'lib/player-join' import { Quest } from 'lib/quest/quest' -import { separateNumberWithDots } from 'lib/util' import { Region } from 'lib/region' -import { Sidebar } from 'lib/sidebar' import { Menu } from 'lib/rpg/menu' import { Settings } from 'lib/settings' +import { Sidebar } from 'lib/sidebar' +import { separateNumberWithDots } from 'lib/util' import { Minigame } from 'modules/minigames/Builder' import { BaseRegion } from 'modules/places/base/region' @@ -142,7 +143,7 @@ export function showSurvivalHud(player: Player) { system.runPlayerInterval( player => { - if (player.database.join) return // Do not show sidebar until player actually joins the world + if (Join.getInstance().isJoining(player)) return // Do not show sidebar until player actually joins the world const settings = getSidebarSettings(player) diff --git a/src/modules/survival/speedrun/target.ts b/src/modules/survival/speedrun/target.ts index 848333d0..3914b987 100644 --- a/src/modules/survival/speedrun/target.ts +++ b/src/modules/survival/speedrun/target.ts @@ -66,10 +66,9 @@ declare module '@minecraft/server' { } } -const baseTypeId = BaseItem.itemStack.typeId InventoryInterval.slots.subscribe(({ player, slot }) => { if (!isSpeedRunningFor(player, SpeedRunTarget.GetBaseItem)) return - if (slot.isValid && slot.typeId === baseTypeId && BaseItem.isItem(slot.getItem())) { + if (slot.isValid && BaseItem.isItem(slot.getItem())) { finishSpeedRun(player, SpeedRunTarget.GetBaseItem) } }) diff --git a/src/modules/world-edit/commands/region/set/block-is-avaible.ts b/src/modules/world-edit/commands/region/set/block-is-avaible.ts index 77efc405..2dd2276d 100644 --- a/src/modules/world-edit/commands/region/set/block-is-avaible.ts +++ b/src/modules/world-edit/commands/region/set/block-is-avaible.ts @@ -1,13 +1,14 @@ import { BlockTypes, Player } from '@minecraft/server' import { suggest } from 'lib/command/utils' import { noI18n } from 'lib/i18n/text' +import { onLoad } from 'lib/utils/load-ref' const prefix = 'minecraft:' -const blocks = BlockTypes.getAll().map(e => e.id.substring(prefix.length)) +const blocks = onLoad(() => BlockTypes.getAll().map(e => e.id.substring(prefix.length))) export function blockIsAvaible(block: string, player: Pick): boolean { - if (blocks.includes(block)) return true + if (blocks.value.includes(block)) return true player.tell(noI18n.error`Блока ${block} не существует.`) - suggest(player, block, blocks) + suggest(player, block, blocks.value) return false } diff --git a/src/modules/world-edit/config.ts b/src/modules/world-edit/config.ts index e1d998f7..c05cb428 100644 --- a/src/modules/world-edit/config.ts +++ b/src/modules/world-edit/config.ts @@ -4,6 +4,7 @@ import { MolangVariableMap, world, } from '@minecraft/server' +import { onLoad } from 'lib/utils/load-ref' import { Vec } from 'lib/vector' export const WE_CONFIG = { @@ -17,15 +18,13 @@ export const WE_CONFIG = { DRAW_SELECTION_PARTICLE: 'minecraft:balloon_gas_particle', DRAW_SELECTION_MAX_SIZE: 5000, - DRAW_SELECTION_PARTICLE_OPTIONS: new MolangVariableMap(), + DRAW_SELECTION_PARTICLE_OPTIONS: onLoad(() => { + const map = new MolangVariableMap() + map.setVector3('direction', { x: 0, y: 0, z: 0 }) + return map + }), } -WE_CONFIG.DRAW_SELECTION_PARTICLE_OPTIONS.setVector3('direction', { - x: 0, - y: 0, - z: 0, -}) - export function spawnParticlesInArea( pos1: Vector3, pos2: Vector3, @@ -44,7 +43,7 @@ export function spawnParticlesInArea( world.overworld.spawnParticle( WE_CONFIG.DRAW_SELECTION_PARTICLE, { x, y, z }, - WE_CONFIG.DRAW_SELECTION_PARTICLE_OPTIONS, + WE_CONFIG.DRAW_SELECTION_PARTICLE_OPTIONS.value, ) } catch (e) { if (e instanceof LocationInUnloadedChunkError || e instanceof LocationOutOfWorldBoundariesError) continue diff --git a/src/modules/world-edit/menu.ts b/src/modules/world-edit/menu.ts index fb8eefd5..07fd00ae 100644 --- a/src/modules/world-edit/menu.ts +++ b/src/modules/world-edit/menu.ts @@ -12,6 +12,7 @@ import { translateTypeId } from 'lib/i18n/lang' import { i18n } from 'lib/i18n/text' import { is } from 'lib/roles' import { inspect, noNullable, stringify } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' import { Vec } from 'lib/vector' import { WorldEdit } from 'modules/world-edit/lib/world-edit' import { weRandomizerTool } from 'modules/world-edit/tools/randomizer' @@ -543,7 +544,7 @@ function WEeditBlocksSetMenu(o: { form.show(player) } -const allStates = BlockStates.getAll() +const allStates = onLoad(() => BlockStates.getAll()) export function WEeditBlockStatesMenu( player: Player, @@ -563,7 +564,7 @@ export function WEeditBlockStatesMenu( // eslint-disable-next-line prefer-const for (let [stateName, stateValue] of Object.entries(states)) { - const stateDef = allStates.find(e => e.id === stateName) + const stateDef = allStates.value.find(e => e.id === stateName) if (!stateDef) continue form.button( diff --git a/src/modules/world-edit/tools/brush.ts b/src/modules/world-edit/tools/brush.ts index ed65b21c..c8ef869c 100644 --- a/src/modules/world-edit/tools/brush.ts +++ b/src/modules/world-edit/tools/brush.ts @@ -2,7 +2,12 @@ import { ContainerSlot, Entity, Player, system, world } from '@minecraft/server' import { CustomEntityTypes } from 'lib/assets/custom-entity-types' import { Items } from 'lib/assets/custom-items' +import { ModalForm } from 'lib/form/modal' import { i18n } from 'lib/i18n/text' +import { is } from 'lib/roles' +import { isKeyof } from 'lib/util' +import { isLocationError, onLoad } from 'lib/utils/game' +import { Vec } from 'lib/vector' import { WeakPlayerMap } from 'lib/weak-player-storage' import { Cuboid } from '../../../lib/utils/cuboid' import { WE_CONFIG } from '../config' @@ -23,11 +28,6 @@ import { } from '../utils/blocks-set' import { shortenBlocksSetName } from '../utils/default-block-sets' import { SHAPES, ShapeFormula } from '../utils/shapes' -import { isLocationError } from 'lib/utils/game' -import { isKeyof } from 'lib/util' -import { is } from 'lib/roles' -import { ModalForm } from 'lib/form/modal' -import { Vec } from 'lib/vector' interface Storage { shape: string @@ -170,12 +170,14 @@ class BrushTool extends WorldEditToolBrush { ctx.player.success() }) - world.overworld - .getEntities({ - type: CustomEntityTypes.FloatingText, - name: WE_CONFIG.BRUSH_LOCATOR, - }) - .forEach(e => e.remove()) + onLoad(() => { + world.overworld + .getEntities({ + type: CustomEntityTypes.FloatingText, + name: WE_CONFIG.BRUSH_LOCATOR, + }) + .forEach(e => e.remove()) + }) this.onGlobalInterval('global', (player, _, slot) => { if (slot.typeId !== this.typeId && this.brushLocators.has(player.id)) { diff --git a/src/modules/world-edit/tools/tool.ts b/src/modules/world-edit/tools/tool.ts index 261075b7..fd89fed9 100644 --- a/src/modules/world-edit/tools/tool.ts +++ b/src/modules/world-edit/tools/tool.ts @@ -5,6 +5,7 @@ import { ListSounds } from 'lib/assets/sounds' import { ActionForm } from 'lib/form/action' import { ModalForm } from 'lib/form/modal' import { inspect } from 'lib/utils/inspect' +import { onLoad } from 'lib/utils/load-ref' import { Vec } from 'lib/vector' import { WorldEditTool } from '../lib/world-edit-tool' @@ -113,7 +114,7 @@ class Tool extends WorldEditTool { constructor() { super() - const variables = new MolangVariableMap() + const variables = onLoad(() => new MolangVariableMap()) system.runInterval( () => { @@ -137,7 +138,7 @@ class Tool extends WorldEditTool { hit.block.dimension.spawnParticle( lore[1], Vec.add(hit.block.location, { x: 0.5, z: 0.5, y: 1.5 }), - variables, + variables.value, ) } diff --git a/src/modules/world-edit/utils/blocks-set.ts b/src/modules/world-edit/utils/blocks-set.ts index 0e33c7eb..fce6d99f 100644 --- a/src/modules/world-edit/utils/blocks-set.ts +++ b/src/modules/world-edit/utils/blocks-set.ts @@ -93,7 +93,7 @@ export function getBlocksInSet([playerId, blocksSetName]: BlocksSetRef) { } export function getReplaceTargets(ref: BlocksSetRef): ReplaceTarget[] { - const defaultReplaceTarget = DEFAULT_REPLACE_TARGET_SETS[ref[1]] + const defaultReplaceTarget = DEFAULT_REPLACE_TARGET_SETS.value[ref[1]] if (defaultReplaceTarget) return defaultReplaceTarget return getActiveBlocksInSet(ref)?.map(fromBlockStateWeightToReplaceTarget) ?? [] diff --git a/src/modules/world-edit/utils/default-block-sets.ts b/src/modules/world-edit/utils/default-block-sets.ts index 85048a35..dc7ae8b6 100644 --- a/src/modules/world-edit/utils/default-block-sets.ts +++ b/src/modules/world-edit/utils/default-block-sets.ts @@ -1,6 +1,7 @@ import { BlockPermutation, BlockTypes, LiquidType } from '@minecraft/server' import { BlockStateSuperset, MinecraftBlockTypes } from '@minecraft/vanilla-data' import { noNullable } from 'lib/util' +import { onLoad } from 'lib/utils/load-ref' import { BlockStateWeight, BlocksSets, @@ -9,10 +10,15 @@ import { fromBlockStateWeightToReplaceTarget, } from './blocks-set' -const trees: BlockStateWeight[] = BlockTypes.getAll() - .filter(e => e.id.endsWith('_log') || e.id.includes('leaves')) - .map(e => [e.id, void 0, 1]) -trees.push([MinecraftBlockTypes.MangroveRoots, void 0, 1]) +const trees = onLoad(() => { + const trees: BlockStateWeight[] = BlockTypes.getAll() + .filter(e => e.id.endsWith('_log') || e.id.includes('leaves')) + .map(e => [e.id, void 0, 1]) + + trees.push([MinecraftBlockTypes.MangroveRoots, void 0, 1]) + + return trees +}) export const DEFAULT_BLOCK_SETS: BlocksSets = { Земля: [[MinecraftBlockTypes.GrassBlock, void 0, 1]], @@ -48,11 +54,14 @@ function isGlassPane(typeId: string) { } const allBlockTypes = [isSlab, isStairs, isWall, isTrapdoor, isGlass, isGlassPane] -const air = BlockPermutation.resolve(MinecraftBlockTypes.Air) +const air = onLoad(() => BlockPermutation.resolve(MinecraftBlockTypes.Air)) -export const DEFAULT_REPLACE_TARGET_SETS: Record = { - 'Любое дерево': trees.map(fromBlockStateWeightToReplaceTarget).filter(noNullable), -} +export const DEFAULT_REPLACE_TARGET_SETS = onLoad( + () => + ({ + 'Любое дерево': trees.value.map(fromBlockStateWeightToReplaceTarget).filter(noNullable), + }) as Record, +) export const REPLACE_MODES: Record = { 'Не воздух': { @@ -77,7 +86,7 @@ export const REPLACE_MODES: Record = { 'Замена соответств. блока': { matches: () => true, select(block, permutations) { - if (block.isAir) return air + if (block.isAir) return air.value let permutation: BlockPermutation | undefined const { typeId } = block @@ -125,7 +134,7 @@ export function shortenBlocksSetName(name: string | undefined | null) { } addPostfix(DEFAULT_BLOCK_SETS) -addPostfix(DEFAULT_REPLACE_TARGET_SETS) +DEFAULT_REPLACE_TARGET_SETS.onLoad(v => addPostfix(v)) function addPostfix(blocksSet: Record) { Object.keys(blocksSet).forEach(e => { diff --git a/src/test/vitest.d.ts b/src/test/vitest.d.ts index ed4d6a26..e50ef01a 100644 --- a/src/test/vitest.d.ts +++ b/src/test/vitest.d.ts @@ -6,7 +6,12 @@ declare global { const afterAll: (typeof import('@vitest/runner'))['afterAll'] const afterEach: (typeof import('@vitest/runner'))['afterEach'] - const expect: import('@vitest/expect').ExpectStatic + const expect: (( + actual: T, + message?: string, + ) => import('@vitest/expect').Assertion & { not: import('@vitest/expect').Assertion }) & + import('@vitest/expect').ExpectStatic + const expectTypeOf: typeof import('expect-type').expectTypeOf const vi: typeof import('@vitest/spy') & { diff --git a/yarn.lock b/yarn.lock index 63b0f361..46047076 100644 --- a/yarn.lock +++ b/yarn.lock @@ -739,66 +739,51 @@ __metadata: languageName: node linkType: hard -"@minecraft/common@npm:^1.0.0, @minecraft/common@npm:^1.1.0": - version: 1.2.0 - resolution: "@minecraft/common@npm:1.2.0" - checksum: 10c0/597c3ff8ab275ba5d5fb3037e68970e59ac96e8793b7738178c95b17d22a8f48a4412425314ed494f664466dd9a342e2a9eff52af544e57689f103c2ae965e10 - languageName: node - linkType: hard - -"@minecraft/server-admin@npm:^1.0.0-beta.1.21.90-stable": - version: 1.0.0-beta.release.1.19.50 - resolution: "@minecraft/server-admin@npm:1.0.0-beta.release.1.19.50" - checksum: 10c0/81e3b467411c086e21ed29bbc7b73c03db15dd7072aaa344fa4f936a6f1fc7cbce1dcadc68dea8df4e48028367f5d885b3d2ec3904ecab397be404c5ff9ecc51 - languageName: node - linkType: hard - -"@minecraft/server-gametest@npm:1.0.0-beta.1.21.90-stable": - version: 1.0.0-beta.1.21.90-stable - resolution: "@minecraft/server-gametest@npm:1.0.0-beta.1.21.90-stable" - dependencies: - "@minecraft/common": "npm:^1.0.0" - "@minecraft/server": "npm:^1.17.0 || ^2.0.0" - checksum: 10c0/bb72ae9c0f3a90758999c778e83b9fe8af3e87de9cf1a076e273baec11d378ec12672f276cc3b880364578ac344b925fc93fcbcfa361c508295a4a3162cc23dd +"@minecraft/server-gametest@npm:1.0.0-beta.1.21.120-stable": + version: 1.0.0-beta.1.21.120-stable + resolution: "@minecraft/server-gametest@npm:1.0.0-beta.1.21.120-stable" + peerDependencies: + "@minecraft/common": ^1.0.0 + "@minecraft/server": ^1.17.0 || ^2.0.0 || ^2.4.0-beta.1.21.120-stable + checksum: 10c0/00d53fc710025611e772d4d0da98118026d77d07fb9f3752e73a1c517937be909d1e6acefc538ac439508ed896dec0bd4053e502ac29f317154fec7f42a7e975 languageName: node linkType: hard -"@minecraft/server-net@npm:1.0.0-beta.1.21.90-stable": - version: 1.0.0-beta.1.21.90-stable - resolution: "@minecraft/server-net@npm:1.0.0-beta.1.21.90-stable" - dependencies: - "@minecraft/common": "npm:^1.0.0" - "@minecraft/server": "npm:^1.17.0 || ^2.0.0" - "@minecraft/server-admin": "npm:^1.0.0-beta.1.21.90-stable" - checksum: 10c0/9938b03b813623239742da6c6c1ea89590b59be438432692eb09168f4aa8a361b5939cf9ceb178eb76f70d39ca8ed495113ab42aed1797a348e3c3ffc3790e13 +"@minecraft/server-net@npm:1.0.0-beta.1.21.120-stable": + version: 1.0.0-beta.1.21.120-stable + resolution: "@minecraft/server-net@npm:1.0.0-beta.1.21.120-stable" + peerDependencies: + "@minecraft/common": ^1.0.0 + "@minecraft/server": ^1.17.0 || ^2.0.0 + "@minecraft/server-admin": ^1.0.0-beta.1.21.120-stable + checksum: 10c0/a6eb346c2fbb3d4e1ea0814cc08e71d0c922290b74008f73692cd4f980ac482a6bf49037bf4d2704a5bda12b97050117177222341a049e3be8c531595e5a11b2 languageName: node linkType: hard -"@minecraft/server-ui@npm:2.1.0-beta.1.21.90-stable": - version: 2.1.0-beta.1.21.90-stable - resolution: "@minecraft/server-ui@npm:2.1.0-beta.1.21.90-stable" - dependencies: - "@minecraft/common": "npm:^1.0.0" - "@minecraft/server": "npm:^2.0.0" - checksum: 10c0/2e0c761cc596c355757e65e763c89bb851abd6c60e29db688ae7d15cd7fea93af5c5938d1596d48371dcbd0c5391d53d4f2623e71722da13af370c865beb2d6e +"@minecraft/server-ui@npm:2.1.0-beta.1.21.120-stable": + version: 2.1.0-beta.1.21.120-stable + resolution: "@minecraft/server-ui@npm:2.1.0-beta.1.21.120-stable" + peerDependencies: + "@minecraft/common": ^1.0.0 + "@minecraft/server": ^2.0.0 || ^2.4.0-beta.1.21.120-stable + checksum: 10c0/b2ddea1ec213cd31b14c21b0815903ea397616e145808807f644c33fb897230ce31a1275c89a1fcb8d928decda00ca9a00d133185b1422233f83154b5fac4fb3 languageName: node linkType: hard -"@minecraft/server@npm:2.1.0-beta.1.21.90-stable": - version: 2.1.0-beta.1.21.90-stable - resolution: "@minecraft/server@npm:2.1.0-beta.1.21.90-stable" - dependencies: - "@minecraft/common": "npm:^1.1.0" +"@minecraft/server@npm:2.4.0-beta.1.21.120-stable": + version: 2.4.0-beta.1.21.120-stable + resolution: "@minecraft/server@npm:2.4.0-beta.1.21.120-stable" peerDependencies: + "@minecraft/common": ^1.2.0 "@minecraft/vanilla-data": ">=1.20.70" - checksum: 10c0/54cfa429982248721ef087d644264130a137a2233d76ec25796074a6890ea9e52c4e7d4f884425c6d6efb32dcdde03f223ccce16d73c2e0097003c10879e1c00 + checksum: 10c0/ab999a09952c852eefc31467aaee7c129ef207c1e3971e901fed34c4d56d3827a713fa2872083f66a97a137e2e5fea8bbaed3732c0ee771366878a7bfb9836e9 languageName: node linkType: hard -"@minecraft/vanilla-data@npm:1.21.90": - version: 1.21.90 - resolution: "@minecraft/vanilla-data@npm:1.21.90" - checksum: 10c0/5f84fd20917c294c11d00a9de21ae740768ffb39c9dd9bb5e20378e7500760acc6ca896afc03176a1666a3a2d828a9344305e0b3482734d95b8671aaa01123e7 +"@minecraft/vanilla-data@npm:1.21.120": + version: 1.21.120 + resolution: "@minecraft/vanilla-data@npm:1.21.120" + checksum: 10c0/460eb19de9c7ed01fb8b8e3829e0055dbf0c9c2c1050ceb524ae8b2e165bac9c02fecb7f0d75379467256df65006a0601defc49bc6c1efe4312d1898215649cb languageName: node linkType: hard @@ -3703,11 +3688,11 @@ __metadata: "@formatjs/intl-locale": "npm:^4.2.11" "@formatjs/intl-numberformat": "npm:^8.15.4" "@formatjs/intl-pluralrules": "npm:^5.4.4" - "@minecraft/server": "npm:2.1.0-beta.1.21.90-stable" - "@minecraft/server-gametest": "npm:1.0.0-beta.1.21.90-stable" - "@minecraft/server-net": "npm:1.0.0-beta.1.21.90-stable" - "@minecraft/server-ui": "npm:2.1.0-beta.1.21.90-stable" - "@minecraft/vanilla-data": "npm:1.21.90" + "@minecraft/server": "npm:2.4.0-beta.1.21.120-stable" + "@minecraft/server-gametest": "npm:1.0.0-beta.1.21.120-stable" + "@minecraft/server-net": "npm:1.0.0-beta.1.21.120-stable" + "@minecraft/server-ui": "npm:2.1.0-beta.1.21.120-stable" + "@minecraft/vanilla-data": "npm:1.21.120" "@vitest/coverage-istanbul": "npm:3.2.4" "@vitest/ui": "npm:3.2.4" async-mutex: "npm:^0.5.0" From 276afe0deaf27d9eed9c528ec04c43f28536850b Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:30:09 +0300 Subject: [PATCH 13/14] fix: region loading --- src/index.ts | 10 +--- src/lib/database/proxy.ts | 7 +++ src/lib/region/areas/area.ts | 28 +++++------ src/lib/region/areas/chunk-cube.ts | 4 +- src/lib/region/areas/cut.ts | 4 +- src/lib/region/areas/cylinder.ts | 4 +- src/lib/region/areas/flattened-sphere.ts | 4 +- src/lib/region/areas/rectangle.ts | 4 +- src/lib/region/areas/sphere.ts | 4 +- src/lib/region/database.ts | 2 +- src/lib/region/index.ts | 40 ++++++++-------- src/lib/rpg/airdrop.ts | 3 +- src/lib/scheduled-block-place.ts | 8 +++- src/modules/lushway/loader.ts | 4 ++ src/test/__mocks__/minecraft_server.ts | 60 ++++++++++++++++++++++++ 15 files changed, 121 insertions(+), 65 deletions(-) diff --git a/src/index.ts b/src/index.ts index e56de347..b135f28c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,8 @@ // Takes too much to load dynamically which results in interrupted error import 'lib/assets/intl-global-object' + import 'lib/assets/intl' -import('./modules/loader').catch(e => { +import('./modules/loader').catch((e: unknown) => { console.error('Loading error', e) }) - -// system.beforeEvents.startup.subscribe(() => { -// system.run(() => { -// if (__TEST__) import('./test/loader') -// else import('./modules/loader') -// }) -// }) diff --git a/src/lib/database/proxy.ts b/src/lib/database/proxy.ts index 195e8096..1264150c 100644 --- a/src/lib/database/proxy.ts +++ b/src/lib/database/proxy.ts @@ -48,35 +48,42 @@ export abstract class ProxyDatabase { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) return this.value.keys() } values() { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) return [...this.value.values()] as Immutable[] } valuesIterator() { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) return this.value.values() as MapIterator> } entries(): [Key, Value][] { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) const entries: [Key, Value][] = [] for (const [key, value] of this.value.entries()) entries.push([key, this.wrap(value, '') as Value]) return entries } entriesImmutable(): MapIterator<[Key, Immutable]> { + if (!this.loaded) throw new Error(`Proxy table ${this.id} is not yet loaded!`) return this.value.entries() as MapIterator<[Key, Immutable]> } diff --git a/src/lib/region/areas/area.ts b/src/lib/region/areas/area.ts index 65f3074b..a2482e40 100644 --- a/src/lib/region/areas/area.ts +++ b/src/lib/region/areas/area.ts @@ -1,7 +1,6 @@ import { Dimension, system, world } from '@minecraft/server' -import { i18n, noI18n } from 'lib/i18n/text' +import { noI18n } from 'lib/i18n/text' import { stringifyError, util } from 'lib/util' -import { onLoad } from 'lib/utils/load-ref' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' @@ -19,19 +18,18 @@ export abstract class Area { static loaded = false - static asSaveableArea(this: T) { + static asSaveableArea(this: T, type: string) { const b = this as AreaWithType - onLoad(() => { - b.type = new (this as unknown as AreaCreator)({}).type + b.type = type + ;(b.prototype as Area).type = type - if ((this as unknown as typeof Area).loaded) { - throw new Error( - `Registering area type ${b.type} failed. Regions are already restored from json. Registering area should occur on the import-time.`, - ) - } + if ((this as unknown as typeof Area).loaded) { + throw new Error( + `Registering area type ${b.type} failed. Regions are already restored from json. Registering area should occur on the import-time.`, + ) + } - ;(this as unknown as typeof Area).areas.push(b as unknown as AreaWithType) - }) + ;(this as unknown as typeof Area).areas.push(b as unknown as AreaWithType) return b } @@ -40,7 +38,9 @@ export abstract class Area { const area = Area.areas.find(e => e.type === a.t) if (!area) { - console.warn(i18n`[Area][Database] No area found for ${a.t}. Maybe you forgot to register kind or import file?`) + console.warn( + noI18n.warn`[Area][Database] No area found for ${a.t}. Maybe you forgot to register kind or import file?`, + ) return } @@ -52,7 +52,7 @@ export abstract class Area { public dimensionType: DimensionType = 'overworld', ) {} - abstract type: string + type!: string /** Checks if the point is inside the area */ isIn(point: AbstractPoint) { diff --git a/src/lib/region/areas/chunk-cube.ts b/src/lib/region/areas/chunk-cube.ts index 5ed9d2a2..20802a01 100644 --- a/src/lib/region/areas/chunk-cube.ts +++ b/src/lib/region/areas/chunk-cube.ts @@ -19,8 +19,6 @@ class ChunkCube extends Area { super(database, dimensionType) } - type = 'c' - isNear(point: AbstractPoint, distance: number): boolean { const { location: vector, dimensionType } = toPoint(point) if (!this.isOurDimension(dimensionType)) return false @@ -60,4 +58,4 @@ class ChunkCube extends Area { } } -export const ChunkCubeArea = ChunkCube.asSaveableArea() +export const ChunkCubeArea = ChunkCube.asSaveableArea('c') diff --git a/src/lib/region/areas/cut.ts b/src/lib/region/areas/cut.ts index 62097f76..f34f1102 100644 --- a/src/lib/region/areas/cut.ts +++ b/src/lib/region/areas/cut.ts @@ -14,8 +14,6 @@ interface CutDatabase extends JsonObject { } class Cut extends Area { - type = 'cut' - protected parent?: Area constructor(database: CutDatabase, dimensionType?: DimensionType) { @@ -63,4 +61,4 @@ class Cut extends Area { } } -export const CutArea = Cut.asSaveableArea() +export const CutArea = Cut.asSaveableArea('cut') diff --git a/src/lib/region/areas/cylinder.ts b/src/lib/region/areas/cylinder.ts index f691a525..fa1b1fc8 100644 --- a/src/lib/region/areas/cylinder.ts +++ b/src/lib/region/areas/cylinder.ts @@ -3,8 +3,6 @@ import { Vec, VecXZ } from 'lib/vector' import { Area } from './area' class Cylinder extends Area<{ center: { x: number; z: number; y: number }; radius: number; yradius: number }> { - type = 'ss' - isNear(point: AbstractPoint, distance: number): boolean { const { location: vector, dimensionType } = toPoint(point) if (!this.isOurDimension(dimensionType)) return false @@ -45,4 +43,4 @@ class Cylinder extends Area<{ center: { x: number; z: number; y: number }; radiu } } -export const CylinderArea = Cylinder.asSaveableArea() +export const CylinderArea = Cylinder.asSaveableArea('ss') diff --git a/src/lib/region/areas/flattened-sphere.ts b/src/lib/region/areas/flattened-sphere.ts index dcff38e0..a4679b51 100644 --- a/src/lib/region/areas/flattened-sphere.ts +++ b/src/lib/region/areas/flattened-sphere.ts @@ -8,8 +8,6 @@ class FlattenedSphere extends Area<{ rx: number ry: number }> { - type = 'fs' - isNear(point: AbstractPoint, distance: number): boolean { const { location: vector, dimensionType } = toPoint(point) if (!this.isOurDimension(dimensionType)) return false @@ -69,4 +67,4 @@ class FlattenedSphere extends Area<{ } } -export const FlattenedSphereArea = FlattenedSphere.asSaveableArea() +export const FlattenedSphereArea = FlattenedSphere.asSaveableArea('fs') diff --git a/src/lib/region/areas/rectangle.ts b/src/lib/region/areas/rectangle.ts index 71d0ff48..f74ba62f 100644 --- a/src/lib/region/areas/rectangle.ts +++ b/src/lib/region/areas/rectangle.ts @@ -19,8 +19,6 @@ class Rectangle extends Area { super(database, dimensionType) } - type = 'rect' - isNear(point: AbstractPoint, distance: number): boolean { const { location: vector, dimensionType } = toPoint(point) if (!this.isOurDimension(dimensionType)) return false @@ -53,4 +51,4 @@ class Rectangle extends Area { } } -export const RectangleArea = Rectangle.asSaveableArea() +export const RectangleArea = Rectangle.asSaveableArea('rect') diff --git a/src/lib/region/areas/sphere.ts b/src/lib/region/areas/sphere.ts index 3cbee436..16ff711b 100644 --- a/src/lib/region/areas/sphere.ts +++ b/src/lib/region/areas/sphere.ts @@ -3,8 +3,6 @@ import { Vec } from 'lib/vector' import { Area } from './area' class Sphere extends Area<{ center: { x: number; z: number; y: number }; radius: number }> { - type = 's' - isNear(point: AbstractPoint, distance: number): boolean { const { location: vector, dimensionType } = toPoint(point) if (!this.isOurDimension(dimensionType)) return false @@ -40,4 +38,4 @@ class Sphere extends Area<{ center: { x: number; z: number; y: number }; radius: } } -export const SphereArea = Sphere.asSaveableArea() +export const SphereArea = Sphere.asSaveableArea('s') diff --git a/src/lib/region/database.ts b/src/lib/region/database.ts index cde4ec30..88f0ad80 100644 --- a/src/lib/region/database.ts +++ b/src/lib/region/database.ts @@ -43,7 +43,7 @@ export const RegionDatabase = table('region-v2', () => ({ permissions: {}, })) -system.delay(() => { +RegionDatabase.onLoad(() => { system.runJob( (function* regionRestore() { let i = 0 diff --git a/src/lib/region/index.ts b/src/lib/region/index.ts index 2f1b0382..07718c67 100644 --- a/src/lib/region/index.ts +++ b/src/lib/region/index.ts @@ -8,14 +8,14 @@ import { world, } from '@minecraft/server' import { MinecraftEntityTypes, MinecraftItemTypes } from '@minecraft/vanilla-data' -// import { CustomEntityTypes } from 'lib/assets/custom-entity-types' -// import { Items } from 'lib/assets/custom-items' -// import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' +import { CustomEntityTypes } from 'lib/assets/custom-entity-types' +import { PlayerEvents, PlayerProperties } from 'lib/assets/player-json' import { ActionbarPriority } from 'lib/extensions/on-screen-display' import { i18n, noI18n } from 'lib/i18n/text' -// import { onPlayerMove } from 'lib/player-move' +import { onPlayerMove } from 'lib/player-move' import { is } from 'lib/roles' import { isNotPlaying } from 'lib/utils/game' +import { createLogger } from 'lib/utils/logger' import { AbstractPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { EventSignal } from '../event-signal' @@ -29,9 +29,6 @@ import { SWITCHES, TRAPDOORS, } from './config' -// import { RegionEvents } from './events' -import { onPlayerMove } from 'lib/player-move' -import { createLogger } from 'lib/utils/logger' import { RegionEvents } from './events' import './explosion' import { Region } from './kinds/region' @@ -111,6 +108,7 @@ actionGuard((player, region, context) => { if (typeId === MinecraftItemTypes.EnderPearl) return ent.includes(MinecraftEntityTypes.EnderPearl) if (typeId === MinecraftItemTypes.WindCharge) return ent.includes(MinecraftEntityTypes.WindChargeProjectile) if (typeId === MinecraftItemTypes.Snowball) return ent.includes(MinecraftEntityTypes.Snowball) + if (typeId === CustomEntityTypes.Fireball) return ent.includes(CustomEntityTypes.Fireball) } } }, ActionGuardOrder.ProjectileUsePrevent) @@ -203,20 +201,20 @@ onPlayerMove.subscribe(({ player, location, dimensionType }) => { RegionEvents.playerInRegionsCache.set(player, newest) const currentRegion = newest[0] - // const isPlaying = !isNotPlaying(player) - - // const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) - - // if (typeof currentRegion !== 'undefined' && isPlaying) { - // if (currentRegion.permissions.pvp === false) { - // player.triggerEvent( - // player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], - // ) - // player.setProperty(PlayerProperties['lw:newbie'], true) - // } else if (currentRegion.permissions.pvp === 'pve') { - // player.setProperty(PlayerProperties['lw:newbie'], true) - // } else resetNewbie() - // } else resetNewbie() + const isPlaying = !isNotPlaying(player) + + const resetNewbie = () => player.setProperty(PlayerProperties['lw:newbie'], !!player.database.survival.newbie) + + if (typeof currentRegion !== 'undefined' && isPlaying) { + if (currentRegion.permissions.pvp === false) { + player.triggerEvent( + player.database.inv === 'spawn' ? PlayerEvents['player:spawn'] : PlayerEvents['player:safezone'], + ) + player.setProperty(PlayerProperties['lw:newbie'], true) + } else if (currentRegion.permissions.pvp === 'pve') { + player.setProperty(PlayerProperties['lw:newbie'], true) + } else resetNewbie() + } else resetNewbie() EventSignal.emit(RegionEvents.onInterval, { player, currentRegion }) }) diff --git a/src/lib/rpg/airdrop.ts b/src/lib/rpg/airdrop.ts index 56ef53d9..91155ec0 100644 --- a/src/lib/rpg/airdrop.ts +++ b/src/lib/rpg/airdrop.ts @@ -8,7 +8,6 @@ import { createLogger } from 'lib/utils/logger' import { toPoint } from 'lib/utils/point' import { Vec } from 'lib/vector' import { table } from '../database/abstract' -import { Core } from '../extensions/core' import { Temporary } from '../temporary' import { isLocationError } from '../utils/game' import { MinimapNpc, resetMinimapNpcPosition, setMinimapNpcPosition } from './minimap' @@ -300,7 +299,7 @@ const findAndRemove = (arr: Entity[], id: string) => { if (i !== -1) return arr.splice(i, 1)[0] } -Core.afterEvents.worldLoad.subscribe(() => { +Airdrop.db.onLoad(() => { for (const [key, saved] of Airdrop.db.entries()) { if (typeof saved === 'undefined') continue const loot = LootTable.instances.get(saved.loot) diff --git a/src/lib/scheduled-block-place.ts b/src/lib/scheduled-block-place.ts index f4cc6a3d..0ab1df8f 100644 --- a/src/lib/scheduled-block-place.ts +++ b/src/lib/scheduled-block-place.ts @@ -215,7 +215,13 @@ function* scheduledBlockPlaceJob() { function timeout() { system.runTimeout(() => system.runJob(scheduledBlockPlaceJob()), 'scheduled block place', 10) } -timeout() +DB.overworld.onLoad(() => { + DB.nether.onLoad(() => { + DB.end.onLoad(() => { + timeout() + }) + }) +}) let debugLogging = false diff --git a/src/modules/lushway/loader.ts b/src/modules/lushway/loader.ts index 4d9723ce..2d1aeacd 100644 --- a/src/modules/lushway/loader.ts +++ b/src/modules/lushway/loader.ts @@ -3,6 +3,10 @@ import 'lib/anticheat/forbidden-items' import 'lib/anticheat/freeze' import 'lib/anticheat/whitelist' +import 'lib/database/command' + +import 'lib/region/database' + import './config/core' import './config/chat' diff --git a/src/test/__mocks__/minecraft_server.ts b/src/test/__mocks__/minecraft_server.ts index 88465075..4c474798 100644 --- a/src/test/__mocks__/minecraft_server.ts +++ b/src/test/__mocks__/minecraft_server.ts @@ -433,6 +433,66 @@ export enum EntityComponentTypes { WantsJockey = 'minecraft:wants_jockey', } +/** The types of paramaters accepted by a custom command. */ +export enum CustomCommandParamType { + /** + * @remarks + * Block type parameter provides a {@link BlockType}. + */ + BlockType = 'BlockType', + /** + * @remarks + * Boolean parameter. + */ + Boolean = 'Boolean', + /** + * @remarks + * Entity selector parameter provides an {@link Entity}. + */ + EntitySelector = 'EntitySelector', + /** + * @remarks + * Entity type parameter provides an {@link EntityType}. + */ + EntityType = 'EntityType', + /** + * @remarks + * Command enum parameter. + */ + Enum = 'Enum', + /** + * @remarks + * Float parameter. + */ + Float = 'Float', + /** + * @remarks + * Integer parameter. + */ + Integer = 'Integer', + /** + * @remarks + * Item type parameter provides an {@link ItemType}. + */ + ItemType = 'ItemType', + /** + * @remarks + * Location parameter provides a {@link + * @minecraft/server.Location}. + */ + Location = 'Location', + /** + * @remarks + * Player selector parameter provides a {@link Player}. + */ + PlayerSelector = 'PlayerSelector', + /** + * @remarks + * String parameter. + */ + String = 'String', +} + export class EntityEquippableComponent extends EntityComponent { static readonly componentId = 'minecraft:equippable' readonly typeId = 'minecraft:equippable' From ba18219020c20b964bad8d3453590db8fa0e2c50 Mon Sep 17 00:00:00 2001 From: leaftail1880 <110915645+leaftail1880@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:33:26 +0300 Subject: [PATCH 14/14] fix: chat close --- src/lib/form/utils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/form/utils.ts b/src/lib/form/utils.ts index b35c6c48..204b6324 100644 --- a/src/lib/form/utils.ts +++ b/src/lib/form/utils.ts @@ -65,6 +65,17 @@ export async function showForm( if (response.cancelationReason === FormCancelationReason.UserClosed) return false if (response.cancelationReason === FormCancelationReason.UserBusy) { switch (i) { + case 1: + // First attempt failed, maybe chat closed... + player.closeChat() + continue + + case 2: + // Second attempt, tell player to manually close chat... + player.info(i18n`Закрой чат!`) + await system.sleep(10) + continue + default: await system.sleep(10) break