Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rom1504 opened this issue Oct 24, 2020 · 15 comments

Comments

@rom1504
Copy link
Member

rom1504 commented Oct 24, 2020

PrismarineJS/flying-squid#392
PrismarineJS/mineflayer#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
Copy link
Member Author

rom1504 commented Oct 25, 2020

@rom1504 rom1504 changed the title Related issues Create the design explaining how prismarine-world will contain all the state of mineflayer and flying-squid Oct 25, 2020
@rom1504
Copy link
Member Author

rom1504 commented Oct 25, 2020

kind of related PrismarineJS/node-minecraft-protocol#231

@rom1504
Copy link
Member Author

rom1504 commented Oct 25, 2020

PrismarineJS/mineflayer#334 (comment) this comment is describing a lot of tasks to do

@rom1504
Copy link
Member Author

rom1504 commented Oct 25, 2020

@rom1504
Copy link
Member Author

rom1504 commented Oct 25, 2020

@rom1504
Copy link
Member Author

rom1504 commented Oct 25, 2020

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

@TheDudeFromCI
Copy link
Member

Working on the first draft for this in #2

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

@louis030195
Copy link

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
Copy link
Member Author

rom1504 commented Nov 1, 2020

related PrismarineJS/mineflayer#1151

@rom1504
Copy link
Member Author

rom1504 commented Nov 1, 2020

@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
Copy link

Saiv46 commented Dec 30, 2020

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
Copy link

Saiv46 commented Dec 30, 2020

prismarinejs

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

  • mineflayer or flying-squid
    • prismarinejs@reality
    • prismarinejs@network (described later)
    • prismarinejs@world
      • prismarinejs@chunk
        • prismarinejs@block
        • prismarinejs@biome

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
Copy link
Member Author

rom1504 commented Dec 30, 2020

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 PrismarineJS/mineflayer#334 (comment)

@rom1504
Copy link
Member Author

rom1504 commented Dec 30, 2020

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
Copy link
Member Author

rom1504 commented Dec 30, 2020

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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants