From f694feec277c221dbf7d2a3af805eaf1dff5a31e Mon Sep 17 00:00:00 2001 From: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:11:39 +0300 Subject: [PATCH] finshline --- .gitignore | 2 + .../me/topchetoeu/mcscript/MessageQueue.java | 9 +- .../topchetoeu/mcscript/lib/MCInternals.java | 2 +- .../lib/common/entities/EntityLib.java | 3 +- .../mcscript/lib/server/ServerLib.java | 93 +- .../server/inventory/InventoryScreenLib.java | 30 + .../mcscript/lib/utils/EventLib.java | 8 +- src/test-mod/libs/lib.d.ts | 632 +++++++ src/test-mod/libs/mc.d.ts | 174 ++ src/test-mod/manifest.json | 6 + src/test-mod/src/main.ts | 1591 +++++++++++++++++ src/test-mod/tsconfig.json | 10 + 12 files changed, 2535 insertions(+), 25 deletions(-) create mode 100644 src/test-mod/libs/lib.d.ts create mode 100644 src/test-mod/libs/mc.d.ts create mode 100644 src/test-mod/manifest.json create mode 100644 src/test-mod/src/main.ts create mode 100644 src/test-mod/tsconfig.json diff --git a/.gitignore b/.gitignore index f14d2c3..20ef40d 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ !/src !/src/**/* +/src/test-mod/dst + !/doc !/doc/**/* diff --git a/src/java/me/topchetoeu/mcscript/MessageQueue.java b/src/java/me/topchetoeu/mcscript/MessageQueue.java index e56c34a..d060403 100644 --- a/src/java/me/topchetoeu/mcscript/MessageQueue.java +++ b/src/java/me/topchetoeu/mcscript/MessageQueue.java @@ -88,15 +88,20 @@ public class MessageQueue { } public T await(DataNotifier notif) { + if (thread != Thread.currentThread()) { + runQueue(); + System.out.println("Tried to await outside the queue's thread, ignoring..."); + return null; + } if (awaiting) { + runQueue(); System.out.println("Tried to double-await, ignoring..."); return null; } synchronized (Thread.currentThread()) { - runQueue(); - while (true) { + runQueue(); try { awaiting = true; return (T)notif.await(); diff --git a/src/java/me/topchetoeu/mcscript/lib/MCInternals.java b/src/java/me/topchetoeu/mcscript/lib/MCInternals.java index 1e8503c..1783bb6 100644 --- a/src/java/me/topchetoeu/mcscript/lib/MCInternals.java +++ b/src/java/me/topchetoeu/mcscript/lib/MCInternals.java @@ -29,7 +29,7 @@ import net.minecraft.server.world.ServerWorld; public class MCInternals { @ExposeField(target = ExposeTarget.STATIC) - public static final EventLib __serverLoad = new EventLib(); + public static final EventLib __serverLoad = new EventLib(null); static { ServerLifecycleEvents.SERVER_STARTED.register(server -> { diff --git a/src/java/me/topchetoeu/mcscript/lib/common/entities/EntityLib.java b/src/java/me/topchetoeu/mcscript/lib/common/entities/EntityLib.java index 2b4c69f..e19df28 100644 --- a/src/java/me/topchetoeu/mcscript/lib/common/entities/EntityLib.java +++ b/src/java/me/topchetoeu/mcscript/lib/common/entities/EntityLib.java @@ -10,7 +10,6 @@ import me.topchetoeu.mcscript.lib.server.ServerLib; import me.topchetoeu.mcscript.lib.utils.LocationLib; import net.minecraft.entity.Entity; import net.minecraft.nbt.NbtCompound; -import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; import net.minecraft.world.World; @@ -50,7 +49,7 @@ public class EntityLib { @Expose(type = ExposeType.GETTER) public static String __uuid(Arguments args) { - return args.self(ServerPlayerEntity.class).getUuidAsString(); + return args.self(Entity.class).getUuidAsString(); } @Expose public static void __clearName(Arguments args) { diff --git a/src/java/me/topchetoeu/mcscript/lib/server/ServerLib.java b/src/java/me/topchetoeu/mcscript/lib/server/ServerLib.java index e73e06e..0a58073 100644 --- a/src/java/me/topchetoeu/mcscript/lib/server/ServerLib.java +++ b/src/java/me/topchetoeu/mcscript/lib/server/ServerLib.java @@ -4,6 +4,7 @@ import static net.minecraft.server.command.CommandManager.argument; import static net.minecraft.server.command.CommandManager.literal; import java.util.Map; +import java.util.UUID; import java.util.WeakHashMap; import com.mojang.brigadier.Command; @@ -25,6 +26,7 @@ import me.topchetoeu.jscript.utils.interop.ExposeTarget; import me.topchetoeu.jscript.utils.interop.ExposeType; import me.topchetoeu.jscript.utils.interop.WrapperName; import me.topchetoeu.mcscript.MessageQueue; +import me.topchetoeu.mcscript.core.Data; import me.topchetoeu.mcscript.events.PlayerBlockPlaceEvent; import me.topchetoeu.mcscript.events.ScreenHandlerEvents; import me.topchetoeu.mcscript.lib.utils.EventLib; @@ -33,6 +35,8 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +import net.fabricmc.fabric.api.event.player.UseItemCallback; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.entity.Entity; import net.minecraft.registry.Registries; @@ -41,7 +45,10 @@ import net.minecraft.server.command.CommandOutput; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.world.ServerWorld; import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; import net.minecraft.util.Identifier; +import net.minecraft.util.TypedActionResult; import net.minecraft.util.math.Vec2f; import net.minecraft.util.math.Vec3d; @@ -49,22 +56,44 @@ import net.minecraft.util.math.Vec3d; @SuppressWarnings("resource") // for crying outloud public class ServerLib { public static class EventCtx { - @ExposeField public final EventLib blockPlace = new EventLib(); - @ExposeField public final EventLib blockBreak = new EventLib(); - @ExposeField public final EventLib tickStart = new EventLib(); - @ExposeField public final EventLib tickEnd = new EventLib(); - @ExposeField public final EventLib playerJoin = new EventLib(); - @ExposeField public final EventLib playerLeave = new EventLib(); - @ExposeField public final EventLib playerChangeWorld = new EventLib(); - @ExposeField public final EventLib entityDamage = new EventLib(); - @ExposeField public final EventLib inventoryScreenClicked = new EventLib(); - @ExposeField public final EventLib inventoryScreenClosed = new EventLib(); + @ExposeField public final EventLib blockPlace; + @ExposeField public final EventLib blockBreak; + + @ExposeField public final EventLib tickStart; + @ExposeField public final EventLib tickEnd; + + @ExposeField public final EventLib playerJoin; + @ExposeField public final EventLib playerLeave; + @ExposeField public final EventLib playerChangeWorld; + + @ExposeField public final EventLib entityDamage; + @ExposeField public final EventLib entityUse; + + @ExposeField public final EventLib itemUse; + + @ExposeField public final EventLib inventoryScreenClicked; + @ExposeField public final EventLib inventoryScreenClosed; + + public EventCtx(Thread thread) { + blockPlace = new EventLib(thread); + blockBreak = new EventLib(thread); + tickStart = new EventLib(thread); + tickEnd = new EventLib(thread); + playerJoin = new EventLib(thread); + playerLeave = new EventLib(thread); + playerChangeWorld = new EventLib(thread); + entityDamage = new EventLib(thread); + entityUse = new EventLib(thread); + itemUse = new EventLib(thread); + inventoryScreenClicked = new EventLib(thread); + inventoryScreenClosed = new EventLib(thread); + } } private static final WeakHashMap ctxs = new WeakHashMap<>(); public static EventCtx events(MinecraftServer server) { - ctxs.putIfAbsent(server, new EventCtx()); + ctxs.putIfAbsent(server, new EventCtx(server.getThread())); return ctxs.get(server); } public static MessageQueue queue(MinecraftServer server) { @@ -124,6 +153,15 @@ public class ServerLib { public static EventLib __entityDamage(Arguments args) { return events(args.self(MinecraftServer.class)).entityDamage; } + @Expose(type = ExposeType.GETTER) + public static EventLib __entityUse(Arguments args) { + return events(args.self(MinecraftServer.class)).entityUse; + } + + @Expose(type = ExposeType.GETTER) + public static EventLib __itemUse(Arguments args) { + return events(args.self(MinecraftServer.class)).itemUse; + } @Expose(type = ExposeType.GETTER) public static EventLib __inventoryScreenClicked(Arguments args) { @@ -208,6 +246,18 @@ public class ServerLib { server.sendMessage(Text.of(text)); } + @Expose public static Entity __getByUUID(Arguments args) { + var server = args.self(MinecraftServer.class); + var uuid = args.getString(0); + + for (var world : server.getWorlds()) { + var entity = world.getEntity(UUID.fromString(uuid)); + if (entity != null) return entity; + } + + return null; + } + @Expose public static ObjectValue __cmd(Arguments args) { var server = args.self(MinecraftServer.class); var cmd = args.getString(0); @@ -305,15 +355,20 @@ public class ServerLib { handler, player, slotIndex < 0 ? null : slotIndex, rawButton, actType ); }); - // .ALLOW_DAMAGE.register((entity, damageSource, damageAmount) -> { - // var dmgSrc = new ObjectValue(); + UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (!events(player.getServer()).entityUse.invokeCancellable( + player, entity + )) return ActionResult.FAIL; - // dmgSrc.defineProperty(null, "damager", damageSource.getAttacker()); - // dmgSrc.defineProperty(null, "location", LocationLib.of(damageSource.getPosition())); - // dmgSrc.defineProperty(null, "type", damageSource.getType().msgId()); - - // return events(entity.getServer()).entityDamage.invokeCancellable(entity, damageAmount, dmgSrc); - // }); + else return ActionResult.PASS; + }); + UseItemCallback.EVENT.register((player, world, hand) -> { + if (!events(player.getServer()).itemUse.invokeCancellable( + Data.toJS(null, Data.toNBT(player.getStackInHand(hand))), + player, hand == Hand.MAIN_HAND ? "main" : "off" + )) return TypedActionResult.fail(player.getStackInHand(hand)); + else return TypedActionResult.pass(player.getStackInHand(hand)); + }); } @Expose(target = ExposeTarget.STATIC) diff --git a/src/java/me/topchetoeu/mcscript/lib/server/inventory/InventoryScreenLib.java b/src/java/me/topchetoeu/mcscript/lib/server/inventory/InventoryScreenLib.java index 918af5d..b32fdfa 100644 --- a/src/java/me/topchetoeu/mcscript/lib/server/inventory/InventoryScreenLib.java +++ b/src/java/me/topchetoeu/mcscript/lib/server/inventory/InventoryScreenLib.java @@ -3,9 +3,12 @@ package me.topchetoeu.mcscript.lib.server.inventory; import java.util.HashSet; import me.topchetoeu.jscript.runtime.values.ArrayValue; +import me.topchetoeu.jscript.runtime.values.NativeFunction; +import me.topchetoeu.jscript.runtime.values.ObjectValue; import me.topchetoeu.jscript.utils.interop.Arguments; import me.topchetoeu.jscript.utils.interop.Expose; import me.topchetoeu.jscript.utils.interop.WrapperName; +import me.topchetoeu.mcscript.core.Data; import net.minecraft.inventory.Inventory; import net.minecraft.screen.ScreenHandler; @@ -26,6 +29,33 @@ public class InventoryScreenLib { if (tmp.add(inv)) res.set(args, res.size(), inv); } + return res; + } + @Expose public static ObjectValue __cursorItem(Arguments args) { + return Data.toJS(args, Data.toNBT(args.self(ScreenHandler.class).getCursorStack())); + } + @Expose public static ObjectValue __getSlot(Arguments args) { + var self = args.self(ScreenHandler.class); + var i = args.getInt(0); + + var slot = self.getSlot(i); + + var res = new ObjectValue(); + res.defineProperty(args, "i", slot.getIndex()); + res.defineProperty(args, "x", slot.x); + res.defineProperty(args, "y", slot.y); + res.defineProperty(args, "inventory", slot.inventory); + res.defineProperty(args, "item", + new NativeFunction(_args -> { + return Data.toJS(args, Data.toNBT(slot.getStack())); + }), + new NativeFunction(_args -> { + var item = Data.toItemStack(Data.toCompound(_args, 0)); + slot.setStack(item); + return null; + }), + true, true); + return res; } } diff --git a/src/java/me/topchetoeu/mcscript/lib/utils/EventLib.java b/src/java/me/topchetoeu/mcscript/lib/utils/EventLib.java index c8bd6c0..f5066d4 100644 --- a/src/java/me/topchetoeu/mcscript/lib/utils/EventLib.java +++ b/src/java/me/topchetoeu/mcscript/lib/utils/EventLib.java @@ -24,6 +24,7 @@ import me.topchetoeu.mcscript.MessageQueue; public class EventLib { private HashMap handles = new HashMap<>(); private HashMap onceHandles = new HashMap<>(); + private Thread thread; public void invoke(Object ...args) { List> arr; @@ -40,7 +41,8 @@ public class EventLib { var func = handle.getKey(); try { - MessageQueue.get().await(EventLoop.get(env).pushMsg(false, env, func, null, args)); + var awaitable = EventLoop.get(env).pushMsg(false, env, func, null, args); + if (thread != null) MessageQueue.get(thread).await(awaitable); } catch (EngineException e) { Values.printError(e, "in event handler"); } } @@ -77,4 +79,8 @@ public class EventLib { return promise; } + + public EventLib(Thread thread) { + this.thread = thread; + } } diff --git a/src/test-mod/libs/lib.d.ts b/src/test-mod/libs/lib.d.ts new file mode 100644 index 0000000..de114e1 --- /dev/null +++ b/src/test-mod/libs/lib.d.ts @@ -0,0 +1,632 @@ +type PropertyDescriptor = { + value: any; + writable?: boolean; + enumerable?: boolean; + configurable?: boolean; +} | { + get?(this: ThisT): T; + set(this: ThisT, val: T): void; + enumerable?: boolean; + configurable?: boolean; +} | { + get(this: ThisT): T; + set?(this: ThisT, val: T): void; + enumerable?: boolean; + configurable?: boolean; +}; +type Exclude = T extends U ? never : T; +type Extract = T extends U ? T : never; +type Record = { [x in KeyT]: ValT } +type ReplaceFunc = (match: string, ...args: any[]) => string; + +type PromiseResult = { type: 'fulfilled'; value: T; } | { type: 'rejected'; reason: any; } + +// wippidy-wine, this code is now mine :D +type Awaited = + T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode + T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped + F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument + Awaited : // recursively unwrap the value + never : // the argument to `then` was not callable + T; + +type IteratorYieldResult = + { done?: false; } & + (TReturn extends undefined ? { value?: undefined; } : { value: TReturn; }); + +type IteratorReturnResult = + { done: true } & + (TReturn extends undefined ? { value?: undefined; } : { value: TReturn; }); + +type IteratorResult = IteratorYieldResult | IteratorReturnResult; + +interface Thenable { + then(onFulfilled?: (val: T) => NextT, onRejected?: (err: any) => NextT): Promise>; +} + +interface RegExpResultIndices extends Array<[number, number]> { + groups?: { [name: string]: [number, number]; }; +} +interface RegExpResult extends Array { + groups?: { [name: string]: string; }; + index: number; + input: string; + indices?: RegExpResultIndices; + escape(raw: string, flags: string): RegExp; +} + +interface Matcher { + [Symbol.match](target: string): RegExpResult | string[] | null; + [Symbol.matchAll](target: string): IterableIterator; +} +interface Splitter { + [Symbol.split](target: string, limit?: number, sensible?: boolean): string[]; +} +interface Replacer { + [Symbol.replace](target: string, replacement: string | ReplaceFunc): string; +} +interface Searcher { + [Symbol.search](target: string, reverse?: boolean, start?: number): number; +} + +type FlatArray = { + "done": Arr, + "recur": Arr extends Array + ? FlatArray + : Arr +}[Depth extends -1 ? "done" : "recur"]; + +interface IArguments { + [i: number]: any; + length: number; +} + +interface Iterator { + next(...args: [] | [TNext]): IteratorResult; + return?(value?: TReturn): IteratorResult; + throw?(e?: any): IteratorResult; +} +interface Iterable { + [Symbol.iterator](): Iterator; +} +interface IterableIterator extends Iterator { + [Symbol.iterator](): IterableIterator; +} + +interface AsyncIterator { + next(...args: [] | [TNext]): Promise>; + return?(value?: TReturn | Thenable): Promise>; + throw?(e?: any): Promise>; +} +interface AsyncIterable { + [Symbol.asyncIterator](): AsyncIterator; +} +interface AsyncIterableIterator extends AsyncIterator { + [Symbol.asyncIterator](): AsyncIterableIterator; +} + +interface Generator extends Iterator { + [Symbol.iterator](): Generator; + return(value: TReturn): IteratorResult; + throw(e: any): IteratorResult; +} +interface GeneratorFunction { + new (...args: any[]): Generator; + (...args: any[]): Generator; + readonly length: number; + readonly name: string; + readonly prototype: Generator; +} + +interface AsyncGenerator extends AsyncIterator { + return(value: TReturn | Thenable): Promise>; + throw(e: any): Promise>; + [Symbol.asyncIterator](): AsyncGenerator; +} +interface AsyncGeneratorFunction { + new (...args: any[]): AsyncGenerator; + (...args: any[]): AsyncGenerator; + readonly length: number; + readonly name: string; + readonly prototype: AsyncGenerator; +} + + +interface MathObject { + readonly E: number; + readonly PI: number; + readonly SQRT2: number; + readonly SQRT1_2: number; + readonly LN2: number; + readonly LN10: number; + readonly LOG2E: number; + readonly LOG10E: number; + + asin(x: number): number; + acos(x: number): number; + atan(x: number): number; + atan2(y: number, x: number): number; + asinh(x: number): number; + acosh(x: number): number; + atanh(x: number): number; + sin(x: number): number; + cos(x: number): number; + tan(x: number): number; + sinh(x: number): number; + cosh(x: number): number; + tanh(x: number): number; + sqrt(x: number): number; + cbrt(x: number): number; + hypot(...vals: number[]): number; + imul(a: number, b: number): number; + exp(x: number): number; + expm1(x: number): number; + pow(x: number, y: number): number; + log(x: number): number; + log10(x: number): number; + log1p(x: number): number; + log2(x: number): number; + ceil(x: number): number; + floor(x: number): number; + round(x: number): number; + fround(x: number): number; + trunc(x: number): number; + abs(x: number): number; + max(...vals: number[]): number; + min(...vals: number[]): number; + sign(x: number): number; + random(): number; + clz32(x: number): number; +} +interface JSONObject { + stringify(val: unknown): string; + parse(val: string): unknown; +} + +interface Array extends IterableIterator { + [i: number]: T; + + length: number; + + toString(): string; + // toLocaleString(): string; + join(separator?: string): string; + fill(val: T, start?: number, end?: number): T[]; + pop(): T | undefined; + push(...items: T[]): number; + concat(...items: (T | T[])[]): T[]; + concat(...items: (T | T[])[]): T[]; + join(separator?: string): string; + reverse(): T[]; + shift(): T | undefined; + slice(start?: number, end?: number): T[]; + sort(compareFn?: (a: T, b: T) => number): this; + splice(start: number, deleteCount?: number | undefined, ...items: T[]): T[]; + unshift(...items: T[]): number; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + every(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + includes(el: any, start?: number): boolean; + + map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]; + find(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T | undefined; + findIndex(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): number; + findLast(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; + findLastIndex(predicate: (value: T, index: number, array: T[]) => boolean, thisArg?: any): number; + + flat(depth?: D): FlatArray[]; + flatMap(func: (val: T, i: number, arr: T[]) => T | T[], thisAarg?: any): FlatArray; + sort(func?: (a: T, b: T) => number): this; + + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + + entries(): IterableIterator<[number, T]>; + values(): IterableIterator; + keys(): IterableIterator; +} +interface ArrayConstructor { + new (arrayLength?: number): T[]; + new (...items: T[]): T[]; + (arrayLength?: number): T[]; + (...items: T[]): T[]; + isArray(arg: unknown): arg is unknown[]; + prototype: Array; +} + +interface Boolean { + toString(): string; + valueOf(): boolean; +} +interface BooleanConstructor { + (val: any): boolean; + new (val: any): Boolean; + prototype: Boolean; +} + +interface Error { + name: string; + message: string; + stack: string[]; + toString(): string; +} +interface ErrorConstructor { + (msg?: any): Error; + new (msg?: any): Error; + prototype: Error; +} + +interface TypeErrorConstructor extends ErrorConstructor { + (msg?: any): TypeError; + new (msg?: any): TypeError; + prototype: Error; +} +interface TypeError extends Error { + name: 'TypeError'; +} + +interface RangeErrorConstructor extends ErrorConstructor { + (msg?: any): RangeError; + new (msg?: any): RangeError; + prototype: Error; +} +interface RangeError extends Error { + name: 'RangeError'; +} + +interface SyntaxErrorConstructor extends ErrorConstructor { + (msg?: any): RangeError; + new (msg?: any): RangeError; + prototype: Error; +} +interface SyntaxError extends Error { + name: 'SyntaxError'; +} + +interface Function { + apply(this: Function, thisArg: any, argArray?: any): any; + call(this: Function, thisArg: any, ...argArray: any[]): any; + bind(this: Function, thisArg: any, ...argArray: any[]): Function; + + toString(): string; + + prototype: any; + readonly length: number; + name: string; +} +interface CallableFunction extends Function { + (...args: any[]): any; + apply(this: (this: ThisArg, ...args: Args) => RetT, thisArg: ThisArg, argArray?: Args): RetT; + call(this: (this: ThisArg, ...args: Args) => RetT, thisArg: ThisArg, ...argArray: Args): RetT; + bind(this: (this: ThisArg, ...args: [ ...Args, ...Rest ]) => RetT, thisArg: ThisArg, ...argArray: Args): (this: void, ...args: Rest) => RetT; +} +interface NewableFunction extends Function { + new(...args: any[]): any; + apply(this: new (...args: Args) => RetT, thisArg: any, argArray?: Args): RetT; + call(this: new (...args: Args) => RetT, thisArg: any, ...argArray: Args): RetT; + bind(this: new (...args: Args) => RetT, thisArg: any, ...argArray: Args): new (...args: Args) => RetT; +} +interface FunctionConstructor extends Function { + (...args: string[]): (...args: any[]) => any; + new (...args: string[]): (...args: any[]) => any; + prototype: Function; + async( + func: (await: (val: T) => Awaited) => (...args: ArgsT) => RetT + ): (...args: ArgsT) => Promise; + asyncGenerator( + func: (await: (val: T) => Awaited, _yield: (val: T) => void) => (...args: ArgsT) => RetT + ): (...args: ArgsT) => AsyncGenerator; + generator( + func: (_yield: (val: T) => TNext) => (...args: ArgsT) => RetT + ): (...args: ArgsT) => Generator; +} + +interface Number { + toString(): string; + toFixed(i: number): string; + valueOf(): number; +} +interface NumberConstructor { + (val: unknown): number; + new (val: unknown): Number; + prototype: Number; + parseInt(val: unknown): number; + parseFloat(val: unknown): number; +} + +interface Object { + constructor: NewableFunction; + [Symbol.typeName]: string; + + valueOf(): this; + toString(): string; + hasOwnProperty(key: any): boolean; +} +interface ObjectConstructor { + (arg: string): String; + (arg: number): Number; + (arg: boolean): Boolean; + (arg?: undefined | null): {}; + (arg: T): T; + + new (arg: string): String; + new (arg: number): Number; + new (arg: boolean): Boolean; + new (arg?: undefined | null): {}; + new (arg: T): T; + + prototype: Object; + + assign(target: T, ...src: object[]): T; + create(proto: T, props?: { [key: string]: PropertyDescriptor }): T; + + keys(obj: T, onlyString?: true): (keyof T)[]; + keys(obj: T, onlyString: false): any[]; + entries(obj: T, onlyString?: true): [keyof T, T[keyof T]][]; + entries(obj: T, onlyString: false): [any, any][]; + values(obj: T, onlyString?: true): (T[keyof T])[]; + values(obj: T, onlyString: false): any[]; + + fromEntries(entries: Iterable<[any, any]>): object; + + defineProperty(obj: ThisT, key: any, desc: PropertyDescriptor): ThisT; + defineProperties(obj: ThisT, desc: { [key: string]: PropertyDescriptor }): ThisT; + + getOwnPropertyNames(obj: T): (keyof T)[]; + getOwnPropertySymbols(obj: T): (keyof T)[]; + hasOwn(obj: T, key: KeyT): boolean; + + getOwnPropertyDescriptor(obj: T, key: KeyT): PropertyDescriptor; + getOwnPropertyDescriptors(obj: T): { [x in keyof T]: PropertyDescriptor }; + + getPrototypeOf(obj: any): object | null; + setPrototypeOf(obj: T, proto: object | null): T; + + preventExtensions(obj: T): T; + seal(obj: T): T; + freeze(obj: T): T; + + isExtensible(obj: object): boolean; + isSealed(obj: object): boolean; + isFrozen(obj: object): boolean; +} + +interface String { + [i: number]: string; + + toString(): string; + valueOf(): string; + + charAt(pos: number): string; + charCodeAt(pos: number): number; + substring(start?: number, end?: number): string; + slice(start?: number, end?: number): string; + substr(start?: number, length?: number): string; + + startsWith(str: string, pos?: number): string; + endsWith(str: string, pos?: number): string; + + replace(pattern: string | Replacer, val: string | ReplaceFunc): string; + replaceAll(pattern: string | Replacer, val: string | ReplaceFunc): string; + + match(pattern: string | Matcher): RegExpResult | string[] | null; + matchAll(pattern: string | Matcher): IterableIterator; + + split(pattern: string | Splitter, limit?: number, sensible?: boolean): string[]; + + concat(...others: string[]): string; + indexOf(term: string | Searcher, start?: number): number; + lastIndexOf(term: string | Searcher, start?: number): number; + + toLowerCase(): string; + toUpperCase(): string; + + trim(): string; + + includes(term: string, start?: number): boolean; + + length: number; +} +interface StringConstructor { + (val: any): string; + new (val: any): String; + + fromCharCode(val: number): string; + + prototype: String; +} + +interface Symbol { + valueOf(): symbol; +} +interface SymbolConstructor { + (val?: any): symbol; + new(...args: any[]): never; + prototype: Symbol; + for(key: string): symbol; + keyFor(sym: symbol): string; + + readonly typeName: unique symbol; + readonly match: unique symbol; + readonly matchAll: unique symbol; + readonly split: unique symbol; + readonly replace: unique symbol; + readonly search: unique symbol; + readonly iterator: unique symbol; + readonly asyncIterator: unique symbol; +} + + +interface Promise extends Thenable { + catch(func: (err: unknown) => ResT): Promise; + finally(func: () => void): Promise; + constructor: PromiseConstructor; +} +interface PromiseConstructorLike { + new (func: (res: (val: T) => void, rej: (err: unknown) => void) => void): Thenable>; +} +interface PromiseConstructor extends PromiseConstructorLike { + prototype: Promise; + + new (func: (res: (val: T) => void, rej: (err: unknown) => void) => void): Promise>; + resolve(val: T): Promise>; + reject(val: any): Promise; + + isAwaitable(val: unknown): val is Thenable; + any(promises: T[]): Promise>; + race(promises: (Promise|T)[]): Promise; + all(promises: T): Promise<{ [Key in keyof T]: Awaited }>; + allSettled(...promises: T): Promise<[...{ [P in keyof T]: PromiseResult>}]>; +} + +interface FileStat { + type: 'file' | 'folder' | 'none'; + mode: '' | 'r' | 'rw'; +} +interface File { + pointer(): Promise; + length(): Promise; + + read(n: number): Promise; + write(buff: number[]): Promise; + close(): Promise; + seek(offset: number, whence: number): Promise; +} +interface Filesystem { + readonly SEEK_SET: 0; + readonly SEEK_CUR: 1; + readonly SEEK_END: 2; + + open(path: string, mode: 'r' | 'w' | 'rw'): Promise; + ls(path: string): AsyncIterableIterator; + mkdir(path: string): Promise; + mkfile(path: string): Promise; + rm(path: string, recursive?: boolean): Promise; + stat(path: string): Promise; + exists(path: string): Promise; + normalize(...paths: string[]): string; +} + +interface Encoding { + encode(val: string): number[]; + decode(val: number[]): string; +} + +declare var String: StringConstructor; +//@ts-ignore +declare const arguments: IArguments; +declare var NaN: number; +declare var Infinity: number; + +declare var setTimeout: (handle: (...args: [ ...T, ...any[] ]) => void, delay?: number, ...args: T) => number; +declare var setInterval: (handle: (...args: [ ...T, ...any[] ]) => void, delay?: number, ...args: T) => number; + +declare var clearTimeout: (id: number) => void; +declare var clearInterval: (id: number) => void; + +declare var parseInt: typeof Number.parseInt; +declare var parseFloat: typeof Number.parseFloat; + +declare function require(name: string): any; + +declare var Array: ArrayConstructor; +declare var Boolean: BooleanConstructor; +declare var Promise: PromiseConstructor; +declare var Function: FunctionConstructor; +declare var Number: NumberConstructor; +declare var Object: ObjectConstructor; +declare var Symbol: SymbolConstructor; +declare var Promise: PromiseConstructor; +declare var Math: MathObject; +declare var JSON: JSONObject; +declare var Encoding: Encoding; +declare var Filesystem: Filesystem; + +declare var Error: ErrorConstructor; +declare var RangeError: RangeErrorConstructor; +declare var TypeError: TypeErrorConstructor; +declare var SyntaxError: SyntaxErrorConstructor; +declare var self: typeof globalThis; + +declare var stdin: File; +declare var stdout: File; +declare var stderr: File; + +declare class Map { + public [Symbol.iterator](): IterableIterator<[KeyT, ValueT]>; + + public clear(): void; + public delete(key: KeyT): boolean; + + public entries(): IterableIterator<[KeyT, ValueT]>; + public keys(): IterableIterator; + public values(): IterableIterator; + + public get(key: KeyT): ValueT; + public set(key: KeyT, val: ValueT): this; + public has(key: KeyT): boolean; + + public get size(): number; + + public forEach(func: (val: ValueT, key: KeyT, map: Map) => void, thisArg?: any): void; + + public constructor(); +} +declare class Set { + public [Symbol.iterator](): IterableIterator; + + public entries(): IterableIterator<[T, T]>; + public keys(): IterableIterator; + public values(): IterableIterator; + + public clear(): void; + + public add(val: T): this; + public delete(val: T): boolean; + public has(key: T): boolean; + + public get size(): number; + + public forEach(func: (key: T, value: T, set: Set) => void, thisArg?: any): void; + + public constructor(); +} + +declare class RegExp implements Matcher, Splitter, Replacer, Searcher { + static escape(raw: any, flags?: string): RegExp; + + prototype: RegExp; + + exec(val: string): RegExpResult | null; + test(val: string): boolean; + toString(): string; + + [Symbol.match](target: string): RegExpResult | string[] | null; + [Symbol.matchAll](target: string): IterableIterator; + [Symbol.split](target: string, limit?: number, sensible?: boolean): string[]; + [Symbol.replace](target: string, replacement: string | ReplaceFunc): string; + [Symbol.search](target: string, reverse?: boolean, start?: number): number; + + readonly dotAll: boolean; + readonly global: boolean; + readonly hasIndices: boolean; + readonly ignoreCase: boolean; + readonly multiline: boolean; + readonly sticky: boolean; + readonly unicode: boolean; + + readonly source: string; + readonly flags: string; + + lastIndex: number; + + constructor(pattern?: string, flags?: string); + constructor(pattern?: RegExp, flags?: string); +} diff --git a/src/test-mod/libs/mc.d.ts b/src/test-mod/libs/mc.d.ts new file mode 100644 index 0000000..383c22e --- /dev/null +++ b/src/test-mod/libs/mc.d.ts @@ -0,0 +1,174 @@ +declare interface Event void> { + on(handle: T): () => void; +} +declare type CancelEvent void> = Event any ? (cancel: () => void, ...args: ArgT) => void : T>; +declare interface CommandHandle { + execute(args: string, sender: Entity | undefined, sendInfo: (text: any) => void, sendError: (text: any) => void): void; +} +declare interface TitleConfig { + title?: string; + subtitle?: string; + fadeIn?: number; + fadeOut?: number; + duration?: number; +} + +declare type NbtTypeFromKey = + T extends `${string}$${'l'|'i'|'s'|'f'|'d'}` ? (number | number[]) : + T extends `${string}$${'b'}` ? (number | boolean | number[]) : + (NbtElement | NbtElement[]); + +declare type NbtElement = NbtCompound | number | string | boolean; + +declare interface NbtCompound { + [key: string]: NbtElement | NbtElement[]; +} + + +declare interface DamageSource { + damager?: Entity; + location?: Location; + type: string; +} + +declare interface ItemStack extends Item { + count: number; +} + +declare interface Item extends NbtCompound { + id: string; +} + +declare class Location { + readonly x: number; + readonly y: number; + readonly z: number; + + add(...locations: [...([Location] | [number, number, number])]): Location; + subtract(...locations: [...([Location] | [number, number, number])]): Location; + + dot(location: Location): Location; + distance(loc: Location): number; + + dot(x: number, y: number, z: number): number; + distance(x: number, y: number, z: number): number; + length(): number; + + setX(x: number): Location; + setY(y: number): Location; + setZ(z: number): Location; + + toString(): `[${number} ${number} ${number}]`; + + constructor(x: number, y: number, z: number); + constructor(val: Location); +} + +declare interface BlockState { + id: string; + [prop: string]: string | number | boolean; +} + +declare class ServerWorld { + setBlock(loc: Location, state: BlockState, update?: boolean): void; + getBlock(loc: Location): BlockState; + summon(loc: Location, nbt: NbtCompound & { id: string; }): Entity; +} + +declare class Server { + registerCommand(name: string, handle: CommandHandle): void; + cmd(command: string, opts?: { at?: Location, pitch?: number, yaw?: number, as?: Entity, world?: ServerWorld }): { output: string[], code: number }; + sendMessage(text: string): void; + + getByUUID(uuid: string): Entity; + + readonly tickStart: Event<() => void>; + readonly tickEnd: Event<() => void>; + + readonly blockBreak: CancelEvent<(loc: Location, player: ServerPlayer) => void>; + readonly blockPlace: CancelEvent<(loc: Location, player: ServerPlayer) => void>; + + readonly playerJoin: Event<(player: ServerPlayer) => void>; + readonly playerLeave: Event<(player: ServerPlayer) => void>; + + readonly entityDamage: CancelEvent<(entity: Entity, damage: number, source: DamageSource) => void>; + readonly entityUse: CancelEvent<(player: ServerPlayer, entity: Entity) => void>; + + readonly itemUse: CancelEvent<(item: ItemStack, player: ServerPlayer, hand: 'main' | 'off') => void>; + + readonly inventoryScreenClicked: CancelEvent<(screen: InventoryScreen, player: ServerPlayer, index: number, button: number, action: ActionType) => void>; + readonly inventoryScreenClosed: CancelEvent<(screen: InventoryScreen, player: ServerPlayer) => void>; + + readonly players: ServerPlayer[]; + readonly worlds: ServerWorld[]; + + static maxStack(id: string): number; +} + +declare class Entity { + location: Location; + name: string; + readonly uuid: string; + readonly world: ServerWorld; + + discard(): void; +} +declare class LivingEntity extends Entity { + health: number; + readonly maxHealth: number; +} + +declare type Gamemode = 'survival' | 'creative' | 'adventure' | 'spectator'; +declare type InvType = '3x3' | '9x1' | '9x2' | '9x3' | '9x4' | '9x5' | '9x6'; +declare type ActionType = 'clone' | 'pickup' | 'pickupAll' | 'quickCraft' | 'quickMove' | 'swap' | 'throw'; + +declare class Player extends LivingEntity { + sendMessage(text: any): void; +} + +declare class ServerPlayer extends Player { + readonly inventory: Inventory; + readonly screen: InventoryScreen | undefined; + + gamemode: Gamemode; + foodLevel: number; + saturation: number; + sendTitle(title: TitleConfig): void; + + openInventory(name: string, type: InvType, inv: Inventory): InventoryScreen; + closeInventory(): void; +} + +declare class Inventory implements Iterable { + readonly size: number; + + get(i: number): ItemStack; + set(i: number, item?: ItemStack): void; + clear(): void; + + clone(): Inventory; + copyFrom(inv: Inventory): void; + + [Symbol.iterator](): Iterator; + + constructor(n: number); +} + +declare interface Slot { + readonly i: number; + readonly x: number; + readonly y: number; + readonly inventory: Inventory; + + get item(): ItemStack; + set item(val: ItemStack); +} + +declare class InventoryScreen { + readonly inventories: Inventory[]; + readonly id: number; + readonly cursorStack: ItemStack | undefined; + getSlot(i: number): Slot; +} + +declare var serverLoad: Event<(server: Server) => void>; diff --git a/src/test-mod/manifest.json b/src/test-mod/manifest.json new file mode 100644 index 0000000..e46fad3 --- /dev/null +++ b/src/test-mod/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "author": "TopchetoEU", + "version": "1.0.0", + "main": "dst/main.js" +} \ No newline at end of file diff --git a/src/test-mod/src/main.ts b/src/test-mod/src/main.ts new file mode 100644 index 0000000..9e0caeb --- /dev/null +++ b/src/test-mod/src/main.ts @@ -0,0 +1,1591 @@ +const BED_REGEX = /^minecraft:\w+_bed$/; + +function log(...args: any[]) { + stdout.write(Encoding.encode(args.join(' ') + '\n')); +} + +interface GenItem { + item: ItemStack; + freq: number; +} + +interface Gen { + loc: Location; + items: GenItem[]; +} + +interface Team { + name: string; + spawn: Location; + bed: Location; + gens: Gen[]; +} + +interface ShopItem { + type: 'item'; + price: ItemStack; + item: ItemStack; +} +interface ShopTier { + type: 'tier'; + price: ItemStack; + icon: ItemStack | number; + tier: string; + i: number; +} +interface ShopUpgrade { + type: 'upgrade'; + price: ItemStack; + icon: ItemStack; + tier: string; +} + +type ShopBuyable = ShopItem | ShopTier; + +interface ShopCategory { + icon: ItemStack; + items: (ShopBuyable | undefined)[]; +} + +type Hotshop = Array<[number, number] | []>; + +interface ToolTier { + id: string; + type: 'tool'; + forceFirst?: boolean; + + tiers: { + item: ItemStack; + multiple?: boolean; + onDeath?: 'reset' | 'decrease' | number; + }[]; +} +interface ArmorTier { + id: string; + type: 'armor'; + forceFirst?: boolean; + + tiers: { + helmet?: ItemStack; + chestplate?: ItemStack; + leggings?: ItemStack; + boots?: ItemStack; + onDeath?: 'reset' | 'decrease' | number; + }[]; +} +interface EnchantTier { + id: string; + type: 'enchant'; + forceFirst?: boolean; + + tiers: { + targets: string[]; + enchants: Record; + onDeath?: 'reset' | 'decrease' | number; + }[]; +} + +type Tier = ToolTier | ArmorTier | EnchantTier; + +class Config { + public readonly teams: Team[] = [ + { + name: 'red', + spawn: new Location(25, 20, 0), + bed: new Location(20, 20, 0), + gens: [ + { loc: new Location(30.5, 20, 0.5), items: [ + { freq: 1.25, item: { id: 'iron_ingot', count: 1 } }, + { freq: 5, item: { id: 'gold_ingot', count: 1 } } + ] }, + ] + }, + { + name: 'blue', + spawn: new Location(-25, 20, 0), + bed: new Location(-20, 20, 0), + gens: [ + { loc: new Location(-29.5, 20, 0.5), items: [ + { freq: 1.25, item: { id: 'iron_ingot', count: 1 } }, + { freq: 5, item: { id: 'gold_ingot', count: 1 } } + ] }, + ] + }, + ]; + public readonly gens: Gen[] = [ + { loc: new Location(0.5, 20, 0.5), items: [ { freq: 30, item: { id: 'emerald', count: 1 } } ] }, + ]; + public readonly countdowns = [ + { coef: .5, time: -1 }, + { coef: .75, time: 60 }, + { coef: 1, time: 10 }, + ]; + public readonly shop = { + categories: [ + { + icon: { id: 'white_wool', count: 1, display: { Name: '"Blocks"' } }, + items: [ + { type: 'item', price: { id: 'iron_ingot', count: 4 }, item: { id: 'white_wool', count: 16 } }, + { type: 'item', price: { id: 'iron_ingot', count: 12 }, item: { id: 'endstone', count: 12 } }, + { type: 'item', price: { id: 'gold_ingot', count: 4 }, item: { id: 'oak_planks', count: 16 } }, + { type: 'item', price: { id: 'emerald', count: 4 }, item: { id: 'obsidian', count: 4 } }, + ] + }, + { + icon: { id: 'iron_pickaxe', count: 1, display: { Name: '"Tools"' } }, + items: [ + { type: 'tier', price: { id: 'iron_ingot', count: 10 }, tier: 'pickaxe', i: 0 }, + { type: 'tier', price: { id: 'iron_ingot', count: 16 }, tier: 'pickaxe', i: 1 }, + { type: 'tier', price: { id: 'gold_ingot', count: 8 }, tier: 'pickaxe', i: 2 }, + { type: 'tier', price: { id: 'gold_ingot', count: 12 }, tier: 'pickaxe', i: 3 }, + { type: 'tier', price: { id: 'iron_ingot', count: 10 }, tier: 'axe', i: 0 }, + { type: 'tier', price: { id: 'iron_ingot', count: 16 }, tier: 'axe', i: 1 }, + { type: 'tier', price: { id: 'gold_ingot', count: 8 }, tier: 'axe', i: 2 }, + { type: 'tier', price: { id: 'gold_ingot', count: 12 }, tier: 'axe', i: 3 }, + { type: 'tier', price: { id: 'iron_ingot', count: 40 }, tier: 'shears', i: 0 }, + ] + }, + { + icon: { id: 'iron_chestplate', count: 1, display: { Name: '"Armor"' } }, + items: [ + { type: 'tier', price: { id: 'gold_ingot', count: 12 }, tier: 'armor', i: 1, icon: 2 }, + { type: 'tier', price: { id: 'emerald', count: 6 }, tier: 'armor', i: 2, icon: 2 }, + ] + }, + { + icon: { id: 'iron_sword', count: 1, display: { Name: '"Weapons"' } }, + items: [ + { type: 'tier', price: { id: 'iron_ingot', count: 10 }, tier: 'sword', i: 1 }, + { type: 'tier', price: { id: 'gold_ingot', count: 7 }, tier: 'sword', i: 2 }, + { type: 'tier', price: { id: 'emerald', count: 4 }, tier: 'sword', i: 3 }, + ] + }, + { + icon: { id: 'golden_apple', count: 1, display: { Name: '"Utilities"' } }, + items: [ + { type: 'item', price: { id: 'iron_ingot', count: 10 }, item: { id: 'golden_apple', count: 16 } }, + { type: 'tier', price: { id: 'emerald', count: 4 }, tier: 'elytra', i: 0, icon: 2 }, + { type: 'item', price: { id: 'emerald', count: 10 }, item: { id: 'firework_rocket', count: 1 } }, + ] + }, + ] as ShopCategory[], + defaultHotshop: [ + [ 0, 0 ], [ 0, 1 ] + ] as [number, number][] + }; + + public readonly tiers: Tier[] = [ + { + id: 'armor', + type: 'armor', + forceFirst: true, + tiers: [ + { + helmet: { id: 'minecraft:leather_helmet', count: 1 }, + chestplate: { id: 'minecraft:leather_chestplate', count: 1 }, + boots: { id: 'minecraft:leather_boots', count: 1 }, + leggings: { id: 'minecraft:leather_leggings', count: 1 }, + }, + { + helmet: { id: 'minecraft:iron_helmet', count: 1 }, + chestplate: { id: 'minecraft:iron_chestplate', count: 1 }, + boots: { id: 'minecraft:iron_boots', count: 1 }, + leggings: { id: 'minecraft:iron_leggings', count: 1 }, + }, + { + helmet: { id: 'minecraft:diamond_helmet', count: 1 }, + chestplate: { id: 'minecraft:diamond_chestplate', count: 1 }, + boots: { id: 'minecraft:diamond_boots', count: 1 }, + leggings: { id: 'minecraft:diamond_leggings', count: 1 }, + }, + ] + }, + { + id: 'elytra', + type: 'armor', + tiers: [ + { + chestplate: { id: 'minecraft:elytra', count: 1 }, + } + ] + }, + { + id: 'shears', + type: 'tool', + tiers: [ + { item: { id: 'minecraft:shears', count: 1 }, multiple: false }, + ] + }, + { + id: 'sword', + type: 'tool', + forceFirst: true, + tiers: [ + { item: { id: 'minecraft:wooden_sword', count: 1 } }, + { item: { id: 'minecraft:stone_sword', count: 1 }, onDeath: "reset", multiple: true }, + { item: { id: 'minecraft:iron_sword', count: 1 }, onDeath: "reset", multiple: true }, + { item: { id: 'minecraft:diamond_sword', count: 1 }, onDeath: "reset", multiple: true }, + ] + }, + { + id: 'pickaxe', + type: 'tool', + tiers: [ + { item: { id: 'minecraft:wooden_pickaxe', count: 1 } }, + { item: { id: 'minecraft:stone_pickaxe', count: 1 }, onDeath: "decrease", multiple: true }, + { item: { id: 'minecraft:iron_pickaxe', count: 1 }, onDeath: "decrease", multiple: true }, + { item: { id: 'minecraft:diamond_pickaxe', count: 1 }, onDeath: "decrease", multiple: true }, + ] + }, + { + id: 'axe', + type: 'tool', + tiers: [ + { item: { id: 'minecraft:wooden_axe', count: 1 } }, + { item: { id: 'minecraft:stone_axe', count: 1 }, onDeath: "decrease", multiple: true }, + { item: { id: 'minecraft:iron_axe', count: 1 }, onDeath: "decrease", multiple: true }, + { item: { id: 'minecraft:diamond_axe', count: 1 }, onDeath: "decrease", multiple: true }, + ] + }, + ]; + + public readonly playerTiers = [ 'sword', 'armor', 'shears', 'pickaxe', 'axe', 'elytra' ]; + public readonly teamTiers = [ ]; + + public readonly teamSize = 1; + public readonly spawn = new Location(0, 50, 0); + public readonly respawnTimeout = 5; + public readonly yKillPlane = -20; +} + +enum State { + Alive = 'alive', + Dead = 'dead', + Eliminated = 'elim', + Spectator = 'spec', +} + +namespace InvUtils { + export function equal(a: Item | undefined, b: Item | undefined) { + if (a?.id === 'minecraft:air' || a?.id === 'air') a = undefined; + if (b?.id === 'minecraft:air' || b?.id === 'air') b = undefined; + + if (a === b) return true; + if (a == null || b == null) return a == null && b == null; + + a = { ...a }; + b = { ...b }; + + if (!a.id.includes(':')) a.id = 'minecraft:' + a.id; + if (!b.id.includes(':')) b.id = 'minecraft:' + b.id; + + if (a.id === b.id) return true; + + return false; + } + export function give(inv: Inventory, item: ItemStack): number { + if (item.count < 0) return -take(inv, { ...item, count: -item.count }); + + const maxCount = Server.maxStack(item.id); + const invItems: ItemStack[] = []; + for (let i = 0; i < 36; i++) invItems[i] = inv.get(i); + + const giveCandidates = [ + invItems + .map((v, i) => ({ v, i })) + .filter(({v}) => equal(v, item)) + .sort((a, b) => a.v.count - b.v.count), + invItems + .map((v, i) => ({ v, i })) + .filter(({v}) => equal(v, undefined)) + ].flat(); + + let remaining = item.count; + + for (let { i, v } of giveCandidates) { + const addItem = equal(v, { id: 'air' }) ? item : v; + const count = v?.count ?? 0; + + if (remaining > maxCount - count) { + inv.set(i, { ...addItem, count: maxCount }); + remaining -= maxCount - count; + } + else { + inv.set(i, { ...addItem, count: remaining + count }); + remaining = 0; + break; + } + } + + return remaining; + } + + export function take(inv: Inventory, item: ItemStack): number { + if (item.count < 0) return -give(inv, { ...item, count: -item.count }); + + const invItems: ItemStack[] = []; + for (let i = 0; i < 36; i++) invItems[i] = inv.get(i); + + let remaining = item.count; + + const priceCandidates = invItems + .map((v, i) => ({ v, i })) + .filter(({v}) => equal(v, item)) + .sort((a, b) => a.v.count - b.v.count); + + for (const { v: currItem, i } of priceCandidates) { + if (currItem.count > remaining) { + inv.set(i, { ...currItem, count: currItem.count - remaining }); + remaining = 0; + break; + } + else { + remaining -= currItem.count; + inv.set(i, undefined); + } + } + + return remaining; + } + export function takeAll(inv: Inventory, item: Item): number { + const invItems: ItemStack[] = []; + for (let i = 0; i < 36; i++) invItems[i] = inv.get(i); + + let count = 0; + + for (let i = 0; i < 36; i++) { + if (equal(inv.get(i), item)) { + count += inv.get(i).count; + inv.set(i, undefined); + } + } + + return count; + } + + export function tryGive(inv: Inventory, item: ItemStack) { + const tmp = inv.clone(); + + if (give(tmp, item) !== 0) return false; + inv.copyFrom(tmp); + + return true; + } + export function tryTake(inv: Inventory, item: ItemStack) { + const tmp = inv.clone(); + + if (take(tmp, item) !== 0) return false; + inv.copyFrom(tmp); + + return true; + } +} + +class GamePlayer { + public readonly name: string; + public readonly uuid: string; + public player?: ServerPlayer; + public timer = 0; + public tiers: Record = {}; + + private _team?: GameTeam; + private _state: State = undefined!; + + private _pendingGives: { tier: ToolTier, i: number }[] = []; + + public get state() { return this._state; } + public get team() { return this._team; } + + public set team(newTeam: GameTeam | undefined) { + if (this.team == newTeam) return; + + const oldTeam = this.team; + + oldTeam?.players?.delete(this); + newTeam?.players?.add(this); + this._team = newTeam; + + if (oldTeam != null) { + for (const other of oldTeam.players) { + if (other != this) this.player?.sendMessage(`${this.name} left your team.`); + else this.player?.sendMessage(`You left team ${oldTeam?.team.name}`); + } + } + if (newTeam != null) { + for (const other of newTeam.players) { + if (other != this) this.player?.sendMessage(`${this.name} joined your team.`); + else this.player?.sendMessage(`You joined team ${newTeam?.team.name}`); + } + } + + if (newTeam != null && this.game.running) this.setState(State.Dead); + else this.setState(State.Spectator); + } + + private _refreshNativeState() { + if (this.player == null) return; + + this.player.health = 20; + + if (this.state === State.Alive) this.player.gamemode = 'survival'; + else this.player.gamemode = 'spectator'; + + if (this.state === State.Alive) this.player.location = this.team!.team.spawn; + else if (this.state !== State.Dead) this.player.location = this.game.config.spawn; + } + + private _updateInv() { + const inv = this.player?.inventory; + if (inv == null) return; + if (this.state === State.Spectator) return; + + if (this.state !== State.Alive) { + inv.clear(); + return; + } + + for (const { i, tier } of this._pendingGives) { + InvUtils.give(inv, tier.tiers[i].item); + } + + this._pendingGives = []; + + for (const tier of this.game.config.tiers) { + if (!(tier.id in this.tiers)) { + if (tier.forceFirst) this.tiers[tier.id] = 0; + else continue; + } + const tierI = this.tiers[tier.id]; + + switch (tier.type) { + case 'armor': + for (let i = 0; i <= tierI; i++) { + const tierItem = tier.tiers[i]; + if (tierItem.boots != null) inv.set(36, tierItem.boots); + if (tierItem.leggings != null) inv.set(37, tierItem.leggings); + if (tierItem.chestplate != null) inv.set(38, tierItem.chestplate); + if (tierItem.helmet != null) inv.set(39, tierItem.helmet); + } + break; + case 'tool': { + // let highest: number | undefined; + for (let i = tier.tiers.length - 1; i >= 0; i--) { + const tierItem = tier.tiers[i]; + + const allItems = [ + ...(this.player?.inventory ?? []), + this.player?.screen?.cursorStack + ]; + const hasItem = allItems.find(v => InvUtils.equal(v, tierItem.item)) != null; + + if (hasItem) { + if (this.tiers[tier.id] < i) this.tiers[tier.id] = i; + } + + if (!tierItem.multiple) { + if (this.tiers[tier.id] === i && !hasItem) { + InvUtils.give(inv, tierItem.item); + // highest = i; + } + else if (i !== this.tiers[tier.id] && hasItem) { + InvUtils.takeAll(inv, tierItem.item); + } + } + + continue; + } + } + } + } + } + + private _onDeath() { + for (const tier of this.game.config.tiers) { + if (!(tier.id in this.tiers)) continue; + const tierI = this.tiers[tier.id]; + const tierEl = tier.tiers[tierI]; + + switch (tierEl.onDeath) { + case 'decrease': + this.tiers[tier.id]--; + break; + case 'reset': + delete this.tiers[tier.id]; + break; + case undefined: + case null: + break; + default: + this.tiers[tier.id] = tierEl.onDeath; + } + + if (tier.type === 'tool' && tier.id in this.tiers) { + this._pendingGives.push({ tier, i: this.tiers[tier.id] }); + } + } + } + + public setState(state: State, force = false, silent = force) { + if (this.team == null) state = State.Spectator; + if (this.state === state) return; + if (this.state === State.Alive && state !== State.Alive) this._onDeath(); + this._state = state; + + if (state === State.Spectator) this.tiers = {}; + + if (!silent) { + if (state === State.Dead) this.game.sendMessage(`${this.name} died.`); + if (state === State.Eliminated) this.game.sendMessage(`${this.name} was eliminated.`); + if (this.player == null) return; + } + + if (this.state === State.Dead) this.timer = this.game.config.respawnTimeout; + + this._refreshNativeState(); + + if (this.team == null) return; + + if (state === State.Eliminated && ![ ...this.team.players ].find(v => v.state === State.Alive)) { + this.team.setState(State.Eliminated, force, silent); + } + } + + public kill() { + if (!this.game.running) return; + if (this.team == null) return; + + if (this.team.state !== State.Alive) this.setState(State.Eliminated); + else this.setState(State.Dead); + } + + public tick() { + if (this.player == null) return; + + this._updateInv(); + + if (!this.game.running) return; + + if (this.player.location.y < this.game.config.yKillPlane) { + this.kill(); + this.player.location = this.game.config.spawn; + } + + if (this.state === State.Dead) { + if (this.timer < 0) this.setState(State.Alive); + else this.sendTitle({ + title: 'You died!', + subtitle: `Respawning in ${this.timer.toFixed(2)} seconds...`, + duration: 0.1 + }); + } + + this.timer -= 1 / 20; + this.player.foodLevel = 20; + this.player.saturation = 20; + } + + public sendMessage(msg: string) { + if (this.player == null) return; + this.player.sendMessage(msg); + } + public sendTitle(title: TitleConfig) { + if (this.player == null) return; + this.player.sendTitle(title); + } + + public onInvClick(slotI: number, button: number, screen: InventoryScreen, action: ActionType) { + const slot = screen.getSlot(slotI); + const inventories = screen.inventories; + + let movedOutside = false; + let movedItem: ItemStack | undefined; + + switch (action) { + case 'swap': + movedItem = this.player?.inventory?.get(button); + + if (slot.inventory !== this.player?.inventory) { + movedOutside = true; + } + break; + case 'pickup': + movedItem = screen.cursorStack; + if (slot.inventory !== this.player?.inventory && movedItem != null) movedOutside = true; + break; + case 'pickupAll': + movedItem = slot.item; + if (slot.inventory === this.player?.inventory && movedItem != null && inventories.length > 1) { + movedOutside = true; + } + break; + case 'quickMove': + movedItem = slot.item; + if (slot.inventory === this.player?.inventory && movedItem != null && inventories.length > 1) { + movedOutside = true; + } + break; + case 'throw': + movedItem = slot.item; + movedOutside = slot.inventory === this.player?.inventory; + break; + case 'clone': return true; + case 'quickCraft': + if (button === 1 || button === 5) { + movedItem = screen.cursorStack; + movedOutside = slot.inventory !== this.player?.inventory; + } + else return true; + } + + for (const tier of this.game.config.tiers) { + if (!(tier.id in this.tiers)) continue; + const tierI = this.tiers[tier.id]; + + switch (tier.type) { + case 'armor': + if (slotI < 36 || slotI >= 40) continue; + + for (let i = 0; i < tierI; i++) { + const tierItem = tier.tiers[i]; + if (tierItem.boots != null && slotI === 36) return false; + if (tierItem.leggings != null && slotI === 37) return false; + if (tierItem.chestplate != null && slotI === 38) return false; + if (tierItem.helmet != null && slotI === 39) return false; + } + + continue; + case 'tool': + const tierItem = tier.tiers[tierI]; + if (!InvUtils.equal(tierItem.item, movedItem)) continue; + if (tierItem.multiple) continue; + if (movedOutside) return false; + continue; + } + } + + return true; + } + + public giveTier(id: string, i: number) { + const tier = this.game.config.tiers.find(v => v.id === id); + if (tier == null) return false; + + let ok = false; + + if (tier.type === 'tool' && tier.tiers[i].multiple) { + this._pendingGives.push({ i, tier }); + ok = true; + } + + if (!(id in this.tiers) || this.tiers[id] < i) { + this.tiers[id] = i; + return true; + } + else return ok; + } + + public constructor(public readonly game: Game, player: ServerPlayer) { + this.player = player; + this.uuid = player.uuid; + this.name = player.name; + + this.setState(State.Spectator, true); + } +} + +class GameTeam { + public readonly players = new Set(); + public readonly gens = new Set(); + + private _state: State = undefined!; + private _bedBlocks: Array<{ loc: Location, block: BlockState }> = []; + + public get state() { return this._state; } + + private _removeBed() { + const breakBlock = (loc: Location) => { + const block = this.game.world.getBlock(loc); + if (!BED_REGEX.test(block.id)) return; + + this.game.blocks.onBreak(loc); + this._bedBlocks.push({ loc, block }) + this.game.world.setBlock(loc, { id: 'air' }, false); + + // A bed may be on the end of the scan region + breakBlock(loc.add(1, 0, 0)); + breakBlock(loc.add(-1, 0, 0)); + breakBlock(loc.add(0, 0, 1)); + breakBlock(loc.add(0, 0, -1)); + } + + const bedLoc = this.team.bed; + const radius = 2; + + for (let x = bedLoc.x - radius; x < bedLoc.x + radius; x++) { + for (let y = bedLoc.y - radius; y < bedLoc.y + radius; y++) { + for (let z = bedLoc.z - radius; z < bedLoc.z + radius; z++) { + breakBlock(new Location(x, y, z)); + } + } + } + } + private _restoreBed() { + for (const { loc, block } of this._bedBlocks) { + this.game.world.setBlock(loc, block, false); + } + + this._bedBlocks = []; + } + + public setState(state: State, force = false, silent = force) { + if (this._state === state) return; + + this._state = state; + + switch (state) { + case State.Alive: + for (const player of this.players) { + if (player.state === State.Eliminated || player.state === State.Spectator) { + player.setState(State.Alive, force, silent); + } + } + + this._restoreBed(); + + break; + case State.Dead: + for (const player of this.players) { + if (player.state === State.Eliminated || player.state === State.Spectator) { + player.setState(State.Alive, force, silent); + } + } + if (!silent) { + this.game.sendMessage(`Team ${this.team.name}'s bed was broken!`); + this.sendTitle({ + title: 'Your bed was broken!', subtitle: 'You will no longer respawn', + duration: 2, fadeIn: .5, fadeOut: .5, + }); + } + this._removeBed(); + + break; + case State.Eliminated: + for (const player of this.players) player.setState(State.Eliminated, force, silent); + if (!silent) this.game.sendMessage(`Team ${this.team.name} was eliminated!`); + this._removeBed(); + + break; + case State.Spectator: + for (const player of this.players) player.setState(State.Spectator, force, silent); + this._restoreBed(); + break; + } + } + + public sendMessage(msg: string) { + for (const player of this.players) { + player.sendMessage(msg); + } + } + public sendTitle(title: TitleConfig) { + for (const player of this.players) { + player.sendTitle(title); + } + } + + public tick() { + for (const gen of this.gens) gen.tick(); + } + + public constructor( + public readonly game: Game, + public readonly team: Team, + ) { + for (const gen of team.gens) { + this.gens.add(new GameGenerator(game, gen)); + } + } +} + +class GameShop { + public readonly screen: InventoryScreen; + public readonly inventory: Inventory; + + private _border = { id: 'gray_stained_glass_pane', count: 1, display: { Name: '""' } }; + private _selected = { id: 'white_stained_glass_pane', count: 1, display: { Name: '""' } }; + private _close = { id: 'barrier', count: 1, display: { Name: '"Close"' } }; + private _hotshop: Hotshop = []; + private _hotshopPath: string; + + private _pageI = 0; + + private _getTier(id: string) { + const res = this.game.config.tiers.find(v => v.id === id); + if (res == null) throw "Misconfigured game!"; + return res; + } + + private _getCategory(i = this._pageI): ShopCategory | undefined { + if (i === 0) { + const res: ShopCategory = { + icon: { id: 'nether_star', display: { Name: '"Quick Shop"' }, count: 1 }, + items: new Array(27), + } + + for (let i = 0; i < this._hotshop.length; i++) { + const hotshopEl = this._hotshop[i]; + + if (hotshopEl.length === 2) { + const shopItem = this.game.config.shop.categories[hotshopEl[0]].items[hotshopEl[1]]; + res.items[i] = shopItem; + } + } + + return res; + } + else return this.game.config.shop.categories[i - 1]; + } + private _getItems(category: ShopCategory): (ItemStack | undefined)[] { + const res = new Array(27); + + for (let i = 0; i < category.items.length; i++) { + const shopItem = category.items[i]; + let item: any | undefined; + + switch (shopItem?.type) { + case 'item': + item = shopItem.item; + break; + case 'tier': { + if (typeof shopItem.icon === 'object') item = shopItem; + else { + const tier = this._getTier(shopItem.tier); + switch (tier.type) { + case 'armor': + item = (tier.tiers[shopItem.i] as any)[['boots', 'leggings', 'chestplate', 'helmet'][shopItem.icon]]; + break; + case 'tool': + item = tier.tiers[shopItem.i].item; + break; + } + } + break; + } + } + + item = { ...item, count: 1 }; + + item.display ??= {}; + item.display.Lore ??= []; + item.display.Lore.push({ color: 'light_gray', text: 'Click to buy' }); + + res[i] = item; + } + + return res; + } + + private _rebuild() { + this.inventory.clear(); + + for (let i = 0; i < 9; i++) { + this.inventory.set(i + 9, this._border); + this.inventory.set(i + 9 * 5, this._border); + + const category = this._getCategory(i); + if (category == null) continue; + this.inventory.set(i, category.icon); + } + + this.inventory.set(4 + 9 * 5, this._close); + this.inventory.set(this._pageI + 9, this._selected); + + let category = this._getCategory(); + + if (category == null) { + this.selectPage(0); + category = this._getCategory()!; + } + + const items = this._getItems(category); + for (let i = 0; i < 27; i++) { + this.inventory.set(i + 9 * 2, items[i]); + } + } + + private async _loadHotshop() { + switch ((await Filesystem.stat(this._hotshopPath)).type) { + case "file": { + const f = await Filesystem.open(this._hotshopPath, 'rw'); + let raw = JSON.parse(Encoding.decode(await f.read(await f.length()))); + await f.close(); + + if (Array.isArray(raw)) { + this._hotshop = raw.map(v => { + if (!Array.isArray(v)) return []; + if (v.length > 2) v.length = 2; + if (v.length < 2) v.length = 0; + if (typeof v[0] !== 'number' || typeof v[1] !== 'number') return []; + return [ Number(v[0]), Number(v[1]) ]; + }); + + this._rebuild(); + return; + } + } + case "folder": + await Filesystem.rm(this._hotshopPath, true); + default: + this._hotshop = this.game.config.shop.defaultHotshop; + await this._saveHotshop(); + } + + } + private async _saveHotshop() { + try { await Filesystem.mkdir('/shops'); } catch {} + try { await Filesystem.mkfile(this._hotshopPath); } catch {} + + const f = await Filesystem.open(this._hotshopPath, 'w'); + + await f.write(Encoding.encode(JSON.stringify(this._hotshop))); + await f.close(); + } + + private _giveItem(shopItem: ShopItem, playerInv: Inventory) { + if (!InvUtils.tryGive(playerInv, shopItem.item)) return "Items can't fit in your inventory."; + } + private _giveTier(shopItem: ShopTier, playerInv: Inventory) { + const gp = this.game.getPlayer(this.player); + const tier = this.game.config.tiers.find(v => v.id === shopItem.tier && this.game.config.playerTiers.includes(v.id)); + + if (tier == null) return "Misconfigured game, contact an admin!"; + if (gp == null) return "You must be in a game to purchase this!"; + + if (!gp.giveTier(shopItem.tier, shopItem.i)) return "Couldn't buy that item!"; + } + + private _takePrice(shopItem: ShopBuyable, playerInv: Inventory) { + if (!InvUtils.tryTake(playerInv, shopItem.price)) return "Not enough materials to purchase that."; + } + + public selectPage(i: number) { + this._pageI = i; + if (this._pageI > this.game.config.shop.categories.length + 1) this._pageI = this.game.config.shop.categories.length; + this._rebuild(); + } + public close() { + this.player.closeInventory(); + } + public buy(i: number) { + const shopItem = this._getCategory()?.items?.[i]; + const playerInv = this.player.inventory.clone(); + + if (shopItem == null) return; + + let msg = this._takePrice(shopItem, playerInv); + + if (msg != null) { + this.player.sendMessage(msg); + return; + } + + switch (shopItem.type) { + case 'item': msg = this._giveItem(shopItem, playerInv); break; + case 'tier': msg = this._giveTier(shopItem, playerInv); break; + } + + if (msg != null) { + this.player.sendMessage(msg); + return; + } + + this.player.inventory.copyFrom(playerInv); + } + + public onClick(slot: number, button: number, action: ActionType) { + if (action !== 'pickup' || slot >= 54) return; + + const x = Math.floor(slot % 9); + const y = Math.floor(slot / 9); + + if (y === 0) this.selectPage(x); + else if (y >= 2 && y <= 4) { + this.buy(x + (y - 2) * 9); + } + else if (y === 5) { + if (x === 4) this.close(); + } + } + + public constructor( + public readonly game: Game, + public readonly player: ServerPlayer + ) { + this.inventory = new Inventory(6 * 9); + this.screen = player.openInventory('Shop', '9x6', this.inventory); + this._hotshopPath = `/shops/${player.uuid}`; + + this._loadHotshop(); + } +} + +class GameShopkeepers { + private _uuids = new Set(); + + private async _loadUuids() { + log('test'); + const path = `/shopkeepers`; + const stat = await Filesystem.stat(path); + + if (stat.type === 'folder') await Filesystem.rm(path, true); + if (stat.type !== 'file') await Filesystem.mkfile(path); + + const f = await Filesystem.open(path, 'rw'); + const raw = Encoding.decode(await f.read(await f.length())); + await f.close(); + + this._uuids.clear(); + for (const el of raw.split('\n').map(v => v.trim()).filter(v => v !== '')) { + this._uuids.add(el); + } + } + private async _saveUuids() { + const path = `/shopkeepers`; + const f = await Filesystem.open(path, 'rw'); + await f.write(Encoding.encode([ ...this._uuids ].join('\n'))); + await f.close(); + } + + public has(uuid: string) { + return this._uuids.has(uuid); + } + public add(uuid: string) { + this._uuids.add(uuid); + this._saveUuids(); + } + + public create(player: ServerPlayer) { + const loc = new Location( + Math.floor(player.location.x) + .5, + Math.floor(player.location.y), + Math.floor(player.location.z) + .5, + ); + const nbt = { + id: 'villager', + NoAI: true, + NoGravity: true, + Invulberable: true, + PersistenceRequired: true, + Silent: true, + VillagerData: { + level: 99 + }, + }; + const entity = player.world.summon(loc, nbt); + this.add(entity.uuid); + } + public destroy(player: ServerPlayer) { + const villagers = [ ...this._uuids ] + .map(v => this.game.server.getByUUID(v)) + .filter(v => v != null) + .sort((a, b) => b.location.distance(player.location) - a.location.distance(player.location)); + + if (villagers.length > 0) { + this._uuids.delete(villagers[0].uuid); + villagers[0].discard(); + } + } + + public onEntityUse(player: ServerPlayer, entity: Entity) { + if (this.has(entity.uuid)) { + this.game.openShop(player); + return false; + } + else return true; + } + public onEntityDamage(entity: Entity) { + return !this.has(entity.uuid); + } + + public constructor( + public readonly game: Game + ) { + this._loadUuids(); + } +} + +class GameGenerator { + private _timer = 0; + private _items = new Set(); + + public tick() { + for (const { freq, item } of this.gen.items) { + if (freq - this._timer % freq < .05) { + const e = this.game.world.summon( + this.gen.loc, + { id: 'item', Item: { ...item, Count: item.count } } + ); + this._items.add(e); + } + } + this._timer += 1 / 20; + } + public reset() { + for (const item of this._items) item.discard(); + + this._timer = 0; + this._items.clear(); + } + + public constructor( + public readonly game: Game, + public readonly gen: Gen, + ) { } +} + +class GameBlockManager { + private readonly _placed = new Set(); + private readonly _broken = new Map(); + + public onPlace(loc: Location) { + const prev = this.game.world.getBlock(loc); + const strLoc = `${loc.x} ${loc.y} ${loc.z}`; + + if (prev.id !== 'minecraft:air') { + this._broken.set(strLoc, prev); + } + + this._placed.add(strLoc); + + return true; + } + public onBreak(loc: Location) { + return this._placed.delete(`${loc.x} ${loc.y} ${loc.z}`); + } + + public reset() { + for (const strLoc of this._placed) { + const loc = new Location(...strLoc.split(' ').map(v => Number(v)) as [number, number, number]); + this.game.world.setBlock(loc, { id: 'air' }); + } + for (const [ strLoc, state ] of this._broken) { + const loc = new Location(...strLoc.split(' ').map(v => Number(v)) as [number, number, number]); + this.game.world.setBlock(loc, state); + } + } + + public constructor( + public readonly game: Game + ) { } +} + +class GameCountdown { + private _timer?: number; + private _coef?: number; + + private _sendTitle() { + if (this._timer == null) return; + + let title: TitleConfig | undefined = { + title: `Game starts in ${this._timer.toFixed(2)} seconds...` + }; + + if (this._timer > 30) { + if (Math.abs(this._timer % 10) < .01) title = { ...title, duration: 2, fadeIn: .1, fadeOut: .1 }; + else title = undefined; + } + else if (this._timer > 15) { + if (Math.abs(this._timer % 5) < .01) title = { ...title, duration: 2, fadeIn: .1, fadeOut: .1 }; + else title = undefined; + } + else if (this._timer > 5) { + if (Math.abs(this._timer % 1) < .01) title = { ...title, duration: 1.1, fadeIn: 0, fadeOut: 0 }; + else title = undefined; + } + else title = { ...title, duration: 1.1, fadeIn: 0, fadeOut: 0 }; + + if (title != null) this.game.sendTitle(title); + } + + public tick() { + const teams = this.game.teams; + + const coef = teams.filter(v => v.players.size > 0).length / teams.length; + const conf = this.game.config.countdowns.find(v => coef <= v.coef); + + if (conf != null && conf.coef !== this._coef) { + this._coef = conf.coef; + if (conf.time < 0) { + this._timer = undefined; + this.game.sendTitle({ + title: 'Game cancelled', + subtitle: 'Waiting for more players', + duration: 2, fadeIn: .1, fadeOut: .1 + }); + } + else this._timer = conf.time; + } + + if (this._timer != null) { + if (this._timer < 0) this.game.start(); + else { + this._sendTitle(); + this._timer -= 1 / 20; + } + } + } + + public reset() { + this._timer = undefined; + this._coef = undefined; + } + + public constructor( + public readonly game: Game + ) { } +} + +class Game { + private _players: GamePlayer[] = []; + private _teams: GameTeam[] = []; + private _gens: GameGenerator[] = []; + private _shops = new Map(); + + private _running = false; + private _won = false; + + public readonly blocks: GameBlockManager; + public readonly countdown: GameCountdown; + public readonly shopkeepers: GameShopkeepers; + + private _checkWinCond(stop = true) { + if (!this.running || this._won) return; + + const inGameTeams = this._teams.filter(v => v.state === State.Alive || v.state === State.Dead); + + if (inGameTeams.length < 2) { + if (inGameTeams.length < 1) { + this.sendTitle({ + title: "Draw", subtitle: "Nobody won", + fadeIn: 0.1, duration: 1, fadeOut: 0.1 + }); + } + else { + const team = inGameTeams[0]; + + for (const player of this._players) { + if (player.team === team) player.sendTitle({ + title: "You won!", + fadeIn: 0.1, duration: 1, fadeOut: 0.1 + }); + else player.sendTitle({ + title: "You lost!", subtitle: `Team ${team.team.name} won.`, + fadeIn: 0.1, duration: 1, fadeOut: 0.1 + }); + } + } + + this._won = true; + + if (stop) this.stop(); + } + } + + public get running() { + return this._running; + } + + public get players() { + return [...this._players]; + } + public get teams() { + return [...this._teams]; + } + public get gens() { + return [...this._gens]; + } + + public getPlayer(player: ServerPlayer | string) { + const uuid = typeof player === 'string' ? player : player.uuid; + return this._players.find(v => v.uuid === uuid); + } + + public sendMessage(msg: string) { + for (const player of this._players) { + player.sendMessage(msg); + } + } + public sendTitle(title: TitleConfig) { + for (const player of this._players) { + player.sendTitle(title); + } + } + + public start() { + if (this.running) return; + + this._running = true; + this._won = true; + + for (const player of this._players) { + if ((player.team?.players.size ?? 0) > 0) player.setState(State.Alive, true); + else player.setState(State.Spectator, true); + } + + for (const team of this._teams) { + if (team.players.size > 0) team.setState(State.Alive, true); + else team.setState(State.Spectator, true); + } + + this._won = false; + + this._checkWinCond(); + } + public stop() { + if (!this.running) return; + + this._checkWinCond(false); + + this._running = false; + this._won = false; + + for (const team of this._teams) { + team.setState(State.Spectator, false, true); + for (const gen of team.gens) gen.reset(); + } + for (const player of this._players) { + player.player?.inventory?.clear(); + player.setState(State.Spectator, false, true); + } + + for (const gen of this.gens) gen.reset(); + + this.blocks.reset(); + this.countdown.reset(); + } + + public onLogin(player: ServerPlayer) { + let gamePlayer = this.getPlayer(player); + + if (gamePlayer != null) { + if (!this.running) this._players = this._players.filter(v => v.uuid !== player.uuid); + else { + gamePlayer.player = player; + gamePlayer.setState(State.Dead, true); + return; + } + } + + gamePlayer = new GamePlayer(this, player); + this._players.push(gamePlayer); + gamePlayer.setState(State.Spectator, true); + + let candidates = this._teams.filter(v => v.players.size < this.config.teamSize).sort((a, b) => b.players.size - a.players.size); + + if (candidates.length === 0) return; + + candidates = candidates.filter(v => v.players.size === candidates[0].players.size); + + const team = candidates[Math.floor(candidates.length * Math.random())]; + + gamePlayer.team = team; + + return team; + } + public onLogoff(player: ServerPlayer) { + const gp = this.getPlayer(player); + if (gp == null) return; + + if (this.running) { + gp.player = undefined; + gp.setState(State.Dead); + } + else { + gp.team = undefined; + this._players.splice(this._players.indexOf(gp), 1); + } + } + + public onDeath(player: ServerPlayer) { + this.getPlayer(player)?.kill(); + } + + public onEntityDamage(entity: Entity) { + return this.shopkeepers.onEntityDamage(entity); + } + public onEntityUse(player: ServerPlayer, entity: Entity) { + return this.shopkeepers.onEntityUse(player, entity); + } + + public onUpdateConfig() { + this.stop(); + const tmpPlayers = this._players.map(v => v.player!).filter(v => v != null); + + this._teams.length = 0; + this._gens.length = 0; + this._players.length = 0; + + this._teams.push(...this.config.teams.map(v => new GameTeam(this, v))); + + for (const player of tmpPlayers) { + this.onLogin(player); + } + + for (const gen of this.config.gens) { + this._gens.push(new GameGenerator(this, gen)); + } + } + + public onTick() { + for (const player of this._players) player.tick(); + + if (this._running) { + for (const team of this._teams) team.tick(); + for (const gen of this._gens) gen.tick(); + + this._checkWinCond(); + } + else { + this.countdown.tick(); + } + } + + public onBlockPlace(loc: Location) { + if (!this.running) return true; + return this.blocks.onPlace(loc);; + } + public onBlockBreak(loc: Location, player: ServerPlayer) { + if (!this.running) return true; + + const prev = this.world.getBlock(loc); + const gp = this.getPlayer(player); + + if (this.running && BED_REGEX.test(prev.id)) { + for (const team of this._teams) { + if (team.team.bed.distance(loc) < 2) { + if (gp?.team === team) gp.sendMessage("You can't break your bed."); + else team.setState(State.Dead); + + return false; + } + } + } + else return this.blocks.onBreak(loc); + } + + public onInvClick(screen: InventoryScreen, player: ServerPlayer, slot: number, button: number, action: ActionType) { + if (this._shops.has(screen)) { + this._shops.get(screen)?.onClick(slot, button, action); + return false; + } + + const gp = this.getPlayer(player); + if (gp != null) { + if (!gp.onInvClick(slot, button, screen, action)) return false; + } + + return true; + } + public onInvClose(screen: InventoryScreen) { + this._shops.delete(screen); + return true; + } + + public openShop(player: ServerPlayer) { + const shop = new GameShop(this, player); + this._shops.set(shop.screen, shop); + } + + constructor( + public readonly config: Config, + public readonly world: ServerWorld, + public readonly server: Server, + ) { + this.blocks = new GameBlockManager(this); + this.countdown = new GameCountdown(this); + this.shopkeepers = new GameShopkeepers(this); + this.onUpdateConfig(); + } +} + +serverLoad.on(server => { + const config = new Config(); + const game = new Game(config, server.worlds[0], server); + + server.registerCommand('bw', { + execute(_args, sender, sendInfo, sendError) { + const args = _args.split(' ').map(v => v.trim()).filter(v => v !== ''); + + switch (args[0]) { + case 'start': + game.start(); + sendInfo("Started the game!"); + break; + case 'stop': + game.stop(); + sendInfo("Stopped the game!"); + break; + case 'break-bed': + const team = game.teams.find(v => v.team.name === args[1]); + if (team != null) team.setState(State.Dead); + break; + case 'shop': { + game.openShop(sender as ServerPlayer); + break; + } + case 'villager': + case 'shopkeeper': + case 'sk': + case 'v': + if (!(sender instanceof ServerPlayer)) { + sendError("Only players may create/destoy shopkeepers"); + return; + } + switch (args[1]) { + case 'c': + case 'create': + game.shopkeepers.create(sender); + break; + case 'd': + case 'destroy': + case 'delete': + case 'rm': + game.shopkeepers.destroy(sender); + break; + } + break; + default: + sendError("Invalid command"); + } + }, + }); + + server.playerJoin.on(player => game.onLogin(player)); + server.playerLeave.on(player => game.onLogoff(player)); + server.entityDamage.on((cancel, entity, damage) => { + if (!game.onEntityDamage(entity)) cancel(); + else if (entity instanceof ServerPlayer && damage >= entity.health) { + game.onDeath(entity); + cancel(); + } + }); + server.tickEnd.on(() => { + game.onTick(); + }); + server.blockBreak.on((cancel, loc, player) => { + if (!game.onBlockBreak(loc, player)) cancel(); + }); + server.blockPlace.on((cancel, loc) => { + if (!game.onBlockPlace(loc)) cancel(); + }); + server.inventoryScreenClicked.on((cancel, screen, player, slot, button, action) => { + if (!game.onInvClick(screen, player, slot, button, action)) cancel(); + }); + server.inventoryScreenClosed.on((cancel, screen) => { + if (!game.onInvClose(screen)) cancel(); + }); + server.entityUse.on((cancel, player, entity) => { + if (!game.onEntityUse(player, entity)) cancel(); + }); +}); + diff --git a/src/test-mod/tsconfig.json b/src/test-mod/tsconfig.json new file mode 100644 index 0000000..dedd981 --- /dev/null +++ b/src/test-mod/tsconfig.json @@ -0,0 +1,10 @@ +{ + "include": [ "./**.ts", "src/main.ts", "libs/mc.d.ts", "libs/lib.d.ts" ], + "compilerOptions": { + "downlevelIteration": true, + "strict": true, + "lib": [ ], + "outDir": "dst", + "target": "ES5" + } +} \ No newline at end of file