Skip to content

Instantly share code, notes, and snippets.

@gamedevsam
Last active September 11, 2016 03:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gamedevsam/ea06ae3b57fcdf3a4048 to your computer and use it in GitHub Desktop.
Save gamedevsam/ea06ae3b57fcdf3a4048 to your computer and use it in GitHub Desktop.
Haxe Toml Configs
package ;
import flixel.FlxG;
import flixel.addons.nape.FlxNapeState;
import toml.TomlConfig;
/**
* ...
* @author Sam Batista
*/
// Typedef configs so we get instant code completion in FlashDevelop
typedef DebugConfig = {
SHOW_DEBUGGER:Bool,
SHOW_DEBUG_OUTLINES:Bool,
SHOW_NAPE_OUTLINES:Bool,
};
typedef PlayerConfig = {
BODY_MASS:Int,
RUN_DRAG:Int,
MAX_VELOCITY:Int,
POLE_LOWERING_DURATION:Float,
POLE_ENGAGE_IMPULSE:Int,
POLE_LAUNCH_IMPULSE:Int,
POLE_FRAGMENT_LENGTH:Int,
NUM_POLE_FRAGMENTS:Int,
};
typedef ControlsConfig = {
PLAYER_SETTING:Array<String>,
EASY: {
RUN_ACCELERATION:Int,
},
ADVANCED: {
RUN_ACCELERATION:Int,
},
};
typedef GameplayConfig = {
LEVEL:String,
GRAVITY_X:Int,
GRAVITY_Y:Int,
VELOCITY_ITERATIONS:Int,
POSITION_ITERATIONS:Int,
FENCE_HEIGHT:Int,
FENCE_HEIGHT_INCREMENT:Int,
FENCE_DISTANCE:Int,
FENCE_DISTANCE_VARIATION:Int,
LANE_DISTANCE:Int,
LANE_X_OFFSET:Int,
};
typedef GraphicsConfig = {
FULLSCREEN:Bool,
};
class Config
{
public static var Debug:DebugConfig = cast { };
public static var Player:PlayerConfig = cast { };
public static var Controls:ControlsConfig = cast { };
public static var Gameplay:GameplayConfig = cast { };
public static var Graphics:GraphicsConfig = cast { };
public static var loaded:Bool = false;
public static function loadConfigs() {
if (!loaded) {
loaded = true;
TomlConfig.CONFIG_PATH = "./";
TomlConfig.TOML_CONFIG_FILES = ["Config.toml"];
TomlConfig.PROFILE_LOAD_DURATION = false; // ENABLE FOR PROFILING & OPTIMIZATION
TomlConfig.register(Debug, "Debug");
TomlConfig.register(Player, "Player");
TomlConfig.register(Controls, "Controls");
TomlConfig.register(Gameplay, "Gameplay");
TomlConfig.register(Graphics, "Graphics");
Events.addHandler(null, CommonEvents.CONFIGS_LOADED, configsLoaded, true);
}
TomlConfig.loadConfigs(#if (!FLX_NO_DEBUG) true #end);
}
public static function configsLoaded(?o) {
#if !FLX_NO_DEBUG
FlxG.debugger.visible = Config.Debug.SHOW_DEBUGGER;
FlxG.debugger.drawDebug = Config.Debug.SHOW_DEBUG_OUTLINES;
if (FlxG.state != null && Std.is(FlxG.state, FlxNapeState))
cast(FlxG.state, FlxNapeState).napeDebugEnabled = Config.Debug.SHOW_NAPE_OUTLINES;
#end
}
}
# TOML Spec: https://github.com/mojombo/toml/blob/master/versions/toml-v0.1.0.md
[Debug]
SHOW_DEBUGGER = false
SHOW_DEBUG_OUTLINES = false
SHOW_NAPE_OUTLINES = false
[Graphics]
FULLSCREEN = false
[Player]
RUN_DRAG = 1000
BODY_MASS = 75
NUM_POLE_FRAGMENTS = 4
POLE_FRAGMENT_LENGTH = 25
POLE_LOWERING_DURATION = 0.15
POLE_ENGAGE_IMPULSE = 1000
POLE_LAUNCH_IMPULSE = 65000
MAX_VELOCITY = 2000
[Controls]
# controls settings must match an entry in ControlsMode enum (Player.hx)
PLAYER_SETTING = [
"ADVANCED", # setting for lane 0 (player 1)
"ADVANCED" # setting for lane 1 (player 2)
]
[Controls.EASY]
RUN_ACCELERATION = 1000
[Controls.ADVANCED]
RUN_ACCELERATION = 100
[Gameplay]
LEVEL = "test_level.tmx"
GRAVITY_X = 0
GRAVITY_Y = 981
VELOCITY_ITERATIONS = 40
POSITION_ITERATIONS = 40
FENCE_HEIGHT = 100 # starting fence height
FENCE_HEIGHT_INCREMENT = 25 # consecutive fences get higher by this amount
FENCE_DISTANCE = 2000 # objects are layed out every FENCE_DISTANCE pixels
FENCE_DISTANCE_VARIATION = 0 # (temporarily disabled) 500 Formula: fence.x = FENCE_DISTANCE + Math.random() * FENCE_DISTANCE_VARIATION;
LANE_DISTANCE = 0
LANE_X_OFFSET = 0
package toml;
import haxe.Utf8;
import openfl.Assets;
import flixel.FlxG;
#if cpp
import sys.io.File;
import sys.FileSystem;
#elseif flash
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
#end
/**
* Sam Batista
* @author
*/
class TomlConfig
{
// list of file names for all configs
public static var TOML_CONFIG_FILES = ["Config.toml"];
// path to config
public static var CONFIG_PATH = "assets/";
// path to config relative to cpp binaries - REAL_TIME_LOAD
public static var CONFIG_PATH_HOT_RELOAD = "../../../../";
// path to config relative to flash swf - REAL_TIME_LOAD
public static var CONFIG_PATH_HOT_RELOAD_FLASH = "../../../";
// profile duration of configs load, and print the result to the console
public static var PROFILE_LOAD_DURATION = true;
//{ PUBLIC
//
public static function register(obj:Dynamic, ?configName:String):Void
{
if (configName == null)
{
var objClass = Type.getClass(obj);
var className = Type.getClassName(objClass);
configName = className.lastIndexOf(".") < 0 ? className : className.substr(className.lastIndexOf(".") + 1);
}
if(registeredObjects.exists(configName))
registeredObjects.get(configName).push(obj);
else
registeredObjects.set(configName, [obj]);
if (tomlData != null)
copyConfigDataToObjects(configName, [obj]);
}
public static function unregister(obj:Dynamic, ?configName:String):Void
{
if (configName == null)
{
var objClass = Type.getClass(obj);
var className = Type.getClassName(objClass);
configName = className.lastIndexOf(".") < 0 ? className : className.substr(className.lastIndexOf(".") + 1);
}
if(registeredObjects.exists(configName))
registeredObjects.get(configName).remove(obj);
}
// Config Loading functionality - done every frame if REAL_TIME_EDIT_ASSETS is defined
public static function loadConfigs(forceReload:Bool = false):Void
{
configModified = false;
// Perf Profiling
var perfTimer = 0;
if(PROFILE_LOAD_DURATION)
perfTimer = flash.Lib.getTimer();
// Execute whole block once
if (!configsLoaded || forceReload)
{
for (c in TOML_CONFIG_FILES)
loadConfigFile(c, forceReload, true);
if (configModified || forceReload)
{
// only dispatch events if the game's been initialized
if (FlxG.game != null)
{
Events.dispatch(CommonEvents.CONFIGS_LOADED);
}
// Perf Profiling
if (PROFILE_LOAD_DURATION)
{
var configDur = (flash.Lib.getTimer() - perfTimer) / 1000;
trace("Config Load Duration = " + configDur);
}
}
}
configsLoaded = true;
}
public static function cleanup():Void
{
registeredObjects = new Map();
}
//}
//{ PRIVATE
//
private static function loadConfigFile(fileName:String, forceReload:Bool, isToml:Bool):Void
{
// first time configs must load instantly
if (!configsLoaded)
{
parseTomlFile(Assets.getText(CONFIG_PATH + fileName));
configModified = true;
}
else // subsquent times can be asynchronous
{
#if !FLX_NO_DEBUG
#if flash
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event) {
parseTomlFile(e.target.data);
Events.dispatch(CommonEvents.CONFIGS_LOADED);
}); loader.load(new URLRequest(CONFIG_PATH_HOT_RELOAD_FLASH + fileName));
#else
parseTomlFile(File.getContent(CONFIG_PATH_HOT_RELOAD + fileName));
configModified = true;
#end
#end
}
}
private static function parseTomlFile(fileData:String):Void
{
var localData = TomlParser.parseString(fileData, {});
var configObjs = Reflect.fields(localData);
for (objName in configObjs)
{
// store loaded config data in master object
Reflect.setField(tomlData, objName, Reflect.field(localData, objName));
if (registeredObjects.exists(objName))
copyConfigDataToObjects(objName, registeredObjects.get(objName));
}
}
private static function copyConfigDataToObjects(dataType:String, objArr:Array<Dynamic>):Void
{
var objData = Reflect.field(tomlData, dataType);
var objFields = Reflect.fields(objData);
for (obj in objArr)
{
for (field in objFields)
{
Reflect.setField(obj, field, Reflect.field(objData, field));
}
}
}
private static var tomlData = {};
private static var configModified = false;
private static var configsLoaded = false;
private static var registeredObjects:Map <String, Array<Dynamic>> = new Map();
//}
}
package toml;
using haxe.Utf8;
using Lambda;
// A Haxe implementation of TOML v0.1.0 language.
// obtained from: https://github.com/raincole/haxetoml
// TOML v0.1.0 spec:
// https://github.com/mojombo/toml/blob/master/versions/toml-v0.1.0.md
private enum TokenType {
TkInvalid;
TkComment;
TkKey;
TkKeygroup;
TkString; TkInteger; TkFloat; TkBoolean; TkDatetime;
TkAssignment;
TkComma;
TkBBegin; TkBEnd;
}
private typedef Token = {
var type : TokenType;
var value : String;
var lineNum : Int;
var colNum : Int;
}
class TomlParser {
var tokens : Array<Token>;
var root : Dynamic;
var pos = 0;
public var currentToken(get_currentToken, null) : Token;
/** Set up a new TomlParser instance */
public function new() {}
/** Parse a TOML string into a dynamic object. Throws a String containing an error message if an error is encountered. */
public function parse(str : String, ?defaultValue : Dynamic) : Dynamic {
tokens = tokenize(str);
//for(token in tokens)
//trace(token);
if(defaultValue != null) {
root = defaultValue;
} else {
root = {};
}
pos = 0;
parseObj();
return root;
}
function get_currentToken() {
return tokens[pos];
}
function nextToken() {
pos++;
}
function parseObj() {
var keygroup = "";
while(pos < tokens.length) {
switch (currentToken.type) {
case TkKeygroup:
keygroup = decodeKeygroup(currentToken);
createKeygroup(keygroup);
nextToken();
case TkKey:
var pair = parsePair();
setPair(keygroup, pair);
default:
InvalidToken(currentToken);
}
}
}
function parsePair() {
var key = "";
var value = {};
if(currentToken.type == TkKey) {
key = decodeKey(currentToken);
nextToken();
if(currentToken.type == TkAssignment) {
nextToken();
value = parseValue();
} else {
InvalidToken(currentToken);
}
} else {
InvalidToken(currentToken);
}
return { key: key, value: value };
}
function parseValue() : Dynamic {
var value : Dynamic = {};
switch(currentToken.type) {
case TkString:
value = decodeString(currentToken);
nextToken();
case TkDatetime:
value = decodeDatetime(currentToken);
nextToken();
case TkFloat:
value = decodeFloat(currentToken);
nextToken();
case TkInteger:
value = decodeInteger(currentToken);
nextToken();
case TkBoolean:
value = decodeBoolean(currentToken);
nextToken();
case TkBBegin:
value = parseArray();
default:
InvalidToken(currentToken);
};
return value;
}
function parseArray() {
var array = [];
if(currentToken.type == TokenType.TkBBegin) {
nextToken();
while(true) {
if(currentToken.type != TkBEnd) {
array.push(parseValue());
} else {
nextToken();
break;
}
switch(currentToken.type) {
case TkComma:
nextToken();
case TkBEnd:
nextToken();
break;
default:
InvalidToken(currentToken);
}
}
}
return array;
}
function createKeygroup(keygroup : String) {
var keys = keygroup.split(".");
var obj = root;
for(key in keys) {
var next = Reflect.field(obj, key);
if(next == null) {
Reflect.setField(obj, key, {});
next = Reflect.field(obj, key);
}
obj = next;
}
}
function setPair(keygroup : String, pair : { key : String, value : Dynamic }) {
var keys = keygroup.split(".");
var obj = root;
for(key in keys) {
// A Haxe glitch: empty string will be parsed to [""]
if(key != "") {
obj = Reflect.field(obj, key);
}
}
Reflect.setField(obj, pair.key, pair.value);
}
function decode<T>(token : Token, expectedType : TokenType, decoder : String -> T) : T {
var type = token.type;
var value = token.value;
if(type == expectedType)
return decoder(value);
else
throw('Can\'t parse $type as $expectedType');
}
function decodeKeygroup(token : Token) : String {
return decode(token, TokenType.TkKeygroup, function(v) {
return v.substring(1, v.length - 1);
});
}
function decodeString(token : Token) : String {
return decode(token, TokenType.TkString, function(v) {
try {
return unescape(v);
} catch(msg : String) {
InvalidToken(token);
return "";
};
});
}
function decodeDatetime(token : Token) : Date {
return decode(token, TokenType.TkDatetime, function(v) {
var dateStr = ~/(T|Z)/.replace(v, "");
return Date.fromString(dateStr);
});
}
function decodeFloat(token : Token) : Float {
return decode(token, TokenType.TkFloat, function(v) {
return Std.parseFloat(v);
});
}
function decodeInteger(token : Token) : Int {
return decode(token, TokenType.TkInteger, function(v) {
return Std.parseInt(v);
});
}
function decodeBoolean(token : Token) : Bool {
return decode(token, TokenType.TkBoolean, function(v) {
return v == "true";
});
}
function decodeKey(token : Token) : String {
return decode(token, TokenType.TkKey, function(v) { return v; });
}
function unescape(str : String) {
var pos = 0;
var buf = new haxe.Utf8();
var len = Utf8.length(str);
while(pos < len) {
var c = Utf8.charCodeAt(str, pos);
// strip first and last quotation marks
if ((pos == 0 || pos == len-1) && c == "\"".code) {
pos++;
continue;
}
pos++;
if(c == "\\".code) {
c = Utf8.charCodeAt(str, pos);
pos++;
switch(c) {
case "r".code: buf.addChar("\r".code);
case "n".code: buf.addChar("\n".code);
case "t".code: buf.addChar("\t".code);
case "b".code: buf.addChar(8);
case "f".code: buf.addChar(12);
case "/".code, "\\".code, "\"".code: buf.addChar(c);
case "u".code:
var uc = Std.parseInt("0x" + Utf8.sub(str, pos, 4));
buf.addChar(uc);
pos += 4;
default:
throw("Invalid Escape");
}
} else {
buf.addChar(c);
}
}
return buf.toString();
}
function tokenize(str : String) {
var tokens = new Array<Token>();
var lineBreakPattern = ~/\r\n?|\n/g;
var lines = lineBreakPattern.split(str);
var a = ~/abc/;
var patterns = [
{ type: TokenType.TkComment, ereg: ~/^#.*$/},
{ type: TokenType.TkKeygroup, ereg: ~/^\[.+]/},
{ type: TokenType.TkString, ereg: ~/^"((\\")|[^"])*"/},
{ type: TokenType.TkAssignment, ereg: ~/^=/},
{ type: TokenType.TkBBegin, ereg: ~/^\[/},
{ type: TokenType.TkBEnd, ereg: ~/^]/},
{ type: TokenType.TkComma, ereg: ~/^,/},
{ type: TokenType.TkKey, ereg: ~/^\S+/},
{ type: TokenType.TkDatetime, ereg: ~/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/},
{ type: TokenType.TkFloat, ereg: ~/^-?\d+\.\d+/},
{ type: TokenType.TkInteger, ereg: ~/^-?\d+/},
{ type: TokenType.TkBoolean, ereg: ~/^true|^false/},
];
for(lineNum in 0...lines.length) {
var line = lines[lineNum];
var colNum = 0;
var tokenColNum = 0;
while(colNum < line.length) {
while(StringTools.isSpace(line, colNum)) {
colNum++;
}
if(colNum >= line.length) {
break;
}
var subline = line.substring(colNum);
var matched = false;
for(pattern in patterns) {
var type = pattern.type;
var ereg = pattern.ereg;
if(ereg.match(subline)) {
// TkKey has to be the first token of a line
if((type == TokenType.TkKeygroup || type == TokenType.TkKey)
&& tokenColNum != 0) {
continue;
}
if(type != TokenType.TkComment) {
tokens.push({
type: type,
value: ereg.matched(0),
lineNum: lineNum,
colNum: colNum,
});
tokenColNum++;
}
colNum += ereg.matchedPos().len;
matched = true;
break;
}
}
if(!matched) {
InvalidCharacter(line.charAt(colNum), lineNum, colNum);
}
}
}
return tokens;
}
function InvalidCharacter(char : String, lineNum : Int, colNum : Int) {
throw('Line $lineNum Character ${colNum+1}: ' +
'Invalid Character \'$char\', ' +
'Character Code ${char.charCodeAt(0)}');
}
function InvalidToken(token : Token) {
throw('Line ${token.lineNum+1} Character ${token.colNum+1}: ' +
'Invalid Token \'${token.value}\'(${token.type})');
}
/** Static shortcut method to parse toml String into Dynamic object. */
public static function parseString(toml: String, defaultValue: Dynamic)
{
return (new TomlParser()).parse(toml, defaultValue);
}
#if (neko || php || cpp)
/** Static shortcut method to read toml file and parse into Dynamic object. Available on Neko, PHP and CPP. */
public static function parseFile(filename: String, ?defaultValue: Dynamic)
{
return parseString(sys.io.File.getContent(filename), defaultValue);
}
#end
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment