leaderboards initial impl

This commit is contained in:
RandomityGuy 2024-11-20 23:46:22 +05:30
parent 3bf6d913d9
commit 4d2b41711f
15 changed files with 1773 additions and 18 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 {
@ -23,7 +24,7 @@ class Http {
public static function init() {
#if sys
threadPool = new sys.thread.FixedThreadPool(2);
threadPool = new sys.thread.FixedThreadPool(4);
threadPool.run(() -> threadLoop());
threadPool.run(() -> threadLoop());
threadPool.run(() -> threadLoop());
@ -50,15 +51,21 @@ class Http {
responses.add(() -> req.callback(b));
req.fulfilled = true;
};
hl.Gc.enable(false);
hl.Gc.blocking(true); // Wtf is this shit
// hl.Gc.blocking(true); // Wtf is this shit
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);
http.request(req.post);
hl.Gc.blocking(false);
// hl.Gc.blocking(false);
hl.Gc.enable(true);
}
}
@ -114,6 +121,36 @@ 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
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);

80
src/Leaderboards.hx Normal file
View file

@ -0,0 +1,80 @@
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,
}
enum abstract LeaderboardsKind(Int) {
var All;
var Rewind;
var NoRewind;
}
class Leaderboards {
static var host = "http://127.0.0.1:7000";
static var game = "Ultra";
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, 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) -> {
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, kind:LeaderboardsKind, 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}&view=${kind}', (b) -> {
cb(b);
}, (e) -> {
Console.log("Failed to get replay: " + e);
cb(null);
});
}
}

View file

@ -267,7 +267,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();
}));
@ -385,7 +385,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, !multiplayer, multiplayer);
world.init();
}

View file

@ -204,6 +204,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;
@ -602,6 +603,7 @@ class MarbleWorld extends Scheduler {
this.replay.rewind();
this.rewindManager.clear();
this.rewindUsed = false;
this.timeState.currentAttemptTime = 0;
this.timeState.gameplayClock = this.gameMode.getStartTime();
@ -1551,6 +1553,7 @@ class MarbleWorld extends Scheduler {
var actualDt = timeState.currentAttemptTime - rframe.timeState.currentAttemptTime - dt * rewindManager.timeScale;
dt = actualDt;
rewindManager.applyFrame(rframe);
rewindUsed = true;
}
}

View file

@ -500,6 +500,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

@ -19,6 +19,7 @@ import haxe.Json;
import src.Util;
import src.Console;
import src.Renderer;
import net.Uuid;
typedef Score = {
var name:String;
@ -211,6 +212,7 @@ class Settings {
public static var achievementProgression:Int;
public static var highscoreName = "Player";
public static var userId = "";
public static var uiScale = 1.0;
@ -269,6 +271,7 @@ class Settings {
gamepad: gamepadSettings,
stats: playStatistics,
highscoreName: highscoreName,
userId: userId,
marbleIndex: optionsSettings.marbleIndex,
marbleSkin: optionsSettings.marbleSkin,
marbleModel: optionsSettings.marbleModel,
@ -475,6 +478,13 @@ class Settings {
achievementProgression = 0;
#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

@ -246,6 +246,10 @@ class CollisionEntity implements IOctreeObject implements IBVHObject {
if (position.sub(closest).dot(surfacenormal) > 0) {
normal.normalize();
trace(v0);
trace(v);
trace(v2);
// We find the normal that is closest to the surface normal, sort of fixes weird edge cases of when colliding with
// var testDot = normal.dot(surfacenormal);
// if (testDot > bestDot) {

View file

@ -15,6 +15,7 @@ import h3d.Vector;
import src.ResourceLoader;
import src.TimeState;
import src.Util;
import src.Leaderboards;
class EndGameGui extends GuiImage {
var mission:Mission;
@ -134,6 +135,7 @@ class EndGameGui extends GuiImage {
// Settings.saveScore(mission.path, myScore, scoreType);
var scoreData:Array<Score> = Settings.getScores(mission.path);
while (scoreData.length < 1) {
if (scoreType == Score)
scoreData.push({name: "Nardo Polo", time: 0});
@ -167,7 +169,11 @@ class EndGameGui extends GuiImage {
retryButton.horizSizing = Right;
retryButton.gamepadAccelerator = ["B"];
retryButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE];
retryButton.pressedAction = (e) -> restartFunc(retryButton);
retryButton.pressedAction = (e) -> {
if (MarbleGame.canvas.children.length == 1)
restartFunc(retryButton);
}
bottomBar.addChild(retryButton);
}
@ -183,8 +189,34 @@ class EndGameGui extends GuiImage {
nextButton.horizSizing = Right;
nextButton.gamepadAccelerator = ["A"];
nextButton.accelerators = [hxd.Key.ENTER];
nextButton.pressedAction = (e) -> continueFunc(nextButton);
nextButton.pressedAction = (e) -> {
if (MarbleGame.canvas.children.length == 1)
continueFunc(nextButton);
}
bottomBar.addChild(nextButton);
if (bestScore.time == score) {
var submitScore = () -> {
var lbScoreValue = score;
if (scoreType == Score)
lbScoreValue = 1000 - score;
Leaderboards.submitScore(mission.path, lbScoreValue, MarbleGame.instance.world.rewindUsed, (needsReplay, ref) -> {
if (needsReplay) {
Leaderboards.submitReplay(ref, MarbleGame.instance.world.replay.write());
}
});
}
if (Settings.highscoreName == "" || Settings.highscoreName == "Player Name") {
haxe.Timer.delay(() -> {
MarbleGame.canvas.pushDialog(new EnterNamePopupDlg(() -> {
submitScore();
}));
}, 100);
} else {
submitScore();
}
}
}
override function onResize(width:Int, height:Int) {

View file

@ -0,0 +1,75 @@
package gui;
import h2d.Tile;
import hxd.BitmapData;
import h2d.filter.DropShadow;
import src.Settings;
import hxd.res.BitmapFont;
import h3d.Vector;
import src.ResourceLoader;
import src.MarbleGame;
class EnterNamePopupDlg extends GuiImage {
public function new(callback:Void->Void) {
var res = ResourceLoader.getImage("data/ui/xbox/roundedBG.png").resource.toTile();
super(res);
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
var arial14fontdata = ResourceLoader.getFileEntry("data/font/Arial Bold.fnt");
var arial14b = new BitmapFont(arial14fontdata.entry);
@:privateAccess arial14b.loader = ResourceLoader.loader;
var arial14 = arial14b.toSdfFont(cast 21 * Settings.uiScale, h2d.Font.SDFChannel.MultiChannel);
var yesNoFrame = new GuiImage(ResourceLoader.getResource("data/ui/xbox/popupGUI.png", ResourceLoader.getImage, this.imageResources).toTile());
yesNoFrame.horizSizing = Center;
yesNoFrame.vertSizing = Center;
yesNoFrame.position = new Vector(70, 30);
yesNoFrame.extent = new Vector(512, 400);
this.addChild(yesNoFrame);
var text = "Enter your name";
var yesNoText = new GuiMLText(arial14, null);
yesNoText.position = new Vector(103, 85);
yesNoText.extent = new Vector(313, 186);
yesNoText.text.text = text;
yesNoText.text.textColor = 0xEBEBEB;
yesNoFrame.addChild(yesNoText);
var textFrame = new GuiControl();
textFrame.position = new Vector(33, 107);
textFrame.extent = new Vector(232, 40);
textFrame.horizSizing = Center;
yesNoFrame.addChild(textFrame);
var textInput = new GuiTextInput(arial14);
textInput.position = new Vector(6, 5);
textInput.extent = new Vector(216, 40);
textInput.horizSizing = Width;
textInput.vertSizing = Height;
textInput.text.textColor = 0xEBEBEB;
textInput.setCaretColor(0xEBEBEB);
textInput.text.selectionColor.setColor(0x8DFF8D);
textInput.text.selectionTile = h2d.Tile.fromColor(0x88BCEE, 0, hxd.Math.ceil(textInput.text.font.lineHeight));
textFrame.addChild(textInput);
textInput.text.text = Settings.highscoreName == "" ? "Player Name" : Settings.highscoreName;
var okButton = new GuiXboxButton("Ok", 120);
okButton.position = new Vector(211, 248);
okButton.extent = new Vector(120, 94);
okButton.vertSizing = Top;
okButton.accelerators = [hxd.Key.ENTER];
okButton.gamepadAccelerator = ["A"];
okButton.pressedAction = (sender) -> {
Settings.highscoreName = textInput.text.text.substr(0, 15); // Max 15 pls
Settings.save();
MarbleGame.canvas.popDialog(this);
callback();
}
yesNoFrame.addChild(okButton);
}
}

View file

@ -7,7 +7,6 @@ import h2d.Bitmap;
import h3d.Engine;
import h3d.Vector;
import gui.GuiText.Justification;
import h2d.HtmlText;
import h2d.Scene;
import hxd.res.BitmapFont;
import h2d.Text;

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

@ -0,0 +1,904 @@
package gui;
import h2d.Bitmap;
import h2d.TileGroup;
import h2d.Font;
import h2d.Tile;
import h2d.RenderContext;
import h2d.Interactive;
import h2d.Object;
import h2d.Text;
/**
The `HtmlText` line height calculation rules.
**/
enum LineHeightMode {
/**
Accurate line height calculations. Each line will adjust it's height according to it's contents.
**/
Accurate;
/**
Only text adjusts line heights, and `<img>` tags do not affect it (partial legacy behavior).
**/
TextOnly;
/**
Legacy line height mode. When used, line heights remain constant based on `Text.font` variable.
**/
Constant;
}
/**
`HtmlText` img tag vertical alignment rules.
**/
enum ImageVerticalAlign {
/**
Align images along the top of the text line.
**/
Top;
/**
Align images to sit on the base line of the text.
**/
Bottom;
/**
Align images to the middle between the top of the text line its base line.
**/
Middle;
}
/**
A simple HTML text renderer.
See the [Text](https://github.com/HeapsIO/heaps/wiki/Text) section of the manual for more details and a list of the supported HTML tags.
**/
class HtmlText extends Text {
/**
A default method HtmlText uses to load images for `<img>` tag. See `HtmlText.loadImage` for details.
**/
public static dynamic function defaultLoadImage(url:String):h2d.Tile {
return null;
}
/**
A default method HtmlText uses to load fonts for `<font>` tags with `face` attribute. See `HtmlText.loadFont` for details.
**/
public static dynamic function defaultLoadFont(name:String):h2d.Font {
return null;
}
/**
A default method HtmlText uses to format assigned text. See `HtmlText.formatText` for details.
**/
public static dynamic function defaultFormatText(text:String):String {
return text;
}
/**
When enabled, condenses extra spaces (carriage-return, line-feed, tabulation and space character) to one space.
If not set, uncondensed whitespace is left as is, as well as line-breaks.
**/
public var condenseWhite(default, set):Bool = true;
/**
The spacing after `<img>` tags in pixels.
**/
public var imageSpacing(default, set):Float = 1;
/**
Line height calculation mode controls how much space lines take up vertically.
Changing mode to `Constant` restores the legacy behavior of HtmlText.
**/
public var lineHeightMode(default, set):LineHeightMode = Accurate;
/**
Vertical alignment of the images in `<img>` tag relative to the text.
**/
public var imageVerticalAlign(default, set):ImageVerticalAlign = Bottom;
var elements:Array<Object> = [];
var xPos:Float;
var yPos:Float;
var xMax:Float;
var xMin:Float;
var textXml:Xml;
var sizePos:Int;
var dropMatrix:h3d.shader.ColorMatrix;
var prevChar:Int;
var newLine:Bool;
var aHrefs:Array<String>;
var aInteractive:Interactive;
override function draw(ctx:RenderContext) {
if (dropShadow != null) {
var oldX = absX, oldY = absY;
absX += dropShadow.dx * matA + dropShadow.dy * matC;
absY += dropShadow.dx * matB + dropShadow.dy * matD;
if (dropMatrix == null) {
dropMatrix = new h3d.shader.ColorMatrix();
addShader(dropMatrix);
}
dropMatrix.enabled = true;
var m = dropMatrix.matrix;
m.zero();
m._41 = ((dropShadow.color >> 16) & 0xFF) / 255;
m._42 = ((dropShadow.color >> 8) & 0xFF) / 255;
m._43 = (dropShadow.color & 0xFF) / 255;
m._44 = dropShadow.alpha;
for (e in elements) {
if (e is TileGroup)
@:privateAccess (cast(e, TileGroup)).drawWith(ctx, this);
}
@:privateAccess glyphs.drawWith(ctx, this);
dropMatrix.enabled = false;
absX = oldX;
absY = oldY;
} else {
removeShader(dropMatrix);
dropMatrix = null;
}
@:privateAccess glyphs.drawWith(ctx, this);
}
override function getShader<T:hxsl.Shader>(stype:Class<T>):T {
if (shaders != null)
for (s in shaders) {
var c = Std.downcast(s, h3d.shader.ColorMatrix);
if (c != null && !c.enabled)
continue;
var s = hxd.impl.Api.downcast(s, stype);
if (s != null)
return s;
}
return null;
}
/**
Method that should return an `h2d.Tile` instance for `<img>` tags. By default calls `HtmlText.defaultLoadImage` method.
HtmlText does not cache tile instances.
Due to internal structure, method should be deterministic and always return same Tile on consequent calls with same `url` input.
@param url A value contained in `src` attribute.
**/
public dynamic function loadImage(url:String):Tile {
return defaultLoadImage(url);
}
/**
Method that should return an `h2d.Font` instance for `<font>` tags with `face` attribute. By default calls `HtmlText.defaultLoadFont` method.
HtmlText does not cache font instances and it's recommended to perform said caching from outside.
Due to internal structure, method should be deterministic and always return same Font instance on consequent calls with same `name` input.
@param name A value contained in `face` attribute.
@returns Method should return loaded font instance or `null`. If `null` is returned - currently active font is used.
**/
public dynamic function loadFont(name:String):Font {
var f = defaultLoadFont(name);
if (f == null)
return this.font;
else
return f;
}
/**
Called on a <a> tag click
**/
public dynamic function onHyperlink(url:String):Void {}
/**
Called when text is assigned, allowing to process arbitrary text to a valid XHTML.
**/
public dynamic function formatText(text:String):String {
return defaultFormatText(text);
}
override function set_text(t:String) {
super.set_text(formatText(t));
return t;
}
function parseText(text:String) {
return try Xml.parse(text) catch (e:Dynamic) throw "Could not parse " + text + " (" + e + ")";
}
inline function makeLineInfo(width:Float, height:Float, baseLine:Float):LineInfo {
return {width: width, height: height, baseLine: baseLine};
}
override function validateText() {
textXml = parseText(text);
validateNodes(textXml);
}
function validateNodes(xml:Xml) {
switch (xml.nodeType) {
case Element:
var nodeName = xml.nodeName.toLowerCase();
switch (nodeName) {
case "img":
loadImage(xml.get("src"));
case "font":
if (xml.exists("face")) {
loadFont(xml.get("face"));
}
case "b", "bold":
loadFont("bold");
case "i", "italic":
loadFont("italic");
}
for (child in xml)
validateNodes(child);
case Document:
for (child in xml)
validateNodes(child);
default:
}
}
override function initGlyphs(text:String, rebuild = true) {
if (rebuild) {
glyphs.clear();
for (e in elements)
e.remove();
elements = [];
}
glyphs.setDefaultColor(textColor);
var doc:Xml;
if (textXml == null) {
doc = parseText(text);
} else {
doc = textXml;
}
yPos = 0;
xMax = 0;
xMin = Math.POSITIVE_INFINITY;
sizePos = 0;
calcYMin = 0;
var metrics:Array<LineInfo> = [makeLineInfo(0, font.lineHeight, font.baseLine)];
prevChar = -1;
newLine = true;
var splitNode:SplitNode = {
node: null,
pos: 0,
font: font,
prevChar: -1,
width: 0,
height: 0,
baseLine: 0
};
for (e in doc)
buildSizes(e, font, metrics, splitNode);
var max = 0.;
for (info in metrics) {
if (info.width > max)
max = info.width;
}
calcWidth = max;
prevChar = -1;
newLine = true;
nextLine(textAlign, metrics[0].width);
for (e in doc)
addNode(e, font, textAlign, rebuild, metrics);
if (xPos > xMax)
xMax = xPos;
textXml = null;
var y = yPos;
calcXMin = xMin;
calcWidth = xMax - xMin;
calcHeight = y + metrics[sizePos].height;
calcSizeHeight = y + metrics[sizePos].baseLine; // (font.baseLine > 0 ? font.baseLine : font.lineHeight);
calcDone = true;
if (rebuild)
needsRebuild = false;
}
function buildSizes(e:Xml, font:Font, metrics:Array<LineInfo>, splitNode:SplitNode) {
function wordSplit() {
var fnt = splitNode.font;
var str = splitNode.node.nodeValue;
var info = metrics[metrics.length - 1];
var w = info.width;
var cc = str.charCodeAt(splitNode.pos);
// Restore line metrics to ones before split.
// Potential bug: `Text<split> [Image] text<split>text` - third line will use metrics as if image is present in the line.
info.width = splitNode.width;
info.height = splitNode.height;
info.baseLine = splitNode.baseLine;
var char = fnt.getChar(cc);
if (lineBreak && fnt.charset.isSpace(cc)) {
// Space characters are converted to \n
w -= (splitNode.width + letterSpacing + char.width + char.getKerningOffset(splitNode.prevChar));
splitNode.node.nodeValue = str.substr(0, splitNode.pos) + "\n" + str.substr(splitNode.pos + 1);
} else {
w -= (splitNode.width + letterSpacing + char.getKerningOffset(splitNode.prevChar));
splitNode.node.nodeValue = str.substr(0, splitNode.pos + 1) + "\n" + str.substr(splitNode.pos + 1);
}
splitNode.node = null;
return w;
}
inline function lineFont() {
return lineHeightMode == Constant ? this.font : font;
}
if (e.nodeType == Xml.Element) {
inline function makeLineBreak() {
var fontInfo = lineFont();
metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
splitNode.node = null;
newLine = true;
prevChar = -1;
}
var nodeName = e.nodeName.toLowerCase();
switch (nodeName) {
case "p":
if (!newLine) {
makeLineBreak();
}
case "br":
makeLineBreak();
case "img":
// TODO: Support width/height attributes
// Support max-width/max-height attributes (downscale)
// Support min-width/min-height attributes (upscale)
var i:Tile = loadImage(e.get("src"));
if (i == null)
i = Tile.fromColor(0xFF00FF, 8, 8);
var size = metrics[metrics.length - 1].width + i.width + imageSpacing;
if (realMaxWidth >= 0 && size > realMaxWidth && metrics[metrics.length - 1].width > 0) {
if (splitNode.node != null) {
size = wordSplit() + i.width + imageSpacing;
var info = metrics[metrics.length - 1];
// Bug: height/baseLine may be innacurate in case of sizeA sizeB<split>sizeA where sizeB is larger.
switch (lineHeightMode) {
case Accurate:
var grow = i.height - i.dy - info.baseLine;
var h = info.height;
var bl = info.baseLine;
if (grow > 0) {
h += grow;
bl += grow;
}
metrics.push(makeLineInfo(size, Math.max(h, bl + i.dy), bl));
default:
metrics.push(makeLineInfo(size, info.height, info.baseLine));
}
}
} else {
var info = metrics[metrics.length - 1];
info.width = size;
if (lineHeightMode == Accurate) {
var grow = i.height - i.dy - info.baseLine;
if (grow > 0) {
switch (imageVerticalAlign) {
case Top:
info.height += grow;
case Bottom:
info.baseLine += grow;
info.height += grow;
case Middle:
info.height += grow;
info.baseLine += Std.int(grow / 2);
}
}
grow = info.baseLine + i.dy;
if (info.height < grow)
info.height = grow;
}
}
newLine = false;
prevChar = -1;
case "font":
for (a in e.attributes()) {
var v = e.get(a);
switch (a.toLowerCase()) {
case "face": font = loadFont(v);
default:
}
}
case "b", "bold":
font = loadFont("bold");
case "i", "italic":
font = loadFont("italic");
default:
}
for (child in e)
buildSizes(child, font, metrics, splitNode);
switch (nodeName) {
case "p":
if (!newLine) {
makeLineBreak();
}
default:
}
} else if (e.nodeValue.length != 0) {
newLine = false;
var text = htmlToText(e.nodeValue);
var fontInfo = lineFont();
var info:LineInfo = metrics.pop();
var leftMargin = info.width;
var maxWidth = realMaxWidth < 0 ? Math.POSITIVE_INFINITY : realMaxWidth;
var textSplit = [], restPos = 0;
var x = leftMargin;
var breakChars = 0;
for (i in 0...text.length) {
var cc = text.charCodeAt(i);
var g = font.getChar(cc);
var newline = cc == '\n'.code;
var esize = g.width + g.getKerningOffset(prevChar);
var nc = text.charCodeAt(i + 1);
if (font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc))) {
// Case: Very first word in text makes the line too long hence we want to start it off on a new line.
if (x > maxWidth && textSplit.length == 0 && splitNode.node != null) {
metrics.push(makeLineInfo(x, info.height, info.baseLine));
x = wordSplit();
}
var size = x + esize + letterSpacing;
var k = i + 1, max = text.length;
var prevChar = cc;
while (size <= maxWidth && k < max) {
var cc = text.charCodeAt(k++);
if (lineBreak && (font.charset.isSpace(cc) || cc == '\n'.code))
break;
var e = font.getChar(cc);
size += e.width + letterSpacing + e.getKerningOffset(prevChar);
prevChar = cc;
var nc = text.charCodeAt(k);
if (font.charset.isBreakChar(cc) && (nc == null || !font.charset.isComplementChar(nc)))
break;
}
// Avoid empty line when last char causes line-break while being CJK
if (lineBreak && size > maxWidth && i != max - 1) {
// Next word will reach maxWidth
newline = true;
if (font.charset.isSpace(cc)) {
textSplit.push(text.substr(restPos, i - restPos));
g = null;
} else {
textSplit.push(text.substr(restPos, i + 1 - restPos));
breakChars++;
}
splitNode.node = null;
restPos = i + 1;
} else {
splitNode.node = e;
splitNode.pos = i + breakChars;
splitNode.prevChar = this.prevChar;
splitNode.width = x;
splitNode.height = info.height;
splitNode.baseLine = info.baseLine;
splitNode.font = font;
}
}
if (g != null && cc != '\n'.code)
x += esize + letterSpacing;
if (newline) {
metrics.push(makeLineInfo(x, info.height, info.baseLine));
info.height = fontInfo.lineHeight;
info.baseLine = fontInfo.baseLine;
x = 0;
prevChar = -1;
newLine = true;
} else {
prevChar = cc;
newLine = false;
}
}
if (restPos < text.length) {
if (x > maxWidth) {
if (splitNode.node != null && splitNode.node != e) {
metrics.push(makeLineInfo(x, info.height, info.baseLine));
x = wordSplit();
}
}
textSplit.push(text.substr(restPos));
metrics.push(makeLineInfo(x, info.height, info.baseLine));
}
if (newLine || metrics.length == 0) {
metrics.push(makeLineInfo(0, fontInfo.lineHeight, fontInfo.baseLine));
textSplit.push("");
}
// Save node value
e.nodeValue = textSplit.join("\n");
}
}
static var REG_SPACES = ~/[\r\n\t ]+/g;
function htmlToText(t:String) {
if (condenseWhite)
t = REG_SPACES.replace(t, " ");
return t;
}
inline function nextLine(align:Align, size:Float) {
switch (align) {
case Left:
xPos = 0;
if (xMin > 0)
xMin = 0;
case Right, Center, MultilineCenter, MultilineRight:
var max = if (align == MultilineCenter || align == MultilineRight) hxd.Math.ceil(calcWidth) else
calcWidth < 0 ? 0 : hxd.Math.ceil(realMaxWidth);
var k = align == Center || align == MultilineCenter ? 0.5 : 1;
xPos = Math.ffloor((max - size) * k);
if (xPos < xMin)
xMin = xPos;
}
}
override function splitText(text:String):String {
if (realMaxWidth < 0)
return text;
yPos = 0;
xMax = 0;
sizePos = 0;
calcYMin = 0;
var doc = parseText(text);
/*
This might require a global refactoring at some point.
We would need a way to somehow build an AST from the XML representation
with all sizes and word breaks so analysis is much more easy.
*/
var splitNode:SplitNode = {
node: null,
font: font,
width: 0,
height: 0,
baseLine: 0,
pos: 0,
prevChar: -1
};
var metrics = [makeLineInfo(0, font.lineHeight, font.baseLine)];
prevChar = -1;
newLine = true;
for (e in doc)
buildSizes(e, font, metrics, splitNode);
xMax = 0;
function addBreaks(e:Xml) {
if (e.nodeType == Xml.Element) {
for (x in e)
addBreaks(x);
} else {
var text = e.nodeValue;
var startI = 0;
var index = Lambda.indexOf(e.parent, e);
for (i in 0...text.length) {
if (text.charCodeAt(i) == '\n'.code) {
var pre = text.substring(startI, i);
if (pre != "")
e.parent.insertChild(Xml.createPCData(pre), index++);
e.parent.insertChild(Xml.createElement("br"), index++);
startI = i + 1;
}
}
if (startI < text.length) {
e.nodeValue = text.substr(startI);
} else {
e.parent.removeChild(e);
}
}
}
for (d in doc)
addBreaks(d);
return doc.toString();
}
override function getTextProgress(text:String, progress:Float):String {
if (progress >= text.length)
return text;
var doc = parseText(text);
function progressRec(e:Xml) {
if (progress <= 0) {
e.parent.removeChild(e);
return;
}
if (e.nodeType == Xml.Element) {
for (x in [for (x in e) x])
progressRec(x);
} else {
var text = htmlToText(e.nodeValue);
var len = text.length;
if (len > progress) {
text = text.substr(0, Std.int(progress));
e.nodeValue = text;
}
progress -= len;
}
}
for (x in [for (x in doc) x])
progressRec(x);
return doc.toString();
}
function addNode(e:Xml, font:Font, align:Align, rebuild:Bool, metrics:Array<LineInfo>) {
inline function createInteractive() {
if (aHrefs == null || aHrefs.length == 0)
return;
aInteractive = new Interactive(0, metrics[sizePos].height, this);
var href = aHrefs[aHrefs.length - 1];
aInteractive.onClick = function(event) {
onHyperlink(href);
}
aInteractive.x = xPos;
aInteractive.y = yPos;
elements.push(aInteractive);
}
inline function finalizeInteractive() {
if (aInteractive != null) {
aInteractive.width = xPos - aInteractive.x;
aInteractive = null;
}
}
inline function makeLineBreak() {
finalizeInteractive();
if (xPos > xMax)
xMax = xPos;
yPos += metrics[sizePos].height + lineSpacing;
nextLine(align, metrics[++sizePos].width);
createInteractive();
}
if (e.nodeType == Xml.Element) {
var prevColor = null, prevGlyphs = null;
var oldAlign = align;
var nodeName = e.nodeName.toLowerCase();
inline function setFont(v:String) {
font = loadFont(v);
if (prevGlyphs == null)
prevGlyphs = glyphs;
var prev = glyphs;
glyphs = new TileGroup(font == null ? null : font.tile, this);
if (font != null) {
switch (font.type) {
case SignedDistanceField(channel, alphaCutoff, smoothing):
var shader = new h3d.shader.SignedDistanceField();
shader.channel = channel;
shader.alphaCutoff = alphaCutoff;
shader.smoothing = smoothing;
shader.autoSmoothing = smoothing == -1;
glyphs.smooth = this.smooth;
glyphs.addShader(shader);
default:
}
}
@:privateAccess glyphs.curColor.load(prev.curColor);
elements.push(glyphs);
}
switch (nodeName) {
case "font":
for (a in e.attributes()) {
var v = e.get(a);
switch (a.toLowerCase()) {
case "color":
if (prevColor == null)
prevColor = @:privateAccess glyphs.curColor.clone();
if (v.charCodeAt(0) == '#'.code && v.length == 4)
v = "#" + v.charAt(1) + v.charAt(1) + v.charAt(2) + v.charAt(2) + v.charAt(3) + v.charAt(3);
glyphs.setDefaultColor(Std.parseInt("0x" + v.substr(1)));
case "opacity":
if (prevColor == null)
prevColor = @:privateAccess glyphs.curColor.clone();
@:privateAccess glyphs.curColor.a *= Std.parseFloat(v);
case "face":
setFont(v);
default:
}
}
case "p":
for (a in e.attributes()) {
switch (a.toLowerCase()) {
case "align":
var v = e.get(a);
if (v != null) switch (v.toLowerCase()) {
case "left":
align = Left;
case "center":
align = Center;
case "right":
align = Right;
case "multiline-center":
align = MultilineCenter;
case "multiline-right":
align = MultilineRight;
// ?justify
}
default:
}
}
if (!newLine) {
makeLineBreak();
newLine = true;
prevChar = -1;
} else {
nextLine(align, metrics[sizePos].width);
}
case "offset":
for (a in e.attributes()) {
switch (a.toLowerCase()) {
case "value":
var v = e.get(a);
if (v != null) xPos = Std.parseFloat(v);
default:
}
}
// nextLine(align, metrics[sizePos].width);
case "b", "bold":
setFont("bold");
case "i", "italic":
setFont("italic");
case "br":
makeLineBreak();
newLine = true;
prevChar = -1;
case "img":
var i:Tile = loadImage(e.get("src"));
if (i == null)
i = Tile.fromColor(0xFF00FF, 8, 8);
var py = yPos;
switch (imageVerticalAlign) {
case Bottom:
py += metrics[sizePos].baseLine - i.height;
case Middle:
py += metrics[sizePos].baseLine - i.height / 2;
case Top:
}
if (py + i.dy < calcYMin)
calcYMin = py + i.dy;
if (rebuild) {
var b = new Bitmap(i, this);
b.x = xPos;
b.y = py;
elements.push(b);
}
newLine = false;
prevChar = -1;
xPos += i.width + imageSpacing;
case "a":
if (e.exists("href")) {
finalizeInteractive();
if (aHrefs == null)
aHrefs = [];
aHrefs.push(e.get("href"));
createInteractive();
}
default:
}
for (child in e)
addNode(child, font, align, rebuild, metrics);
align = oldAlign;
switch (nodeName) {
case "p":
if (newLine) {
nextLine(align, metrics[sizePos].width);
} else if (sizePos < metrics.length - 2 || metrics[sizePos + 1].width != 0) {
// Condition avoid extra empty line if <p> was the last tag.
makeLineBreak();
newLine = true;
prevChar = -1;
}
case "a":
if (aHrefs.length > 0) {
finalizeInteractive();
aHrefs.pop();
createInteractive();
}
default:
}
if (prevGlyphs != null)
glyphs = prevGlyphs;
if (prevColor != null)
@:privateAccess glyphs.curColor.load(prevColor);
} else if (e.nodeValue.length != 0) {
newLine = false;
var t = e.nodeValue;
var dy = metrics[sizePos].baseLine - font.baseLine;
for (i in 0...t.length) {
var cc = t.charCodeAt(i);
if (cc == "\n".code) {
makeLineBreak();
dy = metrics[sizePos].baseLine - font.baseLine;
prevChar = -1;
continue;
} else {
var fc = font.getChar(cc);
if (fc != null) {
xPos += fc.getKerningOffset(prevChar);
if (rebuild)
glyphs.add(xPos, yPos + dy, fc.t);
if (yPos == 0 && fc.t.dy + dy < calcYMin)
calcYMin = fc.t.dy + dy;
xPos += fc.width + letterSpacing;
}
prevChar = cc;
}
}
}
}
function set_imageSpacing(s) {
if (imageSpacing == s)
return s;
imageSpacing = s;
rebuild();
return s;
}
override function set_textColor(c) {
if (this.textColor == c)
return c;
this.textColor = c;
rebuild();
return c;
}
function set_condenseWhite(value:Bool) {
if (this.condenseWhite != value) {
this.condenseWhite = value;
rebuild();
}
return value;
}
function set_imageVerticalAlign(align) {
if (this.imageVerticalAlign != align) {
this.imageVerticalAlign = align;
rebuild();
}
return align;
}
function set_lineHeightMode(v) {
if (this.lineHeightMode != v) {
this.lineHeightMode = v;
rebuild();
}
return v;
}
override function getBoundsRec(relativeTo:Object, out:h2d.col.Bounds, forSize:Bool) {
if (forSize)
for (i in elements)
if (hxd.impl.Api.isOfType(i, h2d.Bitmap))
i.visible = false;
super.getBoundsRec(relativeTo, out, forSize);
if (forSize)
for (i in elements)
i.visible = true;
}
}
private typedef LineInfo = {
var width:Float;
var height:Float;
var baseLine:Float;
}
private typedef SplitNode = {
var node:Xml;
var prevChar:Int;
var pos:Int;
var width:Float;
var height:Float;
var baseLine:Float;
var font:h2d.Font;
}

291
src/gui/LeaderboardsGui.hx Normal file
View file

@ -0,0 +1,291 @@
package gui;
import net.ClientConnection.NetPlatform;
import src.Util;
import src.Leaderboards.LeaderboardsKind;
import src.Mission;
import hxd.res.BitmapFont;
import src.ResourceLoader;
import src.Settings;
import h3d.Vector;
import src.MarbleGame;
import src.MissionList;
import src.Leaderboards;
import src.Replay;
import gui.HtmlText;
class LeaderboardsGui extends GuiImage {
var innerCtrl:GuiControl;
public function new(index:Int, levelSelectDifficulty:String, levelSelectGui:Bool = false,) {
var res = ResourceLoader.getImage("data/ui/xbox/BG_fadeOutSoftEdge.png").resource.toTile();
super(res);
var domcasual32fontdata = ResourceLoader.getFileEntry("data/font/DomCasualD.fnt");
var domcasual32b = new BitmapFont(domcasual32fontdata.entry);
@:privateAccess domcasual32b.loader = ResourceLoader.loader;
var domcasual32 = domcasual32b.toSdfFont(cast 42 * Settings.uiScale, MultiChannel);
this.horizSizing = Width;
this.vertSizing = Height;
this.position = new Vector();
this.extent = new Vector(640, 480);
#if hl
var scene2d = hxd.Window.getInstance();
#end
#if js
var scene2d = MarbleGame.instance.scene2d;
#end
var offsetX = (scene2d.width - 1280) / 2;
var offsetY = (scene2d.height - 720) / 2;
var subX = 640 - (scene2d.width - offsetX) * 640 / scene2d.width;
var subY = 480 - (scene2d.height - offsetY) * 480 / scene2d.height;
innerCtrl = new GuiControl();
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
innerCtrl.horizSizing = Width;
innerCtrl.vertSizing = Height;
this.addChild(innerCtrl);
var coliseumfontdata = ResourceLoader.getFileEntry("data/font/ColiseumRR.fnt");
var coliseumb = new BitmapFont(coliseumfontdata.entry);
@:privateAccess coliseumb.loader = ResourceLoader.loader;
var coliseum = coliseumb.toSdfFont(cast 44 * Settings.uiScale, MultiChannel);
var rootTitle = new GuiText(coliseum);
rootTitle.position = new Vector(100, 30);
rootTitle.extent = new Vector(1120, 80);
rootTitle.text.textColor = 0xFFFFFF;
rootTitle.text.text = "LEADERBOARDS";
rootTitle.text.alpha = 0.5;
innerCtrl.addChild(rootTitle);
var levelTitle = new GuiText(coliseum);
levelTitle.position = new Vector(-100, 30);
levelTitle.extent = new Vector(1120, 80);
levelTitle.text.textColor = 0xFFFFFF;
levelTitle.text.text = "Level 1";
levelTitle.text.textAlign = Right;
levelTitle.justify = Right;
levelTitle.text.alpha = 0.5;
innerCtrl.addChild(levelTitle);
var scoreWnd = new GuiImage(ResourceLoader.getResource("data/ui/xbox/helpWindow.png", ResourceLoader.getImage, this.imageResources).toTile());
scoreWnd.horizSizing = Right;
scoreWnd.vertSizing = Bottom;
scoreWnd.position = new Vector(260, 107);
scoreWnd.extent = new Vector(736, 325);
innerCtrl.addChild(scoreWnd);
function imgLoader(path:String) {
var t = switch (path) {
case "ready":
ResourceLoader.getResource("data/ui/xbox/Ready.png", ResourceLoader.getImage, this.imageResources).toTile();
case "notready":
ResourceLoader.getResource("data/ui/xbox/NotReady.png", ResourceLoader.getImage, this.imageResources).toTile();
case "pc":
ResourceLoader.getResource("data/ui/xbox/platform_desktop_white.png", ResourceLoader.getImage, this.imageResources).toTile();
case "mac":
ResourceLoader.getResource("data/ui/xbox/platform_mac_white.png", ResourceLoader.getImage, this.imageResources).toTile();
case "web":
ResourceLoader.getResource("data/ui/xbox/platform_web_white.png", ResourceLoader.getImage, this.imageResources).toTile();
case "android":
ResourceLoader.getResource("data/ui/xbox/platform_android_white.png", ResourceLoader.getImage, this.imageResources).toTile();
case "unknown":
ResourceLoader.getResource("data/ui/xbox/platform_unknown_white.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 arial14fontdata = ResourceLoader.getFileEntry("data/font/Arial Bold.fnt");
var arial14b = new BitmapFont(arial14fontdata.entry);
@:privateAccess arial14b.loader = ResourceLoader.loader;
var arial14 = arial14b.toSdfFont(cast 21 * Settings.uiScale, h2d.Font.SDFChannel.MultiChannel);
var arial12 = arial14b.toSdfFont(cast 16 * Settings.uiScale, h2d.Font.SDFChannel.MultiChannel);
function mlFontLoader(text:String) {
switch (text) {
case "arial14":
return arial14;
case "arial12":
return arial12;
}
return null;
}
var headerText = '<font face="arial12">Rank<offset value="50">Name</offset><offset value="500">Score</offset><offset value="600">Platform</offset></font>';
var scores = [
'<offset value="10">1. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="unknown"/></offset>',
'<offset value="10">2. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="pc"/></offset>',
'<offset value="10">3. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="mac"/></offset>',
'<offset value="10">4. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="web"/></offset>',
'<offset value="10">5. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="android"/></offset>',
'<offset value="10">6. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="unknown"/></offset>',
'<offset value="10">7. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="pc"/></offset>',
'<offset value="10">8. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="mac"/></offset>',
'<offset value="10">9. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="web"/></offset>',
'<offset value="10">10. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59:999</offset><offset value="625"><img src="android"/></offset>',
];
var scoreCtrl = new GuiMLText(arial14, mlFontLoader);
scoreCtrl.position = new Vector(30, 20);
scoreCtrl.extent = new Vector(706, 305);
scoreCtrl.text.imageVerticalAlign = Top;
scoreCtrl.text.loadImage = imgLoader;
scoreCtrl.text.text = headerText + "<br/><br/><br/><br/><br/>" + '<p align="center">Loading...</p>';
scoreWnd.addChild(scoreCtrl);
var allMissions = MissionList.missionList.get('ultra')
.get('beginner')
.concat(MissionList.missionList.get('ultra').get('intermediate'))
.concat(MissionList.missionList.get('ultra').get('advanced'));
var actualIndex = allMissions.indexOf(MissionList.missionList.get('ultra').get(levelSelectDifficulty)[index]);
levelTitle.text.text = 'Level ${actualIndex + 1}';
var levelNames = allMissions.map(x -> x.title);
var scoreCategories = ["Overall", "Rewind", "Non-Rewind"];
var scoreView:LeaderboardsKind = All;
var currentMission = allMissions[actualIndex];
var scoreTok = 0;
function fetchScores() {
var ourToken = scoreTok++;
Leaderboards.getScores(currentMission.path, scoreView, (scoreList) -> {
if (ourToken + 1 != scoreTok)
return;
var scoreTexts = [];
var i = 1;
for (score in scoreList) {
var scoreText = '<offset value="10">${i}. </offset><offset value="50">${score.name}</offset><offset value="500">${Util.formatTime(score.score)}</offset><offset value="625"><img src="${platformToString(score.platform)}"/></offset>';
scoreTexts.push(scoreText);
i++;
}
while (i <= 10) {
var scoreText = '<offset value="10">${i}. </offset><offset value="50">Nardo Polo</offset><offset value="500">99:59.99</offset><offset value="625"><img src="unknown"/></offset>';
scoreTexts.push(scoreText);
i++;
}
scoreCtrl.text.text = headerText + "<br/>" + scoreTexts.join('<br/>');
});
scoreCtrl.text.text = headerText + "<br/><br/><br/><br/><br/>" + '<p align="center">Loading...</p>';
}
var levelSelectOpts = new GuiXboxOptionsList(2, "Overall", levelNames);
levelSelectOpts.position = new Vector(380, 485);
levelSelectOpts.extent = new Vector(815, 94);
levelSelectOpts.vertSizing = Bottom;
levelSelectOpts.horizSizing = Right;
levelSelectOpts.alwaysActive = true;
levelSelectOpts.onChangeFunc = (l) -> {
levelTitle.text.text = 'Level ${l + 1}';
currentMission = allMissions[l];
fetchScores();
return true;
}
levelSelectOpts.setCurrentOption(actualIndex);
innerCtrl.addChild(levelSelectOpts);
var bottomBar = new GuiControl();
bottomBar.position = new Vector(0, 590);
bottomBar.extent = new Vector(640, 200);
bottomBar.horizSizing = Width;
bottomBar.vertSizing = Bottom;
innerCtrl.addChild(bottomBar);
var backButton = new GuiXboxButton("Back", 160);
backButton.position = new Vector(400, 0);
backButton.vertSizing = Bottom;
backButton.horizSizing = Right;
backButton.gamepadAccelerator = ["B"];
backButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE];
if (levelSelectGui)
backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new LevelSelectGui(levelSelectDifficulty));
else {
backButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new MainMenuGui());
}
bottomBar.addChild(backButton);
var changeViewButton = new GuiXboxButton("Change View", 200);
changeViewButton.position = new Vector(560, 0);
changeViewButton.vertSizing = Bottom;
changeViewButton.horizSizing = Right;
changeViewButton.gamepadAccelerator = ["X"];
changeViewButton.pressedAction = (e) -> {
scoreView = scoreView == All ? Rewind : (scoreView == Rewind ? NoRewind : All);
levelSelectOpts.labelText.text.text = scoreCategories[cast(scoreView, Int)];
}
bottomBar.addChild(changeViewButton);
var replayButton = new GuiXboxButton("Watch Replay", 220);
replayButton.position = new Vector(750, 0);
replayButton.vertSizing = Bottom;
replayButton.gamepadAccelerator = ["Y"];
replayButton.horizSizing = Right;
replayButton.pressedAction = (e) -> {
Leaderboards.watchTopReplay(currentMission.path, scoreView, (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 = MissionList.missions.get(repmis);
// try with data/ added
if (mi == null) {
if (!StringTools.contains(repmis, "data/"))
repmis = "data/" + repmis;
mi = MissionList.missions.get(repmis);
}
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."));
}
});
}
bottomBar.addChild(replayButton);
fetchScores();
}
override function onResize(width:Int, height:Int) {
var offsetX = (width - 1280) / 2;
var offsetY = (height - 720) / 2;
var subX = 640 - (width - offsetX) * 640 / width;
var subY = 480 - (height - offsetY) * 480 / height;
innerCtrl.position = new Vector(offsetX, offsetY);
innerCtrl.extent = new Vector(640 - subX, 480 - subY);
super.onResize(width, height);
}
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";
}
}
}

View file

@ -160,11 +160,15 @@ class LevelSelectGui extends GuiImage {
}
bottomBar.addChild(recordButton);
// var lbButton = new GuiXboxButton("Leaderboard", 220);
// lbButton.position = new Vector(750, 0);
// lbButton.vertSizing = Bottom;
// lbButton.horizSizing = Right;
// bottomBar.addChild(lbButton);
if (currentDifficultyStatic != "multiplayer") {
var lbButton = new GuiXboxButton("Leaderboard", 220);
lbButton.position = new Vector(750, 0);
lbButton.vertSizing = Bottom;
lbButton.gamepadAccelerator = ["Y"];
lbButton.horizSizing = Right;
lbButton.pressedAction = (e) -> MarbleGame.canvas.setContent(new LeaderboardsGui(currentSelectionStatic, currentDifficultyStatic, true));
bottomBar.addChild(lbButton);
}
var nextButton = new GuiXboxButton("Play", 160);
nextButton.position = new Vector(960, 0);

View file

@ -65,7 +65,7 @@ class MainMenuGui extends GuiImage {
btnList = new GuiXboxList();
btnList.position = new Vector(70 - offsetX, 95);
btnList.horizSizing = Left;
btnList.extent = new Vector(502, 500);
btnList.extent = new Vector(502, 530);
innerCtrl.addChild(btnList);
btnList.addButton(0, "Single Player Game", (sender) -> {
@ -82,7 +82,9 @@ class MainMenuGui extends GuiImage {
cast(this.parent, Canvas).setContent(new MultiplayerGui());
}
});
// btnList.addButton(2, "Leaderboards", (e) -> {}, 20);
btnList.addButton(2, "Leaderboards", (e) -> {
cast(this.parent, Canvas).setContent(new LeaderboardsGui(0, "beginner", false));
}, 20);
btnList.addButton(2, "Achievements", (e) -> {
cast(this.parent, Canvas).setContent(new AchievementsGui());
});

311
src/net/Uuid.hx Normal file
View file

@ -0,0 +1,311 @@
package net;
/*
MIT License
https://github.com/flashultra/uuid
Copyright (c) 2020
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import haxe.ds.Vector;
import haxe.Int64;
import haxe.io.Bytes;
import haxe.crypto.Md5;
import haxe.crypto.Sha1;
class Uuid {
public inline static var DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
public inline static var URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
public inline static var ISO_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
public inline static var X500_DN = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
public inline static var NIL = '00000000-0000-0000-0000-000000000000';
public inline static var LOWERCASE_BASE26 = "abcdefghijklmnopqrstuvwxyz";
public inline static var UPPERCASE_BASE26 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public inline static var NO_LOOK_ALIKES_BASE51 = "2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnpqrtwxyz"; // without 1, l, I, 0, O, o, u, v, 5, S, s
public inline static var FLICKR_BASE58 = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; // without similar characters 0/O, 1/I/l
public inline static var BASE_70 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-+!@#$^";
public inline static var BASE_85 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";
public inline static var COOKIE_BASE90 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+-./:<=>?@[]^_`{|}~";
public inline static var NANO_ID_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public inline static var NUMBERS_BIN = "01";
public inline static var NUMBERS_OCT = "01234567";
public inline static var NUMBERS_DEC = "0123456789";
public inline static var NUMBERS_HEX = "0123456789abcdef";
static var lastMSecs:Float = 0;
static var lastNSecs = 0;
static var clockSequenceBuffer:Int = -1;
static var regexp:EReg = ~/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
static var rndSeed:Int64 = Int64.fromFloat(#if js js.lib.Date.now() #elseif sys Sys.time() * 1000 #else Date.now().getTime() #end);
static var state0 = splitmix64_seed(rndSeed);
static var state1 = splitmix64_seed(rndSeed + Std.random(10000) + 1);
private static var DVS:Int64 = Int64.make(0x00000001, 0x00000000);
private static function splitmix64_seed(index:Int64):Int64 {
var result:Int64 = (index + Int64.make(0x9E3779B9, 0x7F4A7C15));
result = (result ^ (result >> 30)) * Int64.make(0xBF58476D, 0x1CE4E5B9);
result = (result ^ (result >> 27)) * Int64.make(0x94D049BB, 0x133111EB);
return result ^ (result >> 31);
}
public static function randomFromRange(min:Int, max:Int):Int {
var s1:Int64 = state0;
var s0:Int64 = state1;
state0 = s0;
s1 ^= s1 << 23;
state1 = s1 ^ s0 ^ (s1 >>> 18) ^ (s0 >>> 5);
var result:Int = ((state1 + s0) % (max - min + 1)).low;
result = (result < 0) ? -result : result;
return result + min;
}
public static function randomByte():Int {
return randomFromRange(0, 255);
}
public static function fromShort(shortUuid:String, separator:String = '-', fromAlphabet:String = FLICKR_BASE58):String {
var uuid = Uuid.convert(shortUuid, fromAlphabet, NUMBERS_HEX);
return hexToUuid(uuid, separator);
}
public static function toShort(uuid:String, separator:String = '-', toAlphabet:String = FLICKR_BASE58):String {
uuid = StringTools.replace(uuid, separator, '').toLowerCase();
return Uuid.convert(uuid, NUMBERS_HEX, toAlphabet);
}
public static function fromNano(nanoUuid:String, separator:String = '-', fromAlphabet:String = NANO_ID_ALPHABET):String {
var uuid = Uuid.convert(nanoUuid, fromAlphabet, NUMBERS_HEX);
return hexToUuid(uuid, separator);
}
public static function toNano(uuid:String, separator:String = '-', toAlphabet:String = NANO_ID_ALPHABET):String {
uuid = StringTools.replace(uuid, separator, '').toLowerCase();
return Uuid.convert(uuid, NUMBERS_HEX, toAlphabet);
}
public static function v1(node:Bytes = null, optClockSequence:Int = -1, msecs:Float = -1, optNsecs:Int = -1, ?randomFunc:Void->Int,
separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String {
if (randomFunc == null)
randomFunc = randomByte;
var buffer:Bytes = Bytes.alloc(16);
if (node == null) {
node = Bytes.alloc(6);
for (i in 0...6)
node.set(i, randomFunc());
node.set(0, node.get(0) | 0x01);
}
if (clockSequenceBuffer == -1) {
clockSequenceBuffer = (randomFunc() << 8 | randomFunc()) & 0x3fff;
}
var clockSeq = optClockSequence;
if (optClockSequence == -1) {
clockSeq = clockSequenceBuffer;
}
if (msecs == -1) {
msecs = Math.fround(#if js js.lib.Date.now() #elseif sys Sys.time() * 1000 #else Date.now().getTime() #end);
}
var nsecs = optNsecs;
if (optNsecs == -1) {
nsecs = lastNSecs + 1;
}
var dt = (msecs - lastMSecs) + (nsecs - lastNSecs) / 10000;
if (dt < 0 && (optClockSequence == -1)) {
clockSeq = (clockSeq + 1) & 0x3fff;
}
if ((dt < 0 || msecs > lastMSecs) && optNsecs == -1) {
nsecs = 0;
}
if (nsecs >= 10000) {
throw "Can't create more than 10M uuids/sec";
}
lastMSecs = msecs;
lastNSecs = nsecs;
clockSequenceBuffer = clockSeq;
msecs += 12219292800000;
var imsecs:Int64 = Int64.fromFloat(msecs);
var tl:Int = (((imsecs & 0xfffffff) * 10000 + nsecs) % DVS).low;
buffer.set(0, tl >>> 24 & 0xff);
buffer.set(1, tl >>> 16 & 0xff);
buffer.set(2, tl >>> 8 & 0xff);
buffer.set(3, tl & 0xff);
var tmh:Int = ((imsecs / DVS * 10000) & 0xfffffff).low;
buffer.set(4, tmh >>> 8 & 0xff);
buffer.set(5, tmh & 0xff);
buffer.set(6, tmh >>> 24 & 0xf | 0x10);
buffer.set(7, tmh >>> 16 & 0xff);
buffer.set(8, clockSeq >>> 8 | 0x80);
buffer.set(9, clockSeq & 0xff);
for (i in 0...6)
buffer.set(i + 10, node.get(i));
var uuid = stringify(buffer, separator);
if (shortUuid)
uuid = Uuid.toShort(uuid, separator, toAlphabet);
return uuid;
}
public static function v3(name:String, namespace:String = "", separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String {
namespace = StringTools.replace(namespace, '-', '');
var buffer = Md5.make(Bytes.ofHex(namespace + Bytes.ofString(name).toHex()));
buffer.set(6, (buffer.get(6) & 0x0f) | 0x30);
buffer.set(8, (buffer.get(8) & 0x3f) | 0x80);
var uuid = stringify(buffer, separator);
if (shortUuid)
uuid = Uuid.toShort(uuid, separator, toAlphabet);
return uuid;
}
public static function v4(randBytes:Bytes = null, ?randomFunc:Void->Int, separator:String = "-", shortUuid:Bool = false,
toAlphabet:String = FLICKR_BASE58):String {
if (randomFunc == null)
randomFunc = randomByte;
var buffer:Bytes = randBytes;
if (buffer == null) {
buffer = Bytes.alloc(16);
for (i in 0...16) {
buffer.set(i, randomFunc());
}
} else {
if (buffer.length < 16)
throw "Random bytes should be at least 16 bytes";
}
buffer.set(6, (buffer.get(6) & 0x0f) | 0x40);
buffer.set(8, (buffer.get(8) & 0x3f) | 0x80);
var uuid = stringify(buffer, separator);
if (shortUuid)
uuid = Uuid.toShort(uuid, separator, toAlphabet);
return uuid;
}
public static function v5(name:String, namespace:String = "", separator:String = "-", shortUuid:Bool = false, toAlphabet:String = FLICKR_BASE58):String {
namespace = StringTools.replace(namespace, '-', '');
var buffer = Sha1.make(Bytes.ofHex(namespace + Bytes.ofString(name).toHex()));
buffer.set(6, (buffer.get(6) & 0x0f) | 0x50);
buffer.set(8, (buffer.get(8) & 0x3f) | 0x80);
var uuid = stringify(buffer, separator);
if (shortUuid)
uuid = Uuid.toShort(uuid, separator, toAlphabet);
return uuid;
}
public static function stringify(data:Bytes, separator:String = "-"):String {
return hexToUuid(data.toHex(), separator);
}
public static function parse(uuid:String, separator:String = "-"):Bytes {
return Bytes.ofHex(StringTools.replace(uuid, separator, ''));
}
public static function validate(uuid:String, separator:String = "-"):Bool {
if (separator == "") {
uuid = uuid.substr(0, 8)
+ "-"
+ uuid.substr(8, 4)
+ "-"
+ uuid.substr(12, 4)
+ "-"
+ uuid.substr(16, 4)
+ "-"
+ uuid.substr(20, 12);
} else if (separator != "-") {
uuid = StringTools.replace(uuid, separator, '-');
}
return regexp.match(uuid);
}
public static function version(uuid:String, separator:String = "-"):Int {
uuid = StringTools.replace(uuid, separator, '');
return Std.parseInt("0x" + uuid.substr(12, 1));
}
public static function hexToUuid(hex:String, separator:String):String {
return (hex.substr(0, 8) + separator + hex.substr(8, 4) + separator + hex.substr(12, 4) + separator + hex.substr(16, 4) + separator
+ hex.substr(20, 12));
}
public static function convert(number:String, fromAlphabet:String, toAlphabet:String):String {
var fromBase:Int = fromAlphabet.length;
var toBase:Int = toAlphabet.length;
var len = number.length;
var buf:String = "";
var numberMap:Vector<Int> = new Vector<Int>(len);
var divide:Int = 0, newlen:Int = 0;
for (i in 0...len) {
numberMap[i] = fromAlphabet.indexOf(number.charAt(i));
}
do {
divide = 0;
newlen = 0;
for (i in 0...len) {
divide = divide * fromBase + numberMap[i];
if (divide >= toBase) {
numberMap[newlen++] = Math.floor(divide / toBase);
divide = divide % toBase;
} else if (newlen > 0) {
numberMap[newlen++] = 0;
}
}
len = newlen;
buf = toAlphabet.charAt(divide) + buf;
} while (newlen != 0);
return buf;
}
public static function nanoId(len:Int = 21, alphabet:String = NANO_ID_ALPHABET, ?randomFunc:Void->Int):String {
if (randomFunc == null)
randomFunc = randomByte;
if (alphabet == null)
throw "Alphabet cannot be null";
if (alphabet.length == 0 || alphabet.length >= 256)
throw "Alphabet must contain between 1 and 255 symbols";
if (len <= 0)
throw "Length must be greater than zero";
var mask:Int = (2 << Math.floor(Math.log(alphabet.length - 1) / Math.log(2))) - 1;
var step:Int = Math.ceil(1.6 * mask * len / alphabet.length);
var sb = new StringBuf();
while (sb.length != len) {
for (i in 0...step) {
var rnd = randomFunc();
var aIndex:Int = rnd & mask;
if (aIndex < alphabet.length) {
sb.add(alphabet.charAt(aIndex));
if (sb.length == len)
break;
}
}
}
return sb.toString();
}
public static function short(toAlphabet:String = FLICKR_BASE58, ?randomFunc:Void->Int):String {
return Uuid.v4(randomFunc, true, toAlphabet);
}
}