From 2ef28aae5fbbfb9dc8ec20b67ce43dc0dfc812a5 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:15:10 +0000 Subject: [PATCH] - 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 --- .circleci/config.yml | 6 ++- src/Leaderboards.hx | 2 +- src/Marble.hx | 41 +++++++++-------- src/MarbleWorld.hx | 2 +- src/Replay.hx | 25 ++++++----- src/fs/ManifestFileSystem.hx | 29 ++++++++---- src/gui/ChatCtrl.hx | 2 +- src/gui/EndGameGui.hx | 22 ++++----- src/gui/GuiScrollCtrl.hx | 87 +++++++++++++++++++++++++++--------- src/gui/JoinServerGui.hx | 4 +- src/gui/MPPlayMissionGui.hx | 10 ++--- src/gui/PlayGui.hx | 2 +- src/gui/PlayMissionGui.hx | 2 +- src/net/Net.hx | 1 - 14 files changed, 152 insertions(+), 83 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc8bf9ab..4ce6b7f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -462,10 +462,14 @@ workflows: filters: tags: only: /^\d+.\d+.\d+$/ + branches: + ignore: /.*/ build-windows: jobs: - build-win: filters: tags: - only: /^\d+.\d+.\d+$/ \ No newline at end of file + only: /^\d+.\d+.\d+$/ + branches: + ignore: /.*/ \ No newline at end of file diff --git a/src/Leaderboards.hx b/src/Leaderboards.hx index 51d7e3ee..34c63ad1 100644 --- a/src/Leaderboards.hx +++ b/src/Leaderboards.hx @@ -49,7 +49,7 @@ class Leaderboards { public static function getScores(mission:String, kind:LeaderboardsKind, cb:Array->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 = Json.parse(s).scores; cb(scores); diff --git a/src/Marble.hx b/src/Marble.hx index a4c61baa..7d35f13f 100644 --- a/src/Marble.hx +++ b/src/Marble.hx @@ -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) { @@ -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)) { diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index abcb70ac..084d93ce 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -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; diff --git a/src/Replay.hx b/src/Replay.hx index 381ae315..ca4675a5 100644 --- a/src/Replay.hx +++ b/src/Replay.hx @@ -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; + } } } diff --git a/src/fs/ManifestFileSystem.hx b/src/fs/ManifestFileSystem.hx index 48d781bc..4fff2111 100644 --- a/src/fs/ManifestFileSystem.hx +++ b/src/fs/ManifestFileSystem.hx @@ -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) diff --git a/src/gui/ChatCtrl.hx b/src/gui/ChatCtrl.hx index 183d0e99..0719f88c 100644 --- a/src/gui/ChatCtrl.hx +++ b/src/gui/ChatCtrl.hx @@ -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 diff --git a/src/gui/EndGameGui.hx b/src/gui/EndGameGui.hx index 2ca93ebb..5eb2847e 100644 --- a/src/gui/EndGameGui.hx +++ b/src/gui/EndGameGui.hx @@ -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 = '

1. ${scoreData[0].name}

'; - egSecondLine.text.text = '

2. ${scoreData[1].name}

'; - egThirdLine.text.text = '

3. ${scoreData[2].name}

'; - egFourthLine.text.text = '

4. ${scoreData[3].name}

'; - egFifthLine.text.text = '

5. ${scoreData[4].name}

'; - + egFirstLine.text.text = '

1. ${StringTools.htmlEscape(scoreData[0].name)}

'; + egSecondLine.text.text = '

2. ${StringTools.htmlEscape(scoreData[1].name)}

'; + egThirdLine.text.text = '

3. ${StringTools.htmlEscape(scoreData[2].name)}

'; + egFourthLine.text.text = '

4. ${StringTools.htmlEscape(scoreData[3].name)}

'; + egFifthLine.text.text = '

5. ${StringTools.htmlEscape(scoreData[4].name)}

'; 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); } diff --git a/src/gui/GuiScrollCtrl.hx b/src/gui/GuiScrollCtrl.hx index ac0d93dc..32fa399d 100644 --- a/src/gui/GuiScrollCtrl.hx +++ b/src/gui/GuiScrollCtrl.hx @@ -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 = []; 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) { diff --git a/src/gui/JoinServerGui.hx b/src/gui/JoinServerGui.hx index 404fd5da..2da1deae 100644 --- a/src/gui/JoinServerGui.hx +++ b/src/gui/JoinServerGui.hx @@ -186,7 +186,7 @@ class JoinServerGui extends GuiImage { serverInfo.text.text = '

Select a Server

or Host your own

'; } else { var server = ourServerList[curSelection]; - serverInfo.text.text = '

${server.name}

Hosted by ${server.host}

${server.description}

'; + serverInfo.text.text = '

${StringTools.htmlEscape(server.name)}

Hosted by ${StringTools.htmlEscape(server.host)}

${StringTools.htmlEscape(server.description)}

'; } } serverListContainer.addChild(serverList); @@ -197,7 +197,7 @@ class JoinServerGui extends GuiImage { function updateServerListDisplay() { serverDisplays = ourServerList.map(x -> - '${x.name} ${x.players}/${x.maxPlayers}'); + '${StringTools.htmlEscape(x.name)} ${x.players}/${x.maxPlayers}'); serverList.setTexts(serverDisplays); } diff --git a/src/gui/MPPlayMissionGui.hx b/src/gui/MPPlayMissionGui.hx index bf64d997..4661c092 100644 --- a/src/gui/MPPlayMissionGui.hx +++ b/src/gui/MPPlayMissionGui.hx @@ -585,11 +585,11 @@ class MPPlayMissionGui extends GuiImage { currentSelection = -1; } - pmDesc.text.text = '

#${currentSelection + 1}: ${currentMission.title}

' - + '${currentMission.description}'; + pmDesc.text.text = '

#${currentSelection + 1}: ${StringTools.htmlEscape(currentMission.title)}

' + + '${StringTools.htmlEscape(currentMission.description)}'; parTime.text.text = 'Duration: ${Util.formatTime(currentMission.qualifyTime)}
' - + 'Author: ${currentMission.artist}'; + + 'Author: ${StringTools.htmlEscape(currentMission.artist)}'; // pmPreview.bmp.tile = tmpprevtile; #if js @@ -718,7 +718,7 @@ class MPPlayMissionGui extends GuiImage { } var playerListCompiled = playerListArr.map(player -> - '${player.name}${player.ready ? "Ready" : ""}'); + '${StringTools.htmlEscape(player.name)}${player.ready ? "Ready" : ""}'); 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); diff --git a/src/gui/PlayGui.hx b/src/gui/PlayGui.hx index 58b9bdd5..17ced146 100644 --- a/src/gui/PlayGui.hx +++ b/src/gui/PlayGui.hx @@ -738,7 +738,7 @@ class PlayGui { } else { isSpectating = Net.clientIdMap[item.id].spectator; } - pl.push('${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(item.name, 25, 3)}'); + pl.push('${i + 1}. ${isSpectating ? "[S] " : ""}${Util.rightPad(StringTools.htmlEscape(item.name), 25, 3)}'); 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) diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index 9fd00348..6b99621e 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -1234,7 +1234,7 @@ class PlayMissionGui extends GuiImage { var i = 1; for (score in scoreList) { sFmt.push('${i}. - ${score.name.substr(0, 30)} + ${StringTools.htmlEscape(score.name.substr(0, 30))} ${Util.formatTime(score.score)} ${score.rewind == 1 ? ' ' : ""}'); diff --git a/src/net/Net.hx b/src/net/Net.hx index aba5b989..419bc555 100644 --- a/src/net/Net.hx +++ b/src/net/Net.hx @@ -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) {