diff --git a/src/Console.hx b/src/Console.hx index 6ec8c1bc..18a76104 100644 --- a/src/Console.hx +++ b/src/Console.hx @@ -1,5 +1,6 @@ package src; +import net.Net; #if !js import sys.FileSystem; #end @@ -187,6 +188,8 @@ class Console { } else if (cmdType == 'rollback') { var t = Std.parseFloat(cmdSplit[1]); MarbleGame.instance.world.rollback(t); + } else if (cmdType == 'addDummy') { + Net.addDummyConnection(); } else { error("Unknown command"); } diff --git a/src/Marble.hx b/src/Marble.hx index dc796ca1..f244a1e9 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1,5 +1,7 @@ package src; +import net.ClientConnection; +import net.ClientConnection.GameConnection; import net.NetPacket.MarbleUpdatePacket; import net.MoveManager; import net.MoveManager.NetMove; @@ -292,7 +294,7 @@ class Marble extends GameObject { public var cubemapRenderer:CubemapRenderer; - var connection:net.Net.ClientConnection; + var connection:GameConnection; var moveMotionDir:Vector; var lastMove:Move; var isNetUpdate:Bool = false; @@ -332,7 +334,7 @@ class Marble extends GameObject { this.helicopterSound.pause = true; } - public function init(level:MarbleWorld, connection:ClientConnection, onFinish:Void->Void) { + public function init(level:MarbleWorld, connection:GameConnection, onFinish:Void->Void) { this.level = level; this.connection = connection; if (this.level != null) @@ -1060,6 +1062,7 @@ class Marble extends GameObject { searchbox.addSpherePos(position.x + velocity.x * deltaT, position.y + velocity.y * deltaT, position.z + velocity.z * deltaT, _radius); var foundObjs = this.collisionWorld.boundingSearch(searchbox); + foundObjs.push(this.collisionWorld.staticWorld); var finalT = deltaT; var found = false; @@ -1102,7 +1105,7 @@ class Marble extends GameObject { // var iterationFound = false; for (obj in foundObjs) { // Its an MP so bruh - if (!obj.go.isCollideable) + if (obj.go != null && !obj.go.isCollideable) continue; var invMatrix = @:privateAccess obj.invTransform; diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 3a6f1841..9bff67e7 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -7,7 +7,8 @@ import net.NetPacket.MarbleMovePacket; import net.MoveManager; import net.NetCommands; import net.Net; -import net.Net.ClientConnection; +import net.ClientConnection; +import net.ClientConnection.GameConnection; import rewind.InputRecorder; import gui.AchievementsGui; import src.Radar; @@ -203,7 +204,7 @@ class MarbleWorld extends Scheduler { var tickAccumulator:Float = 0.0; var maxPredictionTicks:Int = 16; - var clientMarbles:Map = []; + var clientMarbles:Map = []; public var lastMoves:MarbleUpdateQueue; @@ -307,7 +308,7 @@ class MarbleWorld extends Scheduler { this.gameMode.missionScan(this.mission); this.resourceLoadFuncs.push(fwd -> this.initScene(fwd)); if (this.isMultiplayer) { - for (client in Net.clients) { + for (client in Net.clientIdMap) { this.resourceLoadFuncs.push(fwd -> this.initMarble(client, fwd)); // Others } } @@ -322,6 +323,7 @@ class MarbleWorld extends Scheduler { public function postInit() { // Add the sky at the last so that cubemap reflections work + this.collisionWorld.finalizeStaticGeometry(); this.playGui.init(this.scene2d, this.mission.game.toLowerCase()); this.scene.addChild(this.sky); this._ready = true; @@ -415,7 +417,7 @@ class MarbleWorld extends Scheduler { worker.run(); } - public function initMarble(client:ClientConnection, onFinish:Void->Void) { + public function initMarble(client:GameConnection, onFinish:Void->Void) { Console.log("Initializing marble"); var worker = new ResourceLoaderWorker(onFinish); var marblefiles = [ @@ -767,8 +769,11 @@ class MarbleWorld extends Scheduler { var tmat = Matrix.T(interiorPosition.x, interiorPosition.y, interiorPosition.z); mat.multiply(mat, tmat); + if (hasCollision) + this.collisionWorld.addStaticInterior(interior.collider, mat); + + interior.isCollideable = false; interior.setTransform(mat); - interior.isCollideable = hasCollision; onFinish(); }); @@ -886,7 +891,7 @@ class MarbleWorld extends Scheduler { public function addInterior(obj:InteriorObject, onFinish:Void->Void) { this.interiors.push(obj); obj.init(cast this, () -> { - this.collisionWorld.addEntity(obj.collider); + // this.collisionWorld.addEntity(obj.collider); if (obj.useInstancing) this.instanceManager.addObject(obj); else @@ -970,7 +975,7 @@ class MarbleWorld extends Scheduler { }); } - public function addMarble(marble:Marble, client:ClientConnection, onFinish:Void->Void) { + public function addMarble(marble:Marble, client:GameConnection, onFinish:Void->Void) { marble.level = cast this; if (marble.controllable) { marble.init(cast this, client, () -> { @@ -1379,7 +1384,7 @@ class MarbleWorld extends Scheduler { // return (a.c == client.id) ? 1 : (b.c == client.id) ? -1 : 0; // }); for (packet in packets) { - client.datachannel.sendBytes(packet); + client.sendBytes(packet); } } } diff --git a/src/collision/CollisionEntity.hx b/src/collision/CollisionEntity.hx index bd0c0729..cebc020b 100644 --- a/src/collision/CollisionEntity.hx +++ b/src/collision/CollisionEntity.hx @@ -40,6 +40,7 @@ class CollisionEntity implements IOctreeObject implements IBVHObject { public var userData:Int; public var fastTransform:Bool = false; + public var isWorldStatic:Bool = false; var _transformKey:Int = 0; @@ -187,6 +188,11 @@ class CollisionEntity implements IOctreeObject implements IBVHObject { var tform = transform.clone(); // tform.setPosition(tform.getPosition().add(this.velocity.multiply(timeState.dt))); + if (isWorldStatic) { + tform.load(Matrix.I()); + invtform.load(Matrix.I()); + } + var contacts = []; for (obj in surfaces) { @@ -246,7 +252,8 @@ class CollisionEntity implements IOctreeObject implements IBVHObject { cinfo.force = surface.force; cinfo.friction = surface.friction; contacts.push(cinfo); - this.go.onMarbleContact(collisionEntity.marble, timeState, cinfo); + if (this.go != null) + this.go.onMarbleContact(collisionEntity.marble, timeState, cinfo); // surfaceBestContact = cinfo; // } } diff --git a/src/collision/CollisionSurface.hx b/src/collision/CollisionSurface.hx index d3e14f09..52d963b8 100644 --- a/src/collision/CollisionSurface.hx +++ b/src/collision/CollisionSurface.hx @@ -219,6 +219,42 @@ class CollisionSurface implements IOctreeObject implements IBVHObject { new Vector(_transformedNormals[p1 * 3], _transformedNormals[p1 * 3 + 1], _transformedNormals[p1 * 3 + 2])); } + public inline function getTriangle(idx:Int) { + var p1 = indices[idx]; + var p2 = indices[idx + 1]; + var p3 = indices[idx + 2]; + + return new TransformedCollisionTriangle(getPoint(p1), getPoint(p2), getPoint(p3), getNormal(p1)); + } + + public function getTransformed(m:Matrix, invtform:Matrix) { + var tformed = new CollisionSurface(); + tformed.points = this.points.copy(); + tformed.normals = this.normals.copy(); + tformed.indices = this.indices.copy(); + tformed.friction = this.friction; + tformed.force = this.force; + tformed.restitution = this.restitution; + tformed.transformKeys = this.transformKeys.copy(); + + for (i in 0...Std.int(points.length / 3)) { + var v = getPoint(i); + var v2 = v.transformed(m); + tformed.points[i * 3] = v2.x; + tformed.points[i * 3 + 1] = v2.y; + tformed.points[i * 3 + 2] = v2.z; + + var n = getNormal(i); + var n2 = n.transformed3x3(invtform).normalized(); + tformed.normals[i * 3] = n2.x; + tformed.normals[i * 3 + 1] = n2.y; + tformed.normals[i * 3 + 2] = n2.z; + } + tformed.generateBoundingBox(); + + return tformed; + } + public function dispose() { points = null; normals = null; diff --git a/src/collision/CollisionWorld.hx b/src/collision/CollisionWorld.hx index fa24a649..be4b1f44 100644 --- a/src/collision/CollisionWorld.hx +++ b/src/collision/CollisionWorld.hx @@ -1,5 +1,6 @@ package collision; +import h3d.Matrix; import src.MarbleGame; import src.TimeState; import h3d.col.Bounds; @@ -8,6 +9,7 @@ import h3d.Vector; import octree.Octree; class CollisionWorld { + public var staticWorld:CollisionEntity; public var octree:Octree; public var entities:Array = []; public var dynamicEntities:Array = []; @@ -20,6 +22,7 @@ class CollisionWorld { public function new() { this.octree = new Octree(); this.dynamicOctree = new Octree(); + this.staticWorld = new CollisionEntity(null); } public function sphereIntersection(spherecollision:SphereCollisionEntity, timeState:TimeState) { @@ -50,6 +53,8 @@ class CollisionWorld { } } + contacts = contacts.concat(this.staticWorld.sphereIntersection(spherecollision, timeState)); + var dynSearch = dynamicOctree.boundingSearch(box).map(x -> cast(x, CollisionEntity)); for (obj in dynSearch) { if (obj != spherecollision) { @@ -116,6 +121,7 @@ class CollisionWorld { for (obj in objs) { results = results.concat(obj.rayCast(rayStart, rayDirection)); } + results = results.concat(this.staticWorld.rayCast(rayStart, rayDirection)); return results; } @@ -153,6 +159,17 @@ class CollisionWorld { } } + public function addStaticInterior(entity:CollisionEntity, transform:Matrix) { + var invTform = transform.getInverse(); + for (surf in entity.surfaces) { + staticWorld.addSurface(surf.getTransformed(transform, invTform)); + } + } + + public function finalizeStaticGeometry() { + this.staticWorld.finalize(); + } + public function dispose() { for (e in entities) { e.dispose(); @@ -165,5 +182,7 @@ class CollisionWorld { dynamicEntities = null; dynamicOctree = null; dynamicEntitySet = null; + staticWorld.dispose(); + staticWorld = null; } } diff --git a/src/modes/HuntMode.hx b/src/modes/HuntMode.hx index ed838fcf..32079b6b 100644 --- a/src/modes/HuntMode.hx +++ b/src/modes/HuntMode.hx @@ -151,6 +151,7 @@ class HuntState implements RewindableState { class HuntMode extends NullMode { var gemSpawnPoints:Array = []; var playerSpawnPoints:Array = []; + var spawnPointTaken = []; var gemOctree:Octree; var gemGroupRadius:Float; @@ -170,8 +171,10 @@ class HuntMode extends NullMode { if ([MissionElementType.SpawnSphere].contains(element._type)) { var spawnSphere:MissionElementSpawnSphere = cast element; var dbname = spawnSphere.datablock.toLowerCase(); - if (dbname == "spawnspheremarker") + if (dbname == "spawnspheremarker") { playerSpawnPoints.push(spawnSphere); + spawnPointTaken.push(false); + } if (dbname == "gemspawnspheremarker") gemSpawnPoints.push(new GemSpawnSphere(spawnSphere)); } else if (element._type == MissionElementType.SimGroup) { @@ -183,7 +186,12 @@ class HuntMode extends NullMode { }; override function getSpawnTransform() { - var randomSpawn = playerSpawnPoints[Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1))]; + var idx = Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1)); + while (spawnPointTaken[idx]) { + idx = Math.floor(rng2.randRange(0, playerSpawnPoints.length - 1)); + } + spawnPointTaken[idx] = true; + var randomSpawn = playerSpawnPoints[idx]; var spawnPos = MisParser.parseVector3(randomSpawn.position); spawnPos.x *= -1; var spawnRot = MisParser.parseRotation(randomSpawn.rotation); diff --git a/src/net/ClientConnection.hx b/src/net/ClientConnection.hx new file mode 100644 index 00000000..e6f87eba --- /dev/null +++ b/src/net/ClientConnection.hx @@ -0,0 +1,59 @@ +package net; + +import haxe.io.Bytes; +import datachannel.RTCPeerConnection; +import datachannel.RTCDataChannel; +import net.MoveManager; + +enum abstract GameplayState(Int) from Int to Int { + var UNKNOWN; + var LOBBY; + var GAME; +} + +@:publicFields +class ClientConnection extends GameConnection { + var socket:RTCPeerConnection; + var datachannel:RTCDataChannel; + var rtt:Float; + var pingSendTime:Float; + var _rttRecords:Array = []; + + public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel) { + super(id); + this.socket = socket; + this.datachannel = datachannel; + this.state = GameplayState.LOBBY; + this.rtt = 0; + } + + override function sendBytes(b:Bytes) { + datachannel.sendBytes(b); + } +} + +@:publicFields +class DummyConnection extends GameConnection { + public function new(id:Int) { + super(id); + this.state = GameplayState.GAME; + } +} + +@:publicFields +abstract class GameConnection { + var id:Int; + var state:GameplayState; + var moveManager:MoveManager; + + public function new(id:Int) { + this.id = id; + this.moveManager = new MoveManager(this); + } + + public function ready() { + state = GameplayState.GAME; + } + + public function sendBytes(b:haxe.io.Bytes) {} +} diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx index 69b548f5..c8124573 100644 --- a/src/net/MoveManager.hx +++ b/src/net/MoveManager.hx @@ -4,7 +4,7 @@ import shapes.PowerUp; import net.NetPacket.MarbleMovePacket; import src.TimeState; import src.Console; -import net.Net.ClientConnection; +import net.ClientConnection; import net.Net.NetPacketType; import src.MarbleWorld; import src.Marble.Move; @@ -38,7 +38,7 @@ class NetMove { } class MoveManager { - var connection:ClientConnection; + var connection:GameConnection; var queuedMoves:Array; var nextMoveId:Int; var lastMove:NetMove; @@ -49,10 +49,12 @@ class MoveManager { public var stall = false; - public function new(connection:ClientConnection) { + public function new(connection:GameConnection) { queuedMoves = []; nextMoveId = 0; this.connection = connection; + var mv = new Move(); + mv.d = new Vector(0, 0); } public function recordMove(marble:Marble, motionDir:Vector, timeState:TimeState) { diff --git a/src/net/Net.hx b/src/net/Net.hx index 018b3411..e688b200 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -1,5 +1,6 @@ package net; +import net.ClientConnection; import net.NetPacket.MarbleUpdatePacket; import net.NetPacket.MarbleMovePacket; import haxe.Json; @@ -11,12 +12,6 @@ import net.NetCommands; import src.MarbleGame; import hx.ws.Types.MessageType; -enum abstract GameplayState(Int) from Int to Int { - var UNKNOWN; - var LOBBY; - var GAME; -} - enum abstract NetPacketType(Int) from Int to Int { var NullPacket; var ClientIdAssign; @@ -25,31 +20,7 @@ enum abstract NetPacketType(Int) from Int to Int { var PingBack; var MarbleUpdate; var MarbleMove; -} - -@:publicFields -class ClientConnection { - var id:Int; - var socket:RTCPeerConnection; - var datachannel:RTCDataChannel; - var state:GameplayState; - var moveManager:MoveManager; - var rtt:Float; - var pingSendTime:Float; - var _rttRecords:Array = []; - - public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel) { - this.socket = socket; - this.datachannel = datachannel; - this.id = id; - this.state = GameplayState.LOBBY; - this.rtt = 0; - this.moveManager = new MoveManager(this); - } - - public function ready() { - state = GameplayState.GAME; - } + var PlayerInfo; } class Net { @@ -66,8 +37,8 @@ class Net { public static var clientId:Int; public static var networkRNG:Float; - public static var clients:Map = []; - public static var clientIdMap:Map = []; + public static var clients:Map = []; + public static var clientIdMap:Map = []; public static var clientConnection:ClientConnection; public static function hostServer() { @@ -105,7 +76,7 @@ class Net { peer.onGatheringStateChange = (s) -> { if (s == RTC_GATHERING_COMPLETE) { var sdpObj = StringTools.trim(peer.localDescription); - sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); + sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n'; masterWs.send(Json.stringify({ type: "connect", sdpObj: { @@ -120,6 +91,11 @@ class Net { } } + static function addGhost(id:Int) { + var ghost = new DummyConnection(id); + clientIdMap[id] = ghost; + } + public static function joinServer(connectedCb:() -> Void) { masterWs = new WebSocket("ws://localhost:8080"); masterWs.onopen = () -> { @@ -134,7 +110,7 @@ class Net { if (s == RTC_GATHERING_COMPLETE) { Console.log("Local Description Set!"); var sdpObj = StringTools.trim(client.localDescription); - sdpObj = sdpObj + '\r\n' + candidates.join('\r\n'); + sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n'; masterWs.send(Json.stringify({ type: "connect", sdpObj: { @@ -160,7 +136,7 @@ class Net { Console.log("Successfully connected!"); clients.set(client, new ClientConnection(0, client, clientDatachannel)); // host is always 0 clientIdMap[0] = clients[client]; - clientConnection = clients[client]; + clientConnection = cast clients[client]; onConnectedToServer(); haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation } @@ -190,7 +166,7 @@ class Net { var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count - clients[c].pingSendTime = Console.time(); + cast(clients[c], ClientConnection).pingSendTime = Console.time(); dc.sendBytes(b); Console.log("Sending ping packet!"); } @@ -201,11 +177,23 @@ class Net { var b = haxe.io.Bytes.alloc(2); b.set(0, Ping); b.set(1, 3); // Count - clients[client].pingSendTime = Console.time(); + cast(clients[client], ClientConnection).pingSendTime = Console.time(); clientDatachannel.sendBytes(b); Console.log("Sending ping packet!"); } + static function sendPlayerInfosBytes() { + var b = new haxe.io.BytesOutput(); + b.writeByte(PlayerInfo); + var cnt = 0; + for (c in clientIdMap) + cnt++; + b.writeByte(cnt); + for (c => v in clientIdMap) + b.writeByte(c); + return b.getBytes(); + } + static function onPacketReceived(c:RTCPeerConnection, dc:RTCDataChannel, input:haxe.io.BytesInput) { var packetType = input.readByte(); switch (packetType) { @@ -227,7 +215,7 @@ class Net { case PingBack: var pingLeft = input.readByte(); Console.log("Got pingback packet!"); - var conn = clients[c]; + var conn:ClientConnection = cast clients[c]; var now = Console.time(); conn._rttRecords.push((now - conn.pingSendTime)); if (pingLeft > 0) { @@ -241,6 +229,10 @@ class Net { conn.rtt += r; conn.rtt /= conn._rttRecords.length; Console.log('Got RTT ${conn.rtt} for client ${conn.id}'); + if (Net.isHost) { + var b = sendPlayerInfosBytes(); + conn.sendBytes(b); + } } case MarbleUpdate: @@ -258,15 +250,25 @@ class Net { var cc = clientIdMap[movePacket.clientId]; cc.moveManager.queueMove(movePacket.move); + case PlayerInfo: + var count = input.readByte(); + for (i in 0...count) { + var id = input.readByte(); + if (id != 0 && id != Net.clientId && !clientIdMap.exists(id)) { + Console.log('Adding ghost connection ${id}'); + addGhost(id); + } + } + case _: - trace("unknown command: " + packetType); + Console.log("unknown command: " + packetType); } } public static function sendPacketToAll(packetData:haxe.io.BytesOutput) { var bytes = packetData.getBytes(); for (c => v in clients) { - v.datachannel.sendBytes(packetData.getBytes()); + v.sendBytes(bytes); } } @@ -274,4 +276,10 @@ class Net { var bytes = packetData.getBytes(); clientDatachannel.sendBytes(bytes); } + + public static function addDummyConnection() { + if (Net.isHost) { + addGhost(Net.clientId++); + } + } } diff --git a/src/net/NetCommands.hx b/src/net/NetCommands.hx index fa679fc7..b40e41f2 100644 --- a/src/net/NetCommands.hx +++ b/src/net/NetCommands.hx @@ -1,6 +1,6 @@ package net; -import net.Net.GameplayState; +import net.ClientConnection.GameplayState; import net.Net.NetPacketType; import gui.MultiplayerLevelSelectGui; import src.MarbleGame; @@ -46,7 +46,7 @@ class NetCommands { @:rpc(server) public static function setStartTime(t:Float) { if (MarbleGame.instance.world != null) { if (Net.isClient) { - t -= Net.clientIdMap[0].rtt / 2; // Subtract receving time + t -= cast(Net.clientIdMap[0], ClientConnection).rtt / 2; // Subtract receving time } MarbleGame.instance.world.startRealTime = MarbleGame.instance.world.timeState.timeSinceLoad + t; }