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 | ||
|  | } |