455 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
			
		
		
	
	
			455 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
package kha.netsync;
 | 
						|
 | 
						|
import haxe.io.Bytes;
 | 
						|
#if sys_server
 | 
						|
import js.node.Http;
 | 
						|
import js.Node;
 | 
						|
#end
 | 
						|
#if js
 | 
						|
import js.Browser;
 | 
						|
import js.html.BinaryType;
 | 
						|
import js.html.WebSocket;
 | 
						|
#end
 | 
						|
import kha.System;
 | 
						|
 | 
						|
class State {
 | 
						|
	public var time: Float;
 | 
						|
	public var data: Bytes;
 | 
						|
 | 
						|
	public function new(time: Float, data: Bytes) {
 | 
						|
		this.time = time;
 | 
						|
		this.data = data;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
class Session {
 | 
						|
	public static inline var START = 0;
 | 
						|
	public static inline var ENTITY_UPDATES = 1;
 | 
						|
	public static inline var CONTROLLER_UPDATES = 2;
 | 
						|
	public static inline var REMOTE_CALL = 3;
 | 
						|
	public static inline var PING = 4;
 | 
						|
	public static inline var SESSION_ERROR = 5;
 | 
						|
	public static inline var PLAYER_UPDATES = 6;
 | 
						|
 | 
						|
	public static inline var RPC_SERVER = 0;
 | 
						|
	public static inline var RPC_ALL = 1;
 | 
						|
 | 
						|
	static var instance: Session = null;
 | 
						|
 | 
						|
	var entities: Map<Int, Entity> = new Map();
 | 
						|
	var controllers: Map<Int, Controller> = new Map();
 | 
						|
 | 
						|
	public var maxPlayers: Int;
 | 
						|
	public var currentPlayers: Int = 0;
 | 
						|
	public var ping: Float = 1;
 | 
						|
 | 
						|
	var address: String;
 | 
						|
	var port: Int;
 | 
						|
	var startCallback: Void->Void;
 | 
						|
	var refusedCallback: Void->Void;
 | 
						|
	var resetCallback: Void->Void;
 | 
						|
	#if sys_server
 | 
						|
	var server: Server;
 | 
						|
	var clients: Array<Client> = new Array();
 | 
						|
	var current: Client;
 | 
						|
	var isJoinable: Bool = false;
 | 
						|
	var lastStates: Array<State> = new Array();
 | 
						|
 | 
						|
	static inline var stateCount = 60 * 10; // 10 seconds with 60 fps
 | 
						|
 | 
						|
	#else
 | 
						|
	var localClient: Client;
 | 
						|
 | 
						|
	public var network: Network;
 | 
						|
 | 
						|
	var updateTaskId: Int;
 | 
						|
	var pingTaskId: Int;
 | 
						|
	#end
 | 
						|
 | 
						|
	public var me(get, null): Client;
 | 
						|
 | 
						|
	function get_me(): Client {
 | 
						|
		#if sys_server
 | 
						|
		return current;
 | 
						|
		#else
 | 
						|
		return localClient;
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	public function new(maxPlayers: Int, address: String, port: Int) {
 | 
						|
		instance = this;
 | 
						|
		this.maxPlayers = maxPlayers;
 | 
						|
		this.address = address;
 | 
						|
		this.port = port;
 | 
						|
	}
 | 
						|
 | 
						|
	public static function the(): Session {
 | 
						|
		return instance;
 | 
						|
	}
 | 
						|
 | 
						|
	public function addEntity(entity: Entity): Void {
 | 
						|
		entities.set(entity._id(), entity);
 | 
						|
	}
 | 
						|
 | 
						|
	public function addController(controller: Controller): Void {
 | 
						|
		trace("Adding controller id " + controller._id());
 | 
						|
		controller._inputBufferIndex = 0;
 | 
						|
		controllers.set(controller._id(), controller);
 | 
						|
	}
 | 
						|
 | 
						|
	#if sys_server
 | 
						|
	private function send(): Bytes {
 | 
						|
		var size = 0;
 | 
						|
		for (entity in entities) {
 | 
						|
			size += entity._size();
 | 
						|
		}
 | 
						|
		var offset = 0;
 | 
						|
		var bytes = Bytes.alloc(size + 9);
 | 
						|
		bytes.set(offset, ENTITY_UPDATES);
 | 
						|
		offset += 1;
 | 
						|
		bytes.setDouble(offset, Scheduler.time());
 | 
						|
		offset += 8;
 | 
						|
		for (entity in entities) {
 | 
						|
			entity._send(offset, bytes);
 | 
						|
			offset += entity._size();
 | 
						|
		}
 | 
						|
 | 
						|
		lastStates.push(new State(Scheduler.time(), bytes));
 | 
						|
		if (lastStates.length > stateCount) {
 | 
						|
			lastStates.splice(0, 1);
 | 
						|
		}
 | 
						|
 | 
						|
		return bytes;
 | 
						|
	}
 | 
						|
	#end
 | 
						|
 | 
						|
	public function sendControllerUpdate(id: Int, bytes: haxe.io.Bytes) {
 | 
						|
		#if !sys_server
 | 
						|
		if (controllers.exists(id)) {
 | 
						|
			if (controllers[id]._inputBuffer.length < controllers[id]._inputBufferIndex + 4 + bytes.length) {
 | 
						|
				var newBuffer = Bytes.alloc(controllers[id]._inputBufferIndex + 4 + bytes.length);
 | 
						|
				newBuffer.blit(0, controllers[id]._inputBuffer, 0, controllers[id]._inputBufferIndex);
 | 
						|
				controllers[id]._inputBuffer = newBuffer;
 | 
						|
			}
 | 
						|
 | 
						|
			controllers[id]._inputBuffer.setInt32(controllers[id]._inputBufferIndex, bytes.length);
 | 
						|
			controllers[id]._inputBuffer.blit(controllers[id]._inputBufferIndex + 4, bytes, 0, bytes.length);
 | 
						|
			controllers[id]._inputBufferIndex += (4 + bytes.length);
 | 
						|
		}
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	function sendPing() {
 | 
						|
		#if !sys_server
 | 
						|
		var bytes = haxe.io.Bytes.alloc(5);
 | 
						|
		bytes.set(0, kha.netsync.Session.PING);
 | 
						|
		bytes.setFloat(1, Scheduler.realTime());
 | 
						|
 | 
						|
		sendToServer(bytes);
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	function sendPlayerUpdate() {
 | 
						|
		#if sys_server
 | 
						|
		currentPlayers = clients.length;
 | 
						|
		var bytes = haxe.io.Bytes.alloc(5);
 | 
						|
		bytes.set(0, PLAYER_UPDATES);
 | 
						|
		bytes.setInt32(1, currentPlayers);
 | 
						|
 | 
						|
		sendToEverybody(bytes);
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	public function receive(bytes: Bytes, client: Client = null): Void {
 | 
						|
		#if sys_server
 | 
						|
		switch (bytes.get(0)) {
 | 
						|
			case CONTROLLER_UPDATES:
 | 
						|
				var id = bytes.getInt32(1);
 | 
						|
				var time = bytes.getDouble(5);
 | 
						|
 | 
						|
				var width = bytes.getInt32(13);
 | 
						|
				var height = bytes.getInt32(17);
 | 
						|
				var rotation = bytes.get(21);
 | 
						|
				SystemImpl._updateSize(width, height);
 | 
						|
				SystemImpl._updateScreenRotation(rotation);
 | 
						|
 | 
						|
				if (controllers.exists(id)) {
 | 
						|
					processEventRetroactively(function() {
 | 
						|
						current = client;
 | 
						|
						var offset = 22;
 | 
						|
						while (offset < bytes.length) {
 | 
						|
							var length = bytes.getInt32(offset);
 | 
						|
							controllers[id]._receive(bytes.sub(offset + 4, length));
 | 
						|
							offset += (4 + length);
 | 
						|
						}
 | 
						|
						current = null;
 | 
						|
					}, time);
 | 
						|
				}
 | 
						|
			case REMOTE_CALL:
 | 
						|
				processRPC(bytes);
 | 
						|
			case PING:
 | 
						|
				// PONG, i.e. just return the packet to the client
 | 
						|
				if (client != null)
 | 
						|
					client.send(bytes, false);
 | 
						|
		}
 | 
						|
		#else
 | 
						|
		switch (bytes.get(0)) {
 | 
						|
			case START:
 | 
						|
				var index = bytes.get(1);
 | 
						|
				localClient = new LocalClient(index);
 | 
						|
				Scheduler.resetTime();
 | 
						|
				startCallback();
 | 
						|
			case ENTITY_UPDATES:
 | 
						|
				var time = bytes.getDouble(1);
 | 
						|
				var offset = 9;
 | 
						|
				for (entity in entities) {
 | 
						|
					entity._receive(offset, bytes);
 | 
						|
					offset += entity._size();
 | 
						|
				}
 | 
						|
				Scheduler.warp(time);
 | 
						|
			case REMOTE_CALL:
 | 
						|
				switch (bytes.get(1)) {
 | 
						|
					case RPC_SERVER:
 | 
						|
					// Mainly a safeguard, packets with RPC_SERVER should not be received here
 | 
						|
					case RPC_ALL:
 | 
						|
						executeRPC(bytes);
 | 
						|
				}
 | 
						|
			case PING:
 | 
						|
				var sendTime = bytes.getFloat(1);
 | 
						|
				ping = Scheduler.realTime() - sendTime;
 | 
						|
			case SESSION_ERROR:
 | 
						|
				refusedCallback();
 | 
						|
			case PLAYER_UPDATES:
 | 
						|
				currentPlayers = bytes.getInt32(1);
 | 
						|
		}
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	#if sys_server
 | 
						|
	private function processEventRetroactively(event: Void->Void, time: Float) {
 | 
						|
		if (time <= Scheduler.time()) {
 | 
						|
			// var temp = time;
 | 
						|
			// Process after earliest saved state if it is too far back
 | 
						|
			if (time <= lastStates[0].time) {
 | 
						|
				time = lastStates[0].time + 0.00001;
 | 
						|
			}
 | 
						|
 | 
						|
			var i = lastStates.length - 1;
 | 
						|
			while (i >= 0) {
 | 
						|
				if (lastStates[i].time < time) {
 | 
						|
					var offset = 9;
 | 
						|
					for (entity in entities) {
 | 
						|
						entity._receive(offset, lastStates[i].data);
 | 
						|
						offset += entity._size();
 | 
						|
					}
 | 
						|
					// Invalidate states in which the new event is missing
 | 
						|
					if (i < lastStates.length - 1) {
 | 
						|
						lastStates.splice(i + 1, lastStates.length - i - 1);
 | 
						|
					}
 | 
						|
					Scheduler.warp(lastStates[i].time);
 | 
						|
					break;
 | 
						|
				}
 | 
						|
				--i;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		Scheduler.addTimeTask(event, time - Scheduler.time());
 | 
						|
	}
 | 
						|
	#end
 | 
						|
 | 
						|
	#if sys_server
 | 
						|
	public function processRPC(bytes: Bytes) {
 | 
						|
		switch (bytes.get(1)) {
 | 
						|
			case RPC_SERVER:
 | 
						|
				executeRPC(bytes);
 | 
						|
			case RPC_ALL:
 | 
						|
				sendToEverybody(bytes);
 | 
						|
				executeRPC(bytes);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	#end
 | 
						|
 | 
						|
	function executeRPC(bytes: Bytes) {
 | 
						|
		var args = new Array<Dynamic>();
 | 
						|
		var syncId = bytes.getInt32(2);
 | 
						|
		var index: Int = 6;
 | 
						|
 | 
						|
		var classnamelength = bytes.getUInt16(index);
 | 
						|
		index += 2;
 | 
						|
		var classname = "";
 | 
						|
		for (i in 0...classnamelength) {
 | 
						|
			classname += String.fromCharCode(bytes.get(index));
 | 
						|
			++index;
 | 
						|
		}
 | 
						|
 | 
						|
		var methodnamelength = bytes.getUInt16(index);
 | 
						|
		index += 2;
 | 
						|
		var methodname = "";
 | 
						|
		for (i in 0...methodnamelength) {
 | 
						|
			methodname += String.fromCharCode(bytes.get(index));
 | 
						|
			++index;
 | 
						|
		}
 | 
						|
 | 
						|
		while (index < bytes.length) {
 | 
						|
			var type = bytes.get(index);
 | 
						|
			++index;
 | 
						|
			switch (type) {
 | 
						|
				case 0x42: // B
 | 
						|
					var value: Bool = bytes.get(index) == 1;
 | 
						|
					++index;
 | 
						|
					trace("Bool: " + value);
 | 
						|
					args.push(value);
 | 
						|
				case 0x46: // F
 | 
						|
					var value: Float = bytes.getDouble(index);
 | 
						|
					index += 8;
 | 
						|
					trace("Float: " + value);
 | 
						|
					args.push(value);
 | 
						|
				case 0x49: // I
 | 
						|
					var value: Int = bytes.getInt32(index);
 | 
						|
					index += 4;
 | 
						|
					trace("Int: " + value);
 | 
						|
					args.push(value);
 | 
						|
				case 0x53: // S
 | 
						|
					var length = bytes.getUInt16(index);
 | 
						|
					index += 2;
 | 
						|
					var str = "";
 | 
						|
					for (i in 0...length) {
 | 
						|
						str += String.fromCharCode(bytes.get(index));
 | 
						|
						++index;
 | 
						|
					}
 | 
						|
					trace("String: " + str);
 | 
						|
					args.push(str);
 | 
						|
				default:
 | 
						|
					trace("Unknown argument type.");
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (syncId == -1) {
 | 
						|
			Reflect.callMethod(null, Reflect.field(Type.resolveClass(classname), methodname + "_remotely"), args);
 | 
						|
		}
 | 
						|
		else {
 | 
						|
			Reflect.callMethod(SyncBuilder.objects[syncId], Reflect.field(SyncBuilder.objects[syncId], methodname + "_remotely"), args);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public function waitForStart(callback: Void->Void, refuseCallback: Void->Void, errorCallback: Void->Void, closeCallback: Void->Void,
 | 
						|
			resCallback: Void->Void): Void {
 | 
						|
		startCallback = callback;
 | 
						|
		refusedCallback = refuseCallback;
 | 
						|
		resetCallback = resCallback;
 | 
						|
		#if sys_server
 | 
						|
		isJoinable = true;
 | 
						|
		#if direct_connection
 | 
						|
		trace("Starting server at " + port + ".");
 | 
						|
		#end
 | 
						|
		server = new Server(port);
 | 
						|
		server.onConnection(function(client: Client) {
 | 
						|
			if (!isJoinable) {
 | 
						|
				var bytes = Bytes.alloc(1);
 | 
						|
				bytes.set(0, SESSION_ERROR);
 | 
						|
				client.send(bytes, true);
 | 
						|
				return;
 | 
						|
			}
 | 
						|
 | 
						|
			clients.push(client);
 | 
						|
			current = client;
 | 
						|
 | 
						|
			Node.console.log(clients.length + " client" + (clients.length > 1 ? "s " : " ") + "connected.");
 | 
						|
			sendPlayerUpdate();
 | 
						|
 | 
						|
			client.receive(function(bytes: Bytes) {
 | 
						|
				receive(bytes, client);
 | 
						|
			});
 | 
						|
 | 
						|
			client.onClose(function() {
 | 
						|
				Node.console.log("Removing client " + client.id + ".");
 | 
						|
				clients.remove(client);
 | 
						|
				sendPlayerUpdate();
 | 
						|
				// isJoinable is intentionally not reset here immediately, as late joining is currently unsupported
 | 
						|
				if (clients.length == 0) {
 | 
						|
					reset();
 | 
						|
				}
 | 
						|
			});
 | 
						|
 | 
						|
			if (clients.length >= maxPlayers) {
 | 
						|
				isJoinable = false;
 | 
						|
				Node.console.log("Starting game.");
 | 
						|
				var index = 0;
 | 
						|
				for (c in clients) {
 | 
						|
					trace("Starting client " + c.id);
 | 
						|
					var bytes = Bytes.alloc(2);
 | 
						|
					bytes.set(0, START);
 | 
						|
					bytes.set(1, index);
 | 
						|
					c.send(bytes, true);
 | 
						|
					++index;
 | 
						|
				}
 | 
						|
				Scheduler.resetTime();
 | 
						|
				startCallback();
 | 
						|
			}
 | 
						|
		});
 | 
						|
		#else
 | 
						|
		network = new Network(address, port, errorCallback, function() {
 | 
						|
			closeCallback();
 | 
						|
			reset();
 | 
						|
		});
 | 
						|
		network.listen(function(bytes: Bytes) {
 | 
						|
			receive(bytes);
 | 
						|
		});
 | 
						|
		updateTaskId = Scheduler.addFrameTask(update, 0);
 | 
						|
		ping = 1;
 | 
						|
		pingTaskId = Scheduler.addTimeTask(sendPing, 0, 1);
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	function reset() {
 | 
						|
		#if sys_server
 | 
						|
		isJoinable = true;
 | 
						|
		server.reset();
 | 
						|
		#else
 | 
						|
		Scheduler.removeFrameTask(updateTaskId);
 | 
						|
		Scheduler.removeTimeTask(pingTaskId);
 | 
						|
		#end
 | 
						|
		currentPlayers = 0;
 | 
						|
		ping = 1;
 | 
						|
		controllers = new Map();
 | 
						|
		entities = new Map();
 | 
						|
		resetCallback();
 | 
						|
	}
 | 
						|
 | 
						|
	public function update(): Void {
 | 
						|
		#if sys_server
 | 
						|
		var bytes = send();
 | 
						|
		sendToEverybody(bytes);
 | 
						|
		#else
 | 
						|
		for (controller in controllers) {
 | 
						|
			if (controller._inputBufferIndex > 0) {
 | 
						|
				var bytes = haxe.io.Bytes.alloc(22 + controller._inputBufferIndex);
 | 
						|
				bytes.set(0, kha.netsync.Session.CONTROLLER_UPDATES);
 | 
						|
				bytes.setInt32(1, controller._id());
 | 
						|
				bytes.setDouble(5, Scheduler.time());
 | 
						|
				bytes.setInt32(13, System.windowWidth(0));
 | 
						|
				bytes.setInt32(17, System.windowHeight(0));
 | 
						|
				bytes.set(21, 0); // System.screenRotation.getIndex());
 | 
						|
 | 
						|
				bytes.blit(22, controller._inputBuffer, 0, controller._inputBufferIndex);
 | 
						|
 | 
						|
				sendToServer(bytes);
 | 
						|
				controller._inputBufferIndex = 0;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		#end
 | 
						|
	}
 | 
						|
 | 
						|
	#if sys_server
 | 
						|
	public function sendToEverybody(bytes: Bytes): Void {
 | 
						|
		for (client in clients) {
 | 
						|
			client.send(bytes, false);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	#end
 | 
						|
 | 
						|
	#if !sys_server
 | 
						|
	public function sendToServer(bytes: Bytes): Void {
 | 
						|
		network.send(bytes, false);
 | 
						|
	}
 | 
						|
	#end
 | 
						|
}
 |