add initial netcode

This commit is contained in:
RandomityGuy 2024-06-16 14:48:25 +05:30
parent 2e9917d9f1
commit 3120674b66
40 changed files with 5957 additions and 706 deletions

View file

@ -56,6 +56,10 @@ class Console {
return Std.int((haxe.Timer.stamp() - timeSinceStart) * 1000) / 1000;
}
public static inline function time() {
return haxe.Timer.stamp();
}
function addEntry(type:String, msg:String) {
var e = new ConsoleEntry(getTime(), type, msg);
entries.push(e);

View file

@ -302,6 +302,7 @@ class DifBuilder {
var difresource = ResourceLoader.loadInterior(path);
difresource.acquire();
var dif = difresource.resource;
dumbDownDif(dif);
var geo = so == -1 ? dif.interiors[0] : dif.subObjects[so];
var triangles = [];
var textures = [];
@ -748,4 +749,55 @@ class DifBuilder {
worker.run();
});
}
// Keeps only relevant parts of the dif to reduce memory footprint
static function dumbDownDif(dif:Dif) {
dif.aiSpecialNodes = null;
dif.forceFields = null;
dif.triggers = null;
dif.gameEntities = null;
dif.interiorPathfollowers = null;
dif.triggers = null;
dif.vehicleCollision = null;
for (itr in dif.interiors.concat(dif.subObjects)) {
itr.alarmAmbientColor = null;
itr.alarmLMapIndices = null;
itr.animatedLights = null;
itr.baseAmbientColor = null;
itr.bspNodes = null;
itr.bspSolidLeaves = null;
itr.convexHullEmitStrings = null;
itr.convexHulls = null;
itr.coordBinIndices = null;
itr.boundingSphere = null;
itr.coordBins = null;
itr.edges = null;
itr.edges2 = null;
itr.hullEmitStringIndices = null;
itr.hullIndices = null;
itr.hullPlaneIndices = null;
itr.hullSurfaceIndices = null;
itr.lightMaps = null;
itr.lightStates = null;
itr.nameBuffer = null;
itr.normalIndices = null;
itr.normalLMapIndices = null;
itr.nullSurfaces = null;
itr.pointVisibilities = null;
itr.polyListPlanes = null;
itr.polyListPoints = null;
itr.polyListStrings = null;
itr.portals = null;
itr.solidLeafSurfaces = null;
itr.stateDataBuffers = null;
itr.zones = null;
itr.zoneSurfaces = null;
itr.zoneStaticMeshes = null;
itr.windingIndices = null;
itr.texNormals = null;
itr.texMatrices = null;
itr.texMatIndices = null;
itr.stateDatas = null;
}
}
}

View file

@ -22,6 +22,7 @@ import h3d.Vector;
import src.ProfilerUI;
import src.Gamepad;
import src.Http;
import datachannel.RTC;
class Main extends hxd.App {
var marbleGame:MarbleGame;
@ -72,6 +73,7 @@ class Main extends hxd.App {
#end
try {
RTC.init();
Http.init();
haxe.MainLoop.add(() -> Http.loop());
Settings.init();
@ -126,6 +128,7 @@ class Main extends hxd.App {
// marbleGame.update(1 / 60);
// timeAccumulator -= 1 / 60;
// }
RTC.processEvents();
marbleGame.update(dt);
// } catch (e) {
// Console.error(e.message);

View file

@ -1,5 +1,14 @@
package src;
import net.Net;
import gui.MarbleSelectGui;
import net.NetPacket.MarbleNetFlags;
import net.BitStream.OutputBitStream;
import net.ClientConnection;
import net.ClientConnection.GameConnection;
import net.NetPacket.MarbleUpdatePacket;
import net.MoveManager;
import net.MoveManager.NetMove;
import collision.CollisionPool;
import collision.CollisionHull;
import dif.Plane;
@ -63,14 +72,7 @@ import src.ResourceLoaderWorker;
import src.InteriorObject;
import src.Console;
import src.Gamepad;
class Move {
public var d:Vector;
public var jump:Bool;
public var powerup:Bool;
public function new() {}
}
import net.Move;
enum Mode {
Start;
@ -314,6 +316,7 @@ class Marble extends GameObject {
public var cubemapRenderer:CubemapRenderer;
var connection:GameConnection;
var moveMotionDir:Vector;
var lastMove:Move;
var isNetUpdate:Bool = false;
@ -328,6 +331,7 @@ class Marble extends GameObject {
this.velocity = new Vector();
this.omega = new Vector();
this.camera = new CameraController(cast this);
this.isCollideable = true;
this.bounceEmitterData = new ParticleData();
this.bounceEmitterData.identifier = "MarbleBounceParticle";
@ -362,11 +366,13 @@ class Marble extends GameObject {
this.helicopterSound.pause = true;
}
public function init(level:MarbleWorld, onFinish:Void->Void) {
public function init(level:MarbleWorld, connection:GameConnection, onFinish:Void->Void) {
this.level = level;
if (this.level != null)
this.collisionWorld = this.level.collisionWorld;
this.connection = connection;
var isUltra = level.mission.game.toLowerCase() == "ultra";
this.posStore = new Vector();
@ -375,9 +381,16 @@ class Marble extends GameObject {
this.netCorrected = false;
var marbleDts = new DtsObject();
Console.log("Marble: " + Settings.optionsSettings.marbleModel + " (" + Settings.optionsSettings.marbleSkin + ")");
marbleDts.dtsPath = Settings.optionsSettings.marbleModel;
marbleDts.matNameOverride.set("base.marble", Settings.optionsSettings.marbleSkin + ".marble");
if (connection == null) {
Console.log("Marble: " + Settings.optionsSettings.marbleModel + " (" + Settings.optionsSettings.marbleSkin + ")");
marbleDts.dtsPath = Settings.optionsSettings.marbleModel;
marbleDts.matNameOverride.set("base.marble", Settings.optionsSettings.marbleSkin + ".marble");
} else {
var marbleData = MarbleSelectGui.marbleData[0][connection.getMarbleId()]; // FIXME category support
Console.log("Marble: " + marbleData.dts + " (" + marbleData.skin + ")");
marbleDts.dtsPath = marbleData.dts;
marbleDts.matNameOverride.set("base.marble", marbleData.skin + ".marble");
}
marbleDts.showSequences = false;
marbleDts.useInstancing = false;
marbleDts.init(null, () -> {}); // SYNC
@ -858,7 +871,7 @@ class Marble extends GameObject {
}
if (sv < this._jumpImpulse) {
this.velocity.load(this.velocity.add(bestContact.normal.multiply((this._jumpImpulse - sv))));
if (!playedSounds.contains("data/sound/jump.wav")) {
if (!playedSounds.contains("data/sound/jump.wav") && !this.isNetUpdate && this.controllable) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/jump.wav", ResourceLoader.getAudio, this.soundResources));
playedSounds.push("data/sound/jump.wav");
}
@ -937,6 +950,8 @@ class Marble extends GameObject {
}
function bounceEmitter(speed:Float, normal:Vector) {
if (!this.controllable || this.isNetUpdate)
return;
if (this.bounceEmitDelay == 0 && this._minBounceSpeed <= speed) {
this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData, this.getAbsPos().getPosition());
this.bounceEmitDelay = 0.3;
@ -969,6 +984,8 @@ class Marble extends GameObject {
}
function playBoundSound(time:Float, contactVel:Float) {
if (this.isNetUpdate)
return;
if (minVelocityBounceSoft <= contactVel) {
var hardBounceSpeed = minVelocityBounceHard;
var bounceSoundNum = Math.floor(Math.random() * 4);
@ -1486,6 +1503,21 @@ class Marble extends GameObject {
var piTime = timeRemaining;
if (this.isNetUpdate) {
lastMove = m;
}
if (m == null) {
m = new Move();
m.d = new Vector();
}
if (this.blastTicks < (30000 >> 5))
this.blastTicks += 1;
if (Net.isClient)
this.serverTicks++;
_bounceYet = false;
var contactTime = 0.0;
@ -1493,7 +1525,8 @@ class Marble extends GameObject {
var passedTime = timeState.currentAttemptTime;
var oldPos = this.getAbsPos().getPosition().clone();
oldPos = this.collider.transform.getPosition();
prevRot = this.getRotationQuat().clone();
if (this.controllable) {
for (interior in pathedInteriors) {
@ -1502,6 +1535,14 @@ class Marble extends GameObject {
}
}
// Blast
if (m != null && m.blast) {
this.useBlast(timeState);
if (level.isRecording) {
level.replay.recordMarbleStateFlags(false, false, false, true);
}
}
do {
if (timeRemaining <= 0)
break;
@ -1566,7 +1607,7 @@ class Marble extends GameObject {
velocity.w = 0;
var pos = this.getAbsPos().getPosition();
var pos = this.collider.transform.getPosition();
this.prevPos = pos.clone();
var tdiff = timeStep;
@ -1605,24 +1646,36 @@ class Marble extends GameObject {
var quat = new Quat();
quat.initRotation(omega.x * timeStep, omega.y * timeStep, omega.z * timeStep);
quat.multiply(quat, rot);
this.setRotationQuat(quat);
if (!Net.isMP)
this.setRotationQuat(quat);
var totMatrix = quat.toMatrix();
newPos.w = 1; // Fix shit blowing up
totMatrix.setPosition(newPos);
this.setPosition(newPos.x, newPos.y, newPos.z);
if (!Net.isMP)
this.setPosition(newPos.x, newPos.y, newPos.z);
this.collider.setTransform(totMatrix);
this.collisionWorld.updateTransform(this.collider);
this.collider.velocity = this.velocity;
if (this.heldPowerup != null && m.powerup && !this.outOfBounds) {
if (this.heldPowerup != null
&& (m.powerup || (Net.isClient && this.serverUsePowerup && !this.controllable))
&& !this.outOfBounds) {
var pTime = timeState.clone();
pTime.dt = timeStep;
pTime.currentAttemptTime = passedTime;
var netUpdate = this.isNetUpdate;
if (this.serverUsePowerup)
this.isNetUpdate = false;
this.heldPowerup.use(this, pTime);
this.isNetUpdate = netUpdate;
this.heldPowerup = null;
this.serverUsePowerup = false;
if (!this.isNetUpdate) {
this.netFlags |= MarbleNetFlags.PickupPowerup | MarbleNetFlags.UsePowerup;
}
if (this.level.isRecording) {
this.level.replay.recordPowerupPickup(null);
}
@ -1654,49 +1707,333 @@ class Marble extends GameObject {
}
this.queuedContacts = [];
var newPos = this.getAbsPos().getPosition().clone();
newPos = this.collider.transform.getPosition(); // this.getAbsPos().getPosition().clone();
if (this.controllable && this.prevPos != null) {
if (this.prevPos != null && this.level != null) {
var tempTimeState = timeState.clone();
tempTimeState.currentAttemptTime = passedTime;
this.level.callCollisionHandlers(cast this, tempTimeState, oldPos, newPos);
}
this.updateRollSound(timeState, contactTime / timeState.dt, this._slipAmount);
// if (this.megaMarbleUseTick > 0) {
// if (Net.isHost) {
// if ((timeState.ticks - this.megaMarbleUseTick) <= 312 && this.megaMarbleUseTick > 0) {
// this._radius = 0.675;
// this.collider.radius = 0.675;
// } else if ((timeState.ticks - this.megaMarbleUseTick) > 312) {
// this.collider.radius = this._radius = 0.3;
// if (!this.isNetUpdate && this.controllable)
// AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null,
// false);
// this.megaMarbleUseTick = 0;
// this.netFlags |= MarbleNetFlags.DoMega;
// }
// }
// if (Net.isClient) {
// if (this.serverTicks - this.megaMarbleUseTick <= 312 && this.megaMarbleUseTick > 0) {
// this._radius = 0.675;
// this.collider.radius = 0.675;
// } else {
// this.collider.radius = this._radius = 0.3;
// if (!this.isNetUpdate && this.controllable)
// AudioManager.playSound(ResourceLoader.getResource("data/sound/MegaShrink.wav", ResourceLoader.getAudio, this.soundResources), null,
// false);
// this.megaMarbleUseTick = 0;
// }
// }
// }
// if (Net.isClient && this.megaMarbleUseTick == 0) {
// this.collider.radius = this._radius = 0.3;
// }
if (Net.isMP) {
if (m.jump && this.outOfBounds) {
this.level.cancel(this.oobSchedule);
this.level.restart(cast this);
}
}
}
// MP Only Functions
public inline function clearNetFlags() {
this.netFlags = 0;
}
public function packUpdate(move:NetMove, timeState:TimeState) {
var b = new OutputBitStream();
b.writeByte(NetPacketType.MarbleUpdate);
var marbleUpdate = new MarbleUpdatePacket();
marbleUpdate.clientId = connection != null ? connection.id : 0;
marbleUpdate.serverTicks = timeState.ticks;
marbleUpdate.position = this.newPos;
marbleUpdate.velocity = this.velocity;
marbleUpdate.omega = this.omega;
marbleUpdate.move = move;
marbleUpdate.moveQueueSize = this.connection != null ? this.connection.moveManager.getQueueSize() : 255;
marbleUpdate.blastAmount = this.blastTicks;
marbleUpdate.blastTick = this.blastUseTick;
marbleUpdate.heliTick = this.helicopterUseTick;
marbleUpdate.megaTick = this.megaMarbleUseTick;
marbleUpdate.oob = this.outOfBounds;
marbleUpdate.powerUpId = this.heldPowerup != null ? this.heldPowerup.netIndex : 0x1FF;
marbleUpdate.netFlags = this.netFlags;
marbleUpdate.gravityDirection = this.currentUp;
marbleUpdate.serialize(b);
return b.getBytes();
}
public function unpackUpdate(p:MarbleUpdatePacket) {
// Assume packet header is already read
// Check if we aren't colliding with a marble
// for (marble in this.level.collisionWorld.marbleEntities) {
// if (marble != this.collider && marble.transform.getPosition().distance(p.position) < marble.radius + this._radius) {
// Console.log("Marble updated inside another one!");
// return false;
// }
// }
this.serverTicks = p.serverTicks;
this.recvServerTick = p.serverTicks;
// this.oldPos = this.newPos;
// this.newPos = p.position;
this.collider.transform.setPosition(p.position);
this.velocity = p.velocity;
this.omega = p.omega;
this.blastTicks = p.blastAmount;
this.blastUseTick = p.blastTick;
this.helicopterUseTick = p.heliTick;
this.megaMarbleUseTick = p.megaTick;
this.serverUsePowerup = p.netFlags & MarbleNetFlags.UsePowerup > 0;
// this.currentUp = p.gravityDirection;
this.level.setUp(cast this, p.gravityDirection, this.level.timeState);
if (this.outOfBounds && !p.oob && this.controllable)
@:privateAccess this.level.playGui.setCenterText('');
this.outOfBounds = p.oob;
this.camera.oob = p.oob;
if (p.powerUpId == 0x1FF) {
if (!this.serverUsePowerup)
this.level.deselectPowerUp(cast this);
else
Console.log("Using powerup");
} else {
this.level.pickUpPowerUp(cast this, this.level.powerUps[p.powerUpId]);
}
if (p.moveQueueSize == 0 && this.connection != null) {
// Pad null move on client
this.connection.moveManager.duplicateLastMove();
}
// if (Net.isClient && !this.controllable && (this.serverTicks - this.blastUseTick) < 12) {
// var ticksSince = (this.serverTicks - this.blastUseTick);
// if (ticksSince >= 0) {
// this.blastWave.doSequenceOnceBeginTime = this.level.timeState.timeSinceLoad - ticksSince * 0.032;
// this.blastUseTime = this.level.timeState.currentAttemptTime - ticksSince * 0.032;
// }
// }
// if (this.controllable && Net.isClient) {
// // We are client, need to do something about the queue
// var mm = Net.clientConnection.moveManager;
// // trace('Queue size: ${mm.getQueueSize()}, server: ${p.moveQueueSize}');
// if (mm.getQueueSize() / p.moveQueueSize < 2) {
// mm.stall = true;
// } else {
// mm.stall = false;
// }
// }
return true;
}
function calculateNetSmooth() {
if (this.netCorrected) {
this.netCorrected = false;
this.netSmoothOffset.load(this.lastRenderPos.sub(this.oldPos));
// this.oldPos.load(this.posStore);
}
}
public function updateServer(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array<PathedInterior>) {
var move:NetMove = null;
if (this.controllable && this.mode != Finish) {
if (Net.isClient) {
var axis = getMarbleAxis()[1];
move = Net.clientConnection.recordMove(cast this, axis, timeState, recvServerTick);
} else if (Net.isHost) {
var axis = getMarbleAxis()[1];
var innerMove = recordMove();
if (MarbleGame.instance.paused) {
innerMove.d.x = 0;
innerMove.d.y = 0;
innerMove.blast = innerMove.jump = innerMove.powerup = false;
} else {
var qx = Std.int((innerMove.d.x * 16) + 16);
var qy = Std.int((innerMove.d.y * 16) + 16);
innerMove.d.x = (qx - 16) / 16.0;
innerMove.d.y = (qy - 16) / 16.0;
}
move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535);
}
}
var moveId = 65535;
if (!this.controllable && this.connection != null && Net.isHost) {
var nextMove = this.connection.getNextMove();
// trace('Moves left: ${@:privateAccess this.connection.moveManager.queuedMoves.length}');
if (nextMove == null) {
var axis = moveMotionDir != null ? moveMotionDir : getMarbleAxis()[1];
var innerMove = lastMove;
if (innerMove == null) {
innerMove = new Move();
innerMove.d = new Vector(0, 0);
}
move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535);
} else {
move = nextMove;
moveMotionDir = nextMove.motionDir;
moveId = nextMove.id;
lastMove = move.move;
}
}
if (move == null && !this.controllable) {
var axis = moveMotionDir != null ? moveMotionDir : new Vector(0, -1, 0);
var innerMove = lastMove;
if (innerMove == null) {
innerMove = new Move();
innerMove.d = new Vector(0, 0);
}
move = new NetMove(innerMove, axis, timeState, recvServerTick, 65535);
}
if (move != null) {
playedSounds = [];
advancePhysics(timeState, move.move, collisionWorld, pathedInteriors);
physicsAccumulator = 0;
} else {
physicsAccumulator = 0;
newPos.load(oldPos);
}
return move;
// if (Net.isHost) {
// packets.push({b: packUpdate(move, timeState), c: this.connection != null ? this.connection.id : 0});
// }
}
public function updateClient(timeState:TimeState, pathedInteriors:Array<PathedInterior>) {
calculateNetSmooth();
this.level.updateBlast(cast this, timeState);
var newDt = 2.3 * (timeState.dt / 0.4);
var smooth = 1.0 / (newDt * (newDt * 0.235 * newDt) + newDt + 1.0 + 0.48 * newDt * newDt);
this.netSmoothOffset.scale(smooth);
var smoothScale = this.netSmoothOffset.lengthSq();
if (smoothScale < 0.1 || smoothScale > 10.0)
this.netSmoothOffset.set(0, 0, 0);
if (oldPos != null && newPos != null) {
var deltaT = physicsAccumulator / 0.032;
if (Net.isClient && !this.controllable)
deltaT *= 0.75; // Don't overshoot
var renderPos = Util.lerpThreeVectors(this.oldPos, this.newPos, deltaT);
if (Net.isClient) {
renderPos.load(renderPos.add(this.netSmoothOffset));
}
this.setPosition(renderPos.x, renderPos.y, renderPos.z);
this.lastRenderPos.load(renderPos);
var rot = this.getRotationQuat();
var quat = new Quat();
quat.initRotation(omega.x * timeState.dt, omega.y * timeState.dt, omega.z * timeState.dt);
quat.multiply(quat, rot);
this.setRotationQuat(quat);
var adt = timeState.clone();
adt.dt = physicsAccumulator;
for (pi in pathedInteriors) {
pi.update(adt);
}
}
physicsAccumulator += timeState.dt;
if (this.controllable && this.level != null && !this.level.rewinding) {
// this.camera.startCenterCamera();
this.camera.update(timeState.currentAttemptTime, timeState.dt);
}
updatePowerupStates(timeState);
// if (isMegaMarbleEnabled(timeState)) {
// this._marbleScale = this._defaultScale * 2.25;
// } else {
// this._marbleScale = this._defaultScale;
// }
// var s = this._renderScale * this._renderScale;
// if (s <= this._marbleScale * this._marbleScale)
// s = 0.1;
// else
// s = 0.4;
// s = timeState.dt / s * 2.302585124969482;
// s = 1.0 / (s * (s * 0.235 * s) + s + 1.0 + 0.48 * s * s);
// this._renderScale *= s;
// s = 1 - s;
// this._renderScale += s * this._marbleScale;
// var marbledts = cast(this.getChildAt(0), DtsObject);
// marbledts.setScale(this._renderScale);
this.trailEmitter();
if (bounceEmitDelay > 0)
bounceEmitDelay -= timeState.dt;
if (bounceEmitDelay < 0)
bounceEmitDelay = 0;
}
public function recordMove() {
var move = new Move();
move.d = new Vector();
move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis);
move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis);
// if (@:privateAccess !MarbleGame.instance.world.playGui.isChatFocused()) {
if (Key.isDown(Settings.controlsSettings.forward)) {
move.d.x -= 1;
}
if (Key.isDown(Settings.controlsSettings.backward)) {
move.d.x += 1;
}
if (Key.isDown(Settings.controlsSettings.left)) {
move.d.y += 1;
}
if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1;
}
if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) {
move.jump = true;
}
if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup))
|| (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.powerup)) {
move.powerup = true;
}
if (Key.isDown(Settings.controlsSettings.blast)
|| (MarbleGame.instance.touchInput.blastbutton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.blast))
move.blast = true;
if (MarbleGame.instance.touchInput.movementInput.pressed) {
move.d.y = -MarbleGame.instance.touchInput.movementInput.value.x;
move.d.x = MarbleGame.instance.touchInput.movementInput.value.y;
}
// }
return move;
}
public function update(timeState:TimeState, collisionWorld:CollisionWorld, pathedInteriors:Array<PathedInterior>) {
var move = new Move();
move.d = new Vector();
if (this.controllable && this.mode != Finish && !MarbleGame.instance.paused && !this.level.isWatching) {
move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis);
move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis);
if (Key.isDown(Settings.controlsSettings.forward)) {
move.d.x -= 1;
}
if (Key.isDown(Settings.controlsSettings.backward)) {
move.d.x += 1;
}
if (Key.isDown(Settings.controlsSettings.left)) {
move.d.y += 1;
}
if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1;
}
if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) {
move.jump = true;
}
if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup))
|| (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.powerup)) {
move.powerup = true;
}
if (MarbleGame.instance.touchInput.movementInput.pressed) {
move.d.y = -MarbleGame.instance.touchInput.movementInput.value.x;
move.d.x = MarbleGame.instance.touchInput.movementInput.value.y;
}
var move:Move = null;
if (this.controllable && !this.level.isWatching) {
move = recordMove();
}
if (this.level.isWatching) {
@ -1711,6 +2048,10 @@ class Marble extends GameObject {
this.level.replay.recordMarbleInput(move.d.x, move.d.y);
}
}
if (!this.controllable && (this.connection != null || this.level == null)) {
move = new Move();
move.d = new Vector(0, 0);
}
playedSounds = [];
advancePhysics(timeState, move, collisionWorld, pathedInteriors);
@ -1738,7 +2079,7 @@ class Marble extends GameObject {
this.camera.update(timeState.currentAttemptTime, timeState.dt);
}
updatePowerupStates(timeState.currentAttemptTime, timeState.dt);
updatePowerupStates(timeState);
if (this._radius != 0.6666 && timeState.currentAttemptTime - this.megaMarbleEnableTime < 10) {
this._prevRadius = this._radius;
@ -1766,30 +2107,30 @@ class Marble extends GameObject {
// this.camera.target.load(this.getAbsPos().getPosition().toPoint());
}
public function updatePowerupStates(currentTime:Float, dt:Float) {
if (currentTime - this.shockAbsorberEnableTime < 5) {
public function updatePowerupStates(timeState:TimeState) {
if (timeState.currentAttemptTime - this.shockAbsorberEnableTime < 5) {
this.shockabsorberSound.pause = false;
} else {
this.shockabsorberSound.pause = true;
}
if (currentTime - this.superBounceEnableTime < 5) {
if (timeState.currentAttemptTime - this.superBounceEnableTime < 5) {
this.superbounceSound.pause = false;
} else {
this.superbounceSound.pause = true;
}
if (currentTime - this.shockAbsorberEnableTime < 5) {
if (timeState.currentAttemptTime - this.shockAbsorberEnableTime < 5) {
this.forcefield.setPosition(0, 0, 0);
} else if (currentTime - this.superBounceEnableTime < 5) {
} else if (timeState.currentAttemptTime - this.superBounceEnableTime < 5) {
this.forcefield.setPosition(0, 0, 0);
} else {
this.forcefield.x = 1e8;
this.forcefield.y = 1e8;
this.forcefield.z = 1e8;
}
if (currentTime - this.helicopterEnableTime < 5) {
if (timeState.currentAttemptTime - this.helicopterEnableTime < 5) {
this.helicopter.setPosition(x, y, z);
this.helicopter.setRotationQuat(this.level.getOrientationQuat(currentTime));
this.helicopter.setRotationQuat(this.level.getOrientationQuat(timeState.currentAttemptTime));
this.helicopterSound.pause = false;
} else {
this.helicopter.setPosition(1e8, 1e8, 1e8);
@ -1807,18 +2148,18 @@ class Marble extends GameObject {
}
}
public function useBlast() {
if (this.level.blastAmount < 0.2 || this.level.game != "ultra")
public function useBlast(timeState:TimeState) {
if (this.blastAmount < 0.2 || this.level.game != "ultra")
return;
var impulse = this.currentUp.multiply(Math.max(Math.sqrt(this.level.blastAmount), this.level.blastAmount) * 10);
var impulse = this.currentUp.multiply(Math.max(Math.sqrt(this.blastAmount), this.blastAmount) * 10);
this.applyImpulse(impulse);
AudioManager.playSound(ResourceLoader.getResource('data/sound/blast.wav', ResourceLoader.getAudio, this.soundResources));
this.level.particleManager.createEmitter(this.level.blastAmount > 1 ? blastMaxParticleOptions : blastParticleOptions,
this.level.blastAmount > 1 ? blastMaxEmitterData : blastEmitterData, this.getAbsPos().getPosition(), () -> {
this.level.particleManager.createEmitter(this.blastAmount > 1 ? blastMaxParticleOptions : blastParticleOptions,
this.blastAmount > 1 ? blastMaxEmitterData : blastEmitterData, this.getAbsPos().getPosition(), () -> {
this.getAbsPos().getPosition().add(this.currentUp.multiply(-this._radius * 0.4));
},
new Vector(1, 1, 1).add(new Vector(Math.abs(this.currentUp.x), Math.abs(this.currentUp.y), Math.abs(this.currentUp.z)).multiply(-0.8)));
this.level.blastAmount = 0;
this.blastAmount = 0;
}
public function getForce(position:Vector, tick:Int) {
@ -1909,11 +2250,23 @@ class Marble extends GameObject {
}
}
public inline function setMode(mode:Mode) {
this.mode = mode;
}
public function setMarblePosition(x:Float, y:Float, z:Float) {
this.collider.transform.setPosition(new Vector(x, y, z));
this.setPosition(x, y, z);
}
public inline function getConnectionId() {
if (this.connection == null) {
return Net.isHost ? 0 : Net.clientId;
} else {
return this.connection.id;
}
}
public override function reset() {
this.velocity = new Vector();
this.collider.velocity = new Vector();
@ -1922,6 +2275,11 @@ class Marble extends GameObject {
this.shockAbsorberEnableTime = Math.NEGATIVE_INFINITY;
this.helicopterEnableTime = Math.NEGATIVE_INFINITY;
this.megaMarbleEnableTime = Math.NEGATIVE_INFINITY;
this.blastUseTick = 0;
this.blastTicks = 0;
this.helicopterUseTick = 0;
this.megaMarbleUseTick = 0;
this.netFlags = MarbleNetFlags.DoBlast | MarbleNetFlags.DoMega | MarbleNetFlags.DoHelicopter | MarbleNetFlags.PickupPowerup | MarbleNetFlags.GravityChange | MarbleNetFlags.UsePowerup;
this.lastContactNormal = new Vector(0, 0, 1);
this.contactEntities = [];
this.cloak = false;
@ -1933,6 +2291,15 @@ class Marble extends GameObject {
this.teleporting = false;
this.teleportDisableTime = null;
this.teleportEnableTime = null;
this.physicsAccumulator = 0;
this.prevRot = this.getRotationQuat().clone();
this.oldPos = this.getAbsPos().getPosition();
this.newPos = this.getAbsPos().getPosition();
this.posStore = new Vector();
this.netSmoothOffset = new Vector();
this.lastRenderPos = new Vector();
this.netCorrected = false;
this.serverUsePowerup = false;
if (this._radius != this._prevRadius) {
this._radius = this._prevRadius;
this.collider.radius = this._radius;
@ -1942,6 +2309,15 @@ class Marble extends GameObject {
}
public override function dispose() {
if (this.rollSound != null)
this.rollSound.stop();
if (this.rollMegaSound != null)
this.rollMegaSound.stop();
if (this.slipSound != null)
this.slipSound.stop();
if (this.helicopterSound != null)
this.helicopterSound.stop();
this.helicopter.remove();
super.dispose();
removeChildren();
camera = null;

View file

@ -1,5 +1,6 @@
package src;
import gui.MPPlayMissionGui;
import gui.MainMenuGui;
#if !js
import gui.ReplayCenterGui;
@ -27,6 +28,9 @@ import src.Console;
import src.Debug;
import src.Gamepad;
import src.Analytics;
import net.Net;
import net.MasterServerClient;
import net.NetCommands;
@:publicFields
class MarbleGame {
@ -181,6 +185,8 @@ class MarbleGame {
}
public function update(dt:Float) {
MasterServerClient.process();
Net.checkPacketTimeout(dt);
if (world != null) {
if (world._disposed) {
world = null;
@ -190,7 +196,7 @@ class MarbleGame {
if (Util.isTouchDevice()) {
touchInput.update();
}
if (!paused) {
if (!paused || world.isMultiplayer) {
world.update(dt * Debug.timeScale);
}
if (((Key.isPressed(Key.ESCAPE) #if js && paused #end) || Gamepad.isPressed(["start"]))
@ -236,7 +242,10 @@ class MarbleGame {
quitMission();
}));
} else {
quitMission();
quitMission(Net.isClient);
if (Net.isMP && Net.isClient) {
Net.disconnect();
}
}
}, (sender) -> {
canvas.popDialog(exitGameDlg);
@ -266,8 +275,13 @@ class MarbleGame {
return world;
}
public function quitMission() {
public function quitMission(weDisconnecting:Bool = false) {
Console.log("Quitting mission");
if (Net.isMP) {
if (Net.isHost) {
NetCommands.endGame();
}
}
world.setCursorLock(false);
if (!Settings.levelStatistics.exists(world.mission.path)) {
Settings.levelStatistics.set(world.mission.path, {
@ -287,13 +301,18 @@ class MarbleGame {
canvas.setContent(new MainMenuGui());
#end
} else {
if (!world.mission.isClaMission && !world.mission.isCustom) {
PlayMissionGui.currentCategoryStatic = world.mission.type;
if (Net.isMP) {
var lobby = new MPPlayMissionGui(Net.isHost);
canvas.setContent(lobby);
} else {
if (!world.mission.isClaMission && !world.mission.isCustom) {
PlayMissionGui.currentCategoryStatic = world.mission.type;
}
var pmg = new PlayMissionGui();
PlayMissionGui.currentSelectionStatic = world.mission.index;
PlayMissionGui.currentGameStatic = world.mission.game;
canvas.setContent(pmg);
}
var pmg = new PlayMissionGui();
PlayMissionGui.currentSelectionStatic = world.mission.index;
PlayMissionGui.currentGameStatic = world.mission.game;
canvas.setContent(pmg);
}
world.dispose();
world = null;
@ -301,13 +320,13 @@ class MarbleGame {
Settings.save();
}
public function playMission(mission:Mission) {
public function playMission(mission:Mission, multiplayer:Bool = false) {
canvas.clearContent();
if (world != null) {
world.dispose();
}
Analytics.trackLevelPlay(mission.title, mission.path);
world = new MarbleWorld(scene, scene2d, mission, toRecord);
world = new MarbleWorld(scene, scene2d, mission, toRecord, multiplayer);
world.init();
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,11 @@
package src;
import haxe.Json;
import mis.MisParser;
import src.ResourceLoader;
import src.Mission;
import src.Console;
import src.MissionList;
@:publicFields
class MissionList {

View file

@ -6,6 +6,7 @@ import src.ResourceLoader;
import src.MarbleGame;
import src.Settings;
import src.Mission;
import src.MissionList;
class AchievementsGui extends GuiImage {
public function new() {

View file

@ -10,6 +10,7 @@ import gui.GuiControl.MouseState;
class Canvas extends GuiControl {
var scene2d:Scene;
var marbleGame:MarbleGame;
var content:GuiControl;
public function new(scene, marbleGame:MarbleGame) {
super();
@ -25,6 +26,7 @@ class Canvas extends GuiControl {
public function setContent(content:GuiControl) {
this.dispose();
this.content = content;
this.addChild(content);
this.render(scene2d);
}

904
src/gui/Graphics.hx Normal file
View file

@ -0,0 +1,904 @@
package gui;
import h2d.RenderContext;
import h2d.impl.BatchDrawState;
import hxd.Math;
import hxd.impl.Allocator;
import h2d.Drawable;
private typedef GraphicsPoint = hxd.poly2tri.Point;
@:dox(hide)
class GPoint {
public var x:Float;
public var y:Float;
public var r:Float;
public var g:Float;
public var b:Float;
public var a:Float;
public function new() {}
public function load(x, y, r, g, b, a) {
this.x = x;
this.y = y;
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
private class GraphicsContent extends h3d.prim.Primitive {
var tmp:hxd.FloatBuffer;
var index:hxd.IndexBuffer;
var state:BatchDrawState;
var bufferDirty:Bool;
var indexDirty:Bool;
#if track_alloc
var allocPos:hxd.impl.AllocPos;
#end
var bufferSize:Int;
var ibufferSize:Int;
public function new() {
state = new BatchDrawState();
#if track_alloc
this.allocPos = new hxd.impl.AllocPos();
#end
}
public inline function addIndex(i) {
index.push(i);
state.add(1);
indexDirty = true;
}
public inline function add(x:Float, y:Float, u:Float, v:Float, r:Float, g:Float, b:Float, a:Float) {
tmp.push(x);
tmp.push(y);
tmp.push(u);
tmp.push(v);
tmp.push(r);
tmp.push(g);
tmp.push(b);
tmp.push(a);
bufferDirty = true;
}
public function setTile(tile:h2d.Tile) {
state.setTile(tile);
}
public function next() {
var nvect = tmp.length >> 3;
if (nvect < 1 << 15)
return false;
tmp = new hxd.FloatBuffer();
index = new hxd.IndexBuffer();
var tex = state.currentTexture;
state = new BatchDrawState();
state.setTexture(tex);
super.dispose();
return true;
}
override function alloc(engine:h3d.Engine) {
if (index.length <= 0)
return;
var alloc = Allocator.get();
buffer = alloc.ofFloats(tmp, 8, RawFormat);
bufferSize = tmp.length;
#if track_alloc
@:privateAccess buffer.allocPos = allocPos;
#end
indexes = alloc.ofIndexes(index);
ibufferSize = index.length;
bufferDirty = false;
indexDirty = false;
}
public function doRender(ctx:h2d.RenderContext) {
if (index.length == 0)
return;
flush();
state.drawIndexed(ctx, buffer, indexes, 0, tmp.length >> 3);
}
public function flush() {
if (buffer == null || buffer.isDisposed()) {
alloc(h3d.Engine.getCurrent());
} else {
var allocator = Allocator.get();
if (bufferDirty) {
if (tmp.length > bufferSize) {
allocator.disposeBuffer(buffer);
buffer = new h3d.Buffer(tmp.length >> 3, 8, [RawFormat, Dynamic]);
buffer.uploadVector(tmp, 0, tmp.length >> 3);
bufferSize = tmp.length;
} else {
buffer.uploadVector(tmp, 0, tmp.length >> 3);
}
bufferDirty = false;
}
if (indexDirty) {
if (index.length > ibufferSize) {
allocator.disposeIndexBuffer(indexes);
indexes = allocator.ofIndexes(index);
ibufferSize = index.length;
} else {
indexes.upload(index, 0, index.length);
}
indexDirty = false;
}
}
}
override function dispose() {
state.clear();
// disposeBuffers();
// super.dispose();
}
function disposeBuffers() {
if (buffer != null) {
Allocator.get().disposeBuffer(buffer);
buffer = null;
}
if (indexes != null) {
Allocator.get().disposeIndexBuffer(indexes);
indexes = null;
}
}
public function clear() {
dispose();
tmp = new hxd.FloatBuffer();
index = new hxd.IndexBuffer();
}
public function disposeForReal() {
state.clear();
disposeBuffers();
super.dispose();
}
}
/**
A simple interface to draw arbitrary 2D geometry.
Usage notes:
* While Graphics allows for multiple unique textures, each texture swap causes a new drawcall,
and due to that it's recommended to minimize the amount of used textures per Graphics instance,
ideally limiting to only one texture.
* Due to how Graphics operate, removing them from the active `h2d.Scene` will cause a loss of all data.
**/
class Graphics extends Drawable {
var content:GraphicsContent;
var tmpPoints:Array<GPoint>;
var pindex:Int;
var curR:Float;
var curG:Float;
var curB:Float;
var curA:Float;
var lineSize:Float;
var lineR:Float;
var lineG:Float;
var lineB:Float;
var lineA:Float;
var doFill:Bool;
var xMin:Float;
var yMin:Float;
var xMax:Float;
var yMax:Float;
var xMinSize:Float;
var yMinSize:Float;
var xMaxSize:Float;
var yMaxSize:Float;
var ma:Float = 1.;
var mb:Float = 0.;
var mc:Float = 0.;
var md:Float = 1.;
var mx:Float = 0.;
var my:Float = 0.;
/**
The Tile used as source of Texture to render.
**/
public var tile:h2d.Tile;
/**
Adds bevel cut-off at line corners.
The value is a percentile in range of 0...1, dictating at which point edges get beveled based on their angle.
Value of 0 being not beveled and 1 being always beveled.
**/
public var bevel = 0.25; // 0 = not beveled, 1 = always beveled
/**
Create a new Graphics instance.
@param parent An optional parent `h2d.Object` instance to which Graphics adds itself if set.
**/
public function new(?parent) {
super(parent);
content = new GraphicsContent();
tile = h2d.Tile.fromColor(0xFFFFFF);
clear();
}
override function onRemove() {
super.onRemove();
clear();
content.disposeForReal();
}
/**
Clears the Graphics contents.
**/
public function clear() {
content.clear();
tmpPoints = [];
pindex = 0;
lineSize = 0;
xMin = Math.POSITIVE_INFINITY;
yMin = Math.POSITIVE_INFINITY;
yMax = Math.NEGATIVE_INFINITY;
xMax = Math.NEGATIVE_INFINITY;
xMinSize = Math.POSITIVE_INFINITY;
yMinSize = Math.POSITIVE_INFINITY;
yMaxSize = Math.NEGATIVE_INFINITY;
xMaxSize = Math.NEGATIVE_INFINITY;
}
override function getBoundsRec(relativeTo, out, forSize) {
super.getBoundsRec(relativeTo, out, forSize);
if (tile != null) {
if (forSize)
addBounds(relativeTo, out, xMinSize, yMinSize, xMaxSize - xMinSize, yMaxSize - yMinSize);
else
addBounds(relativeTo, out, xMin, yMin, xMax - xMin, yMax - yMin);
}
}
function isConvex(points:Array<GPoint>) {
var first = true, sign = false;
for (i in 0...points.length) {
var p1 = points[i];
var p2 = points[(i + 1) % points.length];
var p3 = points[(i + 2) % points.length];
var s = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0;
if (first) {
first = false;
sign = s;
} else if (sign != s)
return false;
}
return true;
}
function flushLine(start) {
var pts = tmpPoints;
var last = pts.length - 1;
var prev = pts[last];
var p = pts[0];
content.setTile(h2d.Tile.fromColor(0xFFFFFF));
var closed = p.x == prev.x && p.y == prev.y;
var count = pts.length;
if (!closed) {
var prevLast = pts[last - 1];
if (prevLast == null)
prevLast = p;
var gp = new GPoint();
gp.load(prev.x * 2 - prevLast.x, prev.y * 2 - prevLast.y, 0, 0, 0, 0);
pts.push(gp);
var pNext = pts[1];
if (pNext == null)
pNext = p;
var gp = new GPoint();
gp.load(p.x * 2 - pNext.x, p.y * 2 - pNext.y, 0, 0, 0, 0);
prev = gp;
} else if (p != prev) {
count--;
last--;
prev = pts[last];
}
for (i in 0...count) {
var next = pts[(i + 1) % pts.length];
var nx1 = prev.y - p.y;
var ny1 = p.x - prev.x;
var ns1 = Math.invSqrt(nx1 * nx1 + ny1 * ny1);
var nx2 = p.y - next.y;
var ny2 = next.x - p.x;
var ns2 = Math.invSqrt(nx2 * nx2 + ny2 * ny2);
var nx = nx1 * ns1 + nx2 * ns2;
var ny = ny1 * ns1 + ny2 * ns2;
var ns = Math.invSqrt(nx * nx + ny * ny);
nx *= ns;
ny *= ns;
var size = nx * nx1 * ns1 + ny * ny1 * ns1; // N.N1
// *HACK* we should instead properly detect limits when the angle is too small
if (size < 0.1)
size = 0.1;
var d = lineSize * 0.5 / size;
nx *= d;
ny *= d;
if (size > bevel) {
content.add(p.x + nx, p.y + ny, 0, 0, p.r, p.g, p.b, p.a);
content.add(p.x - nx, p.y - ny, 0, 0, p.r, p.g, p.b, p.a);
var pnext = i == last ? start : pindex + 2;
if (i < count - 1 || closed) {
content.addIndex(pindex);
content.addIndex(pindex + 1);
content.addIndex(pnext);
content.addIndex(pindex + 1);
content.addIndex(pnext);
content.addIndex(pnext + 1);
}
pindex += 2;
} else {
// bevel
var n0x = next.x - p.x;
var n0y = next.y - p.y;
var sign = n0x * nx + n0y * ny;
var nnx = -ny;
var nny = nx;
var size = nnx * nx1 * ns1 + nny * ny1 * ns1;
var d = lineSize * 0.5 / size;
nnx *= d;
nny *= d;
var pnext = i == last ? start : pindex + 3;
if (sign > 0) {
content.add(p.x + nx, p.y + ny, 0, 0, p.r, p.g, p.b, p.a);
content.add(p.x - nnx, p.y - nny, 0, 0, p.r, p.g, p.b, p.a);
content.add(p.x + nnx, p.y + nny, 0, 0, p.r, p.g, p.b, p.a);
content.addIndex(pindex);
content.addIndex(pnext);
content.addIndex(pindex + 2);
content.addIndex(pindex + 2);
content.addIndex(pnext);
content.addIndex(pnext + 1);
} else {
content.add(p.x + nnx, p.y + nny, 0, 0, p.r, p.g, p.b, p.a);
content.add(p.x - nx, p.y - ny, 0, 0, p.r, p.g, p.b, p.a);
content.add(p.x - nnx, p.y - nny, 0, 0, p.r, p.g, p.b, p.a);
content.addIndex(pindex + 1);
content.addIndex(pnext);
content.addIndex(pindex + 2);
content.addIndex(pindex + 1);
content.addIndex(pnext);
content.addIndex(pnext + 1);
}
content.addIndex(pindex);
content.addIndex(pindex + 1);
content.addIndex(pindex + 2);
pindex += 3;
}
prev = p;
p = next;
}
content.setTile(tile);
}
static var EARCUT = null;
function flushFill(i0) {
if (tmpPoints.length < 3)
return;
var pts = tmpPoints;
var p0 = pts[0];
var p1 = pts[pts.length - 1];
var last = null;
// closed poly
if (hxd.Math.abs(p0.x - p1.x) < 1e-9 && hxd.Math.abs(p0.y - p1.y) < 1e-9)
last = pts.pop();
if (isConvex(pts)) {
for (i in 1...pts.length - 1) {
content.addIndex(i0);
content.addIndex(i0 + i);
content.addIndex(i0 + i + 1);
}
} else {
var ear = EARCUT;
if (ear == null)
EARCUT = ear = new hxd.earcut.Earcut();
for (i in ear.triangulate(pts))
content.addIndex(i + i0);
}
if (last != null)
pts.push(last);
}
function flush() {
if (tmpPoints.length == 0)
return;
if (doFill) {
flushFill(pindex);
pindex += tmpPoints.length;
if (content.next())
pindex = 0;
}
if (lineSize > 0) {
flushLine(pindex);
if (content.next())
pindex = 0;
}
tmpPoints = [];
}
/**
Begins a solid color fill.
Beginning new fill will finish previous fill operation without need to call `Graphics.endFill`.
@param color An RGB color with which to fill the drawn shapes.
@param alpha A transparency of the fill color.
**/
public function beginFill(color:Int = 0, alpha = 1.) {
flush();
tile = h2d.Tile.fromColor(0xFFFFFF);
content.setTile(tile);
setColor(color, alpha);
doFill = true;
}
/**
Position a virtual tile at the given position and scale. Every draw will display a part of this tile relative
to these coordinates.
Note that in by default, Tile is not wrapped, and in order to render tiling texture, `Drawable.tileWrap` have to be set.
Additionally, both `Tile.dx` and `Tile.dy` are ignored (use `dx`/`dy` arguments instead)
as well as tile defined size of the tile through `Tile.width` and `Tile.height` (use `scaleX`/`scaleY` relative to texture size).
Beginning new fill will finish previous fill operation without need to call `Graphics.endFill`.
@param dx An X offset of the Tile relative to Graphics.
@param dy An Y offset of the Tile relative to Graphics.
@param scaleX A horizontal scale factor applied to the Tile texture.
@param scaleY A vertical scale factor applied to the Tile texture.
@param tile The tile to fill with. If null, uses previously used Tile with `beginTileFill` or throws an error.
Previous tile is remembered across `Graphics.clear` calls.
**/
public function beginTileFill(?dx:Float, ?dy:Float, ?scaleX:Float, ?scaleY:Float, ?tile:h2d.Tile) {
if (tile == null)
tile = this.tile;
if (tile == null)
throw "Tile not specified";
flush();
this.tile = tile;
content.setTile(tile);
setColor(0xFFFFFF);
doFill = true;
if (dx == null)
dx = 0;
if (dy == null)
dy = 0;
if (scaleX == null)
scaleX = 1;
if (scaleY == null)
scaleY = 1;
dx -= tile.x;
dy -= tile.y;
var tex = tile.getTexture();
var pixWidth = 1 / tex.width;
var pixHeight = 1 / tex.height;
ma = pixWidth / scaleX;
mb = 0;
mc = 0;
md = pixHeight / scaleY;
mx = -dx * ma;
my = -dy * md;
}
/**
Draws a Tile at given position.
See `Graphics.beginTileFill` for limitations.
This methods ends current fill operation.
@param x The X position of the tile.
@param y The Y position of the tile.
@param tile The tile to draw.
**/
public function drawTile(x:Float, y:Float, tile:h2d.Tile) {
beginTileFill(x, y, tile);
drawRect(x, y, tile.width, tile.height);
endFill();
}
/**
Sets an outline style. Changing the line style ends the currently drawn line.
@param size Width of the outline. Setting size to 0 will remove the outline.
@param color An outline RGB color.
@param alpha An outline transparency.
**/
public function lineStyle(size:Float = 0, color = 0, alpha = 1.) {
flush();
this.lineSize = size;
lineA = alpha;
lineR = ((color >> 16) & 0xFF) / 255.;
lineG = ((color >> 8) & 0xFF) / 255.;
lineB = (color & 0xFF) / 255.;
}
/**
Ends the current line and starts new one at given position.
**/
public inline function moveTo(x, y) {
flush();
lineTo(x, y);
}
/**
Ends the current fill operation.
**/
public function endFill() {
flush();
doFill = false;
}
/**
Changes current fill color.
Does not interrupt current fill operation and can be utilized to customize color per vertex.
During tile fill operation, color serves as a tile color multiplier.
@param color The new fill color.
@param alpha The new fill transparency.
**/
public inline function setColor(color:Int, alpha:Float = 1.) {
curA = alpha;
curR = ((color >> 16) & 0xFF) / 255.;
curG = ((color >> 8) & 0xFF) / 255.;
curB = (color & 0xFF) / 255.;
}
/**
Draws a rectangle with given parameters.
@param x The rectangle top-left corner X position.
@param y The rectangle top-left corner Y position.
@param w The rectangle width.
@param h The rectangle height.
**/
public function drawRect(x:Float, y:Float, w:Float, h:Float) {
flush();
lineTo(x, y);
lineTo(x + w, y);
lineTo(x + w, y + h);
lineTo(x, y + h);
lineTo(x, y);
var e = 0.01; // see #776
tmpPoints[0].x += e;
tmpPoints[0].y += e;
tmpPoints[1].y += e;
tmpPoints[3].x += e;
tmpPoints[4].x += e;
tmpPoints[4].y += e;
flush();
}
/**
Draws a rounded rectangle with given parameters.
@param x The rectangle top-left corner X position.
@param y The rectangle top-left corner Y position.
@param w The rectangle width.
@param h The rectangle height.
@param radius Radius of the rectangle corners.
@param nsegments Amount of segments used for corners. When `0` segment count calculated automatically.
**/
public function drawRoundedRect(x:Float, y:Float, w:Float, h:Float, radius:Float, nsegments = 0) {
if (radius <= 0) {
return drawRect(x, y, w, h);
}
x += radius;
y += radius;
w -= radius * 2;
h -= radius * 2;
flush();
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(radius * hxd.Math.degToRad(90) / 4));
if (nsegments < 3)
nsegments = 3;
var angle = hxd.Math.degToRad(90) / (nsegments - 1);
inline function corner(x, y, angleStart) {
for (i in 0...nsegments) {
var a = i * angle + hxd.Math.degToRad(angleStart);
lineTo(x + Math.cos(a) * radius, y + Math.sin(a) * radius);
}
}
lineTo(x, y - radius);
lineTo(x + w, y - radius);
corner(x + w, y, 270);
lineTo(x + w + radius, y + h);
corner(x + w, y + h, 0);
lineTo(x, y + h + radius);
corner(x, y + h, 90);
lineTo(x - radius, y);
corner(x, y, 180);
flush();
}
/**
Draws a circle centered at given position.
@param cx X center position of the circle.
@param cy Y center position of the circle.
@param radius Radius of the circle.
@param nsegments Amount of segments used to draw the circle. When `0`, amount of segments calculated automatically.
**/
public function drawCircle(cx:Float, cy:Float, radius:Float, nsegments = 0) {
flush();
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(radius * 3.14 * 2 / 4));
if (nsegments < 3)
nsegments = 3;
var angle = Math.PI * 2 / nsegments;
for (i in 0...nsegments + 1) {
var a = i * angle;
lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius);
}
flush();
}
/**
Draws an ellipse centered at given position.
@param cx X center position of the ellipse.
@param cy Y center position of the ellipse.
@param radiusX Horizontal radius of an ellipse.
@param radiusY Vertical radius of an ellipse.
@param rotationAngle Ellipse rotation in radians.
@param nsegments Amount of segments used to draw an ellipse. When `0`, amount of segments calculated automatically.
**/
public function drawEllipse(cx:Float, cy:Float, radiusX:Float, radiusY:Float, rotationAngle:Float = 0, nsegments = 0) {
flush();
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(radiusY * 3.14 * 2 / 4));
if (nsegments < 3)
nsegments = 3;
var angle = Math.PI * 2 / nsegments;
var x1, y1;
for (i in 0...nsegments + 1) {
var a = i * angle;
x1 = Math.cos(a) * Math.cos(rotationAngle) * radiusX - Math.sin(a) * Math.sin(rotationAngle) * radiusY;
y1 = Math.cos(rotationAngle) * Math.sin(a) * radiusY + Math.cos(a) * Math.sin(rotationAngle) * radiusX;
lineTo(cx + x1, cy + y1);
}
flush();
}
/**
Draws a pie centered at given position.
@param cx X center position of the pie.
@param cy Y center position of the pie.
@param radius Radius of the pie.
@param angleStart Starting angle of the pie in radians.
@param angleLength The pie size in clockwise direction with `2*PI` being full circle.
@param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically.
**/
public function drawPie(cx:Float, cy:Float, radius:Float, angleStart:Float, angleLength:Float, nsegments = 0) {
if (Math.abs(angleLength) >= Math.PI * 2) {
return drawCircle(cx, cy, radius, nsegments);
}
flush();
lineTo(cx, cy);
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(radius * angleLength / 4));
if (nsegments < 3)
nsegments = 3;
var angle = angleLength / (nsegments - 1);
for (i in 0...nsegments) {
var a = i * angle + angleStart;
lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius);
}
lineTo(cx, cy);
flush();
}
/**
Draws a double-edged pie centered at given position.
@param cx X center position of the pie.
@param cy Y center position of the pie.
@param radius The outer radius of the pie.
@param innerRadius The inner radius of the pie.
@param angleStart Starting angle of the pie in radians.
@param angleLength The pie size in clockwise direction with `2*PI` being full circle.
@param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically.
**/
public function drawPieInner(cx:Float, cy:Float, radius:Float, innerRadius:Float, angleStart:Float, angleLength:Float, nsegments = 0) {
flush();
if (Math.abs(angleLength) >= Math.PI * 2 + 1e-3)
angleLength = Math.PI * 2 + 1e-3;
var cs = Math.cos(angleStart);
var ss = Math.sin(angleStart);
var ce = Math.cos(angleStart + angleLength);
var se = Math.sin(angleStart + angleLength);
lineTo(cx + cs * innerRadius, cy + ss * innerRadius);
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(radius * angleLength / 4));
if (nsegments < 3)
nsegments = 3;
var angle = angleLength / (nsegments - 1);
for (i in 0...nsegments) {
var a = i * angle + angleStart;
lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius);
}
lineTo(cx + ce * innerRadius, cy + se * innerRadius);
for (i in 0...nsegments) {
var a = (nsegments - 1 - i) * angle + angleStart;
lineTo(cx + Math.cos(a) * innerRadius, cy + Math.sin(a) * innerRadius);
}
flush();
}
/**
Draws a rectangular pie centered at given position.
@param cx X center position of the pie.
@param cy Y center position of the pie.
@param width Width of the pie.
@param height Height of the pie.
@param angleStart Starting angle of the pie in radians.
@param angleLength The pie size in clockwise direction with `2*PI` being solid rectangle.
@param nsegments Amount of segments used to draw the pie. When `0`, amount of segments calculated automatically.
**/
public function drawRectanglePie(cx:Float, cy:Float, width:Float, height:Float, angleStart:Float, angleLength:Float, nsegments = 0) {
if (Math.abs(angleLength) >= Math.PI * 2) {
return drawRect(cx - (width / 2), cy - (height / 2), width, height);
}
flush();
lineTo(cx, cy);
if (nsegments == 0)
nsegments = Math.ceil(Math.abs(Math.max(width, height) * angleLength / 4));
if (nsegments < 3)
nsegments = 3;
var angle = angleLength / (nsegments - 1);
var square2 = Math.sqrt(2);
for (i in 0...nsegments) {
var a = i * angle + angleStart;
var _width = Math.cos(a) * (width / 2 + 1) * square2;
var _height = Math.sin(a) * (height / 2 + 1) * square2;
_width = Math.abs(_width) >= width / 2 ? (Math.cos(a) < 0 ? width / 2 * -1 : width / 2) : _width;
_height = Math.abs(_height) >= height / 2 ? (Math.sin(a) < 0 ? height / 2 * -1 : height / 2) : _height;
lineTo(cx + _width, cy + _height);
}
lineTo(cx, cy);
flush();
}
/**
* Draws a quadratic Bezier curve using the current line style from the current drawing position to (cx, cy) and using the control point that (bx, by) specifies.
* IvanK Lib port ( http://lib.ivank.net )
*/
public function curveTo(bx:Float, by:Float, cx:Float, cy:Float) {
var ax = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].x;
var ay = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].y;
var t = 2 / 3;
cubicCurveTo(ax + t * (bx - ax), ay + t * (by - ay), cx + t * (bx - cx), cy + t * (by - cy), cx, cy);
}
/**
* Draws a cubic Bezier curve from the current drawing position to the specified anchor point.
* IvanK Lib port ( http://lib.ivank.net )
* @param bx control X for start point
* @param by control Y for start point
* @param cx control X for end point
* @param cy control Y for end point
* @param dx end X
* @param dy end Y
* @param nsegments = 40
*/
public function cubicCurveTo(bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, nsegments = 40) {
var ax = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].x;
var ay = tmpPoints.length == 0 ? 0 : tmpPoints[tmpPoints.length - 1].y;
var tobx = bx - ax, toby = by - ay;
var tocx = cx - bx, tocy = cy - by;
var todx = dx - cx, tody = dy - cy;
var step = 1 / nsegments;
for (i in 1...nsegments) {
var d = i * step;
var px = ax + d * tobx, py = ay + d * toby;
var qx = bx + d * tocx, qy = by + d * tocy;
var rx = cx + d * todx, ry = cy + d * tody;
var toqx = qx - px, toqy = qy - py;
var torx = rx - qx, tory = ry - qy;
var sx = px + d * toqx, sy = py + d * toqy;
var tx = qx + d * torx, ty = qy + d * tory;
var totx = tx - sx, toty = ty - sy;
lineTo(sx + d * totx, sy + d * toty);
}
lineTo(dx, dy);
}
/**
Draws a straight line from the current drawing position to the given position.
**/
public inline function lineTo(x:Float, y:Float) {
addVertex(x, y, curR, curG, curB, curA, x * ma + y * mc + mx, x * mb + y * md + my);
}
/**
Advanced usage. Adds new vertex to the current polygon with given parameters and current line style.
@param x Vertex X position
@param y Vertex Y position
@param r Red tint value of the vertex when performing fill operation.
@param g Green tint value of the vertex when performing fill operation.
@param b Blue tint value of the vertex when performing fill operation.
@param a Alpha of the vertex when performing fill operation.
@param u Normalized horizontal Texture position from the current Tile fill operation.
@param v Normalized vertical Texture position from the current Tile fill operation.
**/
public function addVertex(x:Float, y:Float, r:Float, g:Float, b:Float, a:Float, u:Float = 0., v:Float = 0.) {
var half = lineSize / 2.0;
if (x - half < xMin)
xMin = x - half;
if (y - half < yMin)
yMin = y - half;
if (x + half > xMax)
xMax = x + half;
if (y + half > yMax)
yMax = y + half;
if (x < xMinSize)
xMinSize = x;
if (y < yMinSize)
yMinSize = y;
if (x > xMaxSize)
xMaxSize = x;
if (y > yMaxSize)
yMaxSize = y;
if (doFill)
content.add(x, y, u, v, r, g, b, a);
var gp = new GPoint();
gp.load(x, y, lineR, lineG, lineB, lineA);
tmpPoints.push(gp);
}
override function draw(ctx:RenderContext) {
if (!ctx.beginDrawBatchState(this))
return;
content.doRender(ctx);
}
override function sync(ctx:RenderContext) {
super.sync(ctx);
flush();
content.flush();
}
}

View file

@ -0,0 +1,305 @@
package gui;
import h2d.filter.Filter;
import h2d.HtmlText;
import h2d.Flow;
import h3d.Engine;
import h2d.Tile;
import h2d.Bitmap;
import h3d.mat.Texture;
import shaders.GuiClipFilter;
import h2d.Graphics;
import gui.GuiControl.MouseState;
import h2d.Scene;
import h2d.Text;
import h2d.Font;
import src.MarbleGame;
import src.Settings;
class GuiMLTextListCtrl extends GuiControl {
public var texts:Array<String>;
public var onSelectedFunc:Int->Void;
var font:Font;
var textObjs:Array<Text>;
var g:Graphics;
var _prevSelected:Int = -1;
public var selectedColor:Int = 0x206464;
public var selectedFillColor:Int = 0xC8C8C8;
public var textYOffset:Int = 0;
public var scroll:Float = 0;
public var scrollable:Bool = false;
var filter:Filter = null;
var flow:Flow;
var _imageLoader:String->Tile;
public function new(font:Font, texts:Array<String>, imageLoader:String->Tile, ?filter:Filter = null) {
super();
this.font = font;
this.texts = texts;
this._manualScroll = true;
this.textObjs = [];
this.filter = filter;
this._imageLoader = imageLoader;
for (text in texts) {
var tobj = new HtmlText(font);
tobj.lineHeightMode = TextOnly;
tobj.loadImage = imageLoader;
tobj.text = text;
tobj.textColor = 0;
if (filter != null)
tobj.filter = filter;
textObjs.push(tobj);
}
this.g = new Graphics();
}
public function setTexts(texts:Array<String>) {
var renderRect = this.getRenderRectangle();
for (textObj in this.textObjs) {
textObj.remove();
}
this.textObjs = [];
for (text in texts) {
var tobj = new HtmlText(font);
tobj.loadImage = this._imageLoader;
tobj.lineHeightMode = TextOnly;
tobj.text = text;
tobj.textColor = 0;
if (filter != null)
tobj.filter = filter;
textObjs.push(tobj);
if (this.scrollable) {
if (this.flow != null) {
if (this.flow.contains(tobj))
this.flow.removeChild(tobj);
this.flow.addChild(tobj);
this.flow.getProperties(tobj).isAbsolute = true;
}
}
}
this.texts = texts;
this._prevSelected = -1;
if (this.onSelectedFunc != null)
this.onSelectedFunc(-1);
redrawSelectionRect(renderRect);
for (i in 0...textObjs.length) {
var text = textObjs[i];
text.setPosition(Math.floor((!scrollable ? renderRect.position.x : 0) + 5),
Math.floor((!scrollable ? renderRect.position.y : 0)
+ (i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - this.scroll)));
if (_prevSelected == i) {
text.textColor = selectedColor;
}
}
}
public override function render(scene2d:Scene, ?parent:h2d.Flow) {
var renderRect = this.getRenderRectangle();
var htr = this.getHitTestRect(false);
if (parent != null) {
if (parent.contains(g))
parent.removeChild(g);
parent.addChild(g);
var off = this.getOffsetFromParent();
parent.getProperties(g).isAbsolute = true;
g.setPosition(off.x, off.y - this.scroll);
}
if (scrollable) {
this.flow = new Flow();
this.flow.maxWidth = cast htr.extent.x;
this.flow.maxHeight = cast htr.extent.y;
this.flow.multiline = true;
this.flow.layout = Stack;
this.flow.overflow = FlowOverflow.Hidden;
if (parent != null) {
if (parent.contains(this.flow)) {
parent.removeChild(this.flow);
}
parent.addChild(this.flow);
var off = this.getOffsetFromParent();
var props = parent.getProperties(this.flow);
props.isAbsolute = true;
this.flow.setPosition(off.x, off.y);
}
}
for (i in 0...textObjs.length) {
var text = textObjs[i];
if (!scrollable) {
if (scene2d.contains(text))
scene2d.removeChild(text);
scene2d.addChild(text);
} else {
if (this.flow.contains(text))
this.flow.removeChild(text);
this.flow.addChild(text);
this.flow.getProperties(text).isAbsolute = true;
}
text.setPosition(Math.floor((!scrollable ? renderRect.position.x : 0) + 5),
Math.floor((!scrollable ? renderRect.position.y : 0)
+ (i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - this.scroll)));
if (_prevSelected == i) {
text.textColor = selectedColor;
}
}
redrawSelectionRect(htr);
super.render(scene2d, parent);
}
public function calculateFullHeight() {
return (this.texts.length * (font.size + 4 * Settings.uiScale));
}
public override function dispose() {
super.dispose();
for (text in textObjs) {
text.remove();
}
this.g.remove();
if (this.scrollable) {
this.flow.remove();
}
}
public override function onRemove() {
super.onRemove();
for (text in textObjs) {
if (MarbleGame.canvas.scene2d.contains(text)) {
MarbleGame.canvas.scene2d.removeChild(text); // Refresh "layer"
}
text.remove();
}
if (MarbleGame.canvas.scene2d.contains(g))
MarbleGame.canvas.scene2d.removeChild(g);
g.remove();
}
public override function onMouseMove(mouseState:MouseState) {
var mousePos = mouseState.position;
var renderRect = this.getRenderRectangle();
var yStart = renderRect.position.y;
var dy = mousePos.y - yStart;
var hoverIndex = Math.floor(dy / (font.size + 4 * Settings.uiScale));
if (hoverIndex >= this.texts.length) {
hoverIndex = -1;
}
// Update the texts
for (i in 0...textObjs.length) {
var selected = i == hoverIndex || i == this._prevSelected;
var text = textObjs[i];
text.textColor = selected ? selectedColor : 0;
// fill color = 0xC8C8C8
}
// obviously in renderRect
}
public override function onMouseLeave(mouseState:MouseState) {
for (i in 0...textObjs.length) {
if (i == this._prevSelected)
continue;
var text = textObjs[i];
text.textColor = 0;
// fill color = 0xC8C8C8
}
}
public override function onMousePress(mouseState:MouseState) {
super.onMousePress(mouseState);
var mousePos = mouseState.position;
var renderRect = this.getRenderRectangle();
var yStart = renderRect.position.y;
var dy = mousePos.y - yStart;
var selectedIndex = Math.floor((dy + this.scroll) / (font.size + 4 * Settings.uiScale));
if (selectedIndex >= this.texts.length) {
selectedIndex = -1;
}
if (_prevSelected != selectedIndex) {
_prevSelected = selectedIndex;
redrawSelectionRect(renderRect);
}
if (onSelectedFunc != null) {
onSelectedFunc(selectedIndex);
}
}
function redrawSelectionRect(renderRect:Rect) {
if (_prevSelected != -1) {
g.clear();
g.beginFill(selectedFillColor);
var off = this.getOffsetFromParent();
// Check if we are between the top and bottom, render normally in that case
var topY = 2 * Settings.uiScale + (_prevSelected * (font.size + 4 * Settings.uiScale)) + g.y;
var bottomY = 2 * Settings.uiScale + (_prevSelected * (font.size + 4 * Settings.uiScale)) + g.y + font.size + 4 * Settings.uiScale;
var topRectY = off.y;
var bottomRectY = off.y + renderRect.extent.y;
if (topY >= topRectY && bottomY <= bottomRectY)
g.drawRect(0, 5 * Settings.uiScale
+ (_prevSelected * (font.size + 4 * Settings.uiScale))
- 3 * Settings.uiScale, renderRect.extent.x,
font.size
+ 4 * Settings.uiScale);
// We need to do math the draw the partially visible top selected
if (topY <= topRectY && bottomY >= topRectY) {
g.drawRect(0, this.scroll, renderRect.extent.x, topY + font.size + 4 * Settings.uiScale - off.y);
}
// Same for the bottom
if (topY <= bottomRectY && bottomY >= bottomRectY) {
g.drawRect(0, this.scroll
+ renderRect.extent.y
- font.size
- 4 * Settings.uiScale
+ (topY + font.size + 4 * Settings.uiScale - bottomRectY),
renderRect.extent.x, off.y
+ renderRect.extent.y
- (topY));
}
g.endFill();
} else {
g.clear();
}
}
public override function onScroll(scrollX:Float, scrollY:Float) {
super.onScroll(scrollX, scrollY);
var renderRect = this.getRenderRectangle();
this.scroll = scrollY;
var hittestrect = this.getHitTestRect(false);
for (i in 0...textObjs.length) {
var text = textObjs[i];
text.y = Math.floor((i * (text.font.size + 4 * Settings.uiScale) + (5 + textYOffset) * Settings.uiScale - scrollY));
g.y = -scrollY;
}
redrawSelectionRect(hittestrect);
}
}

View file

@ -82,4 +82,9 @@ class GuiTextInput extends GuiControl {
}
#end
}
public function setCaretColor(col:Int) {
text.cursorTile = h2d.Tile.fromColor(col, Std.int(1 / hxd.Window.getInstance().windowToPixelRatio), text.font.size);
text.cursorTile.dy = 2 / hxd.Window.getInstance().windowToPixelRatio;
}
}

View file

@ -25,6 +25,7 @@ class GuiTextListCtrl extends GuiControl {
public var selectedColor:Int = 0x206464;
public var selectedFillColor:Int = 0xC8C8C8;
public var textColor:Int = 0;
public var textYOffset:Int = 0;
@ -34,16 +35,17 @@ class GuiTextListCtrl extends GuiControl {
var flow:Flow;
public function new(font:Font, texts:Array<String>) {
public function new(font:Font, texts:Array<String>, textColor:Int = 0) {
super();
this.font = font;
this.texts = texts;
this._manualScroll = true;
this.textObjs = [];
this.textColor = textColor;
for (text in texts) {
var tobj = new Text(font);
tobj.text = text;
tobj.textColor = 0;
tobj.textColor = textColor;
textObjs.push(tobj);
}
this.g = new Graphics();
@ -58,7 +60,7 @@ class GuiTextListCtrl extends GuiControl {
for (text in texts) {
var tobj = new Text(font);
tobj.text = text;
tobj.textColor = 0;
tobj.textColor = textColor;
textObjs.push(tobj);
if (this.scrollable) {
@ -195,7 +197,7 @@ class GuiTextListCtrl extends GuiControl {
for (i in 0...textObjs.length) {
var selected = i == hoverIndex || i == this._prevSelected;
var text = textObjs[i];
text.textColor = selected ? selectedColor : 0;
text.textColor = selected ? selectedColor : textColor;
// fill color = 0xC8C8C8
}
// obviously in renderRect
@ -206,7 +208,7 @@ class GuiTextListCtrl extends GuiControl {
if (i == this._prevSelected)
continue;
var text = textObjs[i];
text.textColor = 0;
text.textColor = textColor;
// fill color = 0xC8C8C8
}
}

View file

@ -1,5 +1,8 @@
package gui;
import net.MasterServerClient;
import net.MasterServerClient.RemoteServerInfo;
import net.Net;
import h2d.filter.DropShadow;
import hxd.res.BitmapFont;
import src.MarbleGame;
@ -28,48 +31,6 @@ class JoinServerGui extends GuiImage {
return [normal, hover, pressed];
}
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
var window = new GuiImage(ResourceLoader.getResource("data/ui/mp/join/window.png", ResourceLoader.getImage, this.imageResources).toTile());
window.horizSizing = Center;
window.vertSizing = Center;
window.position = new Vector(-60, 5);
window.extent = new Vector(759, 469);
var hostBtn = new GuiButton(loadButtonImages("data/ui/mp/join/host"));
hostBtn.position = new Vector(521, 379);
hostBtn.extent = new Vector(93, 45);
hostBtn.pressedAction = (e) -> {
MarbleGame.canvas.setContent(new MPPlayMissionGui());
}
window.addChild(hostBtn);
var joinBtn = new GuiButton(loadButtonImages("data/ui/mp/join/join"));
joinBtn.position = new Vector(628, 379);
joinBtn.extent = new Vector(93, 45);
window.addChild(joinBtn);
var refreshBtn = new GuiButton(loadButtonImages("data/ui/mp/join/refresh/refresh-1"));
refreshBtn.position = new Vector(126, 379);
refreshBtn.extent = new Vector(45, 45);
window.addChild(refreshBtn);
var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings"));
serverSettingsBtn.position = new Vector(171, 379);
serverSettingsBtn.extent = new Vector(45, 45);
window.addChild(serverSettingsBtn);
var exitBtn = new GuiButton(loadButtonImages("data/ui/mp/join/leave"));
exitBtn.position = new Vector(32, 379);
exitBtn.extent = new Vector(93, 45);
exitBtn.pressedAction = (e) -> {
MarbleGame.canvas.setContent(new MainMenuGui());
}
window.addChild(exitBtn);
var markerFelt32fontdata = ResourceLoader.getFileEntry("data/font/MarkerFelt.fnt");
var markerFelt32b = new BitmapFont(markerFelt32fontdata.entry);
@:privateAccess markerFelt32b.loader = ResourceLoader.loader;
@ -90,6 +51,113 @@ class JoinServerGui extends GuiImage {
}
}
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
var window = new GuiImage(ResourceLoader.getResource("data/ui/mp/join/window.png", ResourceLoader.getImage, this.imageResources).toTile());
window.horizSizing = Center;
window.vertSizing = Center;
window.position = new Vector(-60, 5);
window.extent = new Vector(759, 469);
var serverListContainer = new GuiControl();
serverListContainer.position = new Vector(30, 80);
serverListContainer.extent = new Vector(475, 290);
window.addChild(serverListContainer);
var curSelection = -1;
var serverList = new GuiTextListCtrl(markerFelt18, [], 0xFFFFFF);
serverList.position = new Vector(0, 0);
serverList.extent = new Vector(475, 63);
serverList.onSelectedFunc = (sel) -> {
curSelection = sel;
}
serverListContainer.addChild(serverList);
var serverDisplays = [];
var ourServerList:Array<RemoteServerInfo> = [];
var platformToString = ["unknown", "pc", "mac", "web", "android"];
function updateServerListDisplay() {
serverDisplays = ourServerList.map(x -> '${x.name}');
serverList.setTexts(serverDisplays);
}
MasterServerClient.connectToMasterServer(() -> {
MasterServerClient.instance.getServerList((servers) -> {
ourServerList = servers;
updateServerListDisplay();
});
});
var maxPlayers = 8;
var privateSlots = 0;
var privateGame = false;
var hostBtn = new GuiButton(loadButtonImages("data/ui/mp/join/host"));
hostBtn.position = new Vector(521, 379);
hostBtn.extent = new Vector(93, 45);
hostBtn.pressedAction = (e) -> {
Net.hostServer('${Settings.highscoreName}\'s Server', maxPlayers, privateSlots, privateGame, () -> {
MarbleGame.canvas.setContent(new MPPlayMissionGui(true));
});
}
window.addChild(hostBtn);
var joinBtn = new GuiButton(loadButtonImages("data/ui/mp/join/join"));
joinBtn.position = new Vector(628, 379);
joinBtn.extent = new Vector(93, 45);
joinBtn.pressedAction = (e) -> {
if (curSelection != -1) {
var selectedServerVersion = ourServerList[curSelection].version;
// if (selectedServerVersion != MarbleGame.currentVersion) {
// var pup = new MessageBoxOkDlg("You are using a different version of the game than the server. Please update your game.");
// MarbleGame.canvas.pushDialog(pup);
// return;
// }
// MarbleGame.canvas.setContent(new MultiplayerLoadingGui("Connecting"));
var failed = true;
haxe.Timer.delay(() -> {
if (failed) {
// if (MarbleGame.canvas.content is MultiplayerLoadingGui) {
// var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content;
// if (loadGui != null) {
// loadGui.setErrorStatus("Failed to connect to server. Please try again.");
Net.disconnect();
// }
// }
}
}, 15000);
Net.joinServer(ourServerList[curSelection].name, false, () -> {
failed = false;
Net.remoteServerInfo = ourServerList[curSelection];
});
}
}
window.addChild(joinBtn);
var refreshBtn = new GuiButton(loadButtonImages("data/ui/mp/join/refresh/refresh-1"));
refreshBtn.position = new Vector(126, 379);
refreshBtn.extent = new Vector(45, 45);
window.addChild(refreshBtn);
var serverSettingsBtn = new GuiButton(loadButtonImages("data/ui/mp/play/settings"));
serverSettingsBtn.position = new Vector(171, 379);
serverSettingsBtn.extent = new Vector(45, 45);
window.addChild(serverSettingsBtn);
var exitBtn = new GuiButton(loadButtonImages("data/ui/mp/join/leave"));
exitBtn.position = new Vector(32, 379);
exitBtn.extent = new Vector(93, 45);
exitBtn.pressedAction = (e) -> {
MarbleGame.canvas.setContent(new MainMenuGui());
}
window.addChild(exitBtn);
var titleText = new GuiText(markerFelt32);
titleText.position = new Vector(30, 20);
titleText.extent = new Vector(647, 30);

View file

@ -13,11 +13,19 @@ import h3d.Vector;
import src.Util;
import src.Settings;
import src.Mission;
import src.MissionList;
import net.ClientConnection.NetPlatform;
import net.Net;
import net.NetCommands;
class MPPlayMissionGui extends GuiImage {
static var currentSelectionStatic:Int = -1;
static var currentCategoryStatic:String = "beginner";
static var setLevelFn:(String, Int) -> Void;
static var playSelectedLevel:(String, Int) -> Void;
static var setLevelStr:String->Void;
var currentSelection:Int = 0;
var currentCategory:String = "beginner";
var currentList:Array<Mission>;
@ -36,7 +44,7 @@ class MPPlayMissionGui extends GuiImage {
var previewToken:Int = 0;
#end
public function new() {
public function new(isHost:Bool = true) {
MissionList.buildMissionList();
function chooseBg() {
var rand = Math.random();
@ -268,7 +276,8 @@ class MPPlayMissionGui extends GuiImage {
playBtn.position = new Vector(565, 514);
playBtn.extent = new Vector(93, 44);
playBtn.pressedAction = (sender) -> {
MarbleGame.instance.playMission(currentList[currentSelection]);
NetCommands.toggleReadiness(Net.isClient ? Net.clientId : 0);
// MarbleGame.instance.playMission(currentList[currentSelection], true);
}
window.addChild(playBtn);
@ -484,6 +493,15 @@ class MPPlayMissionGui extends GuiImage {
#end
}
playSelectedLevel = (cat:String, index:Int) -> {
// if (custSelected) {
// NetCommands.playCustomLevel(MPCustoms.missionList[custSelectedIdx].path);
// } else {
var curMission = MissionList.missionList["multiplayer"][cat][index]; // mission[index];
MarbleGame.instance.playMission(curMission, true);
// }
}
currentList = MissionList.missionList["multiplayer"]["beginner"];
setCategoryFunc(currentCategoryStatic, null, false);
@ -504,4 +522,66 @@ class MPPlayMissionGui extends GuiImage {
if (Key.isPressed(Key.RIGHT))
setSelectedFunc(currentSelection + 1);
}
inline function platformToString(platform:NetPlatform) {
return switch (platform) {
case Unknown: return "unknown";
case Android: return "android";
case MacOS: return "mac";
case PC: return "pc";
case Web: return "web";
}
}
public function updateLobbyNames() {
return;
var playerListArr = [];
if (Net.isHost) {
playerListArr.push({
name: Settings.highscoreName,
state: Net.lobbyHostReady,
platform: Net.getPlatform()
});
}
if (Net.isClient) {
playerListArr.push({
name: Settings.highscoreName,
state: Net.lobbyClientReady,
platform: Net.getPlatform()
});
}
if (Net.clientIdMap != null) {
for (c => v in Net.clientIdMap) {
playerListArr.push({
name: v.name,
state: v.lobbyReady,
platform: v.platform
});
}
}
// if (!showingCustoms)
// playerList.setTexts(playerListArr.map(player -> {
// return '<img src="${player.state ? "ready" : "notready"}"></img><img src="${platformToString(player.platform)}"></img>${player.name}';
// }));
var pubCount = 1; // Self
var privCount = 0;
for (cid => cc in Net.clientIdMap) {
if (cc.isPrivate) {
privCount++;
} else {
pubCount++;
}
}
if (Net.isHost) {
// updatePlayerCountFn(pubCount, privCount, Net.serverInfo.maxPlayers - Net.serverInfo.privateSlots, Net.serverInfo.privateSlots);
}
}
public function updatePlayerCount(pub:Int, priv:Int, publicTotal:Int, privateTotal:Int) {
return;
// updatePlayerCountFn(pub, priv, publicTotal, privateTotal);
}
}

View file

@ -13,6 +13,493 @@ import src.Settings;
import src.ResourceLoaderWorker;
class MarbleSelectGui extends GuiImage {
public static var marbleData = [
[
{
name: "Staff's Original",
dts: "data/shapes/balls/ball-superball.dts",
skin: "base",
shader: "Default"
},
{
name: "3D Marble",
dts: "data/shapes/balls/3dMarble.dts",
skin: "base",
shader: "Default"
},
{
name: "Mid P",
dts: "data/shapes/balls/midp.dts",
skin: "base",
shader: "Default"
},
{
name: "Spade",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin4",
shader: "Default"
},
{
name: "GMD Logo",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin5",
shader: "Default"
},
{
name: "Textured Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin6",
shader: "Default"
},
{
name: "Golden Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin7",
shader: "Default"
},
{
name: "Rainbow Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin8",
shader: "Default"
},
{
name: "Brown Swirls",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin9",
shader: "Default"
},
{
name: "Caution Stripes",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin10",
shader: "Default"
},
{
name: "Earth",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin11",
shader: "Default"
},
{
name: "Golf Ball",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin12",
shader: "Default"
},
{
name: "Jupiter",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin13",
shader: "Default"
},
{
name: "MB Gold Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin14",
shader: "Default"
},
{
name: "MBP on the Marble!",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin15",
shader: "Default"
},
{
name: "Moshe",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin16",
shader: "Default"
},
{
name: "Strong Bad",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin17",
shader: "Default"
},
{
name: "Venus",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin18",
shader: "Default"
},
{
name: "Water",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin19",
shader: "Default"
},
{
name: "Evil Eye",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin20",
shader: "Default"
},
{
name: "Desert and Sky",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin21",
shader: "Default"
},
{
name: "Dirt Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin22",
shader: "Default"
},
{
name: "Friction Textured Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin23",
shader: "Default"
},
{
name: "Grass",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin24",
shader: "Default"
},
{
name: "Mars",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin25",
shader: "Default"
},
{
name: "Phil's Golf Ball",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin26",
shader: "Default"
},
{
name: "Molten",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin27",
shader: "Default"
},
{
name: "Lightning",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin28",
shader: "Default"
},
{
name: "Phil'sEmpire",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin29",
shader: "Default"
},
{
name: "Matan's Red Dragon",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin30",
shader: "Default"
},
{
name: "Metallic Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin31",
shader: "Default"
},
{
name: "Sun",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin32",
shader: "Default"
},
{
name: "Underwater",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin33",
shader: "Default"
},
{
name: "GarageGames logo",
dts: "data/shapes/balls/garageGames.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 1",
dts: "data/shapes/balls/bm1.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 2",
dts: "data/shapes/balls/bm2.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 3",
dts: "data/shapes/balls/bm3.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 1",
dts: "data/shapes/balls/sm1.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 2",
dts: "data/shapes/balls/sm2.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 3",
dts: "data/shapes/balls/sm3.dts",
skin: "base",
shader: "Default"
}
],
[
{
name: "Deep Blue",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin1",
shader: "ClassicGlassPureSphere"
},
{
name: "Blood Red",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin2",
shader: "Default"
},
{
name: "Gang Green",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin6",
shader: "ClassicGlassPureSphere"
},
{
name: "Pink Candy",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin27",
shader: "Default"
},
{
name: "Chocolate",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin5",
shader: "ClassicGlassPureSphere"
},
{
name: "Grape",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin4",
shader: "ClassicGlassPureSphere"
},
{
name: "Lemon",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin28",
shader: "Default"
},
{
name: "Lime Green",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin8",
shader: "Default"
},
{
name: "Blueberry",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin7",
shader: "ClassicGlassPureSphere"
},
{
name: "Tangerine",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin3",
shader: "ClassicGlassPureSphere"
},
{
name: "8 Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin9",
shader: "ClassicMarb3"
},
{
name: "Ace of Hearts",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin22",
shader: "ClassicMarb3"
},
{
name: "Football",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin16",
shader: "ClassicMarb3"
},
{
name: "9 Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin29",
shader: "ClassicMarb3"
},
{
name: "Ace of Spades",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin24",
shader: "ClassicMarb3"
},
{
name: "GarageGames",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin10",
shader: "ClassicMarb2"
},
{
name: "Bob",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin30",
shader: "ClassicMarb3"
},
{
name: "Skully",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin18",
shader: "Default"
},
{
name: "Jack-o-Lantern",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin34",
shader: "Default"
},
{
name: "Walled Up",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin25",
shader: "ClassicMarb3"
},
{
name: "Sunny Side Up",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin11",
shader: "ClassicMetal"
},
{
name: "Lunar",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin31",
shader: "ClassicMetal"
},
{
name: "Battery",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin14",
shader: "ClassicMarb3"
},
{
name: "Static",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin32",
shader: "ClassicMarb2"
},
{
name: "Earth",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin20",
shader: "ClassicMarbGlass20"
},
{
name: "Red and X",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin13",
shader: "ClassicMarb3"
},
{
name: "Orange Spiral",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin12",
shader: "ClassicGlassPureSphere"
},
{
name: "Blue Spiral",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin15",
shader: "ClassicGlassPureSphere"
},
{
name: "Sliced Marble",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin21",
shader: "ClassicMarb3"
},
{
name: "Orange Checkers",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin19",
shader: "ClassicMarb3"
},
{
name: "Torque",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin33",
shader: "ClassicMarb3"
},
{
name: "Fred",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin17",
shader: "ClassicMarb3"
},
{
name: "Pirate",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin26",
shader: "ClassicMarbGlass18"
},
{
name: "Shuriken",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin23",
shader: "ClassicMarb3"
},
{
name: "Eyeball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin35",
shader: "Default"
},
{
name: "Woody",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin36",
shader: "Default"
},
{
name: "Dat Nostalgia",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin37",
shader: "Default"
},
{
name: "Graffiti",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin38",
shader: "Default"
},
{
name: "Asteroid",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin39",
shader: "Default"
},
{
name: "Disco Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin40",
shader: "Default"
}
],
];
public function new() {
var img = ResourceLoader.getImage("data/ui/marbleSelect/marbleSelect.png");
super(img.resource.toTile());
@ -21,494 +508,6 @@ class MarbleSelectGui extends GuiImage {
this.position = new Vector(73, -59);
this.extent = new Vector(493, 361);
var marbleData = [
[
{
name: "Staff's Original",
dts: "data/shapes/balls/ball-superball.dts",
skin: "base",
shader: "Default"
},
{
name: "3D Marble",
dts: "data/shapes/balls/3dMarble.dts",
skin: "base",
shader: "Default"
},
{
name: "Mid P",
dts: "data/shapes/balls/midp.dts",
skin: "base",
shader: "Default"
},
{
name: "Spade",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin4",
shader: "Default"
},
{
name: "GMD Logo",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin5",
shader: "Default"
},
{
name: "Textured Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin6",
shader: "Default"
},
{
name: "Golden Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin7",
shader: "Default"
},
{
name: "Rainbow Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin8",
shader: "Default"
},
{
name: "Brown Swirls",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin9",
shader: "Default"
},
{
name: "Caution Stripes",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin10",
shader: "Default"
},
{
name: "Earth",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin11",
shader: "Default"
},
{
name: "Golf Ball",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin12",
shader: "Default"
},
{
name: "Jupiter",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin13",
shader: "Default"
},
{
name: "MB Gold Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin14",
shader: "Default"
},
{
name: "MBP on the Marble!",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin15",
shader: "Default"
},
{
name: "Moshe",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin16",
shader: "Default"
},
{
name: "Strong Bad",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin17",
shader: "Default"
},
{
name: "Venus",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin18",
shader: "Default"
},
{
name: "Water",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin19",
shader: "Default"
},
{
name: "Evil Eye",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin20",
shader: "Default"
},
{
name: "Desert and Sky",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin21",
shader: "Default"
},
{
name: "Dirt Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin22",
shader: "Default"
},
{
name: "Friction Textured Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin23",
shader: "Default"
},
{
name: "Grass",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin24",
shader: "Default"
},
{
name: "Mars",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin25",
shader: "Default"
},
{
name: "Phil's Golf Ball",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin26",
shader: "Default"
},
{
name: "Molten",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin27",
shader: "Default"
},
{
name: "Lightning",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin28",
shader: "Default"
},
{
name: "Phil'sEmpire",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin29",
shader: "Default"
},
{
name: "Matan's Red Dragon",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin30",
shader: "Default"
},
{
name: "Metallic Marble",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin31",
shader: "Default"
},
{
name: "Sun",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin32",
shader: "Default"
},
{
name: "Underwater",
dts: "data/shapes/balls/ball-superball.dts",
skin: "skin33",
shader: "Default"
},
{
name: "GarageGames logo",
dts: "data/shapes/balls/garageGames.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 1",
dts: "data/shapes/balls/bm1.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 2",
dts: "data/shapes/balls/bm2.dts",
skin: "base",
shader: "Default"
},
{
name: "Big Marble 3",
dts: "data/shapes/balls/bm3.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 1",
dts: "data/shapes/balls/sm1.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 2",
dts: "data/shapes/balls/sm2.dts",
skin: "base",
shader: "Default"
},
{
name: "Small Marble 3",
dts: "data/shapes/balls/sm3.dts",
skin: "base",
shader: "Default"
}
],
[
{
name: "Deep Blue",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin1",
shader: "ClassicGlassPureSphere"
},
{
name: "Blood Red",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin2",
shader: "Default"
},
{
name: "Gang Green",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin6",
shader: "ClassicGlassPureSphere"
},
{
name: "Pink Candy",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin27",
shader: "Default"
},
{
name: "Chocolate",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin5",
shader: "ClassicGlassPureSphere"
},
{
name: "Grape",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin4",
shader: "ClassicGlassPureSphere"
},
{
name: "Lemon",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin28",
shader: "Default"
},
{
name: "Lime Green",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin8",
shader: "Default"
},
{
name: "Blueberry",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin7",
shader: "ClassicGlassPureSphere"
},
{
name: "Tangerine",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin3",
shader: "ClassicGlassPureSphere"
},
{
name: "8 Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin9",
shader: "ClassicMarb3"
},
{
name: "Ace of Hearts",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin22",
shader: "ClassicMarb3"
},
{
name: "Football",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin16",
shader: "ClassicMarb3"
},
{
name: "9 Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin29",
shader: "ClassicMarb3"
},
{
name: "Ace of Spades",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin24",
shader: "ClassicMarb3"
},
{
name: "GarageGames",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin10",
shader: "ClassicMarb2"
},
{
name: "Bob",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin30",
shader: "ClassicMarb3"
},
{
name: "Skully",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin18",
shader: "Default"
},
{
name: "Jack-o-Lantern",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin34",
shader: "Default"
},
{
name: "Walled Up",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin25",
shader: "ClassicMarb3"
},
{
name: "Sunny Side Up",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin11",
shader: "ClassicMetal"
},
{
name: "Lunar",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin31",
shader: "ClassicMetal"
},
{
name: "Battery",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin14",
shader: "ClassicMarb3"
},
{
name: "Static",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin32",
shader: "ClassicMarb2"
},
{
name: "Earth",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin20",
shader: "ClassicMarbGlass20"
},
{
name: "Red and X",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin13",
shader: "ClassicMarb3"
},
{
name: "Orange Spiral",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin12",
shader: "ClassicGlassPureSphere"
},
{
name: "Blue Spiral",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin15",
shader: "ClassicGlassPureSphere"
},
{
name: "Sliced Marble",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin21",
shader: "ClassicMarb3"
},
{
name: "Orange Checkers",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin19",
shader: "ClassicMarb3"
},
{
name: "Torque",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin33",
shader: "ClassicMarb3"
},
{
name: "Fred",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin17",
shader: "ClassicMarb3"
},
{
name: "Pirate",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin26",
shader: "ClassicMarbGlass18"
},
{
name: "Shuriken",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin23",
shader: "ClassicMarb3"
},
{
name: "Eyeball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin35",
shader: "Default"
},
{
name: "Woody",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin36",
shader: "Default"
},
{
name: "Dat Nostalgia",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin37",
shader: "Default"
},
{
name: "Graffiti",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin38",
shader: "Default"
},
{
name: "Asteroid",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin39",
shader: "Default"
},
{
name: "Disco Ball",
dts: "data/shapes/balls/pack1/pack1marble.dts",
skin: "uskin40",
shader: "Default"
}
],
];
var categoryNames = ["Official Marbles", "MBUltra"];
var curSelection:Int = Settings.optionsSettings.marbleIndex;

View file

@ -24,6 +24,7 @@ import src.ResourceLoader;
import h3d.Vector;
import src.Util;
import src.MarbleGame;
import src.MissionList;
class PlayMissionGui extends GuiImage {
static var currentSelectionStatic:Int = -1;

View file

@ -10,6 +10,7 @@ import h3d.Vector;
import src.Util;
import src.MarbleGame;
import src.Settings;
import src.MissionList;
class ReplayCenterGui extends GuiImage {
public function new() {

View file

@ -8,6 +8,7 @@ import hxd.res.BitmapFont;
import h3d.Vector;
import src.ResourceLoader;
import src.Settings;
import src.MissionList;
class SearchGui extends GuiImage {
public function new(game:String, isCustom:Bool) {

View file

@ -8,6 +8,7 @@ import src.Settings;
import src.Settings.PlayStatistics;
import src.Mission;
import src.Util;
import src.MissionList;
class StatisticsGui extends GuiImage {
public function new(game:String) {

View file

@ -255,6 +255,28 @@ class HuntMode extends NullMode {
}
}
public inline function setGemHiddenStatus(gemId:Int, status:Bool) {
var gemSpawn = gemSpawnPoints[gemId];
if (gemSpawn.gem != null) {
gemSpawn.gem.pickedUp = status;
gemSpawn.gem.setHide(status);
gemSpawn.gemBeam.setHide(status);
if (status)
this.activeGems.push(gemSpawn.gem);
else
this.activeGems.remove(gemSpawn.gem);
} else {
throw new haxe.Exception("Setting gem status for non existent gem!");
}
}
public function setActiveSpawnSphere(gems:Array<Int>) {
hideExisting();
for (gem in gems) {
spawnGem(gem);
}
}
function getGemWeight(gem:Gem) {
if (gem.gemColor == "red")
return 0;

View file

@ -28,6 +28,7 @@ class NullMode implements GameMode {
// If there's a start pad, start there
position = startPad.getAbsPos().getPosition();
quat = startPad.getRotationQuat().clone();
position.z += 3;
} else {
position = new Vector(0, 0, 300);
}

154
src/net/BitStream.hx Normal file
View file

@ -0,0 +1,154 @@
package net;
import haxe.io.FPHelper;
import haxe.io.BytesOutput;
import haxe.io.BytesInput;
import haxe.io.Bytes;
class InputBitStream {
var data:Bytes;
var position:Int;
var shift:Int;
public function new(data:Bytes) {
this.data = data;
this.position = 0;
this.shift = 0;
}
function readBits(bits:Int = 8) {
if (this.shift + bits >= 8) {
var extra = (this.shift + bits) % 8;
var remain = bits - extra;
var first = data.get(position) >> shift;
var result = first;
this.position++;
if (extra > 0) {
var second = (data.get(position) & (0xFF >> (8 - extra))) << remain;
result |= second;
}
this.shift = extra;
return result;
} else {
var result = (data.get(position) >> shift) & (0xFF >> (8 - bits));
shift += bits;
return result;
}
}
public function readInt(bits:Int = 32) {
var value = 0;
var shift = 0;
while (bits > 0) {
value |= readBits(bits < 8 ? bits : 8) << shift;
shift += 8;
bits -= 8;
}
return value;
}
public function readFlag() {
return readInt(1) != 0;
}
public function readByte() {
return readInt(8);
}
public function readUInt16() {
return readInt(16);
}
public function readInt32() {
return readInt(32);
}
public function readFloat() {
return FPHelper.i32ToFloat(readInt32());
}
public function readString() {
var length = readUInt16();
var str = "";
for (i in 0...length) {
str += String.fromCharCode(readByte());
}
return str;
}
}
class OutputBitStream {
var data:BytesOutput;
var position:Int;
var shift:Int;
var lastByte:Int;
public function new(data:BytesOutput = null) {
this.data = data;
if (this.data == null)
this.data = new BytesOutput();
this.position = 0;
this.shift = 0;
this.lastByte = 0;
}
function writeBits(value:Int, bits:Int) {
value = value & (0xFF >> (8 - bits));
if (this.shift + bits >= 8) {
var extra = (shift + bits) % 8;
var remain = bits - extra;
var first = value & (0xFF >> (8 - remain));
lastByte |= first << shift;
var second = (value >> remain) & (0xFF >> (8 - extra));
this.data.writeByte(this.lastByte);
this.lastByte = second;
this.shift = extra;
} else {
lastByte |= (value << this.shift) & (0xFF >> (8 - bits - this.shift));
this.shift += bits;
}
}
public function writeInt(value:Int, bits:Int = 32) {
while (bits > 0) {
this.writeBits(value & 0xFF, bits < 8 ? bits : 8);
value >>= 8;
bits -= 8;
}
}
public function writeFlag(value:Bool) {
writeInt(value ? 1 : 0, 1);
}
public function writeByte(value:Int) {
writeInt(value, 8);
}
public function writeUInt16(value:Int) {
writeInt(value, 16);
}
public function writeInt32(value:Int) {
writeInt(value, 32);
}
public function getBytes() {
this.data.writeByte(this.lastByte);
return this.data.getBytes();
}
public function writeFloat(value:Float) {
writeInt(FPHelper.floatToI32(value), 32);
}
public function writeString(value:String) {
writeUInt16(value.length);
for (i in 0...value.length) {
writeByte(StringTools.fastCodeAt(value, i));
}
}
}

138
src/net/ClientConnection.hx Normal file
View file

@ -0,0 +1,138 @@
package net;
import haxe.io.Bytes;
import datachannel.RTCPeerConnection;
import datachannel.RTCDataChannel;
import net.MoveManager;
import src.TimeState;
enum abstract GameplayState(Int) from Int to Int {
var UNKNOWN;
var LOBBY;
var GAME;
}
enum abstract NetPlatform(Int) from Int to Int {
var Unknown;
var PC;
var MacOS;
var Web;
var Android;
}
@:publicFields
class ClientConnection extends GameConnection {
var socket:RTCPeerConnection;
var datachannel:RTCDataChannel;
var datachannelUnreliable:RTCDataChannel;
var rtt:Float;
var pingSendTime:Float;
var _rttRecords:Array<Float> = [];
var lastRecvTime:Float;
var didWarnTimeout:Bool = false;
public function new(id:Int, socket:RTCPeerConnection, datachannel:RTCDataChannel, datachannelUnreliable:RTCDataChannel) {
super(id);
this.socket = socket;
this.datachannel = datachannel;
this.datachannelUnreliable = datachannelUnreliable;
this.state = GameplayState.LOBBY;
this.rtt = 0;
this.name = "Unknown";
}
override function sendBytes(b:Bytes) {
datachannel.sendBytes(b);
}
override function sendBytesUnreliable(b:Bytes) {
datachannelUnreliable.sendBytes(b);
}
public inline function needsTimeoutWarn(t:Float) {
return (t - lastRecvTime) > 10 && !didWarnTimeout;
}
public inline function needsTimeoutKick(t:Float) {
return (t - lastRecvTime) > 15 && didWarnTimeout;
}
}
@:publicFields
class DummyConnection extends GameConnection {
public function new(id:Int) {
super(id);
this.state = GameplayState.GAME;
this.lobbyReady = true;
}
}
@:publicFields
abstract class GameConnection {
var id:Int;
var state:GameplayState;
var moveManager:MoveManager;
var name:String;
var lobbyReady:Bool;
var platform:NetPlatform;
var marbleId:Int;
var isPrivate:Bool;
function new(id:Int) {
this.id = id;
this.moveManager = new MoveManager(this);
this.lobbyReady = false;
}
public function ready() {
state = GameplayState.GAME;
}
public function toggleLobbyReady() {
lobbyReady = !lobbyReady;
}
public function queueMove(m:NetMove) {
moveManager.queueMove(m);
}
public inline function acknowledgeMove(m:NetMove, timeState:TimeState) {
return moveManager.acknowledgeMove(m, timeState);
}
public inline function getQueuedMoves() {
return @:privateAccess moveManager.queuedMoves;
}
public inline function getQueuedMovesLength() {
return moveManager.getQueueSize();
}
public function recordMove(marble:src.Marble, motionDir:h3d.Vector, timeState:TimeState, serverTicks:Int) {
return moveManager.recordMove(marble, motionDir, timeState, serverTicks);
}
public function getNextMove() {
return moveManager.getNextMove();
}
public function sendBytes(b:haxe.io.Bytes) {}
public function sendBytesUnreliable(b:haxe.io.Bytes) {}
public inline function getName() {
return name;
}
public inline function setName(value:String) {
name = value;
}
public inline function setMarbleId(value:Int) {
marbleId = value;
}
public inline function getMarbleId() {
return marbleId;
}
}

View file

@ -0,0 +1,29 @@
package net;
import net.NetPacket.GemSpawnPacket;
import net.NetPacket.GemPickupPacket;
class GemPredictionStore {
var predictions:Array<Bool>;
public inline function new() {
predictions = [];
}
public inline function alloc() {
predictions.push(true);
}
public inline function getState(netIndex:Int) {
return predictions[netIndex];
}
public inline function acknowledgeGemPickup(packet:GemPickupPacket) {
predictions[packet.gemId] = true;
}
public inline function acknowledgeGemSpawn(packet:GemSpawnPacket) {
for (gemId in packet.gemIds)
predictions[gemId] = false;
}
}

View file

@ -0,0 +1,86 @@
package net;
import net.NetPacket.MarbleUpdatePacket;
import net.NetPacket.MarbleMovePacket;
import src.TimeState;
import src.Marble;
import h3d.Vector;
@:publicFields
class MarblePrediction {
var tick:Int;
var position:Vector;
var velocity:Vector;
var omega:Vector;
var isControl:Bool;
var blastAmount:Int;
public function new(marble:Marble, tick:Int) {
this.tick = tick;
position = @:privateAccess marble.newPos.clone();
velocity = @:privateAccess marble.velocity.clone();
omega = @:privateAccess marble.omega.clone();
blastAmount = @:privateAccess marble.blastTicks;
isControl = @:privateAccess marble.controllable;
}
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.netFlags != 0)
subs += 1;
// if (p.powerUpId != powerupItemId)
// if (tick % 10 == 0)
// subs += 1; // temp
// if (isControl)
// subs += Math.abs(blastAmount - p.blastAmount);
return subs;
}
}
class MarblePredictionStore {
var predictions:Map<Marble, Array<MarblePrediction>>;
public function new() {
predictions = [];
}
public function storeState(marble:Marble, tick:Int) {
var state = new MarblePrediction(marble, tick);
if (predictions.exists(marble)) {
var arr = predictions[marble];
while (arr.length != 0 && arr[0].tick >= tick)
arr.shift();
arr.push(state);
} else {
predictions.set(marble, [state]);
}
}
public function retrieveState(marble:Marble, tick:Int) {
if (predictions.exists(marble)) {
var arr = predictions[marble];
while (arr.length != 0 && arr[0].tick < tick)
arr.shift();
if (arr.length == 0)
return null;
var p = arr[0];
if (p.tick == tick)
return p;
return null;
}
return null;
}
public function clearStatesAfterTick(marble:Marble, tick:Int) {
if (predictions.exists(marble)) {
var arr = predictions[marble];
while (arr.length != 0 && arr[arr.length - 1].tick >= tick)
arr.pop();
}
}
public function removeMarbleFromPrediction(marble:Marble) {
this.predictions.remove(marble);
}
}

View file

@ -0,0 +1,94 @@
package net;
import h3d.Vector;
import net.NetPacket.MarbleNetFlags;
import net.NetPacket.MarbleUpdatePacket;
import net.Net;
@:publicFields
class OtherMarbleUpdate {
var packets:Array<MarbleUpdatePacket> = [];
var lastBlastTick:Int;
var lastHeliTick:Int;
var lastMegaTick:Int;
var lastPowerUpId:Int;
var lastGravityUp:Vector;
public function new() {}
}
@:publicFields
class MarbleUpdateQueue {
var otherMarbleUpdates:Map<Int, OtherMarbleUpdate> = [];
var myMarbleUpdate:MarbleUpdatePacket;
var ourMoveApplied:Bool = false;
public function new() {}
public function enqueue(update:MarbleUpdatePacket) {
var cc = update.clientId;
if (cc != Net.clientId) {
// if (myMarbleUpdate != null && update.serverTicks > myMarbleUpdate.serverTicks)
// ourMoveApplied = true;
if (otherMarbleUpdates.exists(cc)) {
var otherUpdate = otherMarbleUpdates[cc];
var ourList = otherUpdate.packets;
// Copy the netflagg'd fields
if (update.netFlags & MarbleNetFlags.DoBlast == 0)
update.blastTick = otherUpdate.lastBlastTick;
else
otherUpdate.lastBlastTick = update.blastTick;
if (update.netFlags & MarbleNetFlags.DoHelicopter == 0)
update.heliTick = otherUpdate.lastHeliTick;
else
otherUpdate.lastHeliTick = update.heliTick;
if (update.netFlags & MarbleNetFlags.DoMega == 0)
update.megaTick = otherUpdate.lastMegaTick;
else
otherUpdate.lastMegaTick = update.megaTick;
if (update.netFlags & MarbleNetFlags.PickupPowerup == 0)
update.powerUpId = otherUpdate.lastPowerUpId;
else
otherUpdate.lastPowerUpId = update.powerUpId;
if (update.netFlags & MarbleNetFlags.GravityChange == 0)
update.gravityDirection = otherUpdate.lastGravityUp;
else
otherUpdate.lastGravityUp = update.gravityDirection;
ourList.push(update);
} else {
var otherUpdate = new OtherMarbleUpdate();
otherUpdate.packets.push(update);
// Copy the netflagg'd fields
if (update.netFlags & MarbleNetFlags.DoBlast != 0)
otherUpdate.lastBlastTick = update.blastTick;
if (update.netFlags & MarbleNetFlags.DoHelicopter != 0)
otherUpdate.lastHeliTick = update.heliTick;
if (update.netFlags & MarbleNetFlags.DoMega != 0)
otherUpdate.lastMegaTick = update.megaTick;
if (update.netFlags & MarbleNetFlags.PickupPowerup != 0)
otherUpdate.lastPowerUpId = update.powerUpId;
if (update.netFlags & MarbleNetFlags.GravityChange != 0)
otherUpdate.lastGravityUp = update.gravityDirection;
otherMarbleUpdates[cc] = otherUpdate;
}
} else {
if (myMarbleUpdate == null || update.serverTicks > myMarbleUpdate.serverTicks) {
if (myMarbleUpdate != null) {
// Copy the netflagg'd fields
if (update.netFlags & MarbleNetFlags.DoBlast == 0)
update.blastTick = myMarbleUpdate.blastTick;
if (update.netFlags & MarbleNetFlags.DoHelicopter == 0)
update.heliTick = myMarbleUpdate.heliTick;
if (update.netFlags & MarbleNetFlags.DoMega == 0)
update.megaTick = myMarbleUpdate.megaTick;
if (update.netFlags & MarbleNetFlags.PickupPowerup == 0)
update.powerUpId = myMarbleUpdate.powerUpId;
if (update.netFlags & MarbleNetFlags.GravityChange == 0)
update.gravityDirection = myMarbleUpdate.gravityDirection;
}
myMarbleUpdate = update;
ourMoveApplied = false;
}
}
}
}

View file

@ -0,0 +1,323 @@
package net;
import gui.MessageBoxOkDlg;
import src.MarbleGame;
import haxe.Json;
import net.Net.ServerInfo;
import haxe.net.WebSocket;
import src.Console;
typedef RemoteServerInfo = {
name:String,
players:Int,
maxPlayers:Int,
platform:Int,
version:String
}
class MasterServerClient {
#if js
static var serverIp = "wss://mbomaster.randomityguy.me:8443";
#else
static var serverIp = "ws://89.58.58.191:8080";
#end
public static var instance:MasterServerClient;
var ws:WebSocket;
var serverListCb:Array<RemoteServerInfo>->Void;
var open = false;
static var wsToken:Int = 0;
#if hl
var wsThread:sys.thread.Thread;
static var responses:sys.thread.Deque<() -> Void> = new sys.thread.Deque<() -> Void>();
var toSend:sys.thread.Deque<String> = new sys.thread.Deque<String>();
var stopping:Bool = false;
var stopMutex:sys.thread.Mutex = new sys.thread.Mutex();
#end
public function new(onOpenFunc:() -> Void, onErrorFunc:() -> Void) {
#if hl
wsThread = sys.thread.Thread.create(() -> {
hl.Gc.enable(false);
hl.Gc.blocking(true); // Wtf is this shit
#end
wsToken++;
var myToken = wsToken;
ws = WebSocket.create(serverIp);
#if hl
hl.Gc.enable(true);
hl.Gc.blocking(false);
#end
ws.onopen = () -> {
open = true;
#if hl
responses.add(() -> onOpenFunc());
#end
#if js
onOpenFunc();
#end
}
ws.onmessageString = (m) -> {
#if hl
responses.add(() -> handleMessage(m));
#end
#if js
handleMessage(m);
#end
}
ws.onerror = (m) -> {
#if hl
responses.add(() -> {
MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to connect to master server: " + m));
});
if (onErrorFunc != null)
responses.add(() -> {
onErrorFunc();
});
#end
#if js
MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Failed to connect to master server: " + m));
if (onErrorFunc != null)
onErrorFunc();
#end
#if hl
stopMutex.acquire();
#end
if (myToken == wsToken) {
open = false;
ws = null;
instance = null;
}
#if hl
stopMutex.acquire();
stopping = true;
stopMutex.release();
if (myToken == wsToken) {
wsThread = null;
}
#end
}
ws.onclose = (?e) -> {
#if hl
stopMutex.acquire();
#end
if (myToken == wsToken) {
open = false;
ws = null;
instance = null;
}
#if hl
stopping = true;
stopMutex.release();
if (myToken == wsToken) {
wsThread = null;
}
#end
}
#if hl
while (true) {
stopMutex.acquire();
if (stopping)
break;
while (true) {
var s = toSend.pop(false);
if (s == null)
break;
#if hl
hl.Gc.blocking(true);
#end
ws.sendString(s);
#if hl
hl.Gc.blocking(false);
#end
}
#if hl
hl.Gc.blocking(true);
#end
ws.process();
#if hl
hl.Gc.blocking(false);
#end
stopMutex.release();
Sys.sleep(0.1);
}
#end
#if hl
});
#end
}
public static function process() {
#if sys
var resp = responses.pop(false);
if (resp != null) {
resp();
}
#end
}
public static function connectToMasterServer(onConnect:() -> Void, onError:() -> Void = null) {
if (instance == null)
instance = new MasterServerClient(onConnect, onError);
else {
if (instance.open)
onConnect();
else {
if (instance != null && instance.ws != null)
instance.ws.close();
instance = new MasterServerClient(onConnect, onError);
}
}
}
public static function disconnectFromMasterServer() {
if (instance != null && instance.ws != null) {
instance.ws.close();
if (instance != null) {
instance.open = false;
instance.ws = null;
instance = null;
}
}
}
function queueMessage(m:String) {
#if hl
toSend.add(m);
#end
#if js
ws.sendString(m);
#end
}
public function heartBeat() {
queueMessage(Json.stringify({
type: "heartbeat"
}));
}
public function sendServerInfo(serverInfo:ServerInfo) {
queueMessage(Json.stringify({
type: "serverInfo",
name: serverInfo.name,
players: serverInfo.players,
maxPlayers: serverInfo.maxPlayers,
privateSlots: serverInfo.privateSlots,
privateServer: serverInfo.privateServer,
inviteCode: serverInfo.inviteCode,
state: serverInfo.state,
platform: serverInfo.platform,
version: "MBP" // MarbleGame.currentVersion
}));
}
public function sendConnectToServer(serverName:String, sdp:String, isInvite:Bool = false) {
if (!isInvite) {
queueMessage(Json.stringify({
type: "connect",
serverName: serverName,
sdp: sdp
}));
} else {
queueMessage(Json.stringify({
type: "connectInvite",
sdp: sdp,
inviteCode: serverName
}));
}
}
public function getServerList(serverListCb:Array<RemoteServerInfo>->Void) {
this.serverListCb = serverListCb;
queueMessage(Json.stringify({
type: "serverList"
}));
}
function handleMessage(message:String) {
var conts = Json.parse(message);
Console.log('Received ${conts.type}');
if (conts.type == "serverList") {
if (serverListCb != null) {
serverListCb(conts.servers);
}
}
if (conts.type == "connect") {
if (!Net.isHost) {
queueMessage(Json.stringify({
type: "connectFailed",
success: false,
reason: "The server has shut down"
}));
return;
}
var joiningPrivate = conts.isPrivate;
if (Net.serverInfo.players >= Net.serverInfo.maxPlayers) {
queueMessage(Json.stringify({
type: "connectFailed",
success: false,
reason: "The server is full"
}));
return;
}
var pubSlotsAvail = Net.serverInfo.maxPlayers - Net.serverInfo.privateSlots;
var privSlotsAvail = Net.serverInfo.privateSlots;
var pubCount = 1; // Self
var privCount = 0;
for (cid => cc in Net.clientIdMap) {
if (cc.isPrivate) {
privCount++;
} else {
pubCount++;
}
}
if (!joiningPrivate && pubCount >= pubSlotsAvail) {
queueMessage(Json.stringify({
type: "connectFailed",
success: false,
reason: "The server is full"
}));
return;
}
if (joiningPrivate && privCount >= privSlotsAvail) {
joiningPrivate = false; // Join publicly
}
Net.addClientFromSdp(conts.sdp, joiningPrivate, (sdpReply) -> {
queueMessage(Json.stringify({
success: true,
type: "connectResponse",
sdp: sdpReply,
clientId: conts.clientId
}));
});
}
if (conts.type == "connectResponse") {
Console.log("Remote Description Received!");
var sdpObj = Json.parse(conts.sdp);
if (@:privateAccess Net.client != null)
@:privateAccess Net.client.setRemoteDescription(sdpObj.sdp, sdpObj.type);
}
if (conts.type == "connectFailed") {
// var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content;
// if (loadGui != null) {
// loadGui.setErrorStatus(conts.reason);
// }
}
if (conts.type == "turnserver") {
Net.turnServer = conts.server; // Turn server!
}
}
}

12
src/net/Move.hx Normal file
View file

@ -0,0 +1,12 @@
package net;
import h3d.Vector;
class Move {
public var d:Vector;
public var jump:Bool;
public var powerup:Bool;
public var blast:Bool;
public function new() {}
}

294
src/net/MoveManager.hx Normal file
View file

@ -0,0 +1,294 @@
package net;
import net.BitStream.OutputBitStream;
import net.BitStream.InputBitStream;
import net.NetPacket.MarbleUpdatePacket;
import shapes.PowerUp;
import net.NetPacket.MarbleMovePacket;
import src.TimeState;
import src.Console;
import net.ClientConnection;
import net.Net.NetPacketType;
import src.MarbleWorld;
import net.Move;
import h3d.Vector;
import src.Gamepad;
import src.Settings;
import hxd.Key;
import src.MarbleGame;
import src.Util;
import src.Marble;
@:publicFields
class NetMove {
var motionDir:Vector;
var move:Move;
var id:Int;
var timeState:TimeState;
var serverTicks:Int;
public function new(move:Move, motionDir:Vector, timeState:TimeState, serverTicks:Int, id:Int) {
this.move = move;
this.motionDir = motionDir;
this.id = id;
this.serverTicks = serverTicks;
this.timeState = timeState;
}
}
class MoveManager {
var connection:GameConnection;
var queuedMoves:Array<NetMove>;
var nextMoveId:Int;
var lastMove:NetMove;
var lastAckMoveId:Int = -1;
var ackRTT:Int = -1;
var maxMoves = 45;
var maxSendMoveListSize = 30;
var serverTargetMoveListSize = 3;
var serverMaxMoveListSize = 8;
var serverAvgMoveListSize = 3.0;
var serverSmoothMoveAvg = 0.15;
var serverMoveListSizeSlack = 1.5;
var serverDefaultMinTargetMoveListSize = 3;
var serverAbnormalMoveCount = 0;
var serverLastRecvMove = 0;
var serverLastAckMove = 0;
public var stall = false;
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, serverTicks:Int) {
if (queuedMoves.length >= maxMoves || stall) {
return queuedMoves[queuedMoves.length - 1];
}
var move = new Move();
move.d = new Vector();
if (!MarbleGame.instance.paused) {
move.d.x = Gamepad.getAxis(Settings.gamepadSettings.moveYAxis);
move.d.y = -Gamepad.getAxis(Settings.gamepadSettings.moveXAxis);
// if (@:privateAccess !MarbleGame.instance.world.playGui.isChatFocused()) {
if (Key.isDown(Settings.controlsSettings.forward)) {
move.d.x -= 1;
}
if (Key.isDown(Settings.controlsSettings.backward)) {
move.d.x += 1;
}
if (Key.isDown(Settings.controlsSettings.left)) {
move.d.y += 1;
}
if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1;
}
if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) {
move.jump = true;
}
if ((!Util.isTouchDevice() && Key.isDown(Settings.controlsSettings.powerup))
|| (Util.isTouchDevice() && MarbleGame.instance.touchInput.powerupButton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.powerup)) {
move.powerup = true;
}
if (Key.isDown(Settings.controlsSettings.blast)
|| (MarbleGame.instance.touchInput.blastbutton.pressed)
|| Gamepad.isDown(Settings.gamepadSettings.blast))
move.blast = true;
if (MarbleGame.instance.touchInput.movementInput.pressed) {
move.d.y = -MarbleGame.instance.touchInput.movementInput.value.x;
move.d.x = MarbleGame.instance.touchInput.movementInput.value.y;
}
// }
// quantize moves for client
var qx = Std.int((move.d.x * 16) + 16);
var qy = Std.int((move.d.y * 16) + 16);
move.d.x = (qx - 16) / 16.0;
move.d.y = (qy - 16) / 16.0;
}
var netMove = new NetMove(move, motionDir, timeState.clone(), serverTicks, nextMoveId++);
queuedMoves.push(netMove);
if (nextMoveId >= 65535) // 65535 is reserved for null move
nextMoveId = 0;
var moveStartIdx = queuedMoves.length - maxSendMoveListSize;
if (moveStartIdx < 0)
moveStartIdx = 0;
var b = new OutputBitStream();
var movePacket = new MarbleMovePacket();
movePacket.clientId = Net.clientId;
movePacket.moves = queuedMoves.slice(moveStartIdx);
movePacket.clientTicks = timeState.ticks;
b.writeByte(NetPacketType.MarbleMove);
movePacket.serialize(b);
Net.sendPacketToHostUnreliable(b);
return netMove;
}
function copyMove(to:Int, from:Int) {
queuedMoves[to].move = queuedMoves[from].move;
queuedMoves[to].motionDir.load(queuedMoves[from].motionDir);
}
public inline function duplicateLastMove() {
if (queuedMoves.length == 0)
return;
queuedMoves.insert(0, queuedMoves[0]);
}
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));
b.writeFlag(m.move.jump);
b.writeFlag(m.move.powerup);
b.writeFlag(m.move.blast);
b.writeFloat(m.motionDir.x);
b.writeFloat(m.motionDir.y);
b.writeFloat(m.motionDir.z);
return b;
}
public static inline function unpackMove(b:InputBitStream) {
var moveId = b.readUInt16();
var move = new Move();
move.d = new Vector();
move.d.x = (b.readByte() - 16) / 16.0;
move.d.y = (b.readByte() - 16) / 16.0;
move.jump = b.readFlag();
move.powerup = b.readFlag();
move.blast = b.readFlag();
var motionDir = new Vector();
motionDir.x = b.readFloat();
motionDir.y = b.readFloat();
motionDir.z = b.readFloat();
var netMove = new NetMove(move, motionDir, MarbleGame.instance.world.timeState.clone(), 0, moveId);
return netMove;
}
public inline function queueMove(m:NetMove) {
if (serverLastRecvMove < m.id && serverLastAckMove < m.id) {
queuedMoves.push(m);
serverLastRecvMove = m.id;
}
// if (queuedMoves.length != 0) {
// var lastQueuedMove = queuedMoves[queuedMoves.length - 1];
// if (lastQueuedMove.id < m.id)
// queuedMoves.push(m);
// } else if (lastMove == null || lastMove.id < m.id) {
// queuedMoves.push(m);
// }
}
public function getNextMove() {
if (Net.isHost) {
serverAvgMoveListSize *= (1 - serverSmoothMoveAvg);
serverAvgMoveListSize += serverSmoothMoveAvg * queuedMoves.length;
if (serverAvgMoveListSize < serverTargetMoveListSize - serverMoveListSizeSlack
&& queuedMoves.length < serverTargetMoveListSize
&& queuedMoves.length != 0) {
serverAvgMoveListSize = Math.max(Std.int(serverAvgMoveListSize + serverMoveListSizeSlack + 0.5), queuedMoves.length);
// serverAbnormalMoveCount++;
// if (serverAbnormalMoveCount > 3) {
// serverTargetMoveListSize += 1;
// if (serverTargetMoveListSize > serverMaxMoveListSize)
// serverTargetMoveListSize = serverMaxMoveListSize;
// }
// Send null move
return null;
}
if (queuedMoves.length > serverMaxMoveListSize
|| (serverAvgMoveListSize > serverTargetMoveListSize + serverMoveListSizeSlack
&& queuedMoves.length > serverTargetMoveListSize)) {
// if (queuedMoves.length > serverMaxMoveListSize) {
var dropAmt = queuedMoves.length - serverTargetMoveListSize;
while (dropAmt-- > 0) {
queuedMoves.pop();
}
// }
serverAvgMoveListSize = serverTargetMoveListSize;
// serverAbnormalMoveCount++;
// if (serverAbnormalMoveCount > 3) {
// serverTargetMoveListSize -= 1;
// if (serverTargetMoveListSize < serverDefaultMinTargetMoveListSize)
// serverTargetMoveListSize = serverDefaultMinTargetMoveListSize;
// } else {
// serverAbnormalMoveCount = 0;
// }
}
}
if (queuedMoves.length == 0) {
// if (lastMove != null) {
// lastMove.id++; // So that we force client's move to be overriden by this one
// }
return lastMove;
} else {
lastMove = queuedMoves[0];
queuedMoves.shift();
lastAckMoveId = lastMove.id;
return lastMove;
}
}
public inline function getQueueSize() {
return queuedMoves.length;
}
public function acknowledgeMove(m:NetMove, timeState:TimeState) {
if (m.id == 65535 || m.id == -1) {
return null;
}
if (m.id <= lastAckMoveId)
return null; // Already acked
if (queuedMoves.length == 0)
return null;
if (m.id >= nextMoveId) {
return queuedMoves[0]; // Input lag
}
while (m.id != queuedMoves[0].id) {
queuedMoves.shift();
}
var delta = -1;
var mv = null;
if (m.id == queuedMoves[0].id) {
delta = queuedMoves[0].id - lastAckMoveId;
mv = queuedMoves.shift();
ackRTT = timeState.ticks - mv.timeState.ticks;
// maxMoves = ackRTT + 2;
}
lastAckMoveId = m.id;
return mv;
}
public function getMoveForTick(m:Int) {
if (m <= lastAckMoveId)
return null;
if (m == 65535 || m == -1)
return null;
if (queuedMoves.length == 0)
return null;
for (i in 0...queuedMoves.length) {
if (queuedMoves[i].id == m)
return queuedMoves[i];
if (queuedMoves[i].id > m)
return null;
}
return null;
}
}

836
src/net/Net.hx Normal file
View file

@ -0,0 +1,836 @@
package net;
import net.NetPacket.ScoreboardPacket;
import gui.MPPlayMissionGui;
import gui.Canvas;
import net.MasterServerClient.RemoteServerInfo;
import src.ResourceLoader;
import src.AudioManager;
import net.NetPacket.GemPickupPacket;
import net.NetPacket.GemSpawnPacket;
import net.BitStream.InputBitStream;
import net.BitStream.OutputBitStream;
import net.NetPacket.PowerupPickupPacket;
import net.ClientConnection;
import net.NetPacket.MarbleUpdatePacket;
import net.NetPacket.MarbleMovePacket;
import haxe.Json;
import datachannel.RTCPeerConnection;
import datachannel.RTCDataChannel;
import src.Console;
import net.NetCommands;
import src.MarbleGame;
import src.Settings;
enum abstract NetPacketType(Int) from Int to Int {
var NullPacket;
var ClientIdAssign;
var NetCommand;
var Ping;
var PingBack;
var MarbleUpdate;
var MarbleMove;
var PowerupPickup;
var GemSpawn;
var GemPickup;
var PlayerInfo;
var ScoreBoardInfo;
}
@:publicFields
class ServerInfo {
var name:String;
var players:Int;
var maxPlayers:Int;
var privateSlots:Int;
var privateServer:Bool;
var inviteCode:Int;
var state:String;
var platform:NetPlatform;
public function new(name:String, players:Int, maxPlayers:Int, privateSlots:Int, privateServer:Bool, inviteCode:Int, state:String, platform:NetPlatform) {
this.name = name;
this.players = players;
this.maxPlayers = maxPlayers;
this.privateSlots = privateSlots;
this.privateServer = privateServer;
this.inviteCode = inviteCode;
this.state = state;
this.platform = platform;
}
}
class Net {
static var client:RTCPeerConnection;
static var clientDatachannel:RTCDataChannel;
static var clientDatachannelUnreliable:RTCDataChannel;
public static var isMP:Bool;
public static var isHost:Bool;
public static var isClient:Bool;
public static var lobbyHostReady:Bool;
public static var lobbyClientReady:Bool;
public static var hostReady:Bool;
static var clientIdAllocs:Int = 1;
public static var clientId:Int;
public static var networkRNG:Float;
public static var clients:Map<RTCPeerConnection, GameConnection> = [];
public static var clientIdMap:Map<Int, GameConnection> = [];
public static var clientConnection:ClientConnection;
public static var serverInfo:ServerInfo;
public static var remoteServerInfo:RemoteServerInfo;
static var stunServers = ["stun:stun.l.google.com:19302"];
public static var turnServer:String = "";
public static function hostServer(name:String, maxPlayers:Int, privateSlots:Int, privateServer:Bool, onHosted:() -> Void) {
serverInfo = new ServerInfo(name, 1, maxPlayers, privateSlots, privateServer, Std.int(999999 * Math.random()), "LOBBY", getPlatform());
MasterServerClient.connectToMasterServer(() -> {
isHost = true;
isClient = false;
clientId = 0;
isMP = true;
MasterServerClient.instance.sendServerInfo(serverInfo);
onHosted();
});
}
public static function addClientFromSdp(sdpString:String, privateJoin:Bool, onFinishSdp:String->Void) {
var peer = new RTCPeerConnection(stunServers, "0.0.0.0");
var sdpObj = Json.parse(sdpString);
peer.setRemoteDescription(sdpObj.sdp, sdpObj.type);
addClient(peer, privateJoin, onFinishSdp);
}
static function addClient(peer:RTCPeerConnection, privateJoin:Bool, onFinishSdp:String->Void) {
var candidates = [];
peer.onLocalCandidate = (c) -> {
Console.log('Local candidate: ' + c);
if (c != "")
candidates.push('a=${c}');
}
peer.onStateChange = (s) -> {
switch (s) {
case RTC_CLOSED:
Console.log("RTC State change: Connection closed!");
case RTC_CONNECTED:
Console.log("RTC State change: Connected!");
case RTC_CONNECTING:
Console.log("RTC State change: Connecting...");
case RTC_DISCONNECTED:
Console.log("RTC State change: Disconnected!");
case RTC_FAILED:
Console.log("RTC State change: Failed!");
case RTC_NEW:
Console.log("RTC State change: New...");
}
}
var sdpFinished = false;
var finishSdp = () -> {
if (sdpFinished)
return;
if (peer == null)
return;
sdpFinished = true;
var sdpObj = StringTools.trim(peer.localDescription);
sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n';
onFinishSdp(Json.stringify({
sdp: sdpObj,
type: "answer"
}));
}
peer.onGatheringStateChange = (s) -> {
switch (s) {
case RTC_GATHERING_COMPLETE:
Console.log("Gathering complete!");
case RTC_GATHERING_INPROGRESS:
Console.log("Gathering in progress...");
case RTC_GATHERING_NEW:
Console.log("Gathering new...");
}
if (s == RTC_GATHERING_COMPLETE) {
finishSdp();
}
}
var reliable:datachannel.RTCDataChannel = null;
var unreliable:datachannel.RTCDataChannel = null;
peer.onDataChannel = (dc:datachannel.RTCDataChannel) -> {
if (dc.name == "mp")
reliable = dc;
if (dc.name == "unreliable") {
unreliable = dc;
switch (dc.reliability) {
case Reliable:
Console.log("Error opening unreliable datachannel!");
case Unreliable(maxRetransmits, maxLifetime):
Console.log("Opened unreliable datachannel: " + maxRetransmits + " " + maxLifetime);
}
}
if (reliable != null && unreliable != null)
onClientConnect(peer, reliable, unreliable, privateJoin);
}
}
static function addGhost(id:Int) {
var ghost = new DummyConnection(id);
clientIdMap[id] = ghost;
}
public static function joinServer(serverName:String, isInvite:Bool, connectedCb:() -> Void) {
MasterServerClient.connectToMasterServer(() -> {
client = new RTCPeerConnection(stunServers, "0.0.0.0");
var candidates = [];
var closing = false;
isMP = true;
isHost = false;
isClient = true;
var closeFunc = (msg:String, forceShow:Bool) -> {
if (closing)
return;
closing = true;
var weLeftOurselves = !Net.isClient; // If we left ourselves, this would be set to false due to order of ops, disconnect being called first, and then the datachannel closing
disconnect();
if (MarbleGame.instance.world != null) {
MarbleGame.instance.quitMission();
}
if (!weLeftOurselves || forceShow) {
// if (!(MarbleGame.canvas.content is MultiplayerLoadingGui)) {
// var loadGui = new MultiplayerLoadingGui(msg);
// MarbleGame.canvas.setContent(loadGui);
// loadGui.setErrorStatus(msg);
// } else {
// var loadGui = cast(MarbleGame.canvas.content, MultiplayerLoadingGui);
// loadGui.setErrorStatus(msg);
// }
}
}
client.onLocalCandidate = (c) -> {
Console.log('Local candidate: ' + c);
if (c != "")
candidates.push('a=${c}');
}
client.onStateChange = (s) -> {
switch (s) {
case RTC_CLOSED:
Console.log("RTC State change: Connection closed!");
closeFunc("Connection closed", true);
case RTC_CONNECTED:
Console.log("RTC State change: Connected!");
case RTC_CONNECTING:
Console.log("RTC State change: Connecting...");
case RTC_DISCONNECTED:
Console.log("RTC State change: Disconnected!");
case RTC_FAILED:
Console.log("RTC State change: Failed!");
case RTC_NEW:
Console.log("RTC State change: New...");
}
}
var sdpFinished = false;
var finishSdp = () -> {
if (sdpFinished)
return;
sdpFinished = true;
if (client == null)
return;
Console.log("Local Description Set!");
var sdpObj = StringTools.trim(client.localDescription);
sdpObj = sdpObj + '\r\n' + candidates.join('\r\n') + '\r\n';
MasterServerClient.instance.sendConnectToServer(serverName, Json.stringify({
sdp: sdpObj,
type: "offer"
}), isInvite);
}
client.onGatheringStateChange = (s) -> {
switch (s) {
case RTC_GATHERING_COMPLETE:
Console.log("Gathering complete!");
case RTC_GATHERING_INPROGRESS:
Console.log("Gathering in progress...");
case RTC_GATHERING_NEW:
Console.log("Gathering new...");
}
if (s == RTC_GATHERING_COMPLETE) {
finishSdp();
}
}
// haxe.Timer.delay(() -> {
// finishSdp();
// }, 5000);
clientDatachannel = client.createDatachannel("mp");
clientDatachannelUnreliable = client.createDatachannelWithOptions("unreliable", true, null, 600);
var openFlags = 0;
var onDatachannelOpen = (idx:Int) -> {
if (!Net.isMP) {
// Close
client.close();
return;
}
openFlags |= idx;
if (openFlags == 3) {
// if (MarbleGame.canvas.content is MultiplayerLoadingGui) {
// var loadGui:MultiplayerLoadingGui = cast MarbleGame.canvas.content;
// if (loadGui != null) {
// loadGui.setLoadingStatus("Handshaking");
// }
// }
Console.log("Successfully connected!");
clients.set(client, new ClientConnection(0, client, clientDatachannel, clientDatachannelUnreliable)); // host is always 0
clientIdMap[0] = clients[client];
clientConnection = cast clients[client];
onConnectedToServer();
haxe.Timer.delay(() -> connectedCb(), 1500); // 1.5 second delay to do the RTT calculation
}
}
var onDatachannelMessage = (dc:RTCDataChannel, b:haxe.io.Bytes) -> {
onPacketReceived(clientConnection, client, clientDatachannel, new InputBitStream(b));
}
var onDatachannelClose = (dc:RTCDataChannel) -> {
closeFunc("Disconnected", true);
}
var onDatachannelError = (msg:String) -> {
Console.log('Errored out due to ${msg}');
closeFunc("Connection error", true);
}
clientDatachannel.onOpen = (n) -> {
onDatachannelOpen(1);
}
clientDatachannel.onMessage = (b) -> {
onDatachannelMessage(clientDatachannel, b);
}
clientDatachannel.onClosed = () -> {
onDatachannelClose(clientDatachannel);
}
clientDatachannel.onError = (msg) -> {
onDatachannelError(msg);
}
clientDatachannelUnreliable.onOpen = (n) -> {
onDatachannelOpen(2);
}
clientDatachannelUnreliable.onMessage = (b) -> {
onDatachannelMessage(clientDatachannelUnreliable, b);
}
clientDatachannelUnreliable.onClosed = () -> {
onDatachannelClose(clientDatachannelUnreliable);
}
clientDatachannelUnreliable.onError = (msg) -> {
onDatachannelError(msg);
}
});
}
public static function disconnect() {
if (Net.isClient) {
NetCommands.clientLeave(Net.clientId);
Net.isMP = false;
Net.isClient = false;
Net.isHost = false;
if (Net.client != null)
Net.client.close();
Net.client = null;
Net.clientDatachannel = null;
Net.clientId = 0;
Net.clientIdAllocs = 1;
Net.clients.clear();
Net.clientIdMap.clear();
Net.clientConnection = null;
Net.serverInfo = null;
Net.remoteServerInfo = null;
Net.lobbyHostReady = false;
Net.lobbyClientReady = false;
Net.hostReady = false;
// MultiplayerLevelSelectGui.custSelected = false;
}
if (Net.isHost) {
NetCommands.serverClosed();
for (client => gc in clients) {
client.close();
}
Net.isMP = false;
Net.isClient = false;
Net.isHost = false;
Net.clientId = 0;
Net.clientIdAllocs = 1;
Net.clients.clear();
Net.clientIdMap.clear();
MasterServerClient.disconnectFromMasterServer();
Net.serverInfo = null;
Net.remoteServerInfo = null;
Net.lobbyHostReady = false;
Net.lobbyClientReady = false;
Net.hostReady = false;
// MultiplayerLevelSelectGui.custSelected = false;
}
}
public static function checkPacketTimeout(dt:Float) {
if (!Net.isMP)
return;
static var accum = 0.0;
static var wsAccum = 0.0;
accum += dt;
wsAccum += dt;
if (accum > 1.0) {
accum = 0;
var t = Console.time();
for (dc => cc in clients) {
if (cc is ClientConnection) {
var conn = cast(cc, ClientConnection);
if (conn.needsTimeoutWarn(t)) {
conn.didWarnTimeout = true;
if (Net.isClient) {
NetCommands.requestPing();
}
if (Net.isHost) {
NetCommands.pingClient(cc, t);
}
}
if (conn.needsTimeoutKick(t)) {
if (Net.isHost) {
dc.close();
}
if (Net.isClient) {
disconnect();
if (MarbleGame.instance.world != null) {
MarbleGame.instance.quitMission();
}
// if (!(MarbleGame.canvas.content is MultiplayerLoadingGui)) {
// var loadGui = new MultiplayerLoadingGui("Timed out");
// MarbleGame.canvas.setContent(loadGui);
// loadGui.setErrorStatus("Timed out");
// }
}
}
}
}
}
if (wsAccum >= 15.0) {
wsAccum = 0;
if (Net.isHost) {
if (MasterServerClient.instance != null)
MasterServerClient.instance.sendServerInfo(serverInfo); // Heartbeat
else
MasterServerClient.connectToMasterServer(() -> {
MasterServerClient.instance.sendServerInfo(serverInfo); // Heartbeat
});
}
if (Net.isClient) {
if (MasterServerClient.instance != null)
MasterServerClient.instance.heartBeat();
else
MasterServerClient.connectToMasterServer(() -> {
MasterServerClient.instance.heartBeat();
});
}
}
}
static function onClientConnect(c:RTCPeerConnection, dc:RTCDataChannel, dcu:RTCDataChannel, joiningPrivate:Bool) {
if (!Net.isMP) {
c.close();
return;
}
var clientId = allocateClientId();
if (clientId == -1) {
c.close();
return; // Failed to allocate ID
}
var cc = new ClientConnection(clientId, c, dc, dcu);
clients.set(c, cc);
cc.isPrivate = joiningPrivate;
clientIdMap[clientId] = clients[c];
cc.lastRecvTime = Console.time(); // So it doesnt get timed out
var closing = false;
var onMessage = (dc:RTCDataChannel, msgBytes:haxe.io.Bytes) -> {
onPacketReceived(cc, c, dc, new InputBitStream(msgBytes));
}
var onClosed = () -> {
if (closing)
return;
closing = true;
clients.remove(c);
onClientLeave(cc);
}
var onError = (msg:String) -> {
if (closing)
return;
closing = true;
clients.remove(c);
Console.log('Client ${cc.id} errored out due to: ${msg}');
onClientLeave(cc);
}
dc.onMessage = (msgBytes) -> {
onMessage(dc, msgBytes);
}
dc.onClosed = () -> {
onClosed();
}
dc.onError = (msg) -> {
onError(msg);
}
dcu.onMessage = (msgBytes) -> {
onMessage(dcu, msgBytes);
}
dcu.onClosed = () -> {
onClosed();
}
dcu.onError = (msg) -> {
onError(msg);
}
var b = haxe.io.Bytes.alloc(2);
b.set(0, ClientIdAssign);
b.set(1, clientId);
dc.sendBytes(b);
Console.log("Client has connected!");
// Send the ping packet to calculcate the RTT
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
cast(clients[c], ClientConnection).pingSendTime = Console.time();
dc.sendBytes(b);
Console.log("Sending ping packet!");
// AudioManager.playSound(ResourceLoader.getAudio("data/sound/spawn_alternate.wav").resource);
serverInfo.players = 1;
for (k => v in clients) { // Recount
serverInfo.players++;
}
serverInfo.players++;
MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the new player
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
}
static function onConnectedToServer() {
Console.log("Connected to the server!");
// Send the ping packet to calculate the RTT
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, 3); // Count
cast(clients[client], ClientConnection).pingSendTime = Console.time();
clientDatachannel.sendBytes(b);
Console.log("Sending ping packet!");
}
static function onClientLeave(cc:ClientConnection) {
if (!Net.isMP || cc == null)
return;
NetCommands.clientDisconnected(cc.id);
if (cc.id != 0) {
freeClientId(cc.id);
}
serverInfo.players = 1;
for (k => v in clients) { // Recount
serverInfo.players++;
}
MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the player leave
// AudioManager.playSound(ResourceLoader.getAudio("data/sound/infotutorial.wav").resource);
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
}
static function onClientHandshakeComplete(conn:ClientConnection) {
// Send our current mission to connecting client
// if (MultiplayerLevelSelectGui.custSelected) {
// NetCommands.setLobbyCustLevelNameClient(conn, MultiplayerLevelSelectGui.custPath);
// } else {
NetCommands.setLobbyLevelIndexClient(conn, MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic);
// }
if (serverInfo.state == "PLAYING") { // We initiated the game, directly add in the marble
// if (MultiplayerLevelSelectGui.custSelected) {
// NetCommands.playCustomLevelMidJoinClient(conn, MultiplayerLevelSelectGui.custPath);
// } else
// NetCommands.playLevelMidJoinClient(conn, MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic);
MarbleGame.instance.world.addJoiningClient(conn, () -> {});
var playerInfoBytes = sendPlayerInfosBytes();
for (dc => cc in clients) {
if (cc != conn) {
cc.sendBytes(playerInfoBytes);
NetCommands.addMidGameJoinMarbleClient(cc, conn.id);
}
}
}
if (serverInfo.state == "LOBBY") {
// Connect client to lobby
NetCommands.enterLobbyClient(conn);
}
}
public static function sendPlayerInfosBytes() {
var b = new haxe.io.BytesOutput();
b.writeByte(PlayerInfo);
var cnt = 0;
for (c in clientIdMap)
cnt++;
b.writeByte(cnt + 1); // all + host
for (c => v in clientIdMap) {
b.writeByte(c);
b.writeByte(v.lobbyReady ? 1 : 0);
b.writeByte(v.platform);
b.writeByte(v.marbleId);
var name = v.getName();
b.writeByte(name.length);
for (i in 0...name.length) {
b.writeByte(StringTools.fastCodeAt(name, i));
}
}
// Write host data
b.writeByte(0);
b.writeByte(Net.lobbyHostReady ? 1 : 0);
b.writeByte(getPlatform());
b.writeByte(Settings.optionsSettings.marbleIndex);
var name = Settings.highscoreName;
b.writeByte(name.length);
for (i in 0...name.length) {
b.writeByte(StringTools.fastCodeAt(name, i));
}
return b.getBytes();
}
static function onPacketReceived(conn:ClientConnection, c:RTCPeerConnection, dc:RTCDataChannel, input:InputBitStream) {
if (!Net.isMP)
return; // only for MP
conn.lastRecvTime = Console.time();
conn.didWarnTimeout = false;
var packetType = input.readByte();
switch (packetType) {
case NetCommand:
NetCommands.readPacket(input);
case ClientIdAssign:
clientId = input.readByte(); // 8 bit client id, hopefully we don't exceed this
Console.log('Client ID set to ${clientId}');
NetCommands.setPlayerData(clientId, Settings.highscoreName, Settings.optionsSettings.marbleIndex); // Send our player name to the server
NetCommands.transmitPlatform(clientId, getPlatform()); // send our platform too
case Ping:
var pingLeft = input.readByte();
Console.log("Got ping packet!");
var b = haxe.io.Bytes.alloc(2);
b.set(0, PingBack);
b.set(1, pingLeft);
dc.sendBytes(b);
case PingBack:
var pingLeft = input.readByte();
Console.log("Got pingback packet!");
var now = Console.time();
conn._rttRecords.push((now - conn.pingSendTime));
if (pingLeft > 0) {
conn.pingSendTime = now;
var b = haxe.io.Bytes.alloc(2);
b.set(0, Ping);
b.set(1, pingLeft - 1);
dc.sendBytes(b);
} else {
for (r in conn._rttRecords)
conn.rtt += r;
conn.rtt /= conn._rttRecords.length;
Console.log('Got RTT ${conn.rtt} for client ${conn.id}');
if (Net.isHost) {
var b = sendPlayerInfosBytes();
for (cc in clients) {
cc.sendBytes(b);
}
onClientHandshakeComplete(conn);
}
}
case MarbleUpdate:
var marbleUpdatePacket = new MarbleUpdatePacket();
marbleUpdatePacket.deserialize(input);
var cc = marbleUpdatePacket.clientId;
if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
var m = MarbleGame.instance.world.lastMoves;
m.enqueue(marbleUpdatePacket);
}
case MarbleMove:
if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
var movePacket = new MarbleMovePacket();
movePacket.deserialize(input);
var cc = clientIdMap[movePacket.clientId];
if (cc.state == GAME)
for (move in movePacket.moves)
cc.queueMove(move);
}
case PowerupPickup:
var powerupPickupPacket = new PowerupPickupPacket();
powerupPickupPacket.deserialize(input);
if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
var m = @:privateAccess MarbleGame.instance.world.powerupPredictions;
m.acknowledgePowerupPickup(powerupPickupPacket, MarbleGame.instance.world.timeState, clientConnection.moveManager.getQueueSize());
}
case GemSpawn:
var gemSpawnPacket = new GemSpawnPacket();
gemSpawnPacket.deserialize(input);
if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
MarbleGame.instance.world.spawnHuntGemsClientSide(gemSpawnPacket.gemIds);
@:privateAccess MarbleGame.instance.world.gemPredictions.acknowledgeGemSpawn(gemSpawnPacket);
}
case GemPickup:
var gemPickupPacket = new GemPickupPacket();
gemPickupPacket.deserialize(input);
if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
// @:privateAccess MarbleGame.instance.world.playGui.incrementPlayerScore(gemPickupPacket.clientId, gemPickupPacket.scoreIncr);
@:privateAccess MarbleGame.instance.world.gemPredictions.acknowledgeGemPickup(gemPickupPacket);
}
case PlayerInfo:
var count = input.readByte();
var newP = false;
for (i in 0...count) {
var id = input.readByte();
var cready = input.readByte() == 1;
var platform = input.readByte();
var marble = input.readByte();
if (id != 0 && id != Net.clientId && !clientIdMap.exists(id)) {
Console.log('Adding ghost connection ${id}');
addGhost(id);
newP = true;
}
var nameLength = input.readByte();
var name = "";
for (j in 0...nameLength) {
name += String.fromCharCode(input.readByte());
}
if (clientIdMap.exists(id)) {
clientIdMap[id].setName(name);
clientIdMap[id].setMarbleId(marble);
clientIdMap[id].lobbyReady = cready;
clientIdMap[id].platform = platform;
}
if (Net.clientId == id) {
Net.lobbyClientReady = cready;
}
}
if (newP) {
// AudioManager.playSound(ResourceLoader.getAudio("sounds/spawn_alternate.wav").resource);
}
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
case ScoreBoardInfo:
var scoreboardPacket = new ScoreboardPacket();
scoreboardPacket.deserialize(input);
// if (MarbleGame.instance.world != null && !MarbleGame.instance.world._disposed) {
// @:privateAccess MarbleGame.instance.world.playGui.updatePlayerScores(scoreboardPacket);
// }
case _:
Console.log("unknown command: " + packetType);
}
}
static function allocateClientId() {
for (id in 0...32) {
if (Net.clientIdAllocs & (1 << id) == 0) {
Net.clientIdAllocs |= (1 << id);
return id;
}
}
return -1;
}
static function freeClientId(id:Int) {
Net.clientIdAllocs &= ~(1 << id);
}
public static function sendPacketToAll(packetData:OutputBitStream) {
var bytes = packetData.getBytes();
for (c => v in clients) {
v.sendBytes(bytes);
}
}
public static function sendPacketToIngame(packetData:OutputBitStream) {
var bytes = packetData.getBytes();
for (c => v in clients) {
if (v.state == GAME)
v.sendBytes(bytes);
}
}
public static function sendPacketToHost(packetData:OutputBitStream) {
if (clientDatachannel.state == Open) {
var bytes = packetData.getBytes();
clientDatachannel.sendBytes(bytes);
}
}
public static function sendPacketToHostUnreliable(packetData:OutputBitStream) {
if (clientDatachannelUnreliable.state == Open) {
var bytes = packetData.getBytes();
clientDatachannelUnreliable.sendBytes(bytes);
}
}
public static function sendPacketToClient(client:GameConnection, packetData:OutputBitStream) {
var bytes = packetData.getBytes();
client.sendBytes(bytes);
}
public static function addDummyConnection() {
if (Net.isHost) {
addGhost(Net.clientId++);
}
}
public static inline function getPlatform() {
#if js
return NetPlatform.Web;
#end
#if hl
#if MACOS_BUNDLE
return NetPlatform.MacOS;
#else
#if android
return NetPlatform.Android;
#else
return NetPlatform.PC;
#end
#end
#end
}
}

345
src/net/NetCommands.hx Normal file
View file

@ -0,0 +1,345 @@
package net;
import gui.MPPlayMissionGui;
import net.ClientConnection.NetPlatform;
import gui.EndGameGui;
import modes.HuntMode;
import net.ClientConnection.GameplayState;
import net.Net.NetPacketType;
import src.MarbleGame;
import src.MissionList;
import src.Console;
@:build(net.RPCMacro.build())
class NetCommands {
@:rpc(server) public static function setLobbyLevelIndex(category:String, i:Int) {
if (MPPlayMissionGui.setLevelFn == null) {
MPPlayMissionGui.currentCategoryStatic = category;
MPPlayMissionGui.currentSelectionStatic = i;
} else {
MPPlayMissionGui.setLevelFn(category, i);
}
}
// @:rpc(server) public static function setLobbyCustLevelName(str:String) {
// if (MPPlayMissionGui.setLevelFn != null) {
// MPPlayMissionGui.setLevelStr(str);
// } else {
// MultiplayerLevelSelectGui.custSelected = true;
// MultiplayerLevelSelectGui.custPath = str;
// }
// }
@:rpc(server) public static function playLevel(category:String, levelIndex:Int) {
MPPlayMissionGui.playSelectedLevel(category, levelIndex);
if (Net.isHost) {
Net.serverInfo.state = "WAITING";
MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the wait state
}
}
// @:rpc(server) public static function playCustomLevel(levelPath:String) {
// var levelEntry = MPCustoms.missionList.filter(x -> x.path == levelPath)[0];
// MarbleGame.canvas.setContent(new MultiplayerLoadingGui("Downloading", false));
// MPCustoms.play(levelEntry, () -> {}, () -> {
// MarbleGame.canvas.setContent(new MultiplayerGui());
// Net.disconnect(); // disconnect from the server
// });
// if (Net.isHost) {
// Net.serverInfo.state = "WAITING";
// MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the wait state
// }
// }
@:rpc(server) public static function playLevelMidJoin(index:Int) {
if (Net.isClient) {
var difficultyMissions = MissionList.missionList['ultra']["multiplayer"];
var curMission = difficultyMissions[index];
MarbleGame.instance.playMission(curMission, true);
}
}
// @:rpc(server) public static function playCustomLevelMidJoin(path:String) {
// if (Net.isClient) {
// playCustomLevel(path);
// }
// }
@:rpc(server) public static function enterLobby() {
if (Net.isClient) {
MarbleGame.canvas.setContent(new MPPlayMissionGui(false));
}
}
@:rpc(server) public static function setNetworkRNG(rng:Float) {
Net.networkRNG = rng;
if (MarbleGame.instance.world != null) {
var gameMode = MarbleGame.instance.world.gameMode;
if (gameMode is modes.HuntMode) {
var hunt:modes.HuntMode = cast gameMode;
@:privateAccess hunt.rng.setSeed(cast rng);
@:privateAccess hunt.rng2.setSeed(cast rng);
}
}
}
@:rpc(client) public static function toggleReadiness(clientId:Int) {
if (Net.isHost) {
if (clientId == 0)
Net.lobbyHostReady = !Net.lobbyHostReady;
else
Net.clientIdMap[clientId].toggleLobbyReady();
var allReady = true;
for (id => client in Net.clientIdMap) {
if (!client.lobbyReady) {
allReady = false;
break;
}
}
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
var b = Net.sendPlayerInfosBytes();
for (cc in Net.clients) {
cc.sendBytes(b);
}
if (allReady && Net.lobbyHostReady) {
// if (MultiplayerLevelSelectGui.custSelected) {
// NetCommands.playCustomLevel(MultiplayerLevelSelectGui.custPath);
// } else
NetCommands.playLevel(MPPlayMissionGui.currentCategoryStatic, MPPlayMissionGui.currentSelectionStatic);
}
}
}
@:rpc(client) public static function clientIsReady(clientId:Int) {
if (Net.isHost) {
if (Net.serverInfo.state == "WAITING") {
Console.log('Client ${clientId} is ready!');
if (clientId != -1)
Net.clientIdMap[clientId].ready();
else
Net.hostReady = true;
var allReady = true;
for (id => client in Net.clientIdMap) {
if (client.state != GameplayState.GAME) {
allReady = false;
break;
}
}
if (allReady && Net.hostReady) {
if (MarbleGame.instance.world != null) {
Console.log('All are ready, starting');
MarbleGame.instance.world.allClientsReady();
}
Net.serverInfo.state = "PLAYING";
MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state
}
} else {
// Mid game join
Console.log("Mid game join for client " + clientId);
// Send em our present world state
if (MarbleGame.instance.world != null) {
var packets = MarbleGame.instance.world.getWorldStateForClientJoin();
var c = Net.clientIdMap[clientId];
for (packet in packets) {
c.sendBytes(packet);
}
Net.clientIdMap[clientId].ready();
if (MarbleGame.instance.world.serverStartTicks == 0) {
var allReady = true;
for (id => client in Net.clientIdMap) {
if (client.state != GameplayState.GAME) {
allReady = false;
break;
}
}
if (allReady) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.allClientsReady();
}
}
} else {
// Send the start ticks
NetCommands.setStartTicksMidJoinClient(c, MarbleGame.instance.world.serverStartTicks, MarbleGame.instance.world.timeState.ticks);
}
}
}
}
}
@:rpc(server) public static function addMidGameJoinMarble(cc:Int) {
if (Net.isClient) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.addJoiningClientGhost(Net.clientIdMap[cc], () -> {});
}
}
}
@:rpc(server) public static function setStartTicks(ticks:Int) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.serverStartTicks = ticks + 1; // Extra tick so we don't get 0
if (Net.isClient) {
@:privateAccess MarbleGame.instance.world.marble.serverTicks = ticks;
}
MarbleGame.instance.world.startTime = MarbleGame.instance.world.timeState.timeSinceLoad + 3.5 + 0.032; // 1 extra tick
}
}
@:rpc(server) public static function setStartTicksMidJoin(startTicks:Int, currentTicks:Int) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.serverStartTicks = startTicks + 1; // Extra tick so we don't get 0
MarbleGame.instance.world.startTime = MarbleGame.instance.world.timeState.timeSinceLoad + 0.032; // 1 extra tick
MarbleGame.instance.world.timeState.ticks = currentTicks;
}
}
@:rpc(server) public static function timerRanOut() {
if (Net.isClient && MarbleGame.instance.world != null) {
if (MarbleGame.instance.paused) {
MarbleGame.instance.handlePauseGame(); // Unpause
}
var huntMode:HuntMode = cast MarbleGame.instance.world.gameMode;
huntMode.onTimeExpire();
}
if (Net.isHost) {
Net.serverInfo.state = "WAITING";
MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state
}
}
@:rpc(server) public static function clientDisconnected(clientId:Int) {
var conn = Net.clientIdMap.get(clientId);
if (MarbleGame.instance.world != null) {
MarbleGame.instance.world.removePlayer(conn);
var allReady = true;
for (id => client in Net.clientIdMap) {
if (client.state != GameplayState.GAME && client != conn) {
allReady = false;
break;
}
}
if (allReady && MarbleGame.instance.world.serverStartTicks == 0) {
MarbleGame.instance.world.allClientsReady();
}
}
Net.clientIdMap.remove(clientId);
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
}
@:rpc(server) public static function clientJoin(clientId:Int) {}
@:rpc(client) public static function clientLeave(clientId:Int) {
if (Net.isHost) {
@:privateAccess Net.onClientLeave(cast Net.clientIdMap[clientId]);
}
}
@:rpc(server) public static function serverClosed() {
if (Net.isClient) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.quitMission();
}
// var loadGui = new MultiplayerLoadingGui("Server closed");
// MarbleGame.canvas.setContent(loadGui);
// loadGui.setErrorStatus("Server closed");
}
}
@:rpc(client) public static function setPlayerData(clientId:Int, name:String, marble:Int) {
if (Net.isHost) {
Net.clientIdMap[clientId].setName(name);
Net.clientIdMap[clientId].setMarbleId(marble);
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
}
}
@:rpc(client) public static function transmitPlatform(clientId:Int, platform:Int) {
if (Net.isHost) {
Net.clientIdMap[clientId].platform = platform;
if (MarbleGame.canvas.content is MPPlayMissionGui) {
cast(MarbleGame.canvas.content, MPPlayMissionGui).updateLobbyNames();
}
}
}
@:rpc(server) public static function endGame() {
for (c => v in Net.clientIdMap) {
v.state = LOBBY;
v.lobbyReady = false;
}
if (Net.isClient) {
if (MarbleGame.instance.world != null) {
MarbleGame.instance.quitMission();
}
}
if (Net.isHost) {
Net.lobbyHostReady = false;
Net.hostReady = false;
Net.serverInfo.state = "LOBBY";
MasterServerClient.instance.sendServerInfo(Net.serverInfo); // notify the server of the playing state
var b = Net.sendPlayerInfosBytes();
for (cc in Net.clients) {
cc.sendBytes(b);
}
}
}
@:rpc(server) public static function restartGame() {
var world = MarbleGame.instance.world;
if (Net.isHost) {
world.startTime = 1e8;
haxe.Timer.delay(() -> world.allClientsReady(), 1500);
}
if (Net.isClient) {
var gui = MarbleGame.canvas.children[MarbleGame.canvas.children.length - 1];
if (gui is EndGameGui) {
var egg = cast(gui, EndGameGui);
// egg.retryFunc(null);
world.restartMultiplayerState();
}
}
world.multiplayerStarted = false;
}
@:rpc(server) public static function ping(sendTime:Float) {
if (Net.isClient) {
pingBack(Console.time() - sendTime);
}
}
@:rpc(client) public static function pingBack(ping:Float) {
// Do nothing???
}
@:rpc(client) public static function requestPing() {
if (Net.isHost) {
ping(Console.time());
}
}
// @:rpc(client) public static function sendChatMessage(msg:String) {
// if (Net.isHost) {
// sendServerChatMessage(msg);
// }
// }
// @:rpc(server) public static function sendServerChatMessage(msg:String) {
// if (MarbleGame.instance.world != null) {
// if (MarbleGame.instance.world._ready) {
// @:privateAccess MarbleGame.instance.world.playGui.addChatMessage(msg);
// }
// } else {
// if (MarbleGame.canvas.content is MultiplayerLevelSelectGui) {
// cast(MarbleGame.canvas.content, MultiplayerLevelSelectGui).addChatMessage(msg);
// }
// }
// }
}

258
src/net/NetPacket.hx Normal file
View file

@ -0,0 +1,258 @@
package net;
import net.BitStream.InputBitStream;
import net.BitStream.OutputBitStream;
import h3d.Vector;
import net.MoveManager.NetMove;
interface NetPacket {
public function serialize(b:OutputBitStream):Void;
public function deserialize(b:InputBitStream):Void;
}
@:publicFields
class MarbleMovePacket implements NetPacket {
var clientId:Int;
var clientTicks:Int;
var moves:Array<NetMove>;
public function new() {
moves = [];
}
public inline function deserialize(b:InputBitStream) {
clientId = b.readByte();
clientTicks = b.readUInt16();
var count = b.readInt(5);
moves = [];
for (i in 0...count) {
moves.push(MoveManager.unpackMove(b));
}
}
public inline function serialize(b:OutputBitStream) {
b.writeByte(clientId);
b.writeUInt16(clientTicks);
b.writeInt(moves.length, 5);
for (move in moves)
MoveManager.packMove(move, b);
}
}
enum abstract MarbleNetFlags(Int) from Int to Int {
var NullFlag = 0;
var DoBlast = 1 << 0;
var DoHelicopter = 1 << 1;
var DoMega = 1 << 2;
var PickupPowerup = 1 << 3;
var GravityChange = 1 << 4;
var UsePowerup = 1 << 5;
}
@:publicFields
class MarbleUpdatePacket implements NetPacket {
var clientId:Int;
var move:NetMove;
var serverTicks:Int;
var calculationTicks:Int = -1;
var position:Vector;
var velocity:Vector;
var omega:Vector;
var blastAmount:Int;
var blastTick:Int;
var megaTick:Int;
var heliTick:Int;
var gravityDirection:Vector;
var oob:Bool;
var powerUpId:Int;
var moveQueueSize:Int;
var netFlags:Int;
public function new() {}
public inline function serialize(b:OutputBitStream) {
b.writeByte(clientId);
MoveManager.packMove(move, b);
b.writeUInt16(serverTicks);
b.writeByte(moveQueueSize);
b.writeFloat(position.x);
b.writeFloat(position.y);
b.writeFloat(position.z);
b.writeFloat(velocity.x);
b.writeFloat(velocity.y);
b.writeFloat(velocity.z);
b.writeFloat(omega.x);
b.writeFloat(omega.y);
b.writeFloat(omega.z);
b.writeInt(blastAmount, 11);
if (netFlags & MarbleNetFlags.DoBlast > 0) {
b.writeFlag(true);
b.writeUInt16(blastTick);
} else {
b.writeFlag(false);
}
if (netFlags & MarbleNetFlags.DoHelicopter > 0) {
b.writeFlag(true);
b.writeUInt16(heliTick);
} else {
b.writeFlag(false);
}
if (netFlags & MarbleNetFlags.DoMega > 0) {
b.writeFlag(true);
b.writeUInt16(megaTick);
} else {
b.writeFlag(false);
}
b.writeFlag(oob);
if (netFlags & MarbleNetFlags.UsePowerup > 0) {
b.writeFlag(true);
} else {
b.writeFlag(false);
}
if (netFlags & MarbleNetFlags.PickupPowerup > 0) {
b.writeFlag(true);
b.writeInt(powerUpId, 9);
} else {
b.writeFlag(false);
}
if (netFlags & MarbleNetFlags.GravityChange > 0) {
b.writeFlag(true);
b.writeFloat(gravityDirection.x);
b.writeFloat(gravityDirection.y);
b.writeFloat(gravityDirection.z);
} else {
b.writeFlag(false);
}
}
public inline function deserialize(b:InputBitStream) {
clientId = b.readByte();
move = MoveManager.unpackMove(b);
serverTicks = b.readUInt16();
moveQueueSize = b.readByte();
position = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
velocity = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
omega = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
blastAmount = b.readInt(11);
this.netFlags = 0;
if (b.readFlag()) {
blastTick = b.readUInt16();
this.netFlags |= MarbleNetFlags.DoBlast;
}
if (b.readFlag()) {
heliTick = b.readUInt16();
this.netFlags |= MarbleNetFlags.DoHelicopter;
}
if (b.readFlag()) {
megaTick = b.readUInt16();
this.netFlags |= MarbleNetFlags.DoMega;
}
oob = b.readFlag();
if (b.readFlag())
this.netFlags |= MarbleNetFlags.UsePowerup;
if (b.readFlag()) {
powerUpId = b.readInt(9);
this.netFlags |= MarbleNetFlags.PickupPowerup;
}
if (b.readFlag()) {
gravityDirection = new Vector(b.readFloat(), b.readFloat(), b.readFloat());
this.netFlags |= MarbleNetFlags.GravityChange;
}
}
}
@: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.readByte();
serverTicks = b.readUInt16();
powerupItemId = b.readInt(10);
}
public inline function serialize(b:OutputBitStream) {
b.writeByte(clientId);
b.writeUInt16(serverTicks);
b.writeInt(powerupItemId, 10);
}
}
@:publicFields
class GemSpawnPacket implements NetPacket {
var gemIds:Array<Int>;
public function new() {
gemIds = [];
}
public function serialize(b:OutputBitStream) {
b.writeInt(gemIds.length, 5);
for (gemId in gemIds) {
b.writeInt(gemId, 11);
}
}
public function deserialize(b:InputBitStream) {
var count = b.readInt(5);
for (i in 0...count) {
gemIds.push(b.readInt(11));
}
}
}
@:publicFields
class GemPickupPacket implements NetPacket {
var clientId:Int;
var serverTicks:Int;
var gemId:Int;
var scoreIncr:Int;
public function new() {}
public inline function deserialize(b:InputBitStream) {
clientId = b.readByte();
serverTicks = b.readUInt16();
gemId = b.readInt(11);
scoreIncr = b.readInt(4);
}
public inline function serialize(b:OutputBitStream) {
b.writeByte(clientId);
b.writeUInt16(serverTicks);
b.writeInt(gemId, 11);
b.writeInt(scoreIncr, 4);
}
}
@:publicFields
class ScoreboardPacket implements NetPacket {
var scoreBoard:Map<Int, Int>;
public function new() {
scoreBoard = new Map();
}
public inline function deserialize(b:InputBitStream) {
var count = b.readInt(4);
for (i in 0...count) {
scoreBoard[b.readInt(6)] = b.readInt(10);
}
}
public inline function serialize(b:OutputBitStream) {
var keycount = 0;
for (k => v in scoreBoard)
keycount++;
b.writeInt(keycount, 4);
for (key => v in scoreBoard) {
b.writeInt(key, 6);
b.writeInt(v, 10);
}
}
}

View file

@ -0,0 +1,24 @@
package net;
import src.TimeState;
import net.NetPacket.PowerupPickupPacket;
class PowerupPredictionStore {
var predictions:Array<Float>;
public inline function new() {
predictions = [];
}
public inline function alloc() {
predictions.push(Math.NEGATIVE_INFINITY);
}
public inline function getState(netIndex:Int) {
return predictions[netIndex];
}
public inline function acknowledgePowerupPickup(packet:PowerupPickupPacket, timeState:TimeState, futureTicks:Int) {
predictions[packet.powerupItemId] = timeState.currentAttemptTime - futureTicks * 0.032; // Approximate
}
}

175
src/net/RPCMacro.hx Normal file
View file

@ -0,0 +1,175 @@
package net;
import haxe.macro.Context;
import haxe.macro.Expr;
class RPCMacro {
macro static public function build():Array<Field> {
var fields = Context.getBuildFields();
var rpcFnId = 1;
var idtoFn:Map<Int, {
name:String,
serialize:Array<Expr>,
deserialize:Array<Expr>
}> = new Map();
var fieldsToAdd = [];
for (field in fields) {
if (field.meta.length > 0 && field.meta[0].name == ':rpc') {
switch (field.kind) {
case FFun(f):
{
var serializeFns = [];
var deserializeFns = [];
var callExprs = [];
for (arg in f.args) {
var argName = arg.name;
switch (arg.type) {
case TPath({
name: 'Int'
}): {
deserializeFns.push(macro var $argName = stream.readInt32());
callExprs.push(macro $i{argName});
serializeFns.push(macro stream.writeInt32($i{argName}));
}
case TPath({
name: 'Float'
}): {
deserializeFns.push(macro var $argName = stream.readFloat());
callExprs.push(macro $i{argName});
serializeFns.push(macro stream.writeFloat($i{argName}));
}
case TPath({
name: 'String'
}): {
deserializeFns.push(macro var $argName = stream.readString());
callExprs.push(macro $i{argName});
serializeFns.push(macro stream.writeString($i{argName}));
}
case _: {}
}
}
deserializeFns.push(macro {
$i{field.name}($a{callExprs});
});
idtoFn.set(rpcFnId, {
name: field.name,
serialize: serializeFns,
deserialize: deserializeFns
});
var directionParam = field.meta[0].params[0].expr;
switch (directionParam) {
case EConst(CIdent("server")):
var lastExpr = macro {
if (Net.isHost) {
var stream = new net.BitStream.OutputBitStream();
stream.writeByte(NetPacketType.NetCommand);
stream.writeByte($v{rpcFnId});
$b{serializeFns};
Net.sendPacketToAll(stream);
}
};
var origExpr = f.expr;
var lastExprSingle = macro {
if (Net.isHost) {
var stream = new net.BitStream.OutputBitStream();
stream.writeByte(NetPacketType.NetCommand);
stream.writeByte($v{rpcFnId});
$b{serializeFns};
Net.sendPacketToClient(client, stream);
}
};
var singleClientfn:Field = {
name: field.name + "Client",
pos: Context.currentPos(),
access: [APublic, AStatic],
kind: FFun({
args: [
{
name: "client",
type: haxe.macro.TypeTools.toComplexType(Context.getType('net.ClientConnection.GameConnection'))
}
].concat(f.args),
expr: macro $b{[origExpr, lastExprSingle]}
})
};
fieldsToAdd.push(singleClientfn);
f.expr = macro $b{[f.expr, lastExpr]};
case EConst(CIdent("client")):
var lastExpr = macro {
if (!Net.isHost) {
var stream = new net.BitStream.OutputBitStream();
stream.writeByte(NetPacketType.NetCommand);
stream.writeByte($v{rpcFnId});
$b{serializeFns};
Net.sendPacketToHost(stream);
}
};
f.expr = macro $b{[f.expr, lastExpr]};
case _:
{}
}
rpcFnId++;
}
case _:
{}
}
}
}
var cases:Array<Case> = [];
for (k => v in idtoFn) {
cases.push({
values: [macro $v{k}],
expr: macro {
$b{v.deserialize}
}
});
}
var deserializeField:Field = {
name: "readPacket",
pos: Context.currentPos(),
access: [APublic, AStatic],
kind: FFun({
args: [
{
name: "stream",
type: haxe.macro.TypeTools.toComplexType(Context.getType('net.BitStream.InputBitStream'))
}
],
expr: macro {
var fnId = stream.readByte();
$e{
{
expr: ESwitch(macro fnId, cases, null),
pos: Context.currentPos()
}
}
}
})
};
fields.push(deserializeField);
for (fn in fieldsToAdd) {
fields.push(fn);
}
return fields;
}
}

View file

@ -38,7 +38,7 @@ class RewindManager {
public function recordFrame() {
var rf = new RewindFrame();
rf.timeState = level.timeState.clone();
rf.marblePosition = level.marble.getAbsPos().getPosition().clone();
rf.marblePosition = level.marble.collider.transform.getPosition().clone();
rf.marbleOrientation = level.marble.getRotationQuat().clone();
rf.marbleVelocity = level.marble.velocity.clone();
rf.marbleAngularVelocity = level.marble.omega.clone();
@ -98,7 +98,7 @@ class RewindManager {
rf.powerupStates.push(ab.lastContactTime);
}
}
rf.blastAmt = level.blastAmount;
rf.blastAmt = level.marble.blastAmount;
rf.oobState = {
oob: level.marble.outOfBounds,
timeState: level.marble.outOfBoundsTime != null ? level.marble.outOfBoundsTime.clone() : null
@ -121,7 +121,7 @@ class RewindManager {
public function applyFrame(rf:RewindFrame) {
level.timeState = rf.timeState.clone();
level.marble.setPosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z);
level.marble.setMarblePosition(rf.marblePosition.x, rf.marblePosition.y, rf.marblePosition.z);
level.marble.setRotationQuat(rf.marbleOrientation.clone());
level.marble.velocity.set(rf.marbleVelocity.x, rf.marbleVelocity.y, rf.marbleVelocity.z);
level.marble.omega.set(rf.marbleAngularVelocity.x, rf.marbleAngularVelocity.y, rf.marbleAngularVelocity.z);
@ -219,7 +219,7 @@ class RewindManager {
level.marble.outOfBounds = rf.oobState.oob;
level.marble.camera.oob = rf.oobState.oob;
level.marble.outOfBoundsTime = rf.oobState.timeState != null ? rf.oobState.timeState.clone() : null;
level.blastAmount = rf.blastAmt;
level.marble.blastAmount = rf.blastAmt;
@:privateAccess level.checkpointUp = rf.checkpointState.checkpointUp;
@:privateAccess level.checkpointCollectedGems = rf.checkpointState.checkpointCollectedGems;
@:privateAccess level.cheeckpointBlast = rf.checkpointState.checkpointBlast;

View file

@ -6,6 +6,7 @@ import src.TimeState;
import h3d.Vector;
import src.DtsObject;
import src.MarbleWorld;
import net.NetPacket.MarbleNetFlags;
class AntiGravity extends PowerUp {
public function new(element:MissionElementItem, norespawn:Bool = false) {
@ -33,7 +34,7 @@ class AntiGravity extends PowerUp {
if (marble == level.marble)
this.level.setUp(marble, direction, timeState);
else {
// @:privateAccess marble.netFlags |= MarbleNetFlags.GravityChange;
@:privateAccess marble.netFlags |= MarbleNetFlags.GravityChange;
marble.currentUp.load(direction);
}
}

View file

@ -31,6 +31,7 @@ class Blast extends PowerUp {
}
public function use(marble:src.Marble, timeState:TimeState) {
this.level.blastAmount = 1.03;
marble.blastAmount = 1.03;
marble.blastTicks = 36000 >> 5; // Fix me
}
}

View file

@ -128,10 +128,12 @@ class Nuke extends DtsObject {
this.setCollisionEnabled(false);
// if (!this.level.rewinding)
AudioManager.playSound(ResourceLoader.getResource("data/sound/nukeexplode.wav", ResourceLoader.getAudio, this.soundResources));
this.level.particleManager.createEmitter(nukeParticle, nukeParticleData, this.getAbsPos().getPosition());
this.level.particleManager.createEmitter(nukeSmokeParticle, nukeSmokeParticleData, this.getAbsPos().getPosition());
this.level.particleManager.createEmitter(nukeSparksParticle, nukeSparkParticleData, this.getAbsPos().getPosition());
if (@:privateAccess !marble.isNetUpdate) {
AudioManager.playSound(ResourceLoader.getResource("data/sound/nukeexplode.wav", ResourceLoader.getAudio, this.soundResources));
this.level.particleManager.createEmitter(nukeParticle, nukeParticleData, this.getAbsPos().getPosition());
this.level.particleManager.createEmitter(nukeSmokeParticle, nukeSmokeParticleData, this.getAbsPos().getPosition());
this.level.particleManager.createEmitter(nukeSparksParticle, nukeSparkParticleData, this.getAbsPos().getPosition());
}
var minePos = this.getAbsPos().getPosition();
var dtsCenter = this.dts.bounds.center();

View file

@ -8,6 +8,9 @@ import src.Util;
import h3d.Vector;
import src.DtsObject;
import src.Marble;
import net.Net;
import net.BitStream.OutputBitStream;
import net.NetPacket.PowerupPickupPacket;
abstract class PowerUp extends DtsObject {
public var lastPickUpTime:Float = -1;
@ -40,6 +43,23 @@ 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.sendPacketToIngame(b);
pickupClient = pickupPacket.clientId;
pickupTicks = pickupPacket.serverTicks;
}
if (level.isMultiplayer && Net.isClient) {
pickupClient = @:privateAccess marble.connection != null ? @:privateAccess marble.connection.id : Net.clientId;
}
this.lastPickUpTime = timeState.currentAttemptTime;
if (this.autoUse)
this.use(marble, timeState);