diff --git a/src/Http.hx b/src/Http.hx index 1751f7ea..fcc8bd70 100644 --- a/src/Http.hx +++ b/src/Http.hx @@ -11,6 +11,7 @@ typedef HttpRequest = { var fulfilled:Bool; var post:Bool; var postData:String; + var ?file:haxe.io.Bytes; }; class Http { @@ -52,13 +53,18 @@ class Http { }; if (req.post) { http.setHeader('User-Agent', 'MBHaxe/1.0 ${Util.getPlatform()}'); - http.setHeader('Content-Type', "application/json"); // support json data only (for now) - http.setPostData(req.postData); + if (req.file == null) { + http.setHeader('Content-Type', "application/json"); // support json data only (for now) + http.setPostData(req.postData); + } + } + if (req.post && req.file != null) { + http.fileTransfer("hxfile", "hxfilename", new haxe.io.BytesInput(req.file), req.file.length); } hl.Gc.enable(false); - hl.Gc.blocking(true); // Wtf is this shit + // hl.Gc.blocking(true); // Wtf is this shit http.request(req.post); - hl.Gc.blocking(false); + // hl.Gc.blocking(false); hl.Gc.enable(true); } } @@ -114,6 +120,37 @@ class Http { #end } + // Returns HTTPRequest on sys, Int on js + public static function uploadFile(url:String, data:haxe.io.Bytes, callback:haxe.io.Bytes->Void, errCallback:String->Void) { + var req = { + url: url, + callback: callback, + errCallback: errCallback, + cancelled: false, + fulfilled: false, + post: true, + postData: null, + file: data, + }; + #if sys + requests.add(req); + return req; + #else + // TODO + return js.Browser.window.setTimeout(() -> { + js.Browser.window.fetch(url, + { + method: "POST", + headers: { + "User-Agent": js.Browser.window.navigator.userAgent, + "Content-Type": "application/octet-stream", + }, + body: data.getData() + }).then(r -> r.arrayBuffer().then(b -> callback(haxe.io.Bytes.ofData(b))), e -> errCallback(e.toString())); + }, 75); + #end + } + public static function loop() { #if sys var resp = responses.pop(false); diff --git a/src/Leaderboards.hx b/src/Leaderboards.hx new file mode 100644 index 00000000..27b96a57 --- /dev/null +++ b/src/Leaderboards.hx @@ -0,0 +1,74 @@ +package src; + +import net.Net; +import haxe.Json; +import src.Http; +import src.Console; +import src.Settings; + +typedef LBScore = { + name:String, + has_rec:Int, + score:Float, + platform:Int, + rewind:Int, +} + +class Leaderboards { + static var host = "http://127.0.0.1:7000"; + static var game = "Platinum"; + + public static function submitScore(mission:String, score:Float, rewindUsed:Bool, needsReplayCb:(Bool, Int) -> Void) { + if (!StringTools.startsWith(mission, "data/")) + mission = "data/" + mission; + Http.post('${host}/api/submit', Json.stringify({ + mission: mission, + score: score, + game: game, + name: Settings.highscoreName, + uid: Settings.userId, + rewind: rewindUsed ? 1 : 0, + platform: Net.getPlatform() + }), (b) -> { + var s = b.toString(); + var jd = Json.parse(s); + var status = jd.status; + Console.log("Score submitted"); + needsReplayCb(status == "new_record", status == "new_record" ? jd.rowid : 0); + }, (e) -> { + Console.log("Score submission failed: " + e); + }); + } + + public static function getScores(mission:String, cb:Array->Void) { + if (!StringTools.startsWith(mission, "data/")) + mission = "data/" + mission; + return Http.get('${host}/api/scores?mission=${StringTools.urlEncode(mission)}&game=${game}', (b) -> { + var s = b.toString(); + var scores:Array = Json.parse(s).scores; + cb(scores); + }, (e) -> { + Console.log("Failed to get scores: " + e); + cb([]); + }); + } + + public static function submitReplay(ref:Int, replay:haxe.io.Bytes) { + return Http.uploadFile('${host}/api/record?ref=${ref}', replay, (b) -> { + Console.log("Replay submitted"); + }, (e) -> { + Console.log("Replay submission failed: " + e); + }); + } + + public static function watchTopReplay(mission:String, cb:haxe.io.Bytes->Void) { + if (!StringTools.startsWith(mission, "data/")) + mission = "data/" + mission; + return Http.get('${host}/api/replay?mission=${StringTools.urlEncode(mission)}&game=${game}', (b) -> { + cb(b); + }, (e) -> { + Console.log("Failed to get replay: " + e); + cb(null); + }); + } +} diff --git a/src/MarbleGame.hx b/src/MarbleGame.hx index 8d5df4bd..12e6ad22 100644 --- a/src/MarbleGame.hx +++ b/src/MarbleGame.hx @@ -283,7 +283,7 @@ class MarbleGame { exitGameDlg = new ExitGameDlg((sender) -> { canvas.popDialog(exitGameDlg); var w = getWorld(); - if (w.isRecording) { + if (MarbleGame.instance.toRecord) { MarbleGame.canvas.pushDialog(new ReplayNameDlg(() -> { quitMission(); })); @@ -373,7 +373,7 @@ class MarbleGame { world.dispose(); } Analytics.trackLevelPlay(mission.title, mission.path); - world = new MarbleWorld(scene, scene2d, mission, toRecord, multiplayer); + world = new MarbleWorld(scene, scene2d, mission, true, multiplayer); world.init(); } diff --git a/src/MarbleWorld.hx b/src/MarbleWorld.hx index cf4cfa0c..0ec90530 100644 --- a/src/MarbleWorld.hx +++ b/src/MarbleWorld.hx @@ -216,6 +216,7 @@ class MarbleWorld extends Scheduler { // Rewind public var rewindManager:RewindManager; public var rewinding:Bool = false; + public var rewindUsed:Bool = false; public var inputRecorder:InputRecorder; public var isReplayingMovement:Bool = false; @@ -682,6 +683,7 @@ class MarbleWorld extends Scheduler { this.replay.rewind(); this.rewindManager.clear(); + this.rewindUsed = false; if (!this.isMultiplayer || _skipPreGame) { setCursorLock(true); @@ -1834,6 +1836,7 @@ class MarbleWorld extends Scheduler { var actualDt = timeState.currentAttemptTime - rframe.timeState.currentAttemptTime - dt * rewindManager.timeScale; dt = actualDt; rewindManager.applyFrame(rframe); + rewindUsed = true; } } if (dt < 0) diff --git a/src/Replay.hx b/src/Replay.hx index b2bea8c7..a15d909e 100644 --- a/src/Replay.hx +++ b/src/Replay.hx @@ -497,6 +497,9 @@ class Replay { var compressed = stream.finalize(); #end + if (this.name == null) + this.name = this.mission; + var finalB = new BytesBuffer(); finalB.addByte(version); finalB.addByte(this.name.length); diff --git a/src/Settings.hx b/src/Settings.hx index 3c704198..9661175e 100644 --- a/src/Settings.hx +++ b/src/Settings.hx @@ -1,5 +1,6 @@ package src; +import net.Uuid; import h3d.Vector; import haxe.ds.Option; import gui.Canvas; @@ -220,6 +221,7 @@ class Settings { public static var levelStatistics:Map = []; public static var highscoreName = ""; + public static var userId = ""; public static var uiScale = 1.0; @@ -274,6 +276,7 @@ class Settings { stats: playStatistics, server: serverSettings, highscoreName: highscoreName, + userId: userId, marbleIndex: optionsSettings.marbleIndex, marbleSkin: optionsSettings.marbleSkin, marbleModel: optionsSettings.marbleModel, @@ -454,6 +457,13 @@ class Settings { } #end highscoreName = json.highscoreName; + if (highscoreName == null) { + highscoreName = ""; + } + userId = json.userId; + if (userId == null) { + userId = Uuid.v4(); + } } else { Console.warn("Settings file does not exist"); save(); diff --git a/src/gui/EndGameGui.hx b/src/gui/EndGameGui.hx index 2bc3eb28..0400f062 100644 --- a/src/gui/EndGameGui.hx +++ b/src/gui/EndGameGui.hx @@ -1,5 +1,6 @@ package gui; +import src.Leaderboards; import hxd.BitmapData; import h2d.Tile; import src.MarbleGame; @@ -422,6 +423,11 @@ class EndGameGui extends GuiControl { } Settings.saveScore(mission.path, myScore); + Leaderboards.submitScore(mission.path, myScore.time, MarbleGame.instance.world.rewindUsed, (sendReplay, rowId) -> { + if (sendReplay) { + Leaderboards.submitReplay(rowId, MarbleGame.instance.world.replay.write()); + } + }); scoreSubmitted = true; }); diff --git a/src/gui/PlayMissionGui.hx b/src/gui/PlayMissionGui.hx index 6a28736a..c9748d9d 100644 --- a/src/gui/PlayMissionGui.hx +++ b/src/gui/PlayMissionGui.hx @@ -1,5 +1,8 @@ package gui; +import src.Http; +import src.Leaderboards; +import net.ClientConnection.NetPlatform; import src.Marbleland; import h2d.filter.DropShadow; import src.Replay; @@ -52,9 +55,12 @@ class PlayMissionGui extends GuiImage { #if js var previewTimeoutHandle:Option = None; + var lbRequest:Int = 0; #end #if hl var previewToken:Int = 0; + var lbToken:Int = 0; + var lbRequest:src.Http.HttpRequest = null; #end public function new() { @@ -117,6 +123,7 @@ class PlayMissionGui extends GuiImage { var arial14b = new BitmapFont(arial14fontdata.entry); @:privateAccess arial14b.loader = ResourceLoader.loader; var arial14 = arial14b.toSdfFont(cast 12 * Settings.uiScale, MultiChannel); + var arial12 = arial14b.toSdfFont(cast 10 * Settings.uiScale, MultiChannel); var arialb14fontdata = ResourceLoader.getFileEntry("data/font/Arial Bold.fnt"); var arialb14b = new BitmapFont(arialb14fontdata.entry); @@ -129,6 +136,7 @@ class PlayMissionGui extends GuiImage { var markerFelt32 = markerFelt32b.toSdfFont(cast 26 * Settings.uiScale, MultiChannel); var markerFelt24 = markerFelt32b.toSdfFont(cast 20 * Settings.uiScale, MultiChannel); var markerFelt20 = markerFelt32b.toSdfFont(cast 18.5 * Settings.uiScale, MultiChannel); + var markerFelt16 = markerFelt32b.toSdfFont(cast 14 * Settings.uiScale, MultiChannel); var markerFelt18 = markerFelt32b.toSdfFont(cast 17 * Settings.uiScale, MultiChannel); var markerFelt26 = markerFelt32b.toSdfFont(cast 22 * Settings.uiScale, MultiChannel); @@ -136,6 +144,8 @@ class PlayMissionGui extends GuiImage { switch (text) { case "DomCasual24": return domcasual24; + case "Arial12": + return arial14; case "Arial14": return arial14; case "ArialBold14": @@ -155,6 +165,30 @@ class PlayMissionGui extends GuiImage { } } + function imgLoader(path:String) { + var t = switch (path) { + case "pc": + ResourceLoader.getResource("data/ui/mp/play/platform_desktop_white.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "mac": + ResourceLoader.getResource("data/ui/mp/play/platform_mac_white.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "web": + ResourceLoader.getResource("data/ui/mp/play/platform_web_white.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "android": + ResourceLoader.getResource("data/ui/mp/play/platform_android_white.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "unknown": + ResourceLoader.getResource("data/ui/mp/play/platform_unknown_white.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "rewind": + ResourceLoader.getResource("data/ui/mp/play/rewind_ico.png", ResourceLoader.getImage, this.imageResources).toTile(); + case "watch": + ResourceLoader.getResource("data/ui/play/record.png", ResourceLoader.getImage, this.imageResources).toTile(); + case _: + return null; + }; + if (t != null) + t.scaleToSize(t.width * (Settings.uiScale), t.height * (Settings.uiScale)); + return t; + } + var pmBox = new GuiImage(ResourceLoader.getResource('data/ui/play/window.png', ResourceLoader.getImage, this.imageResources).toTile()); pmBox.horizSizing = Center; pmBox.vertSizing = Center; @@ -775,6 +809,114 @@ class PlayMissionGui extends GuiImage { }; pmMorePopDlg.addChild(pmRecord); + var scoreScroll = new GuiScrollCtrl(ResourceLoader.getResource("data/ui/common/philscroll.png", ResourceLoader.getImage, this.imageResources) + .toTile()); + scoreScroll.position = new Vector(110, 170); + scoreScroll.extent = new Vector(407, 143); + scoreScroll.childrenHandleScroll = true; + scoreScroll.scrollToBottom = true; + // window.addChild(chatScroll); + + var scoreBox = new GuiMLText(markerFelt16, mlFontLoader); + scoreBox.text.loadImage = imgLoader; + scoreBox.text.onHyperlink = (url) -> { + if (url == "watch") { + var currentMission = currentList[currentSelection]; + Leaderboards.watchTopReplay(currentMission.path, (b) -> { + if (b != null) { + var replayF = new Replay(""); + if (replayF.read(b)) { + var repmis = replayF.mission; + // Strip data/ from the mission name + if (StringTools.startsWith(repmis, "data/")) { + repmis = repmis.substr(5); + } + + var mi = replayF.customId == 0 ? MissionList.missions.get(repmis) : Marbleland.missions.get(replayF.customId); + + // try with data/ added + if (mi == null && replayF.customId == 0) { + if (!StringTools.contains(repmis, "data/")) + repmis = "data/" + repmis; + mi = MissionList.missions.get(repmis); + } + + if (mi.isClaMission) { + mi.download(() -> { + MarbleGame.instance.watchMissionReplay(mi, replayF); + }); + } else { + MarbleGame.instance.watchMissionReplay(mi, replayF); + } + } else { + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("Could not load replay for this level.")); + } + } else { + MarbleGame.canvas.pushDialog(new MessageBoxOkDlg("No top replay found for this level.")); + } + }); + } + } + scoreBox.text.textColor = 0xF4E4CE; + scoreBox.text.dropShadow = { + dx: 1 * Settings.uiScale, + dy: 1 * Settings.uiScale, + alpha: 0.5, + color: 0 + }; + scoreBox.text.lineSpacing = -1; + scoreBox.horizSizing = Width; + scoreBox.position = new Vector(0, 0); + scoreBox.extent = new Vector(407, 1184); + var scores = [ + '1. Nardo Polo99:59:999', + '2. Nardo Polo99:59:999', + '3. Nardo Polo99:59:999', + '4. Nardo Polo99:59:999', + '5. Nardo Polo99:59:999', + ]; + scoreBox.text.text = '

Loading scores

'; // scores.join('
'); + scoreBox.text.imageVerticalAlign = Top; + scoreScroll.addChild(scoreBox); + + var lbImgs = loadButtonImages("data/ui/play/lb"); + var infoImgs = loadButtonImages("data/ui/play/info"); + + var showLBs = false; + + var pmLBToggle = new GuiButton(lbImgs); + pmLBToggle.position = new Vector(118, 98); + pmLBToggle.extent = new Vector(43, 43); + pmLBToggle.pressedAction = (e) -> { + showLBs = !showLBs; + if (!showLBs) { + @:privateAccess pmLBToggle.anim.frames = lbImgs; + } else { + @:privateAccess pmLBToggle.anim.frames = infoImgs; + } + + pmScoreButton.disabled = showLBs; + pmScoreText.text.visible = !showLBs; + + setSelectedFunc(currentSelection); + if (showLBs) { + pmBox.addChild(scoreScroll); + } else { + pmBox.removeChild(scoreScroll); + } + pmBox.render(MarbleGame.canvas.scene2d, pmBox.parent._flow); + // setCategoryFunc(currentGame, currentCategoryStatic, currentSortType == 1 ? "date" : "alpha"); + // MarbleGame.canvas.pushDialog(new SearchGui(currentGame, currentCategory == "custom")); + } + + if (!showLBs) { + @:privateAccess pmLBToggle.anim.frames = lbImgs; + } else { + @:privateAccess pmLBToggle.anim.frames = infoImgs; + } + + pmBox.addChild(pmLBToggle); + // var replayPlayButton = new GuiImage(ResourceLoader.getResource("data/ui/play/playback.png", ResourceLoader.getImage, this.imageResources).toTile()); // replayPlayButton.position = new Vector(38, 315); // replayPlayButton.extent = new Vector(18, 18); @@ -931,6 +1073,8 @@ class PlayMissionGui extends GuiImage { } } + var lbToken:Int = 0; + setSelectedFunc = function setSelected(index:Int) { if (index > currentList.length - 1) { index = currentList.length - 1; @@ -1011,38 +1155,74 @@ class PlayMissionGui extends GuiImage { var descText = '

#${currentList.indexOf(currentMission) + 1}: ${currentMission.title}

'; - if (this.scoreShowing) { - var scoreData:Array = Settings.getScores(currentMission.path); - while (scoreData.length < 5) { - scoreData.push({name: "Matan W.", time: 5999.999}); - } - - var rightText = '
'; - - for (i in 0...5) { - var score = scoreData[i]; - - var scoreColor = "#FFFFFF"; - if (score.time < currentMission.ultimateTime) { - scoreColor = "#FFCC33"; - } else if (score.time < currentMission.goldTime) { - if (currentMission.game == "gold" || currentMission.game.toLowerCase() == "ultra") - scoreColor = "#FFFF00"; - else - scoreColor = "#CCCCCC"; + if (!showLBs) { + if (this.scoreShowing) { + var scoreData:Array = Settings.getScores(currentMission.path); + while (scoreData.length < 5) { + scoreData.push({name: "Matan W.", time: 5999.999}); } - var scoreTextTime = '

${Util.formatTime(score.time)}

'; - rightText += scoreTextTime; + var rightText = '
'; - descText += '${i + 1}. ${StringTools.htmlEscape(score.name)}
'; + for (i in 0...5) { + var score = scoreData[i]; + + var scoreColor = "#FFFFFF"; + if (score.time < currentMission.ultimateTime) { + scoreColor = "#FFCC33"; + } else if (score.time < currentMission.goldTime) { + if (currentMission.game == "gold" || currentMission.game.toLowerCase() == "ultra") + scoreColor = "#FFFF00"; + else + scoreColor = "#CCCCCC"; + } + + var scoreTextTime = '

${Util.formatTime(score.time)}

'; + rightText += scoreTextTime; + + descText += '${i + 1}. ${StringTools.htmlEscape(score.name)}
'; + } + + pmDescriptionRight.text.text = rightText; + } else { + descText += '

Author: ${StringTools.htmlEscape(currentMission.artist)}

'; + descText += '${StringTools.htmlEscape(currentMission.description)}'; + pmDescriptionRight.text.text = ''; } - - pmDescriptionRight.text.text = rightText; } else { - descText += '

Author: ${StringTools.htmlEscape(currentMission.artist)}

'; - descText += '${StringTools.htmlEscape(currentMission.description)}'; pmDescriptionRight.text.text = ''; + #if hl + if (lbRequest != null) + Http.cancel(lbRequest); + #end + #if js + if (lbRequest != 0) + Http.cancel(lbRequest); + #end + var lTok = lbToken++; + var req = Leaderboards.getScores(currentMission.path, (scoreList) -> { + if (lTok + 1 != lbToken || !showLBs) + return; + var sFmt = []; + var i = 1; + for (score in scoreList) { + sFmt.push('${i}. + ${score.name} + ${Util.formatTime(score.score)} + + ${score.rewind == 1 ? ' ' : ""}'); + i++; + } + scoreBox.text.text = 'Leaderboards + ${scoreList.length != 0 ? 'Top Replay:' : ""} +
' + + sFmt.join('
'); + }); + lbRequest = req; + scoreBox.text.text = 'Leaderboards

Loading scores

'; + + scoreScroll.setScrollMax(scoreBox.text.textHeight); + scoreScroll.updateScrollVisual(); } pmDescription.text.text = descText; @@ -1058,21 +1238,26 @@ class PlayMissionGui extends GuiImage { alpha: 0.5, color: 0 }; - if (this.scoreShowing) { - if (currentMission.game == "platinum") { - pmParText.text.text = 'Platinum: ${Util.formatTime(currentMission.goldTime)}'; - pmParTextRight.text.text = '

Ultimate: ${Util.formatTime(currentMission.ultimateTime)}

'; - } - if (currentMission.game == "gold") { - pmParText.text.text = 'Qualify: ${(currentMission.qualifyTime != Math.POSITIVE_INFINITY) ? Util.formatTime(currentMission.qualifyTime) : "N/A"}'; - pmParTextRight.text.text = '

Gold: ${Util.formatTime(currentMission.goldTime)}

'; - } - if (currentMission.game.toLowerCase() == "ultra") { - pmParText.text.text = 'Gold: ${Util.formatTime(currentMission.goldTime)}'; - pmParTextRight.text.text = '

Ultimate: ${Util.formatTime(currentMission.ultimateTime)}

'; + if (!showLBs) { + if (this.scoreShowing) { + if (currentMission.game == "platinum") { + pmParText.text.text = 'Platinum: ${Util.formatTime(currentMission.goldTime)}'; + pmParTextRight.text.text = '

Ultimate: ${Util.formatTime(currentMission.ultimateTime)}

'; + } + if (currentMission.game == "gold") { + pmParText.text.text = 'Qualify: ${(currentMission.qualifyTime != Math.POSITIVE_INFINITY) ? Util.formatTime(currentMission.qualifyTime) : "N/A"}'; + pmParTextRight.text.text = '

Gold: ${Util.formatTime(currentMission.goldTime)}

'; + } + if (currentMission.game.toLowerCase() == "ultra") { + pmParText.text.text = 'Gold: ${Util.formatTime(currentMission.goldTime)}'; + pmParTextRight.text.text = '

Ultimate: ${Util.formatTime(currentMission.ultimateTime)}

'; + } + } else { + pmParText.text.text = '

${currentMission.game == "gold" ? "Qualify" : "Par"} Time: ${(currentMission.qualifyTime != Math.POSITIVE_INFINITY) ? Util.formatTime(currentMission.qualifyTime) : "N/A"}

'; + pmParTextRight.text.text = ''; } } else { - pmParText.text.text = '

${currentMission.game == "gold" ? "Qualify" : "Par"} Time: ${(currentMission.qualifyTime != Math.POSITIVE_INFINITY) ? Util.formatTime(currentMission.qualifyTime) : "N/A"}

'; + pmParText.text.text = ''; pmParTextRight.text.text = ''; } @@ -1167,4 +1352,14 @@ class PlayMissionGui extends GuiImage { scoreButtonHover = false; } } + + 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"; + } + } }