mirror of
https://github.com/RandomityGuy/MBHaxe.git
synced 2025-10-30 08:11:25 +00:00
leaderboards initial impl
This commit is contained in:
parent
3bf6d913d9
commit
4d2b41711f
15 changed files with 1773 additions and 18 deletions
49
src/Http.hx
49
src/Http.hx
|
|
@ -11,6 +11,7 @@ typedef HttpRequest = {
|
||||||
var fulfilled:Bool;
|
var fulfilled:Bool;
|
||||||
var post:Bool;
|
var post:Bool;
|
||||||
var postData:String;
|
var postData:String;
|
||||||
|
var ?file:haxe.io.Bytes;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Http {
|
class Http {
|
||||||
|
|
@ -23,7 +24,7 @@ class Http {
|
||||||
|
|
||||||
public static function init() {
|
public static function init() {
|
||||||
#if sys
|
#if sys
|
||||||
threadPool = new sys.thread.FixedThreadPool(2);
|
threadPool = new sys.thread.FixedThreadPool(4);
|
||||||
threadPool.run(() -> threadLoop());
|
threadPool.run(() -> threadLoop());
|
||||||
threadPool.run(() -> threadLoop());
|
threadPool.run(() -> threadLoop());
|
||||||
threadPool.run(() -> threadLoop());
|
threadPool.run(() -> threadLoop());
|
||||||
|
|
@ -50,15 +51,21 @@ class Http {
|
||||||
responses.add(() -> req.callback(b));
|
responses.add(() -> req.callback(b));
|
||||||
req.fulfilled = true;
|
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) {
|
if (req.post) {
|
||||||
http.setHeader('User-Agent', 'MBHaxe/1.0 ${Util.getPlatform()}');
|
http.setHeader('User-Agent', 'MBHaxe/1.0 ${Util.getPlatform()}');
|
||||||
http.setHeader('Content-Type', "application/json"); // support json data only (for now)
|
if (req.file == null) {
|
||||||
http.setPostData(req.postData);
|
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);
|
http.request(req.post);
|
||||||
hl.Gc.blocking(false);
|
// hl.Gc.blocking(false);
|
||||||
hl.Gc.enable(true);
|
hl.Gc.enable(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +121,36 @@ class Http {
|
||||||
#end
|
#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() {
|
public static function loop() {
|
||||||
#if sys
|
#if sys
|
||||||
var resp = responses.pop(false);
|
var resp = responses.pop(false);
|
||||||
|
|
|
||||||
80
src/Leaderboards.hx
Normal file
80
src/Leaderboards.hx
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -267,7 +267,7 @@ class MarbleGame {
|
||||||
exitGameDlg = new ExitGameDlg((sender) -> {
|
exitGameDlg = new ExitGameDlg((sender) -> {
|
||||||
canvas.popDialog(exitGameDlg);
|
canvas.popDialog(exitGameDlg);
|
||||||
var w = getWorld();
|
var w = getWorld();
|
||||||
if (w.isRecording) {
|
if (MarbleGame.instance.toRecord) {
|
||||||
MarbleGame.canvas.pushDialog(new ReplayNameDlg(() -> {
|
MarbleGame.canvas.pushDialog(new ReplayNameDlg(() -> {
|
||||||
quitMission();
|
quitMission();
|
||||||
}));
|
}));
|
||||||
|
|
@ -385,7 +385,7 @@ class MarbleGame {
|
||||||
world.dispose();
|
world.dispose();
|
||||||
}
|
}
|
||||||
Analytics.trackLevelPlay(mission.title, mission.path);
|
Analytics.trackLevelPlay(mission.title, mission.path);
|
||||||
world = new MarbleWorld(scene, scene2d, mission, toRecord, multiplayer);
|
world = new MarbleWorld(scene, scene2d, mission, !multiplayer, multiplayer);
|
||||||
world.init();
|
world.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@ class MarbleWorld extends Scheduler {
|
||||||
// Rewind
|
// Rewind
|
||||||
public var rewindManager:RewindManager;
|
public var rewindManager:RewindManager;
|
||||||
public var rewinding:Bool = false;
|
public var rewinding:Bool = false;
|
||||||
|
public var rewindUsed:Bool = false;
|
||||||
|
|
||||||
public var inputRecorder:InputRecorder;
|
public var inputRecorder:InputRecorder;
|
||||||
public var isReplayingMovement:Bool = false;
|
public var isReplayingMovement:Bool = false;
|
||||||
|
|
@ -602,6 +603,7 @@ class MarbleWorld extends Scheduler {
|
||||||
this.replay.rewind();
|
this.replay.rewind();
|
||||||
|
|
||||||
this.rewindManager.clear();
|
this.rewindManager.clear();
|
||||||
|
this.rewindUsed = false;
|
||||||
|
|
||||||
this.timeState.currentAttemptTime = 0;
|
this.timeState.currentAttemptTime = 0;
|
||||||
this.timeState.gameplayClock = this.gameMode.getStartTime();
|
this.timeState.gameplayClock = this.gameMode.getStartTime();
|
||||||
|
|
@ -1551,6 +1553,7 @@ class MarbleWorld extends Scheduler {
|
||||||
var actualDt = timeState.currentAttemptTime - rframe.timeState.currentAttemptTime - dt * rewindManager.timeScale;
|
var actualDt = timeState.currentAttemptTime - rframe.timeState.currentAttemptTime - dt * rewindManager.timeScale;
|
||||||
dt = actualDt;
|
dt = actualDt;
|
||||||
rewindManager.applyFrame(rframe);
|
rewindManager.applyFrame(rframe);
|
||||||
|
rewindUsed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -500,6 +500,9 @@ class Replay {
|
||||||
var compressed = stream.finalize();
|
var compressed = stream.finalize();
|
||||||
#end
|
#end
|
||||||
|
|
||||||
|
if (this.name == null)
|
||||||
|
this.name = this.mission;
|
||||||
|
|
||||||
var finalB = new BytesBuffer();
|
var finalB = new BytesBuffer();
|
||||||
finalB.addByte(version);
|
finalB.addByte(version);
|
||||||
finalB.addByte(this.name.length);
|
finalB.addByte(this.name.length);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import haxe.Json;
|
||||||
import src.Util;
|
import src.Util;
|
||||||
import src.Console;
|
import src.Console;
|
||||||
import src.Renderer;
|
import src.Renderer;
|
||||||
|
import net.Uuid;
|
||||||
|
|
||||||
typedef Score = {
|
typedef Score = {
|
||||||
var name:String;
|
var name:String;
|
||||||
|
|
@ -211,6 +212,7 @@ class Settings {
|
||||||
public static var achievementProgression:Int;
|
public static var achievementProgression:Int;
|
||||||
|
|
||||||
public static var highscoreName = "Player";
|
public static var highscoreName = "Player";
|
||||||
|
public static var userId = "";
|
||||||
|
|
||||||
public static var uiScale = 1.0;
|
public static var uiScale = 1.0;
|
||||||
|
|
||||||
|
|
@ -269,6 +271,7 @@ class Settings {
|
||||||
gamepad: gamepadSettings,
|
gamepad: gamepadSettings,
|
||||||
stats: playStatistics,
|
stats: playStatistics,
|
||||||
highscoreName: highscoreName,
|
highscoreName: highscoreName,
|
||||||
|
userId: userId,
|
||||||
marbleIndex: optionsSettings.marbleIndex,
|
marbleIndex: optionsSettings.marbleIndex,
|
||||||
marbleSkin: optionsSettings.marbleSkin,
|
marbleSkin: optionsSettings.marbleSkin,
|
||||||
marbleModel: optionsSettings.marbleModel,
|
marbleModel: optionsSettings.marbleModel,
|
||||||
|
|
@ -475,6 +478,13 @@ class Settings {
|
||||||
achievementProgression = 0;
|
achievementProgression = 0;
|
||||||
#end
|
#end
|
||||||
highscoreName = json.highscoreName;
|
highscoreName = json.highscoreName;
|
||||||
|
if (highscoreName == null) {
|
||||||
|
highscoreName = "";
|
||||||
|
}
|
||||||
|
userId = json.userId;
|
||||||
|
if (userId == null) {
|
||||||
|
userId = Uuid.v4();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Console.warn("Settings file does not exist");
|
Console.warn("Settings file does not exist");
|
||||||
save();
|
save();
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,10 @@ class CollisionEntity implements IOctreeObject implements IBVHObject {
|
||||||
if (position.sub(closest).dot(surfacenormal) > 0) {
|
if (position.sub(closest).dot(surfacenormal) > 0) {
|
||||||
normal.normalize();
|
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
|
// 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);
|
// var testDot = normal.dot(surfacenormal);
|
||||||
// if (testDot > bestDot) {
|
// if (testDot > bestDot) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import h3d.Vector;
|
||||||
import src.ResourceLoader;
|
import src.ResourceLoader;
|
||||||
import src.TimeState;
|
import src.TimeState;
|
||||||
import src.Util;
|
import src.Util;
|
||||||
|
import src.Leaderboards;
|
||||||
|
|
||||||
class EndGameGui extends GuiImage {
|
class EndGameGui extends GuiImage {
|
||||||
var mission:Mission;
|
var mission:Mission;
|
||||||
|
|
@ -134,6 +135,7 @@ class EndGameGui extends GuiImage {
|
||||||
// Settings.saveScore(mission.path, myScore, scoreType);
|
// Settings.saveScore(mission.path, myScore, scoreType);
|
||||||
|
|
||||||
var scoreData:Array<Score> = Settings.getScores(mission.path);
|
var scoreData:Array<Score> = Settings.getScores(mission.path);
|
||||||
|
|
||||||
while (scoreData.length < 1) {
|
while (scoreData.length < 1) {
|
||||||
if (scoreType == Score)
|
if (scoreType == Score)
|
||||||
scoreData.push({name: "Nardo Polo", time: 0});
|
scoreData.push({name: "Nardo Polo", time: 0});
|
||||||
|
|
@ -167,7 +169,11 @@ class EndGameGui extends GuiImage {
|
||||||
retryButton.horizSizing = Right;
|
retryButton.horizSizing = Right;
|
||||||
retryButton.gamepadAccelerator = ["B"];
|
retryButton.gamepadAccelerator = ["B"];
|
||||||
retryButton.accelerators = [hxd.Key.ESCAPE, hxd.Key.BACKSPACE];
|
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);
|
bottomBar.addChild(retryButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,8 +189,34 @@ class EndGameGui extends GuiImage {
|
||||||
nextButton.horizSizing = Right;
|
nextButton.horizSizing = Right;
|
||||||
nextButton.gamepadAccelerator = ["A"];
|
nextButton.gamepadAccelerator = ["A"];
|
||||||
nextButton.accelerators = [hxd.Key.ENTER];
|
nextButton.accelerators = [hxd.Key.ENTER];
|
||||||
nextButton.pressedAction = (e) -> continueFunc(nextButton);
|
nextButton.pressedAction = (e) -> {
|
||||||
|
if (MarbleGame.canvas.children.length == 1)
|
||||||
|
continueFunc(nextButton);
|
||||||
|
}
|
||||||
bottomBar.addChild(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) {
|
override function onResize(width:Int, height:Int) {
|
||||||
|
|
|
||||||
75
src/gui/EnterNamePopupDlg.hx
Normal file
75
src/gui/EnterNamePopupDlg.hx
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import h2d.Bitmap;
|
||||||
import h3d.Engine;
|
import h3d.Engine;
|
||||||
import h3d.Vector;
|
import h3d.Vector;
|
||||||
import gui.GuiText.Justification;
|
import gui.GuiText.Justification;
|
||||||
import h2d.HtmlText;
|
|
||||||
import h2d.Scene;
|
import h2d.Scene;
|
||||||
import hxd.res.BitmapFont;
|
import hxd.res.BitmapFont;
|
||||||
import h2d.Text;
|
import h2d.Text;
|
||||||
|
|
|
||||||
904
src/gui/HtmlText.hx
Normal file
904
src/gui/HtmlText.hx
Normal 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
291
src/gui/LeaderboardsGui.hx
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -160,11 +160,15 @@ class LevelSelectGui extends GuiImage {
|
||||||
}
|
}
|
||||||
bottomBar.addChild(recordButton);
|
bottomBar.addChild(recordButton);
|
||||||
|
|
||||||
// var lbButton = new GuiXboxButton("Leaderboard", 220);
|
if (currentDifficultyStatic != "multiplayer") {
|
||||||
// lbButton.position = new Vector(750, 0);
|
var lbButton = new GuiXboxButton("Leaderboard", 220);
|
||||||
// lbButton.vertSizing = Bottom;
|
lbButton.position = new Vector(750, 0);
|
||||||
// lbButton.horizSizing = Right;
|
lbButton.vertSizing = Bottom;
|
||||||
// bottomBar.addChild(lbButton);
|
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);
|
var nextButton = new GuiXboxButton("Play", 160);
|
||||||
nextButton.position = new Vector(960, 0);
|
nextButton.position = new Vector(960, 0);
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class MainMenuGui extends GuiImage {
|
||||||
btnList = new GuiXboxList();
|
btnList = new GuiXboxList();
|
||||||
btnList.position = new Vector(70 - offsetX, 95);
|
btnList.position = new Vector(70 - offsetX, 95);
|
||||||
btnList.horizSizing = Left;
|
btnList.horizSizing = Left;
|
||||||
btnList.extent = new Vector(502, 500);
|
btnList.extent = new Vector(502, 530);
|
||||||
innerCtrl.addChild(btnList);
|
innerCtrl.addChild(btnList);
|
||||||
|
|
||||||
btnList.addButton(0, "Single Player Game", (sender) -> {
|
btnList.addButton(0, "Single Player Game", (sender) -> {
|
||||||
|
|
@ -82,7 +82,9 @@ class MainMenuGui extends GuiImage {
|
||||||
cast(this.parent, Canvas).setContent(new MultiplayerGui());
|
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) -> {
|
btnList.addButton(2, "Achievements", (e) -> {
|
||||||
cast(this.parent, Canvas).setContent(new AchievementsGui());
|
cast(this.parent, Canvas).setContent(new AchievementsGui());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
311
src/net/Uuid.hx
Normal file
311
src/net/Uuid.hx
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue