PrismarineJS / prismarine-design

Discuss about design and big refactoring that can affects many modules
2 stars 2 forks source link

Create the design explaining how prismarine-world will contain all the state of mineflayer and flying-squid #1

Open rom1504 opened 3 years ago

rom1504 commented 3 years ago

https://github.com/PrismarineJS/flying-squid/issues/392 https://github.com/PrismarineJS/mineflayer/issues/334

Basic idea is pworld containing 100% of the state pworld server providing data to pworld client pviewer using pworld client to render data pworld being using in flying-squid and mineflayer

mineflayer and flying-squid being stateless transformers on top of a pworld and network

rom1504 commented 3 years ago

https://github.com/PrismarineJS/prismarine-world/issues/43

rom1504 commented 3 years ago

kind of related https://github.com/PrismarineJS/node-minecraft-protocol/issues/231

rom1504 commented 3 years ago

https://github.com/PrismarineJS/mineflayer/issues/334#issuecomment-670987395 this comment is describing a lot of tasks to do

rom1504 commented 3 years ago

https://docs.google.com/drawings/d/1mztcUKqjJ6E_88bsxppby8Ijrz-pBmlavP8iDbWSjWw/edit?usp=sharing

rom1504 commented 3 years ago
rom1504 commented 3 years ago

@TheDudeFromCI @Karang I made a draft for this design there ^

TheDudeFromCI commented 3 years ago

Working on the first draft for this in https://github.com/PrismarineJS/prismarine-design/pull/2

Should be ready soon, but still lots more information to add before it's ready for a review and merge.

louis030195 commented 3 years ago

a distribution using bounding volume hierarchy (octree, kd-tree ...) works when the entities are spread on the world but what if there is 1 M entities in a small region ? Is there a distributed strategy that uses both location partitioning and another partitioning strategy ? What about the transfer of computing entities when an entity move from a region to another ? I.e. creep is in octant 1 and move on the map, he then has to be handled by octant 2, so you have to have a communication between these octants/nodes couldn't it cause latency/problems ?

image

rom1504 commented 3 years ago

related https://github.com/PrismarineJS/mineflayer/issues/1151

rom1504 commented 3 years ago

@louis030195 yes handling scaling of many entities in one chunk is much harder (but yes you could just distribute by region + uuid % 10) yes there would need to be a strategy to transfer entities between regions ^ this is all about doing a distributed server though, not really needed as a first step

Saiv46 commented 3 years ago

How this should work?

For example, two groups of players (from two continents) are in same Minecraft world, with 24 chunk render distance. They can build blocks yet see other group, latency must be minimized for both groups of players.

  1. Traditional server

    1. Two player groups are connected to single server.
    2. One player group will have better latency than other.
    3. Players can see server lag as server keeps processing all regions in single thread.
  2. Distributed server

    1. Centralized database/storage still needed, but at least it's asynchonous.
    2. Any number of servers can be launched - they will connect to each other.
    3. Each server can spawn workers and transfer chunks if needed.
    4. Player connects to nearest server, yet every server will accept player packets from same IP.
    5. If needed region is assigned to other server, server will proxy player actions to it.
    6. If not, server assignes region to itself and loads it.
    7. Server can assign individual chunks to its workers, thus gameplay can be smoothed.
    8. Once any entity goes to other region - it got transfered (see point 5).
Saiv46 commented 3 years ago

prismarinejs

A monorepo to help keep everything maintained and updated. No more prismarine-* packages - just a single prismarinejs@ prefix for everything else.

fromBuffer/toBuffer methods are using MessagePack (for web and cross-server interaction) fromJSON/toJSON methods are intended for debugging/logging purposes only

prismarinejs@reality

Inspired by Roblox's Reality Engine - the physics engine that distributes computational work across connected clients and servers, algorithmically assigning and managing simulation zones.

An module for intelligent load distribution across processes and/or remote instances and latency minimization.

declare class RealityManager extends EventEmitter {
    idleProcesses: number = 1;
    maxProcesses: number = 4;
    allowReconnect: boolean = true; // Keep reconnecting to dead servers
    maxInstances: number = 50; // Nobody will ever need over 50 connections

    _instances: Set<RealityInstance>;

    /// Events

    // When region assigned to this instance
    // `data` parameter contains a Buffer of region
    on<T = {}>(
        event: 'assign',
        handler: Function<x: number, z: number, data: Buffer>
    ): this;

    // When this instance was been told to reclaim a region
    // Callback function must be called with `true` if region can be reclaimed, `false` otherwise
    on<T = {}>(
        event: 'reclaim_request',
        handler: Function<x: number, z: number, priority: number, callback: function<boolean>>
    ): this;

    /// Soft transfer of regions

    // This method assigns a region to this instance if it isn't assigned yet.
    // Otherwise returns an instance assigned to that region.
    useRegion(x: number, z: number): Promise<RealityInstance | null>;

    // Call this if region is not needed anymore
    // This will pass a region to other instance and returns it.
    // Otherwise method returns null and region needs to be saved.
    freeRegion(x: number, z: number): Promise<RealityInstance | null>;

    /// Hard transfer of regions

    // This will forcefully assign/reclaim region for exclusive modification
    assignRegion(x: number, z: number, inst: RealityInstance): Promise<void>;
    reclaimRegion(x: number, z: number, inst: RealityInstance): Promise<void>;
}

declare class RealityInstance {
    socket: Socket; // IPC, TCP, QUIC? No matter though
    isLocal: boolean; // Do not apply latency measurements
    isAlive: boolean; // Is process/server sending stats
    _clientLatencyStats: number[]; // Aka ping
    _serverLatencyStats: number[]; // Aka TPS
    proxifyPlayer(client: Socket): Promise<Socket>; // Proxy client to instance
    deproxifyPlayer(client: Socket): Promise<Socket>; // Stop proxying client to instance
}

prismarinejs@chunk

// A provider class that handles serialization of a chunk across versions.
declare class ChunkProvider {
    version: string;
    _locks: WeakSet<Chunk>;

    fromBuffer(data: Buffer, version?: string = this.version): Block;
    toBuffer(block: Block, version?: string = this.version): Buffer;
    fromNotch(data: Buffer, version?: string = this.version): Chunk;
    toNotch(chunk: Chunk, version?: string = this.version): Buffer;
    fromJSON(data: string, version?: string = this.version): Chunk;
    toJSON(chunk: Chunk, version?: string = this.version): string;

    // Diff chunks per-block
    diffChunks (old: Chunk, new: Chunk): Iterator<[position: Vec3, old: Block, new: Block]>;
    // Update chunk from part of serialized chunk
    fromNotchDiff (old: Chunk, new: Buffer, bitMap: number = 0xFFFF, hasSkyLight: boolean = true, hasBiomes: boolean = true): Chunk;
    // Serialize diff of chunks per-section instead of full chunk if possible
    toNotchDiff (old: Chunk, new: Chunk): [ new: Buffer, bitMap: number, hasSkyLight: boolean, hasBiomes: boolean ];
};

declare interface ChunkHandler {
    toJSON(): string;
    fromJSON(data: string): Chunk;

    // Using MessagePack for serialization (for web and cross-server interaction)
    toBuffer(): Buffer;
    fromBuffer(data: Buffer): Chunk;

    fromNotch(data: Buffer, bitMap: number = 0xFFFF, hasSkyLight: boolean = true, hasBiomes: boolean = true): Chunk;
    toNotch(bitMap: number = 0xFFFF, includeSkyLight: boolean = true, includeBiomes: boolean = true): Buffer;
}
declare class ChunkHandler_PC1_16 implementing ChunkHandler;
declare class ChunkHandler_PC1_15 implementing ChunkHandler;
declare class ChunkHandler_PC1_14 implementing ChunkHandler;
declare class ChunkHandler_PC1_13 implementing ChunkHandler;
declare class ChunkHandler_PC1_09 implementing ChunkHandler;
declare class ChunkHandler_PC1_08 implementing ChunkHandler;
declare class ChunkHandler_PE1_00 implementing ChunkHandler;
declare class ChunkHandler_PE0_14 implementing ChunkHandler;

// An abstraction of chunk.
// (Actual data and most operations can be sent over network!)
declare class Chunk {
    id: Vec2;
    hasSkyLight: boolean;
    hasBiomes: boolean;
    _rawSkyLight: Buffer;
    _rawBiome: Buffer;
    _provider: WeakRef<ChunkProvider>;

    // For unparralleable/atomic operations on chunks
    // Lock *will* prevent chunk from saving or updating
    get isLocked() : Promise<boolean>;
    wait(): Promise<void>;
    lock(): Promise<void>;
    unlock(): Promise<void>;

    getBlock(pos: Vec3): Promise<Block>;
    setBlock(pos: Vec3, block: Block): Promise<void>;

    // Aliases to chunk accessors, position can be relative to chunk/world
    getBiome(pos: Vec3): Promise<Biome>;
    setBiome(pos: Vec3, biome: Biome): Promise<void>;
    getBlockLight(pos: Vec3): Promise<number>;
    setBlockLight(pos: Vec3, light: number): Promise<void>;
    getSkyLight(pos: Vec3): Promise<number>;
    setSkyLight(pos: Vec3, light: number): Promise<void>;
};

prismarinejs@block

// A provider class that handles block metadata caching and block serialization across versions.
declare class BlockProvider {
    version: string;

    fromBuffer(data: Buffer, version?: string = this.version): Block;
    toBuffer(block: Block, version?: string = this.version): Buffer;
    fromJSON(data: string, version?: string = this.version): Block;
    toJSON(block: Block, version?: string = this.version): string;

    _metadataCache: WeakMap<Block, object>;
    getMetadata(id: number): Promise<object>;
};

// Block bounding box
declare enum BoundingBoxEnum {
    Empty,
    Full,
    Half,
    Stairs,
    Liquid
};

// An object for storing block state
declare class BlockState extends Object {
    toJSON(): string;
    static fromJSON(data: string): BlockState;
    toNBT(): Buffer;
    static fromNBT(data: Buffer): BlockState;
    // Using MessagePack for serialization (for web and cross-server interaction)
    toBuffer(): Buffer;
    static fromBuffer(data: Buffer): BlockState;
};

// An abstraction of a block (aka struct). Stores basic block data, weak references and empty `BlockState`.
declare class Block {
    id: number;
    position: Vec3;

    // Chunk data (lazy-loaded)
    _chunk: WeakRef<Chunk>;
    get biome(): Promise<Biome>;
    set biome(value?: Biome): Promise<void>;
    get light(): Promise<?number>;
    set light(value?: number): Promise<void>;
    get skyLight(): Promise<?number>;
    set skyLight(value?: number): Promise<void>;

    // State and its aliases (lazy-loaded)
    _state: BlockState;
    get state(): Promise<BlockState>;
    get displayName(): Promise<string>;
    set displayName(value?: string): Promise<void>;
    get signText(): Promise<?string>;
    set signText(value?: string): Promise<void>;

    // Metadata (lazy-loaded)
    _provider: WeakRef<BlockProvider>;
    get name(): Promise<string>;
    get hardness(): Promise<number>;
    get boundingBox(): Promise<BoundingBoxEnum>;
    get diggable(): Promise<boolean>;
    get material(): Promise<?string>;
    get transparent(): Promise<boolean>;
    get harvestTools(): Promise<?{ [k: string]: number }>;
    get drops(): Promise<?Array<{ minCount?: number, maxCount?: number, drop: number | { id: number, metadata: number } }>>;

    // Methods using metadata
    canHarvest(heldItemType: number | null): Promise<boolean>;
    digTime(heldItemType: number | null, inWater: boolean, inMidair: boolean, enchantments?: Enchantment[], effects?: Effect[]): Promise<number>;
};

prismarinejs@biome

declare class BiomeProvider {
    version: string;

    getBiome(id: number, version?: string = this.version): Promise<Biome>;

    _metadataCache: WeakMap<Block, object>;
    getMetadata(id: number): Promise<object>;
}

declare class Biome {
    id: number;
    name: string;
    color?: number;
    displayName?: string;
    rainfall: number;
    temperature: number;
    height?: number | null;
}
rom1504 commented 3 years ago

Interesting ideas. I disagree with the monorepo part (because it encourages tight coupling between packages, ie discourage really independent packages) What you described looks a lot like the current design of PrismarineJS.

What I want to introduce (and described above) is a state store which is prismarine-world. That will make it possible to not have monolithic minecraft servers but instead a bunch of small services handling various things. That will also make it possible to distribute them however is required.

This is already ongoing btw but will need some more work to finish it. See https://github.com/PrismarineJS/mineflayer/issues/334#issuecomment-670987395

rom1504 commented 3 years ago

Just to clarify: this issue is already done. The work that needs to be done before we can move further on this is extending prismarine-world. Once that's done and it's integrated in mineflayer and flying-squid for almost all data, we'll have more information on next steps.

rom1504 commented 3 years ago
if we ever need some data to experiment with a distributed world design, here is a 100k x 100k (107Gb compressed) slice of 2b2t's world https://www.reddit.com/r/2b2t/comments/dzvq67/presenting_the_2b2torg_100k_mapping_project_world/