- try to make it case insensitive

- escape all text derived from user input
- fix various race condition issues
- make scrolling smooth for touch controls
- fix leaderboards count
- fix potential crash when joining MP
- clamp input
- fix replay clock when stopped time
- also update ci to only run when tagged
This commit is contained in:
RandomityGuy 2026-01-27 22:15:10 +00:00
parent 51c456e907
commit 2ef28aae5f
14 changed files with 152 additions and 83 deletions

View file

@ -462,10 +462,14 @@ workflows:
filters:
tags:
only: /^\d+.\d+.\d+$/
branches:
ignore: /.*/
build-windows:
jobs:
- build-win:
filters:
tags:
only: /^\d+.\d+.\d+$/
only: /^\d+.\d+.\d+$/
branches:
ignore: /.*/

View file

@ -49,7 +49,7 @@ class Leaderboards {
public static function getScores(mission:String, kind:LeaderboardsKind, cb:Array<LBScore>->Void) {
if (!StringTools.startsWith(mission, "data/"))
mission = "data/" + mission;
return Http.get('${host}/api/scores?mission=${StringTools.urlEncode(mission)}&game=${game}&view=${kind}&count=10', (b) -> {
return Http.get('${host}/api/scores?mission=${StringTools.urlEncode(mission)}&game=${game}&view=${kind}&count=5', (b) -> {
var s = b.toString();
var scores:Array<LBScore> = Json.parse(s).scores;
cb(scores);

View file

@ -394,6 +394,7 @@ class Marble extends GameObject {
this.netSmoothOffset = new Vector();
this.netCorrected = false;
this.currentUp = new Vector(0, 0, 1);
this.lastContactNormal = new Vector(0, 0, 1);
var marbleDts = new DtsObject();
var marbleShader = "";
@ -817,7 +818,7 @@ class Marble extends GameObject {
function computeMoveForces(m:Move, aControl:Vector, desiredOmega:Vector) {
var currentGravityDir = this.currentUp.multiply(-1);
var R = currentGravityDir.multiply(-this._radius);
var R = this.currentUp.multiply(this._radius);
var rollVelocity = this.omega.cross(R);
var axes = this.getMarbleAxis();
// if (!level.isReplayingMovement)
@ -850,15 +851,15 @@ class Marble extends GameObject {
}
var rsq = R.lengthSq();
var crossP = R.cross(motionDir.multiply(desiredYVelocity).add(sideDir.multiply(desiredXVelocity))).multiply(1 / rsq);
desiredOmega.set(crossP.x, crossP.y, crossP.z);
aControl.set(desiredOmega.x - this.omega.x, desiredOmega.y - this.omega.y, desiredOmega.z - this.omega.z);
desiredOmega.load(crossP);
aControl.load(desiredOmega.sub(this.omega));
var aScalar = aControl.length();
if (aScalar > this._angularAcceleration) {
aControl.scale(this._angularAcceleration / aScalar);
}
return false;
}
return return true;
return true;
}
function velocityCancel(timeState:TimeState, surfaceSlide:Bool, noBounce:Bool, stoppedPaths:Bool, pi:Array<PathedInterior>) {
@ -873,7 +874,7 @@ class Marble extends GameObject {
var sVel = this.velocity.sub(contacts[i].velocity);
var surfaceDot = contacts[i].normal.dot(sVel);
if ((!looped && surfaceDot < 0) || surfaceDot < -SurfaceDotThreshold) {
if ((!looped && surfaceDot < 0.0) || surfaceDot < -SurfaceDotThreshold) {
var velLen = this.velocity.length();
var surfaceVel = this.contacts[i].normal.multiply(surfaceDot);
@ -908,7 +909,7 @@ class Marble extends GameObject {
}
contacts[i].velocity.load(otherMarble.velocity);
} else {
if (contacts[i].velocity.length() == 0 && !surfaceSlide && surfaceDot > -this._maxDotSlide * velLen) {
if (contacts[i].velocity.length() == 0.0 && !surfaceSlide && surfaceDot > -this._maxDotSlide * velLen) {
this.velocity.load(this.velocity.sub(surfaceVel));
this.velocity.normalize();
this.velocity.load(this.velocity.multiply(velLen));
@ -934,7 +935,7 @@ class Marble extends GameObject {
vAtC.load(vAtC.sub(contacts[i].normal.multiply(contacts[i].normal.dot(sVel))));
var vAtCMag = vAtC.length();
if (vAtCMag != 0) {
if (vAtCMag != 0.0) {
var friction = this._bounceKineticFriction * contacts[i].friction;
var angVMagnitude = friction * 5 * normalVel / (2 * this._radius);
@ -970,7 +971,7 @@ class Marble extends GameObject {
}
}
} while (!done && itersIn < 1e4); // Maximum limit pls
if (this.velocity.lengthSq() < 625) {
if (this.velocity.lengthSq() < 625.0) {
var gotOne = false;
var dir = new Vector(0, 0, 0);
for (j in 0...contacts.length) {
@ -994,10 +995,10 @@ class Marble extends GameObject {
soFar += (dist - outVel * timeToSeparate) / timeToSeparate / contacts[k].normal.dot(dir);
}
}
if (soFar < -25)
soFar = -25;
if (soFar > 25)
soFar = 25;
if (soFar < -25.0)
soFar = -25.0;
if (soFar > 25.0)
soFar = 25.0;
this.velocity.load(this.velocity.add(dir.multiply(soFar)));
}
}
@ -1066,7 +1067,7 @@ class Marble extends GameObject {
slipping = false;
}
var vAtCDir = vAtC.multiply(1 / vAtCMag);
aFriction.load(bestContact.normal.multiply(-1).cross(vAtCDir.multiply(-1)).multiply(angAMagnitude));
aFriction.load(bestContact.normal.cross(vAtCDir).multiply(angAMagnitude));
AFriction.load(vAtCDir.multiply(-AMagnitude));
this._slipAmount = vAtCMag - totalDeltaV;
}
@ -1093,16 +1094,16 @@ class Marble extends GameObject {
friction2 = this._kineticFriction * bestContact.friction;
Aadd.load(Aadd.multiply(friction2 * bestNormalForce / aAtCMag));
}
A.set(A.x + Aadd.x, A.y + Aadd.y, A.z + Aadd.z);
a.set(a.x + aadd.x, a.y + aadd.y, a.z + aadd.z);
A.load(A.add(Aadd));
a.load(a.add(aadd));
}
A.set(A.x + AFriction.x, A.y + AFriction.y, A.z + AFriction.z);
a.set(a.x + aFriction.x, a.y + aFriction.y, a.z + aFriction.z);
A.load(A.add(AFriction));
a.load(a.add(aFriction));
lastContactNormal = bestContact.normal;
lastContactPosition = this.getAbsPos().getPosition();
}
a.set(a.x + aControl.x, a.y + aControl.y, a.z + aControl.z);
a.load(a.add(aControl));
if (this.mode == Finish) {
a.set(); // Zero it out
}
@ -1371,7 +1372,7 @@ class Marble extends GameObject {
var surfaceNormal = new Vector(verts.nx, verts.ny,
verts.nz); // surface.normals[surface.indices[i]].transformed3x3(obj.transform).normalized();
if (obj is DtsObject)
surfaceNormal.multiply(-1);
surfaceNormal.load(v.sub(v0).cross(v2.sub(v0)).normalized().multiply(-1));
var surfaceD = -surfaceNormal.dot(v0);
// If we're going the wrong direction or not going to touch the plane, ignore...
@ -2390,6 +2391,8 @@ class Marble extends GameObject {
if (Key.isDown(Settings.controlsSettings.right)) {
move.d.y -= 1;
}
move.d.x = Util.clamp(move.d.x, -1, 1);
move.d.y = Util.clamp(move.d.y, -1, 1);
if (Key.isDown(Settings.controlsSettings.jump)
|| MarbleGame.instance.touchInput.jumpButton.pressed
|| Gamepad.isDown(Settings.gamepadSettings.jump)) {

View file

@ -2524,7 +2524,7 @@ class MarbleWorld extends Scheduler {
} else {
nextLevelCode();
}
}, mission, finishTime);
}, mission, finishTime, this.replay.write());
MarbleGame.canvas.pushDialog(egg);
this.setCursorLock(false);
return 0;

View file

@ -52,6 +52,7 @@ class ReplayFrame {
var t = (time - this.time) / (next.time - this.time);
var dt = time - this.time;
var clockDt = next.clockTime - this.clockTime;
var interpFrame = new ReplayFrame();
@ -59,18 +60,20 @@ class ReplayFrame {
interpFrame.time = time;
interpFrame.bonusTime = this.bonusTime;
interpFrame.clockTime = this.clockTime;
if (interpFrame.bonusTime != 0 && time >= 3.5) {
if (dt <= this.bonusTime) {
interpFrame.bonusTime -= dt;
if (clockDt > 0) {
if (interpFrame.bonusTime != 0 && time >= 3.5) {
if (dt <= this.bonusTime) {
interpFrame.bonusTime -= dt;
} else {
interpFrame.clockTime += dt - this.bonusTime;
interpFrame.bonusTime = 0;
}
} else {
interpFrame.clockTime += dt - this.bonusTime;
interpFrame.bonusTime = 0;
}
} else {
if (this.time >= 3.5)
interpFrame.clockTime += dt;
else if (this.time + dt >= 3.5) {
interpFrame.clockTime += (this.time + dt) - 3.5;
if (this.time >= 3.5)
interpFrame.clockTime += dt;
else if (this.time + dt >= 3.5) {
interpFrame.clockTime += (this.time + dt) - 3.5;
}
}
}

View file

@ -130,14 +130,27 @@ class ManifestEntry extends FileEntry {
if (onReady != null)
onReady();
} else {
js.Browser.window.fetch(file).then((res:js.html.Response) -> {
return res.arrayBuffer();
}).then((buf:js.lib.ArrayBuffer) -> {
loaded = true;
bytes = Bytes.ofData(buf);
if (onReady != null)
onReady();
});
js.Browser.window.fetch(file)
.then((res:js.html.Response) -> {
return res.arrayBuffer();
})
.then((buf:js.lib.ArrayBuffer) -> {
loaded = true;
bytes = Bytes.ofData(buf);
if (onReady != null)
onReady();
})
.catchError((e) -> {
// Try the original file path
js.Browser.window.fetch('data/' + originalFile).then((res:js.html.Response) -> {
return res.arrayBuffer();
}).then((buf:js.lib.ArrayBuffer) -> {
loaded = true;
bytes = Bytes.ofData(buf);
if (onReady != null)
onReady();
});
});
}
#else
if (onReady != null)

View file

@ -118,7 +118,7 @@ class ChatCtrl extends GuiControl {
}
public function addChatMessage(text:String) {
var realText = StringTools.htmlUnescape(text);
var realText = StringTools.htmlEscape(text);
this.chats.push({
text: realText,
age: 10.0

View file

@ -19,7 +19,8 @@ class EndGameGui extends GuiControl {
var scoreSubmitted:Bool = false;
public function new(continueFunc:GuiControl->Void, restartFunc:GuiControl->Void, nextLevelFunc:GuiControl->Void, mission:Mission, timeState:TimeState) {
public function new(continueFunc:GuiControl->Void, restartFunc:GuiControl->Void, nextLevelFunc:GuiControl->Void, mission:Mission, timeState:TimeState,
replayData:haxe.io.Bytes) {
super();
this.horizSizing = Width;
this.vertSizing = Height;
@ -312,12 +313,11 @@ class EndGameGui extends GuiControl {
scoreData.push({name: "Matan W.", time: 5999.999});
}
egFirstLine.text.text = '<p align="left"><font color="#EEC884">1. </font>${scoreData[0].name}</p>';
egSecondLine.text.text = '<p align="left"><font color="#CDCDCD">2. </font>${scoreData[1].name}</p>';
egThirdLine.text.text = '<p align="left"><font color="#C9AFA0">3. </font>${scoreData[2].name}</p>';
egFourthLine.text.text = '<p align="left"><font color="#A4A4A4">4. </font>${scoreData[3].name}</p>';
egFifthLine.text.text = '<p align="left"><font color="#949494">5. </font>${scoreData[4].name}</p>';
egFirstLine.text.text = '<p align="left"><font color="#EEC884">1. </font>${StringTools.htmlEscape(scoreData[0].name)}</p>';
egSecondLine.text.text = '<p align="left"><font color="#CDCDCD">2. </font>${StringTools.htmlEscape(scoreData[1].name)}</p>';
egThirdLine.text.text = '<p align="left"><font color="#C9AFA0">3. </font>${StringTools.htmlEscape(scoreData[2].name)}</p>';
egFourthLine.text.text = '<p align="left"><font color="#A4A4A4">4. </font>${StringTools.htmlEscape(scoreData[3].name)}</p>';
egFifthLine.text.text = '<p align="left"><font color="#949494">5. </font>${StringTools.htmlEscape(scoreData[4].name)}</p>';
var lineelems = [
egFirstLineScore,
egSecondLineScore,
@ -393,6 +393,8 @@ class EndGameGui extends GuiControl {
// }
Settings.save();
var rewindUsed = MarbleGame.instance.world.rewindUsed;
if (idx <= 4) {
setButtonStates(false);
var end = new EnterNameDlg(idx, (name) -> {
@ -426,8 +428,7 @@ class EndGameGui extends GuiControl {
var lbPath = mission.path;
if (mission.isClaMission)
lbPath = 'custom/${mission.id}';
var replayData = MarbleGame.instance.world.replay.write();
Leaderboards.submitScore(lbPath, myScore.time, MarbleGame.instance.world.rewindUsed, (sendReplay, rowId) -> {
Leaderboards.submitScore(lbPath, myScore.time, rewindUsed, (sendReplay, rowId) -> {
if (sendReplay && !mission.isClaMission) {
Leaderboards.submitReplay(rowId, replayData);
}
@ -438,7 +439,6 @@ class EndGameGui extends GuiControl {
this.addChild(end);
} else {
// Check if we can submit LB scores
var replayData = MarbleGame.instance.world.replay.write();
var lbPath = mission.path;
if (mission.isClaMission)
lbPath = 'custom/${mission.id}';
@ -453,7 +453,7 @@ class EndGameGui extends GuiControl {
}
}
if (!hasMyScore || (hasMyScore && myTopScoreLB > timeState.gameplayClock)) {
Leaderboards.submitScore(lbPath, timeState.gameplayClock, MarbleGame.instance.world.rewindUsed, (sendReplay, rowId) -> {
Leaderboards.submitScore(lbPath, timeState.gameplayClock, rewindUsed, (sendReplay, rowId) -> {
if (sendReplay && !mission.isClaMission) {
Leaderboards.submitReplay(rowId, replayData);
}

View file

@ -10,6 +10,7 @@ import h2d.Tile;
import h2d.Graphics;
import src.MarbleGame;
import src.Util;
import haxe.Timer;
class GuiScrollCtrl extends GuiControl {
public var scrollY:Float = 0;
@ -40,6 +41,12 @@ class GuiScrollCtrl extends GuiControl {
var dirty:Bool = true;
var prevMousePos:Vector;
var scrollVelocity:Float = 0;
var lastMoveStamp:Float = 0;
var momentumActive:Bool = false;
static inline var MOMENTUM_DAMPING:Float = 8;
var _contentYPositions:Map<h2d.Object, Float> = [];
var deltaY:Float = 0;
@ -239,34 +246,59 @@ class GuiScrollCtrl extends GuiControl {
}
public override function onMousePress(mouseState:MouseState) {
if (Util.isTouchDevice()) {
this.pressed = true;
this.dirty = true;
this.updateScrollVisual();
this.prevMousePos = mouseState.position;
}
// if (Util.isTouchDevice()) {
this.pressed = true;
this.dirty = true;
this.updateScrollVisual();
this.prevMousePos = mouseState.position;
this.scrollVelocity = 0;
this.momentumActive = false;
this.lastMoveStamp = Timer.stamp();
// }
}
public override function onMouseRelease(mouseState:MouseState) {
if (Util.isTouchDevice()) {
this.pressed = false;
this.dirty = true;
deltaY = 0;
this.updateScrollVisual();
}
// if (Util.isTouchDevice()) {
this.pressed = false;
this.dirty = true;
deltaY = 0;
this.updateScrollVisual();
this.momentumActive = Math.abs(scrollVelocity) > 0.01;
this.lastMoveStamp = 0;
// }
}
public override function onMouseMove(mouseState:MouseState) {
if (Util.isTouchDevice()) {
super.onMouseMove(mouseState);
if (this.pressed) {
var dy = (mouseState.position.y - this.prevMousePos.y) * scrollSpeed / this.maxScrollY;
deltaY = -dy;
this.scrollY -= dy;
this.prevMousePos = mouseState.position;
this.updateScrollVisual();
// if (Util.isTouchDevice()) {
super.onMouseMove(mouseState);
if (this.pressed) {
var renderRect = this.getRenderRectangle();
var scrollExtentY = renderRect.extent.y;
var dy = (mouseState.position.y - this.prevMousePos.y) / ((maxScrollY * Settings.uiScale) / scrollExtentY);
deltaY = -dy;
this.scrollY -= dy;
this.prevMousePos = mouseState.position;
var now = Timer.stamp();
if (lastMoveStamp > 0) {
var dt = now - lastMoveStamp;
if (dt > 0)
scrollVelocity = -dy / dt;
}
lastMoveStamp = now;
momentumActive = false;
this.updateScrollVisual();
}
// }
}
public override function onMouseLeave(mouseState:MouseState) {
// if (Util.isTouchDevice()) {
this.pressed = false;
this.dirty = true;
this.updateScrollVisual();
this.momentumActive = Math.abs(scrollVelocity) > 0.01;
this.lastMoveStamp = 0;
// }
}
public override function update(dt:Float, mouseState:MouseState) {
@ -285,6 +317,21 @@ class GuiScrollCtrl extends GuiControl {
this.updateScrollVisual();
}
super.update(dt, mouseState);
if (!pressed && momentumActive) {
var damping = Math.exp(-MOMENTUM_DAMPING * dt);
scrollVelocity *= damping;
if (Math.abs(scrollVelocity) < 0.01) {
scrollVelocity = 0;
momentumActive = false;
return;
}
var before = scrollY;
scrollY += scrollVelocity * dt;
updateScrollVisual();
if (scrollY == 0 || scrollY == before)
momentumActive = false;
}
}
// public override function onMouseDown(mouseState:MouseState) {

View file

@ -186,7 +186,7 @@ class JoinServerGui extends GuiImage {
serverInfo.text.text = '<p align="center">Select a Server</p><p align="center">or Host your own</p>';
} else {
var server = ourServerList[curSelection];
serverInfo.text.text = '<p align="center">${server.name}</p><p align="center"><font face="MarkerFelt18" color="#DDDDEE">Hosted by ${server.host}</font></p><p align="left">${server.description}</p>';
serverInfo.text.text = '<p align="center">${StringTools.htmlEscape(server.name)}</p><p align="center"><font face="MarkerFelt18" color="#DDDDEE">Hosted by ${StringTools.htmlEscape(server.host)}</font></p><p align="left">${StringTools.htmlEscape(server.description)}</p>';
}
}
serverListContainer.addChild(serverList);
@ -197,7 +197,7 @@ class JoinServerGui extends GuiImage {
function updateServerListDisplay() {
serverDisplays = ourServerList.map(x ->
'<img src="${platformToString[x.platform]}"></img><font color="#FFFFFF">${x.name} <offset value="${400 * Settings.uiScale}">${x.players}/${x.maxPlayers}</offset></font>');
'<img src="${platformToString[x.platform]}"></img><font color="#FFFFFF">${StringTools.htmlEscape(x.name)} <offset value="${400 * Settings.uiScale}">${x.players}/${x.maxPlayers}</offset></font>');
serverList.setTexts(serverDisplays);
}

View file

@ -585,11 +585,11 @@ class MPPlayMissionGui extends GuiImage {
currentSelection = -1;
}
pmDesc.text.text = '<font face="MarkerFelt32" color="#E3F3FF"><p align="center">#${currentSelection + 1}: ${currentMission.title}</p></font>'
+ '<font face="MarkerFelt18" color="#CEE0F4">${currentMission.description}</font>';
pmDesc.text.text = '<font face="MarkerFelt32" color="#E3F3FF"><p align="center">#${currentSelection + 1}: ${StringTools.htmlEscape(currentMission.title)}</p></font>'
+ '<font face="MarkerFelt18" color="#CEE0F4">${StringTools.htmlEscape(currentMission.description)}</font>';
parTime.text.text = '<font face="MarkerFelt24" color="#E3F3FF">Duration: <font color="#FFFFFF">${Util.formatTime(currentMission.qualifyTime)}</font></font><br/>'
+ '<font face="MarkerFelt24" color="#E3F3FF">Author: <font color="#FFFFFF">${currentMission.artist}</font></font>';
+ '<font face="MarkerFelt24" color="#E3F3FF">Author: <font color="#FFFFFF">${StringTools.htmlEscape(currentMission.artist)}</font></font>';
// pmPreview.bmp.tile = tmpprevtile;
#if js
@ -718,7 +718,7 @@ class MPPlayMissionGui extends GuiImage {
}
var playerListCompiled = playerListArr.map(player ->
'<img src="${platformToString(player.platform)}"></img><font color="#FFFFFF">${player.name}<offset value="${220 * Settings.uiScale}">${player.ready ? "Ready" : ""}</offset></font>');
'<img src="${platformToString(player.platform)}"></img><font color="#FFFFFF">${StringTools.htmlEscape(player.name)}<offset value="${220 * Settings.uiScale}">${player.ready ? "Ready" : ""}</offset></font>');
playerListCtrl.setTexts(playerListCompiled);
// if (!showingCustoms)
@ -728,7 +728,7 @@ class MPPlayMissionGui extends GuiImage {
}
public static function addChatMessage(s:String) {
var realText = StringTools.htmlUnescape(s);
var realText = StringTools.htmlEscape(s);
allChats.push(realText);
if (allChats.length > 100) {
allChats = allChats.slice(allChats.length - 100);

View file

@ -738,7 +738,7 @@ class PlayGui {
} else {
isSpectating = Net.clientIdMap[item.id].spectator;
}
pl.push('<font color="${color}">${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(item.name, 25, 3)}</font>');
pl.push('<font color="${color}">${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(StringTools.htmlEscape(item.name), 25, 3)}</font>');
var connPing = item.us ? (Net.isHost ? 0 : Net.clientConnection.pingTicks) : (item.id == 0 ? 0 : Net.clientIdMap[item.id].pingTicks);
var pingStatus = "unknown";
if (connPing <= 5)

View file

@ -1234,7 +1234,7 @@ class PlayMissionGui extends GuiImage {
var i = 1;
for (score in scoreList) {
sFmt.push('${i}.
<offset value="15">${score.name.substr(0, 30)}</offset>
<offset value="15">${StringTools.htmlEscape(score.name.substr(0, 30))}</offset>
<offset value="215">${Util.formatTime(score.score)}</offset>
<offset value="279"><img src="${platformToString(score.platform)}"/></offset>
${score.rewind == 1 ? '<offset value="299"><img src="rewind"/></offset> ' : ""}');

View file

@ -562,7 +562,6 @@ class Net {
serverInfo.players++;
}
serverInfo.players++;
MasterServerClient.instance.sendServerInfo(serverInfo); // notify the server of the new player
if (MarbleGame.canvas.content is MPPlayMissionGui) {