diff --git a/src/Marble.hx b/src/Marble.hx index 5730e63d..13734670 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -1,5 +1,6 @@ package src; +import net.BitStream.OutputBitStream; import net.ClientConnection; import net.ClientConnection.GameConnection; import net.NetPacket.MarbleUpdatePacket; @@ -577,7 +578,7 @@ class Marble extends GameObject { A = A.add(force.multiply(1 / mass)); } for (marble in level.marbles) { - if (marble != this) { + if (marble != cast this) { var force = marble.getForce(this.collider.transform.getPosition(), tick); A = A.add(force.multiply(1 / mass)); } @@ -1608,7 +1609,7 @@ class Marble extends GameObject { var pTime = timeState.clone(); pTime.dt = timeStep; pTime.currentAttemptTime = passedTime; - this.heldPowerup.use(this, pTime); + this.heldPowerup.use(cast this, pTime); this.heldPowerup = null; if (this.level.isRecording) { this.level.replay.recordPowerupPickup(null); @@ -1655,7 +1656,7 @@ class Marble extends GameObject { // MP Only Functions public function packUpdate(move:NetMove, timeState:TimeState) { - var b = new haxe.io.BytesOutput(); + var b = new OutputBitStream(); b.writeByte(NetPacketType.MarbleUpdate); var marbleUpdate = new MarbleUpdatePacket(); marbleUpdate.clientId = connection != null ? connection.id : 0; @@ -1670,6 +1671,7 @@ class Marble extends GameObject { marbleUpdate.heliTick = this.helicopterUseTick; marbleUpdate.megaTick = this.megaMarbleUseTick; marbleUpdate.oob = this.outOfBounds; + marbleUpdate.powerUpId = this.heldPowerup != null ? this.heldPowerup.netIndex : 0xFFFF; marbleUpdate.serialize(b); return b.getBytes(); } @@ -1694,6 +1696,11 @@ class Marble extends GameObject { this.megaMarbleUseTick = p.megaTick; this.outOfBounds = p.oob; this.camera.oob = p.oob; + if (p.powerUpId == 0xFFFF) { + this.level.deselectPowerUp(cast this); + } else { + this.level.pickUpPowerUp(cast this, this.level.powerUps[p.powerUpId]); + } if (this.controllable && Net.isClient) { // We are client, need to do something about the queue var mm = Net.clientConnection.moveManager; @@ -1712,7 +1719,7 @@ class Marble extends GameObject { if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching && !this.level.isReplayingMovement) { if (Net.isClient) { var axis = getMarbleAxis()[1]; - move = Net.clientConnection.moveManager.recordMove(this, axis, timeState); + move = Net.clientConnection.moveManager.recordMove(cast this, axis, timeState); } else if (Net.isHost) { var axis = getMarbleAxis()[1]; var innerMove = recordMove(); @@ -1763,6 +1770,11 @@ class Marble extends GameObject { } } + if (move.move.jump && this.outOfBounds) { + this.level.cancel(this.oobSchedule); + this.level.restart(cast this); + } + return move; // if (Net.isHost) { // packets.push({b: packUpdate(move, timeState), c: this.connection != null ? this.connection.id : 0}); @@ -1770,7 +1782,7 @@ class Marble extends GameObject { } public function updateClient(timeState:TimeState, pathedInteriors:Array) { - this.level.updateBlast(this, timeState); + this.level.updateBlast(cast this, timeState); if (oldPos != null && newPos != null) { var deltaT = physicsAccumulator / 0.032; var renderPos = Util.lerpThreeVectors(this.oldPos, this.newPos, deltaT); diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index 392c3e00..bf4ceca3 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -1,5 +1,6 @@ package src; +import net.PowerupPredictionStore; import net.MarblePredictionStore; import net.MarblePredictionStore.MarblePrediction; import net.MarbleUpdateQueue; @@ -208,6 +209,7 @@ class MarbleWorld extends Scheduler { var clientMarbles:Map = []; var predictions:MarblePredictionStore; + var powerupPredictions:PowerupPredictionStore; public var lastMoves:MarbleUpdateQueue; @@ -239,17 +241,18 @@ class MarbleWorld extends Scheduler { this.scene2d = scene2d; this.mission = mission; this.game = mission.game.toLowerCase(); - this.gameMode = GameModeFactory.getGameMode(this, mission.missionInfo.gamemode); + this.gameMode = GameModeFactory.getGameMode(cast this, mission.missionInfo.gamemode); this.replay = new Replay(mission.path, mission.isClaMission ? mission.id : 0); this.isRecording = record; - this.rewindManager = new RewindManager(this); - this.inputRecorder = new InputRecorder(this); + this.rewindManager = new RewindManager(cast this); + this.inputRecorder = new InputRecorder(cast this); this.isMultiplayer = multiplayer; if (this.isMultiplayer) { isRecording = false; isWatching = false; lastMoves = new MarbleUpdateQueue(); predictions = new MarblePredictionStore(); + powerupPredictions = new PowerupPredictionStore(); } // Set the network RNG for hunt @@ -345,7 +348,7 @@ class MarbleWorld extends Scheduler { this.playGui = new PlayGui(); this.instanceManager = new InstanceManager(scene); this.particleManager = new ParticleManager(cast this); - this.radar = new Radar(this, this.scene2d); + this.radar = new Radar(cast this, this.scene2d); radar.init(); var worker = new ResourceLoaderWorker(() -> { @@ -955,8 +958,13 @@ class MarbleWorld extends Scheduler { var worker = new ResourceLoaderWorker(() -> { obj.idInLevel = this.dtsObjects.length; // Set the id of the thing this.dtsObjects.push(obj); - if (obj is PowerUp) + if (obj is PowerUp) { + var pw:PowerUp = cast obj; + pw.netIndex = this.powerUps.length; this.powerUps.push(cast obj); + if (Net.isClient) + powerupPredictions.alloc(); + } if (obj is ForceObject) { this.forceObjects.push(cast obj); } @@ -1142,13 +1150,16 @@ class MarbleWorld extends Scheduler { advanceTimeState.dt = 0.032; advanceTimeState.ticks = ourLastMoveTime; - if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { - if (qm != null) { - var mvs = qm.powerupStates.copy(); - for (pw in marble.level.powerUps) { - pw.lastPickUpTime = mvs.shift(); - } + if (marbleNeedsPrediction > 0) { + // if (qm != null) { + // var mvs = qm.powerupStates.copy(); + for (pw in marble.level.powerUps) { + // var val = mvs.shift(); + // if (pw.lastPickUpTime != val) + // Console.log('Revert powerup pickup: ${pw.lastPickUpTime} -> ${val}'); + pw.lastPickUpTime = powerupPredictions.getState(pw.netIndex); } + // } } ackLag = ourQueuedMoves.length; @@ -1189,7 +1200,6 @@ class MarbleWorld extends Scheduler { var m = move.move; // Debug.drawSphere(@:privateAccess this.marble.newPos, this.marble._radius); if (marbleNeedsPrediction & (1 << Net.clientId) > 0) { - this.marble.heldPowerup = move.powerup; @:privateAccess this.marble.moveMotionDir = move.motionDir; @:privateAccess this.marble.advancePhysics(advanceTimeState, m, this.collisionWorld, this.pathedInteriors); this.predictions.storeState(this.marble, move.timeState.ticks); @@ -1476,12 +1486,14 @@ class MarbleWorld extends Scheduler { ProfilerUI.measure("updateAudio"); AudioManager.update(this.scene); - if (this.marble.outOfBounds - && this.finishTime == null - && (Key.isDown(Settings.controlsSettings.jump) || Gamepad.isDown(Settings.gamepadSettings.jump)) - && !this.isWatching) { - this.restart(this.marble); - return; + if (!this.isMultiplayer) { + if (this.marble.outOfBounds + && this.finishTime == null + && (Key.isDown(Settings.controlsSettings.jump) || Gamepad.isDown(Settings.gamepadSettings.jump)) + && !this.isWatching) { + this.restart(this.marble); + return; + } } if (!this.isWatching) { @@ -1690,8 +1702,8 @@ class MarbleWorld extends Scheduler { this.helpTextTimeState = this.timeState.timeSinceLoad; } - public function pickUpGem(gem:Gem) { - this.gameMode.onGemPickup(gem); + public function pickUpGem(marble:Marble, gem:Gem) { + this.gameMode.onGemPickup(marble, gem); } public function callCollisionHandlers(marble:Marble, timeState:TimeState, start:Vector, end:Vector) { diff --git a/src/modes/GameMode.hx b/src/modes/GameMode.hx index 991d40d5..ff375c60 100644 --- a/src/modes/GameMode.hx +++ b/src/modes/GameMode.hx @@ -26,7 +26,7 @@ interface GameMode { public function onTimeExpire():Void; public function onRestart():Void; public function onRespawn(marble:Marble):Void; - public function onGemPickup(gem:Gem):Void; + public function onGemPickup(marble:Marble, gem:Gem):Void; public function getPreloadFiles():Array; public function constructRewindState():RewindableState; diff --git a/src/modes/HuntMode.hx b/src/modes/HuntMode.hx index 399812ef..76ea646d 100644 --- a/src/modes/HuntMode.hx +++ b/src/modes/HuntMode.hx @@ -282,25 +282,32 @@ class HuntMode extends NullMode { @:privateAccess level.playGui.formatGemHuntCounter(points); } - override function onGemPickup(gem:Gem) { - AudioManager.playSound(ResourceLoader.getResource('data/sound/gem_collect.wav', ResourceLoader.getAudio, @:privateAccess this.level.soundResources)); + override function onGemPickup(marble:Marble, gem:Gem) { + if (marble == level.marble) + AudioManager.playSound(ResourceLoader.getResource('data/sound/gem_collect.wav', ResourceLoader.getAudio, + @:privateAccess this.level.soundResources)); + else + AudioManager.playSound(ResourceLoader.getResource('data/sound/opponent_gem_collect.wav', ResourceLoader.getAudio, + @:privateAccess this.level.soundResources)); activeGems.remove(gem); var beam = gemToBeamMap.get(gem); beam.setHide(true); refillGemGroups(); - switch (gem.gemColor) { - case "red.gem": - points += 1; - @:privateAccess level.playGui.addMiddleMessage('+1', 0xFF6666); - case "yellow.gem": - points += 2; - @:privateAccess level.playGui.addMiddleMessage('+2', 0xFFFF66); - case "blue.gem": - points += 5; - @:privateAccess level.playGui.addMiddleMessage('+5', 0x6666FF); + if (marble == level.marble) { + switch (gem.gemColor) { + case "red.gem": + points += 1; + @:privateAccess level.playGui.addMiddleMessage('+1', 0xFF6666); + case "yellow.gem": + points += 2; + @:privateAccess level.playGui.addMiddleMessage('+2', 0xFFFF66); + case "blue.gem": + points += 5; + @:privateAccess level.playGui.addMiddleMessage('+5', 0x6666FF); + } + @:privateAccess level.playGui.formatGemHuntCounter(points); } - @:privateAccess level.playGui.formatGemHuntCounter(points); } function setupGems() { diff --git a/src/modes/NullMode.hx b/src/modes/NullMode.hx index c406708b..5d12809c 100644 --- a/src/modes/NullMode.hx +++ b/src/modes/NullMode.hx @@ -59,7 +59,7 @@ class NullMode implements GameMode { public function onRespawn(marble:Marble) {} - public function onGemPickup(gem:Gem) { + public function onGemPickup(marble:Marble, gem:Gem) { this.level.gemCount++; var string:String; diff --git a/src/net/BitStream.hx b/src/net/BitStream.hx index 81d170e4..ddace257 100644 --- a/src/net/BitStream.hx +++ b/src/net/BitStream.hx @@ -1,5 +1,6 @@ package net; +import haxe.io.FPHelper; import haxe.io.BytesOutput; import haxe.io.BytesInput; import haxe.io.Bytes; @@ -64,7 +65,7 @@ class InputBitStream { } public function readFloat() { - return readInt32(); + return FPHelper.i32ToFloat(readInt32()); } } @@ -129,4 +130,8 @@ class OutputBitStream { this.data.writeByte(this.lastByte); return this.data.getBytes(); } + + public function writeFloat(value:Float) { + writeInt(FPHelper.floatToI32(value), 32); + } } diff --git a/src/net/MarblePredictionStore.hx b/src/net/MarblePredictionStore.hx index f5df5587..f958319c 100644 --- a/src/net/MarblePredictionStore.hx +++ b/src/net/MarblePredictionStore.hx @@ -14,6 +14,7 @@ class MarblePrediction { var omega:Vector; var isControl:Bool; var blastAmount:Int; + var powerupItemId:Int; public function new(marble:Marble, tick:Int) { this.tick = tick; @@ -22,11 +23,14 @@ class MarblePrediction { omega = @:privateAccess marble.omega.clone(); blastAmount = @:privateAccess marble.blastTicks; isControl = @:privateAccess marble.controllable; + powerupItemId = marble.heldPowerup != null ? marble.heldPowerup.netIndex : 0xFFFF; } public inline function getError(p:MarbleUpdatePacket) { // Just doing position errors is enough to make it work var subs = position.sub(p.position).lengthSq(); // + velocity.sub(p.velocity).lengthSq() + omega.sub(p.omega).lengthSq(); + if (p.powerUpId != powerupItemId) + subs += 1; // if (isControl) // subs += Math.abs(blastAmount - p.blastAmount); return subs; diff --git a/src/net/MoveManager.hx b/src/net/MoveManager.hx index 797be600..c4ca7212 100644 --- a/src/net/MoveManager.hx +++ b/src/net/MoveManager.hx @@ -1,5 +1,7 @@ package net; +import net.BitStream.OutputBitStream; +import net.BitStream.InputBitStream; import net.NetPacket.MarbleUpdatePacket; import shapes.PowerUp; import net.NetPacket.MarbleMovePacket; @@ -23,9 +25,6 @@ class NetMove { var move:Move; var id:Int; var timeState:TimeState; - // For rewind purposes - var powerup:PowerUp; - var powerupStates:Array; public function new(move:Move, motionDir:Vector, timeState:TimeState, id:Int) { this.move = move; @@ -96,17 +95,12 @@ class MoveManager { } var netMove = new NetMove(move, motionDir, timeState.clone(), nextMoveId++); - netMove.powerup = marble.heldPowerup; - netMove.powerupStates = []; - for (pw in marble.level.powerUps) { - netMove.powerupStates.push(pw.lastPickUpTime); - } queuedMoves.push(netMove); if (nextMoveId >= 65535) // 65535 is reserved for null move nextMoveId = 0; - var b = new haxe.io.BytesOutput(); + var b = new OutputBitStream(); var movePacket = new MarbleMovePacket(); movePacket.clientId = Net.clientId; movePacket.move = netMove; @@ -119,7 +113,7 @@ class MoveManager { return netMove; } - public static inline function packMove(m:NetMove, b:haxe.io.BytesOutput) { + public static inline function packMove(m:NetMove, b:OutputBitStream) { b.writeUInt16(m.id); b.writeByte(Std.int((m.move.d.x * 16) + 16)); b.writeByte(Std.int((m.move.d.y * 16) + 16)); @@ -137,7 +131,7 @@ class MoveManager { return b; } - public static inline function unpackMove(b:haxe.io.BytesInput) { + public static inline function unpackMove(b:InputBitStream) { var moveId = b.readUInt16(); var move = new Move(); move.d = new Vector(); @@ -155,7 +149,7 @@ class MoveManager { return netMove; } - public function queueMove(m:NetMove) { + public inline function queueMove(m:NetMove) { queuedMoves.push(m); } @@ -169,7 +163,7 @@ class MoveManager { } } - public function getQueueSize() { + public inline function getQueueSize() { return queuedMoves.length; } diff --git a/src/net/Net.hx b/src/net/Net.hx index e688b200..7a7e9a94 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -1,5 +1,8 @@ package net; +import net.BitStream.InputBitStream; +import net.BitStream.OutputBitStream; +import net.NetPacket.PowerupPickupPacket; import net.ClientConnection; import net.NetPacket.MarbleUpdatePacket; import net.NetPacket.MarbleMovePacket; @@ -20,6 +23,7 @@ enum abstract NetPacketType(Int) from Int to Int { var PingBack; var MarbleUpdate; var MarbleMove; + var PowerupPickup; var PlayerInfo; } @@ -141,7 +145,7 @@ class Net { haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation } clientDatachannel.onMessage = (b) -> { - onPacketReceived(client, clientDatachannel, new haxe.io.BytesInput(b)); + onPacketReceived(client, clientDatachannel, new InputBitStream(b)); } isMP = true; @@ -155,7 +159,7 @@ class Net { clients.set(c, new ClientConnection(clientId, c, dc)); clientIdMap[clientId] = clients[c]; dc.onMessage = (msgBytes) -> { - onPacketReceived(c, dc, new haxe.io.BytesInput(msgBytes)); + onPacketReceived(c, dc, new InputBitStream(msgBytes)); } var b = haxe.io.Bytes.alloc(3); b.set(0, ClientIdAssign); @@ -194,7 +198,7 @@ class Net { return b.getBytes(); } - static function onPacketReceived(c:RTCPeerConnection, dc:RTCDataChannel, input:haxe.io.BytesInput) { + static function onPacketReceived(c:RTCPeerConnection, dc:RTCDataChannel, input:InputBitStream) { var packetType = input.readByte(); switch (packetType) { case NetCommand: @@ -250,6 +254,14 @@ class Net { var cc = clientIdMap[movePacket.clientId]; cc.moveManager.queueMove(movePacket.move); + case PowerupPickup: + var powerupPickupPacket = new PowerupPickupPacket(); + powerupPickupPacket.deserialize(input); + if (MarbleGame.instance.world != null) { + var m = @:privateAccess MarbleGame.instance.world.powerupPredictions; + m.acknowledgePowerupPickup(powerupPickupPacket, MarbleGame.instance.world.timeState, clientConnection.moveManager.getQueueSize()); + } + case PlayerInfo: var count = input.readByte(); for (i in 0...count) { @@ -265,14 +277,14 @@ class Net { } } - public static function sendPacketToAll(packetData:haxe.io.BytesOutput) { + public static function sendPacketToAll(packetData:OutputBitStream) { var bytes = packetData.getBytes(); for (c => v in clients) { v.sendBytes(bytes); } } - public static function sendPacketToHost(packetData:haxe.io.BytesOutput) { + public static function sendPacketToHost(packetData:OutputBitStream) { var bytes = packetData.getBytes(); clientDatachannel.sendBytes(bytes); } diff --git a/src/net/NetPacket.hx b/src/net/NetPacket.hx index e05112e3..17d84b05 100644 --- a/src/net/NetPacket.hx +++ b/src/net/NetPacket.hx @@ -1,11 +1,13 @@ package net; +import net.BitStream.InputBitStream; +import net.BitStream.OutputBitStream; import h3d.Vector; import net.MoveManager.NetMove; interface NetPacket { - public function serialize(b:haxe.io.BytesOutput):Void; - public function deserialize(b:haxe.io.BytesInput):Void; + public function serialize(b:OutputBitStream):Void; + public function deserialize(b:InputBitStream):Void; } @:publicFields @@ -16,13 +18,13 @@ class MarbleMovePacket implements NetPacket { public function new() {} - public inline function deserialize(b:haxe.io.BytesInput) { + public inline function deserialize(b:InputBitStream) { clientId = b.readUInt16(); clientTicks = b.readUInt16(); move = MoveManager.unpackMove(b); } - public inline function serialize(b:haxe.io.BytesOutput) { + public inline function serialize(b:OutputBitStream) { b.writeUInt16(clientId); b.writeUInt16(clientTicks); MoveManager.packMove(move, b); @@ -43,11 +45,12 @@ class MarbleUpdatePacket implements NetPacket { var megaTick:Int; var heliTick:Int; var oob:Bool; + var powerUpId:Int; var moveQueueSize:Int; public function new() {} - public inline function serialize(b:haxe.io.BytesOutput) { + public inline function serialize(b:OutputBitStream) { b.writeUInt16(clientId); MoveManager.packMove(move, b); b.writeUInt16(serverTicks); @@ -66,9 +69,10 @@ class MarbleUpdatePacket implements NetPacket { b.writeUInt16(heliTick); b.writeUInt16(megaTick); b.writeByte(oob ? 1 : 0); + b.writeUInt16(powerUpId); } - public inline function deserialize(b:haxe.io.BytesInput) { + public inline function deserialize(b:InputBitStream) { clientId = b.readUInt16(); move = MoveManager.unpackMove(b); serverTicks = b.readUInt16(); @@ -81,5 +85,27 @@ class MarbleUpdatePacket implements NetPacket { heliTick = b.readUInt16(); megaTick = b.readUInt16(); oob = b.readByte() != 0; + powerUpId = b.readUInt16(); + } +} + +@:publicFields +class PowerupPickupPacket implements NetPacket { + var clientId:Int; + var serverTicks:Int; + var powerupItemId:Int; + + public function new() {} + + public inline function deserialize(b:InputBitStream) { + clientId = b.readUInt16(); + serverTicks = b.readUInt16(); + powerupItemId = b.readUInt16(); + } + + public inline function serialize(b:OutputBitStream) { + b.writeUInt16(clientId); + b.writeUInt16(serverTicks); + b.writeUInt16(powerupItemId); } } diff --git a/src/net/PowerupPredictionStore.hx b/src/net/PowerupPredictionStore.hx new file mode 100644 index 00000000..974d9bb3 --- /dev/null +++ b/src/net/PowerupPredictionStore.hx @@ -0,0 +1,24 @@ +package net; + +import src.TimeState; +import net.NetPacket.PowerupPickupPacket; + +class PowerupPredictionStore { + var predictions:Array; + + public function new() { + predictions = []; + } + + public function alloc() { + predictions.push(Math.NEGATIVE_INFINITY); + } + + public inline function getState(netIndex:Int) { + return predictions[netIndex]; + } + + public function acknowledgePowerupPickup(packet:PowerupPickupPacket, timeState:TimeState, futureTicks:Int) { + predictions[packet.powerupItemId] = timeState.currentAttemptTime - futureTicks * 0.032; // Approximate + } +} diff --git a/src/net/RPCMacro.hx b/src/net/RPCMacro.hx index 8bc19a42..f043ac5c 100644 --- a/src/net/RPCMacro.hx +++ b/src/net/RPCMacro.hx @@ -59,7 +59,7 @@ class RPCMacro { case EConst(CIdent("server")): var lastExpr = macro { if (Net.isHost) { - var stream = new haxe.io.BytesOutput(); + var stream = new net.BitStream.OutputBitStream(); stream.writeByte(NetPacketType.NetCommand); stream.writeByte($v{rpcFnId}); $b{serializeFns}; @@ -72,7 +72,7 @@ class RPCMacro { case EConst(CIdent("client")): var lastExpr = macro { if (!Net.isHost) { - var stream = new haxe.io.BytesOutput(); + var stream = new net.BitStream.OutputBitStream(); stream.writeByte(NetPacketType.NetCommand); stream.writeByte($v{rpcFnId}); $b{serializeFns}; @@ -113,7 +113,7 @@ class RPCMacro { args: [ { name: "stream", - type: haxe.macro.TypeTools.toComplexType(Context.getType('haxe.io.Input')) + type: haxe.macro.TypeTools.toComplexType(Context.getType('net.BitStream.InputBitStream')) } ], expr: macro { diff --git a/src/shapes/Gem.hx b/src/shapes/Gem.hx index ddee676e..a46ca941 100644 --- a/src/shapes/Gem.hx +++ b/src/shapes/Gem.hx @@ -67,7 +67,7 @@ class Gem extends DtsObject { return; this.pickedUp = true; this.setOpacity(0); // Hide the gem - this.level.pickUpGem(this); + this.level.pickUpGem(marble, this); // this.level.replay.recordMarbleInside(this); } diff --git a/src/shapes/PowerUp.hx b/src/shapes/PowerUp.hx index 9abc1555..8f9804b6 100644 --- a/src/shapes/PowerUp.hx +++ b/src/shapes/PowerUp.hx @@ -1,5 +1,8 @@ package shapes; +import net.BitStream.OutputBitStream; +import net.NetPacket.PowerupPickupPacket; +import net.Net; import src.Marble; import src.AudioManager; import hxd.res.Sound; @@ -16,6 +19,7 @@ abstract class PowerUp extends DtsObject { public var pickUpName:String; public var element:MissionElementItem; public var pickupSound:Sound; + public var netIndex:Int; var customPickupMessage:String = null; @@ -35,6 +39,17 @@ abstract class PowerUp extends DtsObject { if (this.pickUp(marble)) { // this.level.replay.recordMarbleInside(this); + if (level.isMultiplayer && Net.isHost) { + var b = new OutputBitStream(); + b.writeByte(NetPacketType.PowerupPickup); + var pickupPacket = new PowerupPickupPacket(); + pickupPacket.clientId = @:privateAccess marble.connection != null ? @:privateAccess marble.connection.id : 0; + pickupPacket.serverTicks = timeState.ticks; + pickupPacket.powerupItemId = this.netIndex; + pickupPacket.serialize(b); + Net.sendPacketToAll(b); + } + this.lastPickUpTime = timeState.currentAttemptTime; if (this.autoUse) this.use(marble, timeState);