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