leaderboards impl

This commit is contained in:
RandomityGuy 2024-11-05 00:01:09 +05:30
parent a92b0eb1b6
commit e1e882098b
8 changed files with 373 additions and 45 deletions

View file

@ -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()}');
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);

74
src/Leaderboards.hx Normal file
View file

@ -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<LBScore>->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<LBScore> = 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);
});
}
}

View file

@ -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();
}

View file

@ -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)

View file

@ -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);

View file

@ -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<String, PlayStatistics> = [];
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();

View file

@ -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;
});

View file

@ -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<Int> = 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. <offset value="15">Nardo Polo</offset><offset value="215">99:59:999</offset><offset value="279"><img src="unknown"/></offset>',
'2. <offset value="15">Nardo Polo</offset><offset value="215">99:59:999</offset><offset value="279"><img src="pc"/></offset>',
'3. <offset value="15">Nardo Polo</offset><offset value="215">99:59:999</offset><offset value="279"><img src="mac"/></offset>',
'4. <offset value="15">Nardo Polo</offset><offset value="215">99:59:999</offset><offset value="279"><img src="web"/></offset>',
'5. <offset value="15">Nardo Polo</offset><offset value="215">99:59:999</offset><offset value="279"><img src="android"/></offset>',
];
scoreBox.text.text = '<p align="center">Loading scores</p>'; // scores.join('<br/>');
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,6 +1155,7 @@ class PlayMissionGui extends GuiImage {
var descText = '<font color="#FDFEFE" face="MarkerFelt26"><p align="center">#${currentList.indexOf(currentMission) + 1}: ${currentMission.title}</p></font>';
if (!showLBs) {
if (this.scoreShowing) {
var scoreData:Array<Score> = Settings.getScores(currentMission.path);
while (scoreData.length < 5) {
@ -1044,6 +1189,41 @@ class PlayMissionGui extends GuiImage {
descText += '<font color="#F4E4CE" face="MarkerFelt18">${StringTools.htmlEscape(currentMission.description)}</font>';
pmDescriptionRight.text.text = '';
}
} else {
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}.
<offset value="15">${score.name}</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> ' : ""}');
i++;
}
scoreBox.text.text = '<font color="#FDFEFE" face="MarkerFelt18">Leaderboards</font>
${scoreList.length != 0 ? '<offset value="220">Top Replay:<a href="watch"><img src="watch" /></a></offset>' : ""}
<br/>'
+ sFmt.join('<br/>');
});
lbRequest = req;
scoreBox.text.text = '<font color="#FDFEFE" face="MarkerFelt18">Leaderboards</font><br/><p align="center">Loading scores</p>';
scoreScroll.setScrollMax(scoreBox.text.textHeight);
scoreScroll.updateScrollVisual();
}
pmDescription.text.text = descText;
pmParText.text.dropShadow = {
@ -1058,6 +1238,7 @@ class PlayMissionGui extends GuiImage {
alpha: 0.5,
color: 0
};
if (!showLBs) {
if (this.scoreShowing) {
if (currentMission.game == "platinum") {
pmParText.text.text = '<font color="#FFE3E3" face="MarkerFelt20">Platinum: <font color="#CCCCCC">${Util.formatTime(currentMission.goldTime)}</font></font>';
@ -1075,6 +1256,10 @@ class PlayMissionGui extends GuiImage {
pmParText.text.text = '<font color="#FFE3E3" face="MarkerFelt24"><p align="center">${currentMission.game == "gold" ? "Qualify" : "Par"} Time: <font color="#FFFFFF">${(currentMission.qualifyTime != Math.POSITIVE_INFINITY) ? Util.formatTime(currentMission.qualifyTime) : "N/A"}</font></p></font>';
pmParTextRight.text.text = '';
}
} else {
pmParText.text.text = '';
pmParTextRight.text.text = '';
}
setScoreHover(scoreButtonHover);
@ -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";
}
}
}