├── .gitignore ├── Client.lnk ├── Client ├── Client.hxproj ├── Project.xml ├── assets │ ├── data │ │ └── data-goes-here.txt │ ├── images │ │ └── images-go-here.txt │ ├── music │ │ └── music-goes-here.txt │ └── sounds │ │ └── sounds-go-here.txt └── source │ ├── AssetPaths.hx │ ├── Client.hx │ ├── Main.hx │ ├── PlayState.hx │ └── Reg.hx ├── Clumsy.lnk ├── README.md ├── Server.lnk ├── Server ├── Project.xml ├── Server.hxproj ├── assets │ ├── data │ │ └── data-goes-here.txt │ ├── images │ │ └── images-go-here.txt │ ├── music │ │ └── music-goes-here.txt │ └── sounds │ │ └── sounds-go-here.txt └── source │ ├── AssetPaths.hx │ ├── Main.hx │ ├── PlayState.hx │ ├── Reg.hx │ └── Server.hx ├── Shared └── code │ ├── KeyState.hx │ ├── Msg.hx │ ├── Player.hx │ └── enet │ ├── BaseEvent.hx │ ├── Client.hx │ ├── ENet.hx │ ├── ENetEvent.hx │ ├── Message.hx │ ├── NetBase.hx │ ├── Server.hx │ ├── inter │ └── INetBase.hx │ └── tcp │ ├── TCPClient.hx │ ├── TCPEvent.hx │ ├── TCPNetBase.hx │ └── TCPServer.hx ├── doc ├── Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo.html └── Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo_files │ ├── 162488195-postmessagerelay.js │ ├── 1ldYU13brY_(1).html │ ├── 1ldYU13brY_.html │ ├── all.js │ ├── api.js │ ├── cb=gapi.loaded_0 │ ├── cb=gapi.loaded_1 │ ├── core-rpc-shindig.random-shindig.sha1.js │ ├── count.json │ ├── fastbutton.html │ ├── ga.js │ ├── like.html │ ├── plusone.js │ ├── postmessageRelay.html │ ├── tweet_button.55a4019ea66c5d005a6e6d9d41c5e068.en.html │ └── widgets.js └── ndll ├── EnetTesting.ndll ├── libgcc_s_dw2-1.dll └── libstdc++-6.dll /.gitignore: -------------------------------------------------------------------------------- 1 | export 2 | -------------------------------------------------------------------------------- /Client.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Client.lnk -------------------------------------------------------------------------------- /Client/Client.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Client/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Client/assets/data/data-goes-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Client/assets/data/data-goes-here.txt -------------------------------------------------------------------------------- /Client/assets/images/images-go-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Client/assets/images/images-go-here.txt -------------------------------------------------------------------------------- /Client/assets/music/music-goes-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Client/assets/music/music-goes-here.txt -------------------------------------------------------------------------------- /Client/assets/sounds/sounds-go-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Client/assets/sounds/sounds-go-here.txt -------------------------------------------------------------------------------- /Client/source/AssetPaths.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | @:build(flixel.system.FlxAssets.buildFileReferences("assets", true)) 4 | class AssetPaths {} -------------------------------------------------------------------------------- /Client/source/Client.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import enet.ENetEvent; 3 | import flixel.FlxG; 4 | 5 | /** 6 | * ... 7 | * @author Ohmnivore 8 | */ 9 | class Client extends enet.Client 10 | { 11 | public var pendingInputs:Array = []; 12 | 13 | public function new(IP:String = "localhost", Port:Int = 1337) 14 | { 15 | trace("Client connecting to " + IP + ":" + Port); 16 | super(IP, Port); 17 | } 18 | 19 | override public function onPeerConnect(e:ENetEvent):Void 20 | { 21 | trace("Peer connected " + e.ID); 22 | peers.set(e.ID, null); 23 | super.onPeerConnect(e); 24 | } 25 | 26 | override public function onPeerDisonnect(e:ENetEvent):Void 27 | { 28 | trace("Peer disconnected " + e.ID); 29 | peers.remove(e.ID); 30 | super.onPeerDisonnect(e); 31 | } 32 | 33 | override public function onReceive(MsgID:Int, E:ENetEvent):Void 34 | { 35 | super.onReceive(MsgID, E); 36 | 37 | if (MsgID == Msg.setPos.ID) 38 | { 39 | var x:Float = Msg.setPos.data.get("x"); 40 | var lastProcessedInput:Int = Msg.setPos.data.get("lastProcessedInput"); 41 | 42 | Reg.state.player.x = x; 43 | 44 | //trace("lastprocessed: " + lastProcessedInput); 45 | //trace("pending: " + pendingInputs.length); 46 | var j:Int = 0; 47 | while (j < pendingInputs.length) 48 | { 49 | var input:KeyState = pendingInputs[j]; 50 | if (input.sequenceNumber <= lastProcessedInput) // recvd from server 51 | { 52 | pendingInputs.splice(j, 1); 53 | } 54 | else 55 | { 56 | //trace("interpreting: " + j); 57 | if (input.left || input.right) 58 | { 59 | Reg.state.player.interpretKeyState(input); 60 | Reg.state.player.update(FlxG.elapsed); 61 | } 62 | j++; 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Client/source/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flash.display.Sprite; 4 | import flash.display.StageAlign; 5 | import flash.display.StageScaleMode; 6 | import flash.events.Event; 7 | import flash.Lib; 8 | import flixel.FlxGame; 9 | import flixel.FlxState; 10 | 11 | class Main extends Sprite 12 | { 13 | var gameWidth:Int = 640; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom). 14 | var gameHeight:Int = 480; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom). 15 | var initialState:Class = PlayState; // The FlxState the game starts with. 16 | var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions. 17 | var framerate:Int = 48; // How many frames per second the game should run at. 18 | var skipSplash:Bool = false; // Whether to skip the flixel splash screen that appears in release mode. 19 | var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets 20 | 21 | // You can pretty much ignore everything from here on - your code should go in your states. 22 | 23 | public static function main():Void 24 | { 25 | Lib.current.addChild(new Main()); 26 | } 27 | 28 | public function new() 29 | { 30 | super(); 31 | 32 | if (stage != null) 33 | { 34 | init(); 35 | } 36 | else 37 | { 38 | addEventListener(Event.ADDED_TO_STAGE, init); 39 | } 40 | } 41 | 42 | private function init(?E:Event):Void 43 | { 44 | if (hasEventListener(Event.ADDED_TO_STAGE)) 45 | { 46 | removeEventListener(Event.ADDED_TO_STAGE, init); 47 | } 48 | 49 | setupGame(); 50 | } 51 | 52 | private function setupGame():Void 53 | { 54 | var stageWidth:Int = Lib.current.stage.stageWidth; 55 | var stageHeight:Int = Lib.current.stage.stageHeight; 56 | 57 | if (zoom == -1) 58 | { 59 | var ratioX:Float = stageWidth / gameWidth; 60 | var ratioY:Float = stageHeight / gameHeight; 61 | zoom = Math.min(ratioX, ratioY); 62 | gameWidth = Math.ceil(stageWidth / zoom); 63 | gameHeight = Math.ceil(stageHeight / zoom); 64 | } 65 | 66 | addChild(new FlxGame(gameWidth, gameHeight, initialState, zoom, framerate, framerate, skipSplash, startFullscreen)); 67 | } 68 | } -------------------------------------------------------------------------------- /Client/source/PlayState.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import cpp.vm.Mutex; 4 | import cpp.vm.Thread; 5 | import enet.ENet; 6 | import flixel.FlxG; 7 | import flixel.FlxSprite; 8 | import flixel.FlxState; 9 | import flixel.text.FlxText; 10 | import flixel.ui.FlxButton; 11 | 12 | /** 13 | * A FlxState which can be used for the actual gameplay. 14 | */ 15 | class PlayState extends FlxState 16 | { 17 | public var player:Player; 18 | public var client:Client; 19 | public var sequenceNumber:Int = 0; 20 | 21 | /** 22 | * Function that is called up when to state is created to set it up. 23 | */ 24 | override public function create():Void 25 | { 26 | super.create(); 27 | Reg.state = this; 28 | FlxG.autoPause = false; 29 | FlxG.log.redirectTraces = true; 30 | FlxG.debugger.visible = true; 31 | 32 | Reg.m = new Mutex(); 33 | ENet.init(); 34 | client = new Client(); 35 | 36 | Msg.init(); 37 | Msg.addToBase(client); 38 | 39 | player = new Player(128, 128); 40 | add(player); 41 | 42 | Thread.create(updateClient); 43 | } 44 | 45 | private function updateClient():Void 46 | { 47 | while (true) 48 | { 49 | Reg.m.acquire(); 50 | client.poll(); 51 | Reg.m.release(); 52 | Sys.sleep(0.001); 53 | } 54 | } 55 | 56 | /** 57 | * Function that is called when this state is destroyed - you might want to 58 | * consider setting all objects this state uses to null to help garbage collection. 59 | */ 60 | override public function destroy():Void 61 | { 62 | super.destroy(); 63 | } 64 | 65 | /** 66 | * Function that is called once every frame. 67 | */ 68 | override public function update(Elapsed:Float):Void 69 | { 70 | Reg.m.acquire(); 71 | client.poll(); 72 | 73 | var k:KeyState = getKeyState(); 74 | player.interpretKeyState(k); 75 | client.pendingInputs.push(k); 76 | 77 | Msg.keyState.data.set("left", k.left); 78 | Msg.keyState.data.set("right", k.right); 79 | Msg.keyState.data.set("sequenceNumber", k.sequenceNumber); 80 | for (p in client.peers.keys()) 81 | client.sendMsg(p, Msg.keyState.ID); 82 | 83 | super.update(Elapsed); 84 | Reg.m.release(); 85 | } 86 | 87 | private function getKeyState():KeyState 88 | { 89 | var k:KeyState = new KeyState(); 90 | k.left = FlxG.keys.pressed.A || FlxG.keys.pressed.LEFT; 91 | k.right = FlxG.keys.pressed.D || FlxG.keys.pressed.RIGHT; 92 | k.sequenceNumber = sequenceNumber++; 93 | return k; 94 | } 95 | } -------------------------------------------------------------------------------- /Client/source/Reg.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import cpp.vm.Mutex; 4 | import flixel.util.FlxSave; 5 | 6 | /** 7 | * Handy, pre-built Registry class that can be used to store 8 | * references to objects and other things for quick-access. Feel 9 | * free to simply ignore it or change it in any way you like. 10 | */ 11 | class Reg 12 | { 13 | public static var state:PlayState; 14 | public static var m:Mutex; 15 | 16 | /** 17 | * Generic levels Array that can be used for cross-state stuff. 18 | * Example usage: Storing the levels of a platformer. 19 | */ 20 | public static var levels:Array = []; 21 | /** 22 | * Generic level variable that can be used for cross-state stuff. 23 | * Example usage: Storing the current level number. 24 | */ 25 | public static var level:Int = 0; 26 | /** 27 | * Generic scores Array that can be used for cross-state stuff. 28 | * Example usage: Storing the scores for level. 29 | */ 30 | public static var scores:Array = []; 31 | /** 32 | * Generic score variable that can be used for cross-state stuff. 33 | * Example usage: Storing the current score. 34 | */ 35 | public static var score:Int = 0; 36 | /** 37 | * Generic bucket for storing different FlxSaves. 38 | * Especially useful for setting up multiple save slots. 39 | */ 40 | public static var saves:Array = []; 41 | } -------------------------------------------------------------------------------- /Clumsy.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Clumsy.lnk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What 2 | This is a port of Gabriel Gambetta's demo of client-side prediction and server reconciliation 3 | in networking. It uses the HaxeFlixel engine (4.0.0, commit a77081706b0c7457270ce9fb8a4285345240065b), 4 | and HaxeNet (https://github.com/Ohmnivore/HaxeNet). I have included the original demo in the /doc directory, 5 | in case its source location (http://www.gabrielgambetta.com/fpm_live.html) becomes inaccessible. 6 | 7 | I use clumsy (https://github.com/jagt/clumsy) to simulate lag on my computer. It's awesome. 8 | 9 | # Why 10 | My previous attempts at multiplayer games were only playable on LAN because I didn't know how to 11 | properly handle lag. Gabriel's demo taught me a lot on how to make multiplayer implementations playable 12 | at high latencies. 13 | 14 | # Note 15 | * Windows-only 16 | * Once you compile the client and server, drop the contents of the /ndll directory into both executables' directories 17 | * Uses localhost and port 1337 18 | * Run server, then run client and use left/right or a/d keys in client -------------------------------------------------------------------------------- /Server.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Server.lnk -------------------------------------------------------------------------------- /Server/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Server/Server.hxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Server/assets/data/data-goes-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Server/assets/data/data-goes-here.txt -------------------------------------------------------------------------------- /Server/assets/images/images-go-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Server/assets/images/images-go-here.txt -------------------------------------------------------------------------------- /Server/assets/music/music-goes-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Server/assets/music/music-goes-here.txt -------------------------------------------------------------------------------- /Server/assets/sounds/sounds-go-here.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/Server/assets/sounds/sounds-go-here.txt -------------------------------------------------------------------------------- /Server/source/AssetPaths.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | @:build(flixel.system.FlxAssets.buildFileReferences("assets", true)) 4 | class AssetPaths {} -------------------------------------------------------------------------------- /Server/source/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import flash.display.Sprite; 4 | import flash.display.StageAlign; 5 | import flash.display.StageScaleMode; 6 | import flash.events.Event; 7 | import flash.Lib; 8 | import flixel.FlxGame; 9 | import flixel.FlxState; 10 | 11 | class Main extends Sprite 12 | { 13 | var gameWidth:Int = 640; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom). 14 | var gameHeight:Int = 480; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom). 15 | var initialState:Class = PlayState; // The FlxState the game starts with. 16 | var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions. 17 | var framerate:Int = 48; // How many frames per second the game should run at. 18 | var skipSplash:Bool = false; // Whether to skip the flixel splash screen that appears in release mode. 19 | var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets 20 | 21 | // You can pretty much ignore everything from here on - your code should go in your states. 22 | 23 | public static function main():Void 24 | { 25 | Lib.current.addChild(new Main()); 26 | } 27 | 28 | public function new() 29 | { 30 | super(); 31 | 32 | if (stage != null) 33 | { 34 | init(); 35 | } 36 | else 37 | { 38 | addEventListener(Event.ADDED_TO_STAGE, init); 39 | } 40 | } 41 | 42 | private function init(?E:Event):Void 43 | { 44 | if (hasEventListener(Event.ADDED_TO_STAGE)) 45 | { 46 | removeEventListener(Event.ADDED_TO_STAGE, init); 47 | } 48 | 49 | setupGame(); 50 | } 51 | 52 | private function setupGame():Void 53 | { 54 | var stageWidth:Int = Lib.current.stage.stageWidth; 55 | var stageHeight:Int = Lib.current.stage.stageHeight; 56 | 57 | if (zoom == -1) 58 | { 59 | var ratioX:Float = stageWidth / gameWidth; 60 | var ratioY:Float = stageHeight / gameHeight; 61 | zoom = Math.min(ratioX, ratioY); 62 | gameWidth = Math.ceil(stageWidth / zoom); 63 | gameHeight = Math.ceil(stageHeight / zoom); 64 | } 65 | 66 | addChild(new FlxGame(gameWidth, gameHeight, initialState, zoom, framerate, framerate, skipSplash, startFullscreen)); 67 | } 68 | } -------------------------------------------------------------------------------- /Server/source/PlayState.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import cpp.vm.Mutex; 4 | import cpp.vm.Thread; 5 | import enet.ENet; 6 | import flixel.FlxG; 7 | import flixel.FlxState; 8 | 9 | /** 10 | * A FlxState which can be used for the actual gameplay. 11 | */ 12 | class PlayState extends FlxState 13 | { 14 | public var server:Server; 15 | public var player:Player; 16 | public var lastProcessedInput:Int = 0; 17 | 18 | /** 19 | * Function that is called up when to state is created to set it up. 20 | */ 21 | override public function create():Void 22 | { 23 | super.create(); 24 | Reg.state = this; 25 | FlxG.autoPause = false; 26 | FlxG.log.redirectTraces = true; 27 | FlxG.debugger.visible = true; 28 | 29 | Reg.m = new Mutex(); 30 | ENet.init(); 31 | server = new Server(); 32 | Msg.init(); 33 | Msg.addToBase(server); 34 | 35 | player = new Player(256, 128); 36 | add(player); 37 | 38 | Thread.create(updateServer); 39 | } 40 | 41 | private function updateServer():Void 42 | { 43 | while (true) 44 | { 45 | Reg.m.acquire(); 46 | server.poll(); 47 | Reg.m.release(); 48 | Sys.sleep(0.001); 49 | } 50 | } 51 | 52 | /** 53 | * Function that is called when this state is destroyed - you might want to 54 | * consider setting all objects this state uses to null to help garbage collection. 55 | */ 56 | override public function destroy():Void 57 | { 58 | super.destroy(); 59 | } 60 | 61 | /** 62 | * Function that is called once every frame. 63 | */ 64 | override public function update(Elapsed:Float):Void 65 | { 66 | Reg.m.acquire(); 67 | server.poll(); 68 | super.update(Elapsed); 69 | Reg.m.release(); 70 | 71 | Msg.setPos.data.set("x", player.x); 72 | Msg.setPos.data.set("lastProcessedInput", lastProcessedInput); 73 | for (p in server.peers.keys()) 74 | server.sendMsg(p, Msg.setPos.ID); 75 | } 76 | } -------------------------------------------------------------------------------- /Server/source/Reg.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import cpp.vm.Mutex; 4 | import flixel.util.FlxSave; 5 | 6 | /** 7 | * Handy, pre-built Registry class that can be used to store 8 | * references to objects and other things for quick-access. Feel 9 | * free to simply ignore it or change it in any way you like. 10 | */ 11 | class Reg 12 | { 13 | public static var state:PlayState; 14 | public static var m:Mutex; 15 | 16 | /** 17 | * Generic levels Array that can be used for cross-state stuff. 18 | * Example usage: Storing the levels of a platformer. 19 | */ 20 | public static var levels:Array = []; 21 | /** 22 | * Generic level variable that can be used for cross-state stuff. 23 | * Example usage: Storing the current level number. 24 | */ 25 | public static var level:Int = 0; 26 | /** 27 | * Generic scores Array that can be used for cross-state stuff. 28 | * Example usage: Storing the scores for level. 29 | */ 30 | public static var scores:Array = []; 31 | /** 32 | * Generic score variable that can be used for cross-state stuff. 33 | * Example usage: Storing the current score. 34 | */ 35 | public static var score:Int = 0; 36 | /** 37 | * Generic bucket for storing different FlxSaves. 38 | * Especially useful for setting up multiple save slots. 39 | */ 40 | public static var saves:Array = []; 41 | } -------------------------------------------------------------------------------- /Server/source/Server.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import enet.ENetEvent; 3 | 4 | /** 5 | * ... 6 | * @author Ohmnivore 7 | */ 8 | class Server extends enet.Server 9 | { 10 | public function new(IP:String = "localhost", Port:Int = 1337) 11 | { 12 | trace("Server listening at " + IP + ":" + Port); 13 | super(IP, Port, 2, 32); 14 | } 15 | 16 | override public function onPeerConnect(e:ENetEvent):Void 17 | { 18 | trace("Peer connected: " + e.ID); 19 | peers.set(e.ID, null); 20 | super.onPeerConnect(e); 21 | } 22 | 23 | override public function onPeerDisonnect(e:ENetEvent):Void 24 | { 25 | trace("Peer disconnected: " + e.ID); 26 | peers.remove(e.ID); 27 | super.onPeerDisonnect(e); 28 | } 29 | 30 | override public function onReceive(MsgID:Int, E:ENetEvent):Void 31 | { 32 | super.onReceive(MsgID, E); 33 | 34 | if (MsgID == Msg.keyState.ID) 35 | { 36 | var left:Bool = Msg.keyState.data.get("left"); 37 | var right:Bool = Msg.keyState.data.get("right"); 38 | var sequenceNumber:Int = Msg.keyState.data.get("sequenceNumber"); 39 | 40 | var k:KeyState = new KeyState(); 41 | k.left = left; 42 | k.right = right; 43 | k.sequenceNumber = sequenceNumber; 44 | Reg.state.player.interpretKeyState(k); 45 | Reg.state.lastProcessedInput = sequenceNumber; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Shared/code/KeyState.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | 3 | /** 4 | * ... 5 | * @author Ohmnivore 6 | */ 7 | class KeyState 8 | { 9 | public var left:Bool = false; 10 | public var right:Bool = false; 11 | public var sequenceNumber:Int = 0; 12 | 13 | public function new() 14 | { 15 | 16 | } 17 | 18 | public function copyFrom(K:KeyState):Void 19 | { 20 | left = K.left; 21 | right = K.right; 22 | sequenceNumber = K.sequenceNumber; 23 | } 24 | } -------------------------------------------------------------------------------- /Shared/code/Msg.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import enet.inter.INetBase; 3 | import enet.Message; 4 | 5 | /** 6 | * ... 7 | * @author Ohmnivore 8 | */ 9 | class Msg 10 | { 11 | static public var keyState:Message; 12 | static public var setPos:Message; 13 | 14 | static public function init():Void 15 | { 16 | keyState = new Message(5, ["left", "right", "sequenceNumber"]); 17 | setPos = new Message(6, ["x", "lastProcessedInput"], true); 18 | } 19 | 20 | static public function addToBase(I:INetBase):Void 21 | { 22 | I.addMessage(keyState); 23 | I.addMessage(setPos); 24 | } 25 | } -------------------------------------------------------------------------------- /Shared/code/Player.hx: -------------------------------------------------------------------------------- 1 | package ; 2 | import flixel.FlxSprite; 3 | 4 | /** 5 | * ... 6 | * @author Ohmnivore 7 | */ 8 | class Player extends FlxSprite 9 | { 10 | public function new(X:Float, Y:Float) 11 | { 12 | super(X, Y); 13 | makeGraphic(16, 16, 0xff00ff00); 14 | } 15 | 16 | public function interpretKeyState(K:KeyState):Void 17 | { 18 | velocity.x = 0; 19 | if (K.left) 20 | velocity.x = -128; 21 | else if (K.right) 22 | velocity.x = 128; 23 | } 24 | } -------------------------------------------------------------------------------- /Shared/code/enet/BaseEvent.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | class BaseEvent 4 | { 5 | /** 6 | * E_NONE, E_CONNECT, E_DISCONNECT or E_RECEIVE 7 | */ 8 | public var type:Int; 9 | 10 | /** 11 | * The sent content 12 | */ 13 | public var message:String = null; 14 | 15 | /** 16 | * The ID of the peer who sent/connected/disconnected 17 | */ 18 | public var ID:Int; 19 | 20 | /** 21 | * Nothing new here 22 | */ 23 | static public inline var E_NONE = 0; 24 | 25 | /** 26 | * Someone has connected/you've succeeded to connect as a client 27 | */ 28 | static public inline var E_CONNECT = 1; 29 | 30 | /** 31 | * A peer has disconnected/you've been disconnected from the server 32 | */ 33 | static public inline var E_DISCONNECT = 2; 34 | 35 | /** 36 | * You've received a message! 37 | */ 38 | static public inline var E_RECEIVE = 3; 39 | 40 | public function new(_type:Int, _ID:Int, _msg:String = null) 41 | { 42 | type = _type; 43 | ID = _ID; 44 | message = _msg; 45 | } 46 | } -------------------------------------------------------------------------------- /Shared/code/enet/Client.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | /** 4 | * ... 5 | * @author Ohmnivore 6 | */ 7 | class Client extends enet.NetBase 8 | { 9 | /** 10 | * Creates a new client, and connects it. 11 | * 12 | * @param IP The IP to connect to (in dotted quad format) 13 | * @param Port The port to connect to 14 | */ 15 | public function new(IP:String, Port:Int, Channels:Int = 2, Players:Int = 1) 16 | { 17 | super(); 18 | 19 | _host = ENet.client(IP, Port, Channels, Players); 20 | } 21 | } -------------------------------------------------------------------------------- /Shared/code/enet/ENet.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | /** 4 | * ... 5 | * @author Ohmnivore 6 | */ 7 | 8 | #if desktop 9 | import cpp.Lib; 10 | 11 | class ENet 12 | { 13 | /** 14 | * packet must be received by the target peer 15 | * and resend attempts should be made until 16 | * the packet is delivered 17 | */ 18 | static public inline var ENET_PACKET_FLAG_RELIABLE = (1 << 0); 19 | 20 | /** 21 | * packet will not be sequenced with other 22 | * packets not supported for reliable packets 23 | */ 24 | static public inline var ENET_PACKET_FLAG_UNSEQUENCED = (1 << 1); 25 | 26 | /** 27 | * packet will be fragmented using 28 | * unreliable (instead of reliable) 29 | * sends if it exceeds the MTU 30 | */ 31 | static public inline var ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT = (1 << 3); 32 | 33 | static public inline var BROADCAST_ADDRESS = "255.255.255.255"; 34 | 35 | /** 36 | * Should be called once before using the library. 37 | * Returns 0 if initialization is successfull. 38 | */ 39 | static public function init():Int 40 | { 41 | return _enet_init(); 42 | } 43 | 44 | /** 45 | * Bind the server. 46 | * Returns an ENetHost* (C object, to pass to other functions like sendMsg) 47 | * @param IP The IP address to bind to in xxx.xxx.xxx.xxx format (pass in null to bind to all available interfaces) 48 | */ 49 | static public function server(IP:String = null, Port:Int = 0, Channels:Int = 2, Players:Int = 32):Dynamic 50 | { 51 | return _enet_create_server(IP, Port, Channels, Players); 52 | } 53 | 54 | /** 55 | * Connect a client to the provided address. 56 | * The client binds to all interfaces, on a random available port. 57 | * Returns an ENetHost* (C object, to pass to other functions like sendMsg) 58 | * @param IP The remote host's IP address in xxx.xxx.xxx.xxx format 59 | */ 60 | static public function client(IP:String = null, Port:Int = 0, Channels:Int = 2, Players:Int = 1):Dynamic 61 | { 62 | return _enet_create_client(IP, Port, Channels, Players); 63 | } 64 | 65 | /** 66 | * This function returns an event, but also handles ENet's internal protocol. 67 | * Buffered messages are sent out when this function is called. Because it 68 | * keeps the ball rolling, call this regularly, like in your Flixel update function 69 | * 70 | * @param Host An ENetHost* 71 | * @param Timeout Just leave this at 0, unless you want your application to block up while waiting for the timeout. 72 | * @return The core, the man, the event of the century, the ENetEvent! 73 | */ 74 | static public function poll(Host:Dynamic, Timeout:Float = 0):ENetEvent 75 | { 76 | var e:Dynamic = _enet_poll(Host, Timeout); 77 | 78 | var r:ENetEvent = new ENetEvent(e); 79 | ENet.event_destroy(e); 80 | 81 | return r; 82 | } 83 | 84 | /** 85 | * Internal, don't touch. 86 | */ 87 | static public function event_type(Event:Dynamic):Int 88 | { 89 | return _enet_event_type(Event); 90 | } 91 | 92 | /** 93 | * Internal, don't touch. 94 | */ 95 | static public function event_channel(Event:Dynamic):Int 96 | { 97 | return _enet_event_channel(Event); 98 | } 99 | 100 | /** 101 | * Internal, don't touch. 102 | */ 103 | static public function event_message(Event:Dynamic):String 104 | { 105 | return _enet_event_message(Event); 106 | } 107 | 108 | /** 109 | * Internal, don't touch. 110 | */ 111 | static public function event_peer(Event:Dynamic):Int 112 | { 113 | return _enet_event_peer(Event); 114 | } 115 | 116 | /** 117 | * Internal, don't touch. 118 | */ 119 | static public function event_destroy(Event:Dynamic):Void 120 | { 121 | _enet_event_destroy(Event); 122 | } 123 | 124 | /** 125 | * Does what it says. 126 | * 127 | * @param Host An ENetHost* 128 | * @param ID Peer's ID 129 | * @param Content The string to send 130 | * @param Channel Which channel to send through 131 | * @param Flags ENet flags, use | to unite flags, if they don't conflict 132 | */ 133 | static public function sendMsg(Host:Dynamic, ID:Int, 134 | Content:String, Channel:Int = 0, Flags:Int = 0):Void 135 | { 136 | _enet_peer_send(Host, ID, Channel, Content, Flags); 137 | } 138 | 139 | /** 140 | * Still figuring out how to use this beast myself. 141 | */ 142 | static public function punchNAT(Host:Dynamic, Address:String, Port:Int, Data:String):Bool 143 | { 144 | return _enet_send_oob(Host, Address, Port, Data); 145 | } 146 | 147 | /** 148 | * Disconnects a connected peer, smoothly or forcefully. 149 | * The advantage of smoothly disconnecting is that the peer 150 | * will be notified of the disconnection. 151 | * 152 | * @param Host An ENetHost* 153 | * @param ID The peer's ID 154 | * @param Force Wether the peer will be notified of the disconnection or just outright dropped off the host 155 | */ 156 | static public function peerDisconnect(Host:Dynamic, ID:Int, Force:Bool):Void 157 | { 158 | _enet_peer_disconnect(Host, ID, Force); 159 | } 160 | 161 | static public function getPeerPing(ID:Int):Int 162 | { 163 | return _enet_get_peer_ping(ID); 164 | } 165 | 166 | static public function getLocalIP():String 167 | { 168 | var ip:Dynamic = _enet_get_local_ip(); 169 | 170 | if (ip == false) 171 | { 172 | return ""; 173 | } 174 | 175 | else 176 | { 177 | return ip; 178 | } 179 | } 180 | 181 | //All the C/C++ external loading 182 | static var _enet_init = Lib.load("EnetTesting", "enet_init", 0); 183 | static var _enet_create_server = Lib.load("EnetTesting", "enet_create_server", 4); 184 | static var _enet_create_client = Lib.load("EnetTesting", "enet_create_client", 4); 185 | static var _enet_poll = Lib.load("EnetTesting", "enet_poll", 2); 186 | static var _enet_send_oob = Lib.load("EnetTesting", "enet_send_oob", 4); 187 | 188 | static var _enet_event_type = Lib.load("EnetTesting", "enet_event_type", 1); 189 | static var _enet_event_channel = Lib.load("EnetTesting", "enet_event_channel", 1); 190 | static var _enet_event_message = Lib.load("EnetTesting", "enet_event_message", 1); 191 | static var _enet_event_peer = Lib.load("EnetTesting", "enet_event_peer", 1); 192 | static var _enet_event_destroy = Lib.load("EnetTesting", "enet_destroy_event", 1); 193 | static var _enet_get_peer_ping = Lib.load("EnetTesting", "enet_get_peer_ping", 1); 194 | static var _enet_get_local_ip = Lib.load("EnetTesting", "enet_get_printable_ip", 0); 195 | 196 | static var _enet_peer_send = Lib.load("EnetTesting", "enet_send_packet", 5); 197 | static var _enet_peer_disconnect = Lib.load("EnetTesting", "enet_disconnect_peer", 3); 198 | } 199 | #else 200 | class ENet 201 | { 202 | public function new() 203 | { 204 | 205 | } 206 | } 207 | #end -------------------------------------------------------------------------------- /Shared/code/enet/ENetEvent.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | #if desktop 4 | class ENetEvent extends BaseEvent 5 | { 6 | /** 7 | * The channel on which the event occured 8 | */ 9 | public var channel:Int; //-1 if null 10 | 11 | ///** 12 | //* The peer's IP, in int_32 format (decimal/long ip format, not dotted quad) 13 | //*/ 14 | //public var address:String = null; 15 | 16 | ///** 17 | //* The peer's port 18 | //*/ 19 | //public var port:Int; 20 | 21 | /** 22 | * Internal, you shouldn't create instances of this out of the blue 23 | */ 24 | public function new(EventFromC:Dynamic):Void 25 | { 26 | super(0, 0); 27 | 28 | type = ENet.event_type(EventFromC); 29 | 30 | channel = ENet.event_channel(EventFromC); 31 | 32 | if (type > BaseEvent.E_NONE) 33 | { 34 | //Setting address and port 35 | //var _addbuff:String = ENet.event_peer(EventFromC); 36 | //var _addbuff2:Array = _addbuff.split(":"); 37 | //address = _addbuff2[0]; 38 | //port = Std.parseInt(_addbuff2[1]); 39 | ID = ENet.event_peer(EventFromC); 40 | 41 | if (type == BaseEvent.E_RECEIVE) 42 | { 43 | message = ENet.event_message(EventFromC); 44 | } 45 | 46 | //ENet.event_destroy(EventFromC); 47 | } 48 | ENet.event_destroy(EventFromC); 49 | } 50 | } 51 | #else 52 | class ENetEvent 53 | { 54 | public function new() 55 | { 56 | 57 | } 58 | } 59 | #end -------------------------------------------------------------------------------- /Shared/code/enet/Message.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | import haxe.Serializer; 3 | import haxe.Unserializer; 4 | 5 | /** 6 | * ... 7 | * @author Ohmnivore 8 | */ 9 | class Message 10 | { 11 | /** 12 | * Unique identifier of this message. MUST be unique. 13 | */ 14 | public var ID:Int; 15 | 16 | /** 17 | * Use this to obtain received data and set data to be sent. 18 | * As keys, use the strings you pass in the Fields array 19 | * when you create the message. 20 | */ 21 | public var data:Map; 22 | 23 | /** 24 | * Whether incoming packets can set this message's data's contents. 25 | * Set this to true if your server only sends out this message, and 26 | * doesn't receive it. 27 | */ 28 | public var isServerSideOnly:Bool; 29 | 30 | /** 31 | * Internal, don't touch. 32 | */ 33 | private var fields:Array; 34 | 35 | /** 36 | * Internal, don't touch. 37 | */ 38 | private var _arr:Array; 39 | 40 | /** 41 | * @param id The message's ID. Every message should have an unique ID. 42 | * @param Fields An array of all the message's data object's fields 43 | * @param IsServerSideOnly If only a client may receive this message (necessary for security reasons) 44 | */ 45 | public function new(id:Int, Fields:Array, IsServerSideOnly:Bool = false) 46 | { 47 | ID = id; 48 | isServerSideOnly = IsServerSideOnly; 49 | fields = Fields; 50 | _arr = new Array(); 51 | data = new Map(); 52 | 53 | for (f in fields) 54 | { 55 | data.set(f, null); 56 | } 57 | } 58 | 59 | /** 60 | * Internal, don't touch. 61 | */ 62 | public function serialize():String 63 | { 64 | _arr.splice(0, _arr.length); 65 | 66 | var res:String = ""; 67 | 68 | res += Std.string(ID) + "."; 69 | 70 | for (f in fields) 71 | { 72 | _arr.push(data.get(f)); 73 | } 74 | 75 | res += Serializer.run(_arr); 76 | //trace(res); 77 | return res; 78 | } 79 | 80 | /** 81 | * Internal, don't touch. 82 | */ 83 | public function unserialize(S:String):Void 84 | { 85 | _arr.splice(0, _arr.length); 86 | 87 | _arr = Unserializer.run(S); 88 | 89 | var x:Int = 0; 90 | 91 | for (f in fields) 92 | { 93 | data.set(f, _arr[x]); 94 | //trace(_arr[x]); 95 | x++; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /Shared/code/enet/NetBase.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | import enet.inter.INetBase; 4 | 5 | /** 6 | * ... 7 | * @author Ohmnivore 8 | */ 9 | 10 | #if desktop 11 | class NetBase implements INetBase 12 | { 13 | /** 14 | * Whether this instance is a client or a server 15 | */ 16 | public var isServer:Bool; 17 | 18 | /** 19 | * A hash table you should use for keeping track of your clients, use ENet.peerKey() 20 | * to generate a key from an IP and a port 21 | */ 22 | public var peers:Map; 23 | 24 | /** 25 | * Internal host, stores an object returned by an external C++ function 26 | * You really shouldn't touch this. 27 | */ 28 | private var _host:Dynamic; 29 | 30 | /** 31 | * Internal hash table that keeps track of registered messages 32 | * You really shouldn't touch this either. 33 | */ 34 | private var messages:Map; 35 | 36 | /** 37 | * Initializes hash tables 38 | */ 39 | public function new() 40 | { 41 | peers = new Map(); 42 | messages = new Map(); 43 | } 44 | 45 | /** 46 | * Register a Message instance to this host. 47 | */ 48 | public function addMessage(M:Message):Void 49 | { 50 | messages.set(M.ID, M); 51 | } 52 | 53 | /** 54 | * Internal function, don't touch 55 | */ 56 | private function separateMessage(Str:String):Array 57 | { 58 | var _res:Array = []; 59 | 60 | var sep:Int = Str.indexOf("."); 61 | 62 | _res.push(Std.parseInt(Str.substr(0, sep))); 63 | 64 | _res.push(Str.substr(sep + 1)); 65 | 66 | return _res; 67 | } 68 | 69 | /** 70 | * Call this as regularly and as often as possible 71 | */ 72 | public function poll(Timeout:Float = 0):Void 73 | { 74 | var e:ENetEvent = ENet.poll(_host, Timeout); 75 | 76 | if (e.type == BaseEvent.E_RECEIVE) 77 | { 78 | try 79 | { 80 | var res:Array = separateMessage(e.message); 81 | 82 | var m:Message = messages.get(res[0]); 83 | 84 | if (isServer && m.isServerSideOnly) 85 | { 86 | //don't allow this message's content to be set by an incoming packet 87 | } 88 | 89 | else 90 | { 91 | m.unserialize(res[1]); 92 | } 93 | 94 | onReceive(res[0], e); 95 | } 96 | 97 | catch (e:Dynamic) 98 | { 99 | trace("Error receiving message, content: ", e.message); 100 | } 101 | } 102 | 103 | else if (e.type == BaseEvent.E_CONNECT) 104 | { 105 | onPeerConnect(e); 106 | } 107 | 108 | else if (e.type == BaseEvent.E_DISCONNECT) 109 | { 110 | onPeerDisonnect(e); 111 | } 112 | } 113 | 114 | /** 115 | * Override this to handle receiving. MsgID is the ID of the message 116 | * that was received. It's important to fetch the message's contents 117 | * immediately when this is called, as the message's contents will change 118 | * the next time it's received. 119 | */ 120 | public function onReceive(MsgID:Int, E:ENetEvent):Void 121 | { 122 | 123 | } 124 | 125 | /** 126 | * Override this to handle peer connecting. 127 | */ 128 | public function onPeerConnect(e:ENetEvent):Void 129 | { 130 | 131 | } 132 | 133 | /** 134 | * Override this to handle peer disconnecting. 135 | */ 136 | public function onPeerDisonnect(e:ENetEvent):Void 137 | { 138 | 139 | } 140 | 141 | /** 142 | * Does what it says. Also returns the target client's RTT. 143 | * 144 | * @param ID The peer's ID 145 | * @param MsgID The ID of the message you intend to send. It's contents at the moment of the call will be sent. 146 | * @param Channel Which channel to send through 147 | * @param Flags ENet flags, use | to unite flags, if they don't conflict 148 | * @return Returns the target client's RTT, divide by two to obtain the traditional "ping" 149 | */ 150 | public function sendMsg(ID:Int, MsgID:Int, Channel:Int = 0, Flags:Int = 0):Void 151 | { 152 | ENet.sendMsg(_host, ID, messages.get(MsgID).serialize(), Channel, Flags); 153 | } 154 | 155 | /** 156 | * Still figuring out how to use this beast myself. 157 | */ 158 | public function punchNAT(Address:String, Port:Int, Data:String):Bool 159 | { 160 | return ENet.punchNAT(_host, Address, Port, Data); 161 | } 162 | 163 | /** 164 | * Disconnects a connected peer, smoothly or forcefully. 165 | * The advantage of smoothly disconnecting is that the peer 166 | * will be notified of the disconnection. 167 | * 168 | * @param ID The peer's ID 169 | * @param Force Wether the peer will be notified of the disconnection or just outright dropped off the host 170 | */ 171 | public function peerDisconnect(ID:Int, Force:Bool):Void 172 | { 173 | ENet.peerDisconnect(_host, ID, Force); 174 | } 175 | } 176 | #else 177 | class NetBase 178 | { 179 | public function new() 180 | { 181 | 182 | } 183 | } 184 | #end -------------------------------------------------------------------------------- /Shared/code/enet/Server.hx: -------------------------------------------------------------------------------- 1 | package enet; 2 | 3 | /** 4 | * ... 5 | * @author Ohmnivore 6 | */ 7 | class Server extends enet.NetBase 8 | { 9 | /** 10 | * The server's bind IP in dotted quad format (xxx.xxx.xxx.xxx) 11 | */ 12 | public var ip:String; 13 | 14 | /** 15 | * The server's bind port 16 | */ 17 | public var port:Int; 18 | 19 | /** 20 | * Creates a new server. 21 | * 22 | * @param IP The IP to bind to (in dotted quad format). Pass in null to bind to all available interfaces. 23 | * @param Port The port to bind to 24 | */ 25 | public function new(IP:String = null, Port:Int = 0, Channels:Int = 2, Players:Int = 32) 26 | { 27 | super(); 28 | 29 | _host = ENet.server(IP, Port, Channels, Players); 30 | 31 | ip = IP; 32 | port = Port; 33 | } 34 | } -------------------------------------------------------------------------------- /Shared/code/enet/inter/INetBase.hx: -------------------------------------------------------------------------------- 1 | package enet.inter; 2 | 3 | import enet.Message; 4 | 5 | /** 6 | * ... 7 | * @author Ohmnivore 8 | */ 9 | interface INetBase 10 | { 11 | public var isServer:Bool; 12 | public var peers:Map; 13 | private var messages:Map; 14 | 15 | public function addMessage(M:Message):Void; 16 | private function separateMessage(Str:String):Array; 17 | public function poll(Timeout:Float = 0):Void; 18 | } -------------------------------------------------------------------------------- /Shared/code/enet/tcp/TCPClient.hx: -------------------------------------------------------------------------------- 1 | package enet.tcp; 2 | 3 | import anette.Client; 4 | import enet.inter.INetBase; 5 | 6 | /** 7 | * ... 8 | * @author Ohmnivore 9 | */ 10 | class TCPClient extends TCPNetBase 11 | { 12 | private var _client:anette.Client; 13 | 14 | public function new() 15 | { 16 | _client = new anette.Client(); 17 | _sock = _client; 18 | super(); 19 | } 20 | 21 | public function connect(IP:String, Port:Int) 22 | { 23 | _client.connect(IP, Port); 24 | } 25 | 26 | override public function poll(Timeout:Float = 0) 27 | { 28 | _client.pump(); 29 | _client.flush(); 30 | } 31 | } -------------------------------------------------------------------------------- /Shared/code/enet/tcp/TCPEvent.hx: -------------------------------------------------------------------------------- 1 | package enet.tcp; 2 | import enet.BaseEvent; 3 | 4 | class TCPEvent extends BaseEvent 5 | { 6 | 7 | } -------------------------------------------------------------------------------- /Shared/code/enet/tcp/TCPNetBase.hx: -------------------------------------------------------------------------------- 1 | package enet.tcp; 2 | 3 | import anette.Connection; 4 | import enet.Message; 5 | import enet.inter.INetBase; 6 | 7 | /** 8 | * ... 9 | * @author Ohmnivore 10 | */ 11 | class TCPNetBase implements INetBase 12 | { 13 | private var _sock:anette.BaseHandler; 14 | 15 | public var isServer:Bool = false; 16 | public var peers:Map; 17 | private var _peers:Map; 18 | private var peerIDCounter:Int = 0; 19 | private var messages:Map; 20 | 21 | public function new() 22 | { 23 | _sock.onData = _onReceive; 24 | _sock.onConnection = _onPeerConnect; 25 | _sock.onDisconnection = _onPeerDisonnect; 26 | _sock.protocol = new anette.Protocol.Prefixed(); 27 | _sock.timeout = 10; 28 | 29 | _peers = new Map(); 30 | messages = new Map(); 31 | } 32 | 33 | public function addMessage(M:Message):Void 34 | { 35 | messages.set(M.ID, M); 36 | } 37 | private function separateMessage(Str:String):Array 38 | { 39 | var _res:Array = []; 40 | 41 | var sep:Int = Str.indexOf("."); 42 | 43 | _res.push(Std.parseInt(Str.substr(0, sep))); 44 | 45 | _res.push(Str.substr(sep + 1)); 46 | 47 | return _res; 48 | } 49 | 50 | public function poll(Timeout:Float = 0) 51 | { 52 | 53 | } 54 | 55 | private function _sendMsg(c:Connection, s:String):Void 56 | { 57 | c.output.writeInt32(s.length); 58 | c.output.writeString(s); 59 | } 60 | public function sendMsg(ID:Int, MsgID:Int):Void 61 | { 62 | _sendMsg(_peers.get(ID), messages.get(MsgID).serialize()); 63 | } 64 | 65 | private function _onReceive(c:Connection):Void 66 | { 67 | var theid:Int = -1; 68 | 69 | for (id in _peers.keys()) 70 | { 71 | if (_peers.get(id) == c) 72 | { 73 | theid = id; 74 | } 75 | } 76 | 77 | var msgLength:Int = c.input.readInt32(); 78 | var msg = c.input.readString(msgLength); 79 | 80 | try 81 | { 82 | var e:TCPEvent = new TCPEvent(BaseEvent.E_RECEIVE, theid, msg); 83 | 84 | var res:Array = separateMessage(msg); 85 | 86 | var m:Message = messages.get(res[0]); 87 | 88 | if (isServer && m.isServerSideOnly) 89 | { 90 | //don't allow this message's content to be set by an incoming packet 91 | } 92 | else 93 | { 94 | m.unserialize(res[1]); 95 | } 96 | 97 | onReceive(res[0], e); 98 | } 99 | catch (e:Dynamic) 100 | { 101 | trace("Error receiving message, content: ", e); 102 | } 103 | } 104 | public function onReceive(MsgID:Int, E:TCPEvent):Void 105 | { 106 | 107 | } 108 | 109 | public function _onPeerConnect(c:Connection):Void 110 | { 111 | _peers.set(peerIDCounter, c); 112 | peerIDCounter++; 113 | 114 | onPeerConnect(new TCPEvent(BaseEvent.E_CONNECT, peerIDCounter - 1)); 115 | } 116 | public function onPeerConnect(e:TCPEvent):Void 117 | { 118 | 119 | } 120 | 121 | private function _onPeerDisonnect(c:Connection):Void 122 | { 123 | var theid:Int = -1; 124 | 125 | for (id in _peers.keys()) 126 | { 127 | if (_peers.get(id) == c) 128 | { 129 | _peers.remove(id); 130 | theid = id; 131 | } 132 | } 133 | 134 | onPeerDisonnect(new TCPEvent(BaseEvent.E_DISCONNECT, theid)); 135 | } 136 | public function onPeerDisonnect(e:TCPEvent):Void 137 | { 138 | 139 | } 140 | 141 | public function peerDisconnect(ID:Int):Void 142 | { 143 | _peers.get(ID).disconnect(); 144 | } 145 | } -------------------------------------------------------------------------------- /Shared/code/enet/tcp/TCPServer.hx: -------------------------------------------------------------------------------- 1 | package enet.tcp; 2 | 3 | import anette.*; 4 | import enet.inter.INetBase; 5 | 6 | /** 7 | * ... 8 | * @author Ohmnivore 9 | */ 10 | class TCPServer extends TCPNetBase 11 | { 12 | private var _server:anette.Server; 13 | 14 | public var ip:String; 15 | public var port:Int; 16 | 17 | public function new(IP:String, Port:Int) 18 | { 19 | _server = new anette.Server(IP, Port); 20 | _sock = _server; 21 | super(); 22 | isServer = true; 23 | 24 | ip = IP; 25 | port = Port; 26 | } 27 | 28 | override public function poll(Timeout:Float = 0) 29 | { 30 | _server.pump(); 31 | _server.flush(); 32 | } 33 | } -------------------------------------------------------------------------------- /doc/Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Gabriel Gambetta - Fast-Paced Multiplayer: Sample Code and Live Demo 8 | 9 | 12 | 13 | 14 | 24 | 25 | 28 | 29 |
30 | 33 |
34 |

Fast-Paced Multiplayer: Sample Code and Live Demo

35 |
36 |

Part I - Part II - Part III - Part IV - Live Demo

37 | 43 | 44 |

This is a sample implementation of a client-server architecture demonstrating the main concepts explained in my Fast-Paced Multiplayer series of articles (except for Entity Interpolation, which I haven’t done yet). It won’t make much sense unless you’ve read the articles first.

45 |

The code is pure JavaScript and it’s fully contained in this page. It’s less than 400 lines of code, including a lot of comments, demonstrating that once you really understand the concepts, implementing them is relatively straightforward.

46 |

Player View

47 |

Lag =ms · Prediction · Reconciliation

48 | 49 | 50 |

Non-acknowledged inputs: 1

51 |

Server View

52 |

Update times per second

53 | 54 |

55 | 468 | 469 |

Guided Tour

470 |

The two views show what the player sees, and the state of the world according to the server. You can control the green ball using the right and left arrow keys. Give it a try!

471 |

The ideal case

472 |

Start with Lag = 0 and Update = 60. This is an ideal case: the Server processes the world state as fast as the client produces it, and there’s no delay whatsoever between the Client and the Server. Of course, it works perfectly.

473 |

Slow server

474 |

Now set Update = 5. The Server now sends only 5 updates per second, so the animation on the Client side looks choppy. But the whole thing still feels somewhat responsive.

475 |

Laaaaaaaaaaaaaaaaaag

476 |

Let’s add some Lag - set it to 250 ms. The game doesn’t feel so responsive anymore; the player’s view is not updated until the Server has acknowledged the inputs sent by the Client. Because of the two-way lag, the character starts moving half a second after you press the key.

477 |

Client-Side Prediction

478 |

Enable Prediction and lower Update to 1. Keep the right key pressed for a while. Now the animation feels very smooth, because it’s predicted on the Client. But whenever the Server finally gets around to processing all the client inputs, the state it sends back is delayed respect to the player’s prediction because of the lag, so the Player jumps back.

479 |

Server Reconciliation

480 |

Now enable Reconciliation. Whenever the Server sends its state, we take all the not-yet-acknowledged inputs and redo the prediction, starting from the authoritative position sent by the Server. No matter how much lag you add or how infrequent the server udpates are, the Client is always in sync!

481 |
482 | Series Index 483 |
484 | 515 |
516 | 517 | 518 | 519 | 520 | -------------------------------------------------------------------------------- /doc/Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo_files/162488195-postmessagerelay.js: -------------------------------------------------------------------------------- 1 | var e=this,aa=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null"; 2 | else if("function"==b&&"undefined"==typeof a.call)return"object";return b};Math.random();var l=function(a,b){var c=a.split("."),d=e;c[0]in d||!d.execScript||d.execScript("var "+c[0]);for(var f;c.length&&(f=c.shift());)c.length||void 0===b?d=d[f]?d[f]:d[f]={}:d[f]=b},n=function(a,b){function c(){}c.prototype=b.prototype;a.o=b.prototype;a.prototype=new c;a.m=function(a,c,g){for(var k=Array(arguments.length-2),h=2;hb?1:0};Math.random();var u=function(a,b){b.unshift(a);p.call(this,ba.apply(null,b));b.shift()};n(u,p);var w=function(a,b,c){if(!a){var d="Assertion failed";if(b)var d=d+(": "+b),f=Array.prototype.slice.call(arguments,2);throw new u(""+d,f||[]);}};var x;a:{var y=e.navigator;if(y){var z=y.userAgent;if(z){x=z;break a}}x=""}var A=function(a){return-1!=x.indexOf(a)};var B=function(){return A("Opera")||A("OPR")},C=function(){return(A("Chrome")||A("CriOS"))&&!B()&&!A("Edge")};var ca=B(),D=A("Trident")||A("MSIE"),da=A("Edge"),E=A("Gecko")&&!(-1!=x.toLowerCase().indexOf("webkit")&&!A("Edge"))&&!(A("Trident")||A("MSIE"))&&!A("Edge"),G=-1!=x.toLowerCase().indexOf("webkit")&&!A("Edge"),ea=G&&A("Mobile"),fa=function(){var a=x;if(E)return/rv\:([^\);]+)(\)|;)/.exec(a);if(da)return/Edge\/([\d\.]+)/.exec(a);if(D)return/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(a);if(G)return/WebKit\/(\S+)/.exec(a)},H=function(){var a=e.document;return a?a.documentMode:void 0},I=function(){if(ca&&e.opera){var a= 3 | e.opera.version;return"function"==aa(a)?a():a}var a="",b=fa();b&&(a=b?b[1]:"");return D&&(b=H(),b>parseFloat(a))?String(b):a}(),J={},K=function(a){if(!J[a]){for(var b=0,c=q(String(I)).split("."),d=q(String(a)).split("."),f=Math.max(c.length,d.length),g=0;0==b&&g 2 | 3 | Facebook Cross-Domain Messaging helper -------------------------------------------------------------------------------- /doc/Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo_files/1ldYU13brY_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Facebook Cross-Domain Messaging helper -------------------------------------------------------------------------------- /doc/Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo_files/api.js: -------------------------------------------------------------------------------- 1 | var gapi=window.gapi=window.gapi||{};gapi._bs=new Date().getTime();(function(){var f=decodeURIComponent,g=window,k=encodeURIComponent,n="split",p="join",q="replace",t="push",x="shift",z="length",A="test";var E=g,F=document,aa=E.location,ba=function(){},ca=/\[native code\]/,G=function(a,b,c){return a[b]=a[b]||c},ea=function(a){for(var b=0;bea.call(b,e)&&c[t](e)}return c},ta=function(a){"loading"!=F.readyState?Z(a):F.write("<"+X+' src="'+encodeURI(a)+'">")},Z=function(a){var b=F.createElement(X);b.setAttribute("src",a);b.async="true";(a=F.getElementsByTagName(X)[0])?a.parentNode.insertBefore(b,a):(F.head||F.body||F.documentElement).appendChild(b)},ua=function(a,b){var c=b&&b._c;if(c)for(var d=0;d/g,Sa=/"/g,Ta=/'/g,Ua=function(a){return q(a)[E](Pa,"&")[E](Qa,"<")[E](Ra,">")[E](Sa,""")[E](Ta,"'")},R=function(){var a;if((a=ba.create)&&Ma[K](a))a=a(null);else{a={};for(var b in a)a[b]= 5 | void 0}return a},S=function(a,b){return ba[B].hasOwnProperty[A](a,b)},Va=function(a){if(Ma[K](ba.keys))return ba.keys(a);var b=[],c;for(c in a)S(a,c)&&b[F](c);return b},T=function(a,b){a=a||{};for(var c in a)S(a,c)&&(b[c]=a[c])},Wa=function(a){return function(){N.setTimeout(a,0)}},Xa=function(a,b){if(!a)throw Error(b||"");},U=Q(N,"gapi",{});var V=function(a,b,c){var d=new RegExp("([#].*&|[#])"+b+"=([^&#]*)","g");b=new RegExp("([?#].*&|[?#])"+b+"=([^&#]*)","g");if(a=a&&(d[ja](a)||b[ja](a)))try{c=aa(a[2])}catch(e){}return c},Ya=/^([^?#]*)(\?([^#]*))?(\#(.*))?$/,Za=function(a){a=a[ha](Ya);var b=R();b.K=a[1];b.l=a[3]?[a[3]]:[];b.v=a[5]?[a[5]]:[];return b},$a=function(a){return a.K+(0Na[A](b,e)&&c[F](e)}return c},Qb=function(a){"loading"!=P.readyState?Pb(a):P.write("<"+Nb+' src="'+encodeURI(a)+'">")},Pb=function(a){var b=P[M](Nb);b[Fa]("src",a);b.async="true";(a=P[va](Nb)[0])?a[D].insertBefore(b,a):(P.head||P.body||P.documentElement)[la](b)},Rb=function(a,b){var c=b&&b._c;if(c)for(var d=0;de;e++)d[e]=b[Da](c)<<24|b[Da](c+1)<<16|b[Da](c+2)<<8|b[Da](c+3),c+=4;else for(e=0;16>e;e++)d[e]=b[c]<<24|b[c+1]<<16|b[c+2]<<8|b[c+3],c+=4;for(e=16;80>e;e++){var f=d[e-3]^d[e-8]^d[e-14]^d[e-16];d[e]=(f<<1|f>>>31)&4294967295}b=a.b[0];c=a.b[1];for(var g=a.b[2],k=a.b[3],h=a.b[4],t,e=0;80>e;e++)40>e?20>e?(f=k^c&(g^k),t=1518500249):(f=c^g^k,t=1859775393):60>e?(f=c&g|k&(c|g),t=2400959708):(f=c^g^k,t=3395469782),f=(b<<5|b>>>27)+ 19 | f+h+t+d[e]&4294967295,h=k,k=g,g=(c<<30|c>>>2)&4294967295,c=b,b=f;a.b[0]=a.b[0]+b&4294967295;a.b[1]=a.b[1]+c&4294967295;a.b[2]=a.b[2]+g&4294967295;a.b[3]=a.b[3]+k&4294967295;a.b[4]=a.b[4]+h&4294967295}; 20 | uc[B].update=function(a,b){if(null!=a){void 0===b&&(b=a[I]);for(var c=b-this.c,d=0,e=this.C,f=this.j;dthis.j?this[ma](this.w,56-this.j):this[ma](this.w,this.c-(this.j-56));for(var c=this.c-1;56<=c;c--)this.C[c]=b&255,b/=256;vc(this,this.C);for(c=b=0;5>c;c++)for(var d=24;0<=d;d-=8)a[b]=this.b[c]>>d&255,++b;return a};var wc=function(){this.G=new uc};wc[B].reset=function(){this.G.reset()};var xc=N.crypto,yc=!1,zc=0,Ac=0,Bc=1,Cc=0,Dc="",Ec=function(a){a=a||N.event;var b=a.screenX+a.clientX<<16,b=b+(a.screenY+a.clientY),b=(new Date)[Ga]()%1E6*b;Bc=Bc*b%Cc;0'))}catch(h){}finally{f||(f=a[M]("iframe"), 23 | g&&(f.onload=function(){f.onload=null;g[A](this)},Lc(d)))}for(var t in c)a=c[t],"style"===t&&"object"===typeof a?T(a,f[ia]):Nc[t]||f[Fa](t,q(a));(t=e&&e.beforeNode||null)||e&&e.dontclear||jb(b);b.insertBefore(f,t);f=t?t.previousSibling:b.lastChild;c.allowtransparency&&(f.allowTransparency=!0);return f};var Rc=/^:[\w]+$/,Sc=/:([a-zA-Z_]+):/g,Tc=function(){var a=rc()||"0",b=sc(),c;c=rc(void 0)||a;var d=sc(void 0),e="";c&&(e+="u/"+c+"/");d&&(e+="b/"+d+"/");c=e||null;(e=(d=!1===Y("isLoggedIn"))?"_/im/":"")&&(c="");var f=Y("iframes/:socialhost:"),g=Y("iframes/:im_socialhost:");return oc={socialhost:f,ctx_socialhost:d?g:f,session_index:a,session_delegate:b,session_prefix:c,im_prefix:e}},Uc=function(a,b){return Tc()[b]||""},Vc=function(a){return function(b,c){return a?Tc()[c]||a[c]||"":Tc()[c]||""}};var Wc={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},Xc=function(a){var b,c,d;b=/[\"\\\x00-\x1f\x7f-\x9f]/g;if(void 0!==a){switch(typeof a){case "string":return b[K](a)?'"'+a[E](b,function(a){var b=Wc[a];if(b)return b;b=a[Da]();return"\\u00"+Math.floor(b/16)[L](16)+(b%16)[L](16)})+'"':'"'+a+'"';case "number":return isFinite(a)?q(a):"null";case "boolean":case "null":return q(a);case "object":if(!a)return"null";b=[];if("number"===typeof a[I]&&!a.propertyIsEnumerable("length")){d= 24 | a[I];for(c=0;c=c&&(f.ic="1");h=/^#|^fr-/;c={};for(var t in f)S(f,t)&&h[K](t)&&(c[t[E](h,"")]=f[t],delete f[t]);t="q"==Y("iframes/"+a+"/params/si")?f:c;h=gc();for(var m in h)!S(h,m)||S(f,m)||S(c,m)||(t[m]=h[m]);m=[][ka](id);(t=Y("iframes/"+ 28 | a+"/methods"))&&"object"===typeof t&&Ma[K](t[F])&&(m=m[ka](t));for(var n in b)S(b,n)&&/^on/[K](n)&&("plus"!=a||"onconnect"!=n)&&(m[F](n),delete f[n]);delete f.callback;c._methods=m[y](",");return bb(d,f,c)},ld=["style","data-gapiscan"],nd=function(a){for(var b=R(),c=0!=a[za][Ea]()[C]("g:"),d=0,e=a[ra][I];dtype"]=a;T(c,b);f=h;c=m;h=e||{};b=h[ra]||{};Xa(!h.allowPost|| 34 | !b.onload,"onload is not supported by post iframe");e=b=f;Rc[K](b)&&(e=Y("iframes/"+e[qa](1)+"/url"),Xa(!!e,"Unknown iframe url config for - "+b));f=cb(P,e[E](Sc,Uc));b=c.ownerDocument||P;m=0;do e=h.id||["I",Oc++,"_",(new Date)[Ga]()][y]("");while(b[na](e)&&5>++m);Xa(5>m,"Error creating iframe id");m={};var n={};b[sa]&&9>b[sa]&&(m.hostiemode=b[sa]);T(h.queryParams||{},m);T(h.fragmentParams||{},n);var w=h.connectWithQueryParams?m:n,O=h.pfname,u=R();u.id=e;u.parent=b[G][wa]+"//"+b[G].host;var J=V(b[G][r], 35 | "parent"),O=O||"";!O&&J&&(J=V(b[G][r],"id",""),O=V(b[G][r],"pfname",""),O=J?O+"/"+J:"");u.pfname=O;T(u,w);(u=V(f,"rpctoken")||m.rpctoken||n.rpctoken)||(u=w.rpctoken=h.rpctoken||q(Math.round(1E8*(yc?Hc():Gc()))));h.rpctoken=u;u=b[G][r];w=R();(J=V(u,"_bsh",W.bsh))&&(w._bsh=J);(u=lb(u))&&(w.jsh=u);h.hintInFragment?T(w,n):T(w,m);f=bb(f,m,n,h.paramsSerializer);n=R();T(Mc,n);T(h[ra],n);n.name=n.id=e;n.src=f;h.eurl=f;if((h||{}).allowPost&&2E3a.i)a=e,b=d}});return{X:a,B:b}};var Zd=function(a){if(0!==a[C]("GCSC"))return null;var b={O:!1};a=a[x](4);if(!a)return b;var c=a[ta](0);a=a[x](1);var d=a.lastIndexOf("_");if(-1==d)return b;var e=Xd(a[x](d+1));if(null==e)return b;a=a[qa](0,d);if("_"!==a[ta](0))return b;d="E"===c&&e.g;return!d&&("U"!==c||e.g)||d&&!Vd?b:{O:!0,g:d,da:a[x](1),domain:e.domain,i:e.i}},$d=function(a){if(!a)return[];a=a[v]("=");return a[1]?a[1][v]("|"):[]},ae=function(a){a=a[v](":");return{D:a[0][v]("=")[1],ca:$d(a[1]),fa:$d(a[2]),ea:$d(a[3])}},be=function(){var a= 43 | Yd(),b=a.X,a=a.B;if(null!==a){var c;Wd.iterate(function(a,d){var e=Zd(a);e&&e.O&&e.g==b.g&&e.i==b.i&&(c=d)});if(c){var d=ae(c),e=d&&d.ca[Number(a)],d=d&&d.D;if(e)return{B:a,ba:e,D:d}}}return null};var ce=function(a){this.L=a};ce[B].o=0;ce[B].H=2;ce[B].L=null;ce[B].F=!1;ce[B].U=function(){this.F||(this.o=0,this.F=!0,this.S())};ce[B].S=function(){this.F&&(this.L()?this.o=this.H:this.o=Math.min(2*(this.o||this.H),120),l.setTimeout(Ja(this.S,this),1E3*this.o))};for(var de=0;64>de;++de);var ee=null,kc=function(){return W.oa=!0},lc=function(){W.oa=!0;var a=be();(a=a&&a.B)&&bc("googleapis.config/sessionIndex",a);ee||(ee=Q(W,"ss",new ce(fe)));a=ee;a.U&&a.U()},fe=function(){var a=be(),b=a&&a.ba||null,c=a&&a.D;Vb("auth",{callback:function(){var a=N.gapi.auth,e={client_id:c,session_state:b};a.checkSessionState(e,function(b){var c=e.session_state,k=Y("isLoggedIn");b=Y("debug/forceIm")?!1:c&&b||!c&&!b;if(k=k!=b)bc("isLoggedIn",b),lc(),pd(),b||((b=a.signOut)?b():(b=a.setToken)&&b(null)); 44 | b=gc();var h=Y("savedUserState"),c=a._guss(b.cookiepolicy),h=h!=c&&"undefined"!=typeof h;bc("savedUserState",c);(k||h)&&hc(b)&&!Y("disableRealtimeCallback")&&a._pimf(b,!0)})}});return!0};vb("bs0",!0,l.gapi._bs);vb("bs1",!0);delete l.gapi._bs;})(); 45 | gapi.load("plusone",{callback:window["gapi_onload"],_c:{"jsl":{"ci":{"deviceType":"desktop","oauth-flow":{"authUrl":"https://accounts.google.com/o/oauth2/auth","proxyUrl":"https://accounts.google.com/o/oauth2/postmessageRelay","disableOpt":true,"idpIframeUrl":"https://accounts.google.com/o/oauth2/iframe","usegapi":false},"debug":{"reportExceptionRate":0.05,"forceIm":false,"rethrowException":false,"host":"https://apis.google.com"},"lexps":[81,97,99,122,123,61,45,30,79,127],"enableMultilogin":true,"googleapis.config":{"auth":{"useFirstPartyAuthV2":true}},"isPlusUser":true,"inline":{"css":1},"disableRealtimeCallback":false,"drive_share":{"useStandaloneSharingService":true},"csi":{"rate":0.01},"report":{"apiRate":{"gapi\\.signin\\..*":0.05},"apis":["iframes\\..*","gadgets\\..*","gapi\\.appcirclepicker\\..*","gapi\\.auth\\..*","gapi\\.client\\..*"],"rate":0.001,"host":"https://apis.google.com"},"client":{"headers":{"request":["Accept","Accept-Language","Authorization","Cache-Control","Content-Disposition","Content-Encoding","Content-Language","Content-Length","Content-MD5","Content-Range","Content-Type","Date","GData-Version","Host","If-Match","If-Modified-Since","If-None-Match","If-Unmodified-Since","Origin","OriginToken","Pragma","Range","Slug","Transfer-Encoding","X-ClientDetails","X-GData-Client","X-GData-Key","X-Goog-AuthUser","X-Goog-PageId","X-Goog-Encode-Response-If-Executable","X-Goog-Correlation-Id","X-Goog-Request-Info","X-Goog-Experiments","x-goog-iam-role","x-goog-iam-authorization-token","X-Goog-Spatula","X-Goog-Upload-Command","X-Goog-Upload-Content-Disposition","X-Goog-Upload-Content-Length","X-Goog-Upload-Content-Type","X-Goog-Upload-File-Name","X-Goog-Upload-Offset","X-Goog-Upload-Protocol","X-Goog-Visitor-Id","X-HTTP-Method-Override","X-JavaScript-User-Agent","X-Pan-Versionid","X-Origin","X-Referer","X-Upload-Content-Length","X-Upload-Content-Type","X-Use-HTTP-Status-Code-Override","X-YouTube-VVT","X-YouTube-Page-CL","X-YouTube-Page-Timestamp"],"response":["Cache-Control","Content-Disposition","Content-Encoding","Content-Language","Content-Length","Content-MD5","Content-Range","Content-Type","Date","ETag","Expires","Last-Modified","Location","Pragma","Range","Server","Transfer-Encoding","WWW-Authenticate","Vary","X-Goog-Safety-Content-Type","X-Goog-Safety-Encoding","X-Goog-Upload-Chunk-Granularity","X-Goog-Upload-Control-URL","X-Goog-Upload-Size-Received","X-Goog-Upload-Status","X-Goog-Upload-URL","X-Goog-Diff-Download-Range","X-Goog-Hash","X-Goog-Updated-Authorization","X-Server-Object-Version","X-Guploader-Customer","X-Guploader-Upload-Result","X-Guploader-Uploadid"]},"rms":"migrated","cors":false},"isLoggedIn":true,"include_granted_scopes":true,"llang":"en","plus_layer":{"isEnabled":false},"iframes":{"youtube":{"params":{"location":["search","hash"]},"url":":socialhost:/:session_prefix:_/widget/render/youtube?usegapi\u003d1","methods":["scroll","openwindow"]},"ytsubscribe":{"url":"https://www.youtube.com/subscribe_embed?usegapi\u003d1"},"plus_circle":{"params":{"url":""},"url":":socialhost:/:session_prefix::se:_/widget/plus/circle?usegapi\u003d1"},"plus_share":{"params":{"url":""},"url":":socialhost:/:session_prefix::se:_/+1/sharebutton?plusShare\u003dtrue\u0026usegapi\u003d1"},"rbr_s":{"params":{"url":""},"url":":socialhost:/:session_prefix::se:_/widget/render/recobarsimplescroller"},"udc_webconsentflow":{"params":{"url":""},"url":"https://www.google.com/settings/webconsent?usegapi\u003d1"},":source:":"3p","blogger":{"params":{"location":["search","hash"]},"url":":socialhost:/:session_prefix:_/widget/render/blogger?usegapi\u003d1","methods":["scroll","openwindow"]},"evwidget":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/events/widget?usegapi\u003d1"},":socialhost:":"https://apis.google.com","shortlists":{"url":""},"hangout":{"url":"https://talkgadget.google.com/:session_prefix:talkgadget/_/widget"},"plus_followers":{"params":{"url":""},"url":":socialhost:/_/im/_/widget/render/plus/followers?usegapi\u003d1"},"post":{"params":{"url":""},"url":":socialhost:/:session_prefix::im_prefix:_/widget/render/post?usegapi\u003d1"},"photocomments":{"url":":socialhost:/:session_prefix:_/widget/render/photocomments?usegapi\u003d1"},":gplus_url:":"https://plus.google.com","signin":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/widget/render/signin?usegapi\u003d1","methods":["onauth"]},"rbr_i":{"params":{"url":""},"url":":socialhost:/:session_prefix::se:_/widget/render/recobarinvitation"},"share":{"url":":socialhost:/:session_prefix::im_prefix:_/widget/render/share?usegapi\u003d1"},"plusone":{"params":{"count":"","size":"","url":""},"url":":socialhost:/:session_prefix::se:_/+1/fastbutton?usegapi\u003d1"},"comments":{"params":{"location":["search","hash"]},"url":":socialhost:/:session_prefix:_/widget/render/comments?usegapi\u003d1","methods":["scroll","openwindow"]},":im_socialhost:":"https://plus.googleapis.com","backdrop":{"url":"https://clients3.google.com/cast/chromecast/home/widget/backdrop?usegapi\u003d1"},"visibility":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/widget/render/visibility?usegapi\u003d1"},"autocomplete":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/widget/render/autocomplete"},"additnow":{"url":"https://apis.google.com/additnow/additnow.html?usegapi\u003d1","methods":["launchurl"]},":signuphost:":"https://plus.google.com","appcirclepicker":{"url":":socialhost:/:session_prefix:_/widget/render/appcirclepicker"},"follow":{"url":":socialhost:/:session_prefix:_/widget/render/follow?usegapi\u003d1"},"community":{"url":":ctx_socialhost:/:session_prefix::im_prefix:_/widget/render/community?usegapi\u003d1"},"ytshare":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/widget/render/ytshare?usegapi\u003d1"},"plus":{"url":":socialhost:/:session_prefix:_/widget/render/badge?usegapi\u003d1"},"reportabuse":{"params":{"url":""},"url":":socialhost:/:session_prefix:_/widget/render/reportabuse?usegapi\u003d1"},"commentcount":{"url":":socialhost:/:session_prefix:_/widget/render/commentcount?usegapi\u003d1"},"configurator":{"url":":socialhost:/:session_prefix:_/plusbuttonconfigurator?usegapi\u003d1"},"zoomableimage":{"url":"https://ssl.gstatic.com/microscope/embed/"},"savetowallet":{"url":"https://clients5.google.com/s2w/o/savetowallet"},"person":{"url":":socialhost:/:session_prefix:_/widget/render/person?usegapi\u003d1"},"savetodrive":{"url":"https://drive.google.com/savetodrivebutton?usegapi\u003d1","methods":["save"]},"page":{"url":":socialhost:/:session_prefix:_/widget/render/page?usegapi\u003d1"},"card":{"url":":socialhost:/:session_prefix:_/hovercard/card"}}},"h":"m;/_/scs/apps-static/_/js/k\u003doz.gapi.en.gKmhOycH3WM.O/m\u003d__features__/am\u003dAQ/rt\u003dj/d\u003d1/t\u003dzcms/rs\u003dAGLTcCMO1egpdt63yfFOvaheZfmhqK-KOw","u":"https://apis.google.com/js/plusone.js","hee":true,"fp":"87ad603184519a319ee048ef56eb172823defed7","dpo":false},"platform":["additnow","backdrop","blogger","comments","commentcount","community","follow","page","person","playreview","plus","plusone","post","reportabuse","savetodrive","savetowallet","shortlists","signin2","visibility","youtube","ytsubscribe","zoomableimage","photocomments","hangout","udc_webconsentflow"],"fp":"87ad603184519a319ee048ef56eb172823defed7","annotation":["interactivepost","recobar","signin2","autocomplete","profile"],"bimodal":["signin","share"]}}); -------------------------------------------------------------------------------- /doc/Gabriel Gambetta - Fast-Paced Multiplayer_ Sample Code and Live Demo_files/postmessageRelay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ndll/EnetTesting.ndll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/ndll/EnetTesting.ndll -------------------------------------------------------------------------------- /ndll/libgcc_s_dw2-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/ndll/libgcc_s_dw2-1.dll -------------------------------------------------------------------------------- /ndll/libstdc++-6.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ohmnivore/ClientSidePredictionAndServerReconciliation/738979dbbd159e6e92837012e409fd5d584db424/ndll/libstdc++-6.dll --------------------------------------------------------------------------------