├── .gitignore ├── .gitmodules ├── package.json ├── src ├── auth.js ├── chunk.js ├── client.js ├── id_manager.js ├── main.js ├── player.js ├── protocol.js ├── server.js ├── user.js ├── utils │ ├── arraylist.js │ ├── compression.js │ └── hashmap.js ├── world.js ├── world_loader.js └── world_manager.js └── worlds └── main └── 0 ├── -1.-1.png ├── -1.0.png ├── 0.-1.png └── 0.0.png /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | npm-debug.log 4 | src/config/config.json 5 | src/config/key.json 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "owop-protocol"] 2 | path = owop-protocol 3 | url = https://github.com/OurSources/owop-protocol 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owop-server", 3 | "version": "1.0.0", 4 | "description": "The OWOP server written in Node", 5 | "main": "src/main.js", 6 | "dependencies": { 7 | "firebase-admin": "^5.8.1", 8 | "fs-extra": "^5.0.0", 9 | "protodef": "^1.6.4", 10 | "recaptcha2": "^1.3.2", 11 | "ws": "^4.0.0" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "start": "node src/main.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/OurSources/owop-server.git" 20 | }, 21 | "author": "", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/OurSources/owop-server/issues" 25 | }, 26 | "homepage": "https://github.com/OurSources/owop-server#readme" 27 | } 28 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CONFIG = require("./config/config.json"); 4 | const firebase = require("firebase-admin"); 5 | 6 | const key = require("./config/key.json"); 7 | 8 | firebase.initializeApp({ 9 | credential: firebase.credential.cert(key), 10 | databaseURL: CONFIG.firebase.databaseURL 11 | }); 12 | 13 | console.log("Initialized Firebase"); 14 | -------------------------------------------------------------------------------- /src/chunk.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const compression = require("./utils/compression"); 4 | 5 | class Chunk { 6 | constructor(data, cache) { 7 | this._data = data; 8 | this._changed = false; 9 | this._cache = cache; 10 | } 11 | 12 | putPixel(x, y, color) { 13 | this._data.writeUInt16(color, ((y << 8) + x) << 1); 14 | this._changed = true; 15 | } 16 | 17 | getPixel(x, y) { 18 | return this._data.readUInt16(color, ((y << 8) + x) << 1); 19 | } 20 | 21 | get data() { 22 | return this._data; 23 | } 24 | 25 | get cache() { 26 | if (this._changed) { 27 | this._changed = false; 28 | this._cache = compression.compress(this.data); 29 | } 30 | 31 | return this._cache; 32 | } 33 | } 34 | 35 | module.exports = Chunk; 36 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const config = require("./config/config.json"); 4 | const ReCaptcha = require("recaptcha2"); 5 | const recaptcha = new ReCaptcha(config.captcha); 6 | 7 | const { States, protocol } = require("./protocol"); 8 | 9 | const firebase = require("firebase-admin"); 10 | 11 | const { User, Guest } = require("./user"); 12 | const { Player, Position } = require("./player"); 13 | 14 | class Client { 15 | constructor(socket, worldManager) { 16 | this.socket = socket; 17 | this.worldManager = worldManager; 18 | 19 | this.userId = null; 20 | 21 | this.world = null; 22 | this.localId = null; 23 | this.position = new Position(0, 0); 24 | this.user = null; 25 | 26 | this.state = States.LOGIN; 27 | 28 | //this.player = new Player(); 29 | 30 | this.socket.on("message", (message) => { 31 | this.messageHandler(message); 32 | }); 33 | this.socket.on("close", () => { 34 | this.close(); 35 | }); 36 | } 37 | 38 | messageHandler(message) { 39 | let packet = protocol.states[this.state].deserialize(message); 40 | console.log(packet); 41 | 42 | if (this.state == States.LOGIN) { 43 | switch(packet.name) { 44 | case "login": 45 | if (packet.params.guest) { 46 | recaptcha.validate(packet.params.token).then(() => { 47 | this.userId = "guest"; 48 | this.sendPacket({ 49 | name: "loginResponse", 50 | params: { 51 | statusCode: 0, 52 | statusMessage: "" 53 | } 54 | }); 55 | this.state = States.PLAY; 56 | this.joinWorld(packet.params.worldName); 57 | this.sendPacket({ 58 | name: "clientData", 59 | params: { 60 | userId: "", 61 | localId: this.id, 62 | pos: { 63 | x: 0, 64 | y: 0 65 | }, 66 | username: "", 67 | xp: 0 68 | } 69 | }); 70 | }).catch((errorCodes) => { 71 | // TODO 72 | console.log("Captcha failed", recaptcha.translateErrors(errorCodes)); 73 | }); 74 | } else { 75 | firebase.auth().verifyIdToken(packet.params.token).then((decodedToken) => { 76 | console.log(decodedToken); 77 | this.sendPacket({ 78 | name: "loginResponse", 79 | params: { 80 | statusCode: 0, 81 | statusMessage: "" 82 | } 83 | }); 84 | }).catch((error) => { 85 | // TODO 86 | }); 87 | } 88 | break; 89 | } 90 | } else { 91 | switch(packet.name) { 92 | case "login": 93 | 94 | break; 95 | } 96 | } 97 | } 98 | 99 | joinWorld(worldName) { 100 | let world = this.worldManager.getWorld(worldName); 101 | this.world = world; 102 | 103 | this.id = world.clientJoin(this); 104 | } 105 | 106 | close() { 107 | if (this.world) { 108 | this.world.clientLeave(this); 109 | } 110 | } 111 | 112 | sendPacket(packet) { 113 | this.socket.send(protocol.states[this.state].serialize(packet)); 114 | } 115 | } 116 | 117 | module.exports = Client; 118 | -------------------------------------------------------------------------------- /src/id_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class IdManager { 4 | constructor() { 5 | this.currentId = 0; 6 | this.freeIds = []; 7 | this.freeIds.sortedPush = function(item) { 8 | let index = 0; 9 | if (item >= this[this.length - 1]) { 10 | return this.push(item) - 1; 11 | } 12 | while (this[index] < item) index++; 13 | this.splice(index, 0, item); 14 | return index; 15 | }; 16 | } 17 | 18 | newId() { 19 | if (this.freeIds.length !== 0) { 20 | return this.freeIds.shift(); 21 | } 22 | return ++this.currentId; 23 | } 24 | 25 | freeId(id) { /* Only free ids returned by newId(), once. */ 26 | if (id === this.currentId) { 27 | --this.currentId; 28 | } else { 29 | this.freeIds.sortedPush(id); 30 | } 31 | this.shrink(); 32 | } 33 | 34 | shrink() { 35 | let len = this.freeIds.length - 1; 36 | let didSomething = false; 37 | for (; len >= 0; len--) { 38 | if (this.freeIds[len] === this.currentId) { 39 | --this.currentId; 40 | didSomething = true; 41 | } else { 42 | break; 43 | } 44 | } 45 | if (didSomething) { 46 | this.freeIds.length = len + 1; 47 | } 48 | } 49 | } 50 | 51 | module.exports = IdManager; 52 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Server = require("./server"); 4 | 5 | const server = new Server({ 6 | port: 9000, 7 | chunkPort: 4334 8 | }); 9 | 10 | module.exports = server; 11 | -------------------------------------------------------------------------------- /src/player.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class Position { 4 | constructor(x, y) { 5 | this.x = x; 6 | this.y = y; 7 | } 8 | } 9 | 10 | class Player { 11 | constructor() { 12 | this.pos = new Position(0, 0); 13 | } 14 | 15 | update(x, y, tool, color) { 16 | this.pos.x = x; 17 | this.pos.y = y; 18 | this._tool = tool; 19 | this._color = color; 20 | } 21 | 22 | get id() { 23 | return this._id; 24 | } 25 | 26 | get world() { 27 | return this._world; 28 | } 29 | 30 | get tool() { 31 | return this._tool; 32 | } 33 | 34 | get color() { 35 | return this._color; 36 | } 37 | 38 | toString() { 39 | return `[Player world:'${this._world.name}' id:${this._id}]`; 40 | } 41 | } 42 | 43 | module.exports = { 44 | Position: Position, 45 | Player: Player 46 | }; 47 | -------------------------------------------------------------------------------- /src/protocol.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ProtoDef = require("protodef"); 4 | const protoData = require("../owop-protocol/protocol.json"); 5 | 6 | const States = { 7 | LOGIN: 0, 8 | PLAY: 1 9 | }; 10 | for (let i in States) { 11 | States[States[i]] = i; 12 | } 13 | 14 | class NetworkState { 15 | constructor() { 16 | this.toClient = new ProtoDef.ProtoDef(); 17 | this.toServer = new ProtoDef.ProtoDef(); 18 | } 19 | 20 | deserialize(msg) { 21 | return this.toServer.parsePacketBuffer("packet", msg).data; 22 | } 23 | 24 | serialize(packet) { 25 | return this.toClient.createPacketBuffer("packet", packet); 26 | } 27 | } 28 | 29 | class LoginState extends NetworkState { 30 | constructor() { 31 | super(); 32 | 33 | this.toClient.addProtocol(protoData, ["login", "toClient"]); 34 | this.toServer.addProtocol(protoData, ["login", "toServer"]); 35 | } 36 | } 37 | 38 | class PlayState extends NetworkState { 39 | constructor() { 40 | super(); 41 | 42 | this.toClient.addProtocol(protoData, ["play", "toClient"]); 43 | this.toServer.addProtocol(protoData, ["play", "toServer"]); 44 | } 45 | } 46 | 47 | const protocol = { 48 | states: { 49 | [States.LOGIN]: new LoginState(), 50 | [States.PLAY]: new PlayState() 51 | } 52 | } 53 | 54 | function isWorldNameValid(worldName) { 55 | /* Validate world name, allowed chars are a..z, 0..9, '_' and '.' 56 | final int size = nameBytes.capacity(); 57 | if (size < 3 || size - 2 > 24 || nameBytes.getShort(size - 2) != 1337) { 58 | return false; 59 | } 60 | nameBytes.limit(size - 2); 61 | for (int i = 0; i < nameBytes.limit(); i++) { 62 | final byte b = nameBytes.get(i); 63 | if (!((b > 96 && b < 123) || (b > 47 && b < 58) || b == 95 || b == 46)) { 64 | return false; 65 | } 66 | }*/ 67 | // TODO: implement this 68 | return true; 69 | } 70 | 71 | module.exports = { 72 | States: States, 73 | protocol: protocol 74 | }; 75 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const WebSocket = require("ws"); 5 | const http = require("http"); 6 | const IdManager = require("./id_manager"); 7 | 8 | require("./auth"); 9 | 10 | const WorldManager = require("./world_manager"); 11 | 12 | const Client = require("./client"); 13 | 14 | class Server { 15 | constructor(options) { 16 | this.clients = new Set(); 17 | 18 | this.worldManager = new WorldManager(); 19 | this.idManager = new IdManager(); /* Per world or global? */ 20 | 21 | // WebSocket Server 22 | this.server = new WebSocket.Server({ 23 | port: options.port 24 | }, () => { 25 | console.log("Server started!"); 26 | }); 27 | 28 | this.server.on("connection", (socket, data) => { 29 | let client = new Client(socket, this.worldManager); 30 | 31 | this.clients.add(client); 32 | socket.on("close", () => { 33 | this.clients.delete(client); 34 | }); 35 | socket.on("error", () => {}); 36 | }); 37 | 38 | this.server.on("error", () => { 39 | 40 | }); 41 | 42 | 43 | // HTTP Chunk Server 44 | const chunksPath = "worlds"; 45 | 46 | this.chunkServer = http.createServer(function(req, res) { 47 | let exists = fs.existsSync(chunksPath + req.url); 48 | 49 | if (exists) { 50 | res.end(fs.readFileSync(chunksPath + req.url), "binary"); 51 | } else { 52 | res.writeHead(404, {}); 53 | res.end(); 54 | } 55 | }); 56 | 57 | this.chunkServer.on("clientError", function(err, socket) { 58 | socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); 59 | }); 60 | 61 | this.chunkServer.listen(options.chunkPort); 62 | } 63 | 64 | broadcast(message) { 65 | this.clients.forEach(client => client.send(message)); 66 | } 67 | } 68 | 69 | module.exports = Server; 70 | -------------------------------------------------------------------------------- /src/user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class User { 4 | constructor() { 5 | 6 | } 7 | } 8 | 9 | class Guest extends User { 10 | constructor() { 11 | 12 | } 13 | } 14 | 15 | module.exports = { 16 | User: User, 17 | Guest: Guest 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/arraylist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ArrayList { 4 | constructor() { 5 | this._data = []; 6 | } 7 | 8 | get size() { 9 | return this._data.length; 10 | } 11 | 12 | add(value) { 13 | this._data.push(value); 14 | } 15 | 16 | remove(value) { 17 | var index = this._data.indexOf(value); 18 | if (index >= 0) { 19 | this._data.splice(index, 1); 20 | } 21 | } 22 | 23 | get(index) { 24 | return this._data[index]; 25 | } 26 | 27 | clear() { 28 | this._data = []; 29 | } 30 | 31 | forEach(callback) { 32 | this._data.forEach(callback); 33 | } 34 | } 35 | 36 | module.exports = ArrayList; 37 | -------------------------------------------------------------------------------- /src/utils/compression.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ArrayList = require(`./arraylist`); 3 | 4 | function compress(rawData) { 5 | rawData = rawData.buffer; /* converts to UInt8Array */ 6 | var compressedSize = 0; 7 | var repeatLocations = new ArrayList(); 8 | var repeatTimes = new ArrayList(); 9 | var repeats = 1; 10 | var lastColor = -1; 11 | 12 | for (var i = 0; i < rawData.length; i += 2) { 13 | var thisColor = (rawData[i + 1] << 8) | rawData[i]; 14 | if (lastColor == thisColor) { 15 | repeats++; 16 | } else { 17 | if (repeats >= 3) { 18 | compressedSize -= (repeats - 1) * 2 - 2 - 2; 19 | repeatLocations.add((compressedSize - 2 - 2 - 2) / 2); 20 | repeatTimes.add(repeats - 1); 21 | repeats = 1; 22 | lastColor = thisColor; 23 | continue; 24 | } 25 | repeats = 1; 26 | lastColor = thisColor; 27 | } 28 | compressedSize += 2; 29 | } 30 | if (repeats >= 3) { 31 | compressedSize -= (repeats) * 2 - 2 - 2; 32 | repeatLocations.add((compressedSize - 2 - 2) / 2); 33 | repeatTimes.add(repeats - 1); 34 | } 35 | 36 | var repeatsSize = repeatLocations.size; 37 | var compressedData = new Uint8Array(2 + 2 + repeatsSize * 2 + compressedSize); 38 | var length = rawData.length / 2 - 1; 39 | var offset = 2 + 2 + repeatsSize * 2; 40 | var cptr = 0; 41 | var dptr = offset; 42 | var optr = 0; 43 | compressedData[cptr++] = length & 0xFF; 44 | compressedData[cptr++] = (length >> 8) & 0xFF; 45 | compressedData[cptr++] = repeatsSize & 0xFF; 46 | compressedData[cptr++] = (repeatsSize >> 8) & 0xFF; 47 | for (var i = 0; i < repeatsSize; i++) { 48 | var loc = repeatLocations.get(i); 49 | var times = repeatTimes.get(i); 50 | compressedData[cptr++] = loc; 51 | compressedData[cptr++] = loc >> 8; 52 | while (dptr < loc * 2 + offset) { 53 | compressedData[dptr++] = rawData[optr++]; 54 | } 55 | compressedData[dptr++] = times; 56 | compressedData[dptr++] = times >> 8; 57 | compressedData[dptr++] = rawData[optr++]; /* RG */ 58 | compressedData[dptr++] = rawData[optr++]; /* GB (565) */ 59 | optr += (1 + times - 1) * 2; 60 | } 61 | while (optr < rawData.size) { 62 | compressedData[dptr++] = rawData[optr++]; 63 | } 64 | 65 | return Buffer.from(compressedData); 66 | } 67 | 68 | function decompress(compressedData) { 69 | compressedData = compressedData.buffer; /* converts to UInt8Array */ 70 | var originalLength = (((compressedData[1] & 0xFF) << 8 | (compressedData[0] & 0xFF)) + 1) * 2; 71 | var rawData = new Uint8Array(originalLength); 72 | var repeatsSize = (compressedData[3] << 8) | compressedData[2]; 73 | var offset = repeatsSize * 2 + 4; 74 | var uptr = 0; 75 | var cptr = offset; 76 | for (var i = 0; i < repeatsSize; i++) { 77 | var currentRepeatLoc = 2 * ((compressedData[4 + i * 2 + 1] << 8) | compressedData[4 + i * 2]) + offset; 78 | while (cptr < currentRepeatLoc) { 79 | rawData[uptr++] = compressedData[cptr++]; 80 | } 81 | var repeatedNum = (compressedData[cptr + 1] << 8 | compressedData[cptr]) + 1; 82 | var repeatedColor = (compressedData[cptr + 3] << 8) | compressedData[cptr + 2]; 83 | cptr += 4; 84 | while (repeatedNum-- != 0) { 85 | rawData[uptr] = repeatedColor; 86 | rawData[uptr + 1] = repeatedColor >> 8; 87 | uptr += 2; 88 | } 89 | } 90 | while (cptr < compressedData.length) { 91 | rawData[uptr++] = compressedData[cptr++]; 92 | } 93 | 94 | return Buffer.from(rawData); 95 | } 96 | 97 | module.exports = { compress, decompress }; 98 | -------------------------------------------------------------------------------- /src/utils/hashmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class HashMap { 4 | constructor() { 5 | this._size = 0; 6 | this._data = {}; 7 | } 8 | 9 | get size() { 10 | return this._size; 11 | } 12 | 13 | add(key, value) { 14 | this._data[key] = value; 15 | this._size++; 16 | } 17 | 18 | get(key) { 19 | return this.contains(key) ? this._data[key] : undefined; 20 | } 21 | 22 | remove(key) { 23 | delete this._data[key]; 24 | this._size--; 25 | } 26 | 27 | contains(key) { 28 | //console.log(`contains ${key} ${this[key]}`); 29 | return this._data.hasOwnProperty(key); 30 | } 31 | 32 | forEach(callback) { 33 | Object.keys(this._data).forEach((key) => callback(key, this.get(key))); 34 | } 35 | } 36 | 37 | module.exports = HashMap; 38 | -------------------------------------------------------------------------------- /src/world.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const HashMap = require("./utils/hashmap"); 4 | const ArrayList = require("./utils/arraylist"); 5 | const WorldLoader = require("./world_loader"); 6 | 7 | const IdManager = require("./id_manager"); 8 | 9 | const { States, protocol } = require("./protocol"); 10 | 11 | class World { 12 | constructor(worldName, requestUpdate) { 13 | console.log("new world"); 14 | this.name = worldName; 15 | this.clients = new Map(); 16 | this.playerUpdates = new Set(); 17 | 18 | this.idManager = new IdManager(); 19 | this.worldLoader = new WorldLoader(worldName); 20 | 21 | /* Should a map be used here? Would prevent sending multiple updates for a single pixel */ 22 | this.pixelUpdates = new ArrayList(); 23 | this.chunks = new Map(); 24 | 25 | this.requestUpdate = requestUpdate; 26 | } 27 | 28 | destroy() { 29 | /* Save chunks here */ 30 | } 31 | 32 | get online() { 33 | return this.clients.size; 34 | } 35 | 36 | /*getNextID() { 37 | this._playerID++; 38 | return this._playerID - 1; 39 | }*/ 40 | 41 | getChunkKey(x, y) { 42 | /* If the chunk position is signed 24-bit (or less), it actually fits in a js number */ 43 | /* >>> 0 'casts' to unsigned, * 0x1000000 shifts the num 24 bits */ 44 | return ((x >>> 0 & 0xFFFFFF) * 0x1000000 + (y >>> 0 & 0xFFFFFF)); 45 | } 46 | 47 | async getChunk(x, y) { 48 | var chunkKey = this.getChunkKey(x, y); 49 | var chunk = this.chunks.get(chunkKey); 50 | if (!chunk) { 51 | chunk = await this.worldLoader.loadChunk(x, y); 52 | this.chunks.set(chunkKey, chunk); 53 | } 54 | return chunk; 55 | } 56 | 57 | async putPixel(x, y, color) { 58 | const chunk = await this.getChunk(x >> 8, y >> 8); 59 | if (!chunk || chunk.getPixel(x, y) == color) { 60 | return; 61 | } 62 | chunk.putPixel(x, y, color); 63 | this.pixelUpdates.add({ 64 | x: x, 65 | y: y, 66 | color: color 67 | }); 68 | } 69 | 70 | playerUpdated(player) { 71 | this.playerUpdates.add(player); 72 | } 73 | 74 | clientJoin(client) { 75 | client.sendPacket({ 76 | name: "worldData", 77 | params: { 78 | worldName: this.name, 79 | cursors: Array.from(this.clients, function(entry) { 80 | let key = entry[0]; 81 | let client = entry[1]; 82 | return { 83 | userId: client.userId, 84 | localId: key, 85 | pos: client.position 86 | }; 87 | }) 88 | } 89 | }); 90 | //player.send(`Joined world ${this.name}. Your ID: ${client.id}`); 91 | let id = this.idManager.newId(); 92 | client.localId = id; 93 | this.clients.set(id, client); 94 | 95 | return id; 96 | } 97 | 98 | clientLeave(client) { 99 | this.clients.delete(client.localId); 100 | this.broadcast(protocol.states[States.PLAY].serialize({ 101 | name: "playerLeave", 102 | params: { 103 | localId: client.localId 104 | } 105 | })); 106 | } 107 | 108 | broadcast(message) { 109 | this.clients.forEach((key, client) => client.socket.send(message)); 110 | } 111 | 112 | sendUpdates() { 113 | var players = this.playerUpdates.size; 114 | var pixels = this.pixelUpdates.size; 115 | var disconnects = this.playerDisconnects.size; 116 | 117 | if (players + pixels + disconnects < 1) { 118 | return false; 119 | } 120 | 121 | var message = Buffer.allocUnsafe(5 + players * 16 + pixels * 11 + disconnects * 4); 122 | message.writeUInt8(0x01, 0); 123 | message.writeUInt8(players, 1); 124 | var offset = 2; 125 | var i = 0; 126 | this.playerUpdates.forEach(player => { 127 | message.writeUInt32LE(player.id, offset + 16 * i); 128 | message.writeInt32LE(player.x, offset + 16 * i + 4); 129 | message.writeInt32LE(player.y, offset + 16 * i + 8); 130 | message.writeUInt16LE(player.color, offset + 16 * i + 12); 131 | message.writeUInt8(player.tool, offset + 16 * i + 14); 132 | i++; 133 | }); 134 | 135 | message.writeUInt16LE(pixels, 2 + 16 * i); 136 | offset += 16 * i + 2; 137 | i = 0; 138 | this.pixelUpdates.forEach(pixel => { 139 | message.writeInt32LE(pixel.x, offset + 11 * i); 140 | message.writeInt32LE(pixel.y, offset + 11 * i + 4); 141 | message.writeUInt16LE(pixel.color, offset + 11 * i + 8); 142 | i++; 143 | }); 144 | 145 | message.writeUInt8(disconnects, offset + 11 * i); 146 | offset += 1 + 11 * i; 147 | i = 0; 148 | this.playerDisconnects.forEach(playerID => { 149 | message.writeUInt32LE(playerID, offset + i * 4); 150 | i++; 151 | }); 152 | 153 | this.playerUpdates.clear(); 154 | this.playerDisconnects.clear(); 155 | this.pixelUpdates.clear(); 156 | 157 | this.clients.forEach((key, player) => player.send(message)); 158 | } 159 | } 160 | 161 | module.exports = World; 162 | -------------------------------------------------------------------------------- /src/world_loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const Chunk = require("./chunk"); 6 | 7 | const worldDir = "worlds"; 8 | 9 | class WorldLoader { 10 | constructor(worldName) { 11 | this.worldPath = path.resolve(worldDir, worldName); 12 | try { 13 | fs.mkdirSync(this.worldPath); 14 | } catch (e) { 15 | if (e.code !== 'EEXIST') { 16 | throw e; 17 | } 18 | } 19 | } 20 | 21 | async saveChunk(x, y, chunk) { 22 | const chunkName = path.resolve(this.worldPath, `c.${x}.${y}.rgb`); 23 | 24 | await fs.writeFile(chunkName, chunk.data); 25 | } 26 | 27 | async loadChunk(x, y) { 28 | const chunkName = path.resolve(this.worldPath, `c.${x}.${y}.rgb`); 29 | 30 | var data; 31 | try { 32 | data = await fs.readFile(chunkName); 33 | } catch (e) { 34 | if (e.code !== 'ENOENT') { 35 | throw e; 36 | } 37 | data = Buffer.alloc(256 * 256 * 3, 0xFF); 38 | } 39 | 40 | return new Chunk(data); 41 | } 42 | } 43 | 44 | module.exports = WorldLoader; 45 | -------------------------------------------------------------------------------- /src/world_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const World = require("./world"); 4 | 5 | class WorldManager { 6 | constructor() { 7 | this.worlds = new Map(); 8 | this.worldsToUpdate = new Set(); 9 | this.updateLoop = setInterval(() => this.updateWorlds(), 1000 / 20); 10 | } 11 | 12 | updateWorlds() { 13 | this.worldsToUpdate.forEach((world) => { 14 | if (!world.sendUpdates()) { /* Returns false if there's nothing remaining to update */ 15 | this.worldsToUpdate.delete(world); 16 | } 17 | }); 18 | } 19 | 20 | requestWorldUpdate(world) { 21 | this.worldsToUpdate.add(world); 22 | } 23 | 24 | getWorld(worldName) { 25 | let world = this.worlds.get(worldName); 26 | if (world) { 27 | return world; 28 | } 29 | 30 | world = new World(worldName, () => this.requestWorldUpdate(world)); 31 | this.worlds.set(worldName, world); 32 | return world; 33 | } 34 | 35 | unloadWorld(worldName) { 36 | let world = this.worlds.get(worldName); 37 | if (world) { 38 | world.destroy(); 39 | this.worlds.delete(worldName); 40 | } 41 | } 42 | 43 | destroy() { 44 | clearInterval(this.updateLoop); 45 | this.worlds.forEach(world => { 46 | world.destroy(); 47 | }); 48 | } 49 | } 50 | 51 | module.exports = WorldManager; 52 | -------------------------------------------------------------------------------- /worlds/main/0/-1.-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-server/9570a5e4693d7369361bd7423a4e1ffa80097171/worlds/main/0/-1.-1.png -------------------------------------------------------------------------------- /worlds/main/0/-1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-server/9570a5e4693d7369361bd7423a4e1ffa80097171/worlds/main/0/-1.0.png -------------------------------------------------------------------------------- /worlds/main/0/0.-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-server/9570a5e4693d7369361bd7423a4e1ffa80097171/worlds/main/0/0.-1.png -------------------------------------------------------------------------------- /worlds/main/0/0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OurSources/owop-server/9570a5e4693d7369361bd7423a4e1ffa80097171/worlds/main/0/0.0.png --------------------------------------------------------------------------------