├── test ├── .gitignore ├── .modifaxe ├── Test.hxml ├── data.modhx └── src │ └── Main.hx ├── extraParams.hxml ├── .github ├── logo.png ├── logo.afphoto └── workflows │ └── Test_DevEnv.yml ├── DevEnv.hxml ├── src ├── modifaxe │ ├── config │ │ ├── MetaArgs.hx │ │ ├── Meta.hx │ │ └── Define.hx │ ├── builder │ │ ├── Entry.hx │ │ ├── Section.hx │ │ ├── File.hx │ │ ├── EntryValue.hx │ │ └── Builder.hx │ ├── format │ │ ├── FormatIdentifier.hx │ │ ├── Format.hx │ │ └── HxModFormat.hx │ ├── InitMacro.hx │ ├── runtime │ │ ├── ModParserError.hx │ │ └── ModParser.hx │ ├── BuildMacro.hx │ ├── FileCollection.hx │ ├── tools │ │ └── ExprTools.hx │ └── Output.hx └── Modifaxe.hx ├── haxelib.json ├── LICENSE └── README.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /extraParams.hxml: -------------------------------------------------------------------------------- 1 | --macro modifaxe.InitMacro.init() -------------------------------------------------------------------------------- /test/.modifaxe: -------------------------------------------------------------------------------- 1 | Z:/Desktop/GithubProjects/modifaxe/test/data.modhx -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeRanDev/modifaxe/HEAD/.github/logo.png -------------------------------------------------------------------------------- /.github/logo.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SomeRanDev/modifaxe/HEAD/.github/logo.afphoto -------------------------------------------------------------------------------- /DevEnv.hxml: -------------------------------------------------------------------------------- 1 | -D modifaxe_runtime 2 | --macro nullSafety("modifaxe") 3 | -cp src 4 | Modifaxe 5 | modifaxe -------------------------------------------------------------------------------- /test/Test.hxml: -------------------------------------------------------------------------------- 1 | -lib modifaxe 2 | -lib modifaxe.json 3 | 4 | -cp src 5 | -main Main 6 | -cpp bin 7 | -------------------------------------------------------------------------------- /test/data.modhx: -------------------------------------------------------------------------------- 1 | [Main_Fields_.getValue] 2 | s.Returned: "Hello world!" 3 | 4 | [Main_Fields_.getNumValue] 5 | i.LeftOfAddition: 123 6 | i.lefthing: 321 7 | f.DividedBy: 0.5 8 | 9 | -------------------------------------------------------------------------------- /test/src/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | function main() { 4 | ModifaxeLoader.load(); 5 | trace(getValue()); 6 | trace(getNumValue()); 7 | } 8 | 9 | @:modifaxe 10 | function getValue(): String { 11 | return "Hello world!"; 12 | } 13 | 14 | @:modifaxe 15 | function getNumValue() { 16 | final calculate = 123 + @:mod(lefthing) 321; 17 | return calculate / 0.5; 18 | } 19 | -------------------------------------------------------------------------------- /src/modifaxe/config/MetaArgs.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.config; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | A list of all the argument that can be used in @:modifaxe. 7 | **/ 8 | enum abstract MetaArgs(String) from String to String { 9 | /** 10 | ModOnly 11 | 12 | Makes it so only constant expressions with `@:mod` are generated. 13 | **/ 14 | var ModOnly = "ModOnly"; 15 | } 16 | 17 | #end 18 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modifaxe", 3 | "version": "2.0.0", 4 | "description": "A tool for modifying hardcoded values after compiling.", 5 | "url": "https://github.com/SomeRanDev/modifaxe", 6 | "license": "MIT", 7 | "tags": [ 8 | "debug", 9 | "testing", 10 | "mod", 11 | "modding", 12 | "macro", 13 | "compiler" 14 | ], 15 | "classPath": "src/", 16 | "releasenote": "Initial Release", 17 | "contributors": ["SomeRanDev"] 18 | } -------------------------------------------------------------------------------- /.github/workflows/Test_DevEnv.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test_runtime: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup Haxe 4.3.3 14 | uses: krdlab/setup-haxe@v1 15 | with: 16 | haxe-version: 4.3.3 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Check Haxe Version 22 | run: haxe -version 23 | 24 | - name: Test DevEnv.hxml 25 | run: haxe DevEnv.hxml -------------------------------------------------------------------------------- /src/modifaxe/config/Meta.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.config; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | A list of all the metadata in Modifaxe. 7 | **/ 8 | enum abstract Meta(String) from String to String { 9 | /** 10 | @:modifaxe(...args: MetaArgs) 11 | 12 | Marks a class or function to be processed by Modifaxe. 13 | **/ 14 | var Modifaxe = ":modifaxe"; 15 | 16 | /** 17 | @:mod(name: Null = null) 18 | 19 | Sets a constant expression's name for its entry. 20 | Can be used without specifying a name, which is useful in `ModOnly` mode. 21 | **/ 22 | var Mod = ":mod"; 23 | } 24 | 25 | #end 26 | -------------------------------------------------------------------------------- /src/modifaxe/builder/Entry.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.builder; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | Represents a modifiable entry. 7 | **/ 8 | class Entry { 9 | public var name(default, null): String; 10 | public var value(default, null): EntryValue; 11 | 12 | var section: Section; 13 | 14 | public function new(name: String, value: EntryValue, section: Section) { 15 | this.name = name; 16 | this.value = value; 17 | this.section = section; 18 | } 19 | 20 | /** 21 | Generates a unique identifier for this entry. 22 | Should be used as the identifier for the entry in the runtime data singleton. 23 | **/ 24 | public function getUniqueName() { 25 | return section.identifierSafeName() + "_" + name; 26 | } 27 | } 28 | 29 | #end 30 | -------------------------------------------------------------------------------- /src/modifaxe/format/FormatIdentifier.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.format; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | A unique identifier for each format. 7 | 8 | Validates a format exists upon creation. 9 | **/ 10 | abstract FormatIdentifier(String) { 11 | public function new(id: String) { 12 | id = id.toLowerCase(); 13 | 14 | if(!Format.formats.exists(id)) { 15 | throw 'Format "$id" does not exist!'; 16 | } 17 | 18 | this = id; 19 | } 20 | 21 | /** 22 | Returns the `Format` this identifier is associated with. 23 | **/ 24 | public function getFormat() { 25 | final result = Format.formats.get(this); 26 | if(result == null) { 27 | throw 'Could not locate format "$this".'; 28 | } 29 | return result; 30 | } 31 | 32 | /** 33 | Handles conversion from `String`. 34 | **/ 35 | @:from 36 | public static function fromString(s: String) { 37 | return new FormatIdentifier(s); 38 | } 39 | } 40 | 41 | #end 42 | -------------------------------------------------------------------------------- /src/modifaxe/builder/Section.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.builder; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | Represents a section in a Modifaxe data file. 7 | **/ 8 | class Section { 9 | public var name(default, null): String; 10 | public var entries(default, null): Array = []; 11 | 12 | public function new(name: String) { 13 | this.name = name; 14 | } 15 | 16 | /** 17 | Checks if any entries have been added. 18 | **/ 19 | public function hasEntries() { 20 | return entries.length > 0; 21 | } 22 | 23 | /** 24 | Used internally to add entries to a `Section` while processing expressions. 25 | **/ 26 | public function addEntry(name: String, value: EntryValue) { 27 | final e = new Entry(name, value, this); 28 | entries.push(e); 29 | return e; 30 | } 31 | 32 | /** 33 | Returns a version of `name` that's safe to use as a Haxe identifier. 34 | **/ 35 | public function identifierSafeName(): String { 36 | return StringTools.replace(name, ".", "_"); 37 | } 38 | } 39 | 40 | #end 41 | -------------------------------------------------------------------------------- /src/Modifaxe.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import haxe.macro.Compiler; 4 | 5 | /** 6 | The main Modifaxe class. 7 | Primarily used for additional configuration in a project's `.hxml` file. 8 | **/ 9 | class Modifaxe { 10 | /** 11 | Used at runtime to check if `reload` has been called. 12 | **/ 13 | public static var refreshCount(default, null): Int = 1; 14 | 15 | /** 16 | Increments `refreshCount`. 17 | 18 | When loaders detect that their "count" does not match `Modifaxe.refreshCount`, 19 | they will update themselves. 20 | **/ 21 | public static function reload() { 22 | refreshCount++; 23 | } 24 | 25 | /** 26 | Applies the Modifaxe build macro to a path filter. 27 | 28 | This already runs once with the value of `-D modifaxe_path_filter`, but can be 29 | run additional times to add additional support for other packages. 30 | **/ 31 | public static function addPath(path: String) { 32 | #if macro 33 | Compiler.addGlobalMetadata(path, "@:build(modifaxe.BuildMacro.build())"); 34 | #end 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2026 Maybee "SomeRanDev" Rezbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modifaxe/InitMacro.hx: -------------------------------------------------------------------------------- 1 | package modifaxe; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Type; 7 | 8 | import modifaxe.Output; 9 | import modifaxe.config.Define; 10 | import modifaxe.format.Format; 11 | import modifaxe.format.HxModFormat; 12 | 13 | function init() { 14 | #if macro 15 | 16 | // Do not run in IDE 17 | if(Context.defined("display")) { 18 | return; 19 | } 20 | 21 | // Register the `.modhx` format 22 | Format.registerFormat("modhx", new HxModFormat()); 23 | 24 | // Apply `@:build` meta to path filter 25 | Modifaxe.addPath(Context.definedValue(Define.PathFilter) ?? ""); 26 | 27 | // Save data files 28 | Context.onAfterTyping(onAfterTyping); 29 | 30 | #end 31 | } 32 | 33 | /** 34 | Called after all `@:build` macros. 35 | **/ 36 | function onAfterTyping(_: Array) { 37 | // Process all files and store their load expressions 38 | for(formatIdent => fileList in Output.generateFileList()) { 39 | final format = formatIdent.getFormat(); 40 | if(format != null) { 41 | format.saveModFiles(fileList); // Saves the mod files for this format 42 | } 43 | } 44 | } 45 | 46 | #end 47 | -------------------------------------------------------------------------------- /src/modifaxe/builder/File.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.builder; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | Represents a file containing sections and entries. 7 | **/ 8 | class File { 9 | public var sections(default, null): Array
= []; 10 | 11 | var originalPath: String; 12 | var generatedPath: Null = null; 13 | 14 | /** 15 | `path` should be the file's path without configurations. 16 | If the file should use the default path, `null` should be passed. 17 | **/ 18 | public function new(path: String) { 19 | if(path.length == 0) { 20 | throw "Path must not be empty."; 21 | } 22 | 23 | originalPath = path; 24 | } 25 | 26 | /** 27 | Returns the path for the file with all configurations applied. 28 | 29 | This includes whether the path should be absolute/relative or within any sub-folders. 30 | **/ 31 | public function getPath(fileExtension: String): String { 32 | if(generatedPath == null) { 33 | generatedPath = Output.generateOutputPath(originalPath); 34 | } 35 | return haxe.io.Path.withExtension(generatedPath, fileExtension); 36 | } 37 | 38 | /** 39 | Used internally to add sections to a `File` while processing expressions. 40 | **/ 41 | public function addSection(section: Section) { 42 | this.sections.push(section); 43 | } 44 | } 45 | 46 | #end 47 | -------------------------------------------------------------------------------- /src/modifaxe/builder/EntryValue.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.builder; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | A union type for all the values that are supported by Modifaxe. 7 | **/ 8 | @:using(modifaxe.builder.EntryValue.EntryValueFunctions) 9 | enum EntryValue { 10 | EBool(value: Bool); 11 | EInt(intString: String); 12 | EFloat(floatString: String); 13 | EString(string: String); 14 | EEnum(identifier: String, type: haxe.macro.Type); 15 | } 16 | 17 | /** 18 | The functions for `EntryValue`. 19 | **/ 20 | class EntryValueFunctions { 21 | public static function toTypeString(v: EntryValue) { 22 | return switch(v) { 23 | case EBool(_): "b"; 24 | case EInt(_): "i"; 25 | case EFloat(_): "f"; 26 | case EString(_): "s"; 27 | case EEnum(_, _): "e"; 28 | } 29 | } 30 | 31 | public static function toTypeCharCode(v: EntryValue) { 32 | return switch(v) { 33 | case EBool(_): 98; 34 | case EInt(_): 105; 35 | case EFloat(_): 102; 36 | case EString(_): 115; 37 | case EEnum(_, _): 101; 38 | } 39 | } 40 | 41 | public static function toValueString(v: EntryValue) { 42 | return switch(v) { 43 | case EBool(value): value ? "true" : "false"; 44 | case EInt(intString): intString; 45 | case EFloat(floatString): floatString; 46 | case EString(string): '"$string"'; 47 | case EEnum(identifier, _): identifier; 48 | } 49 | } 50 | } 51 | 52 | #end 53 | -------------------------------------------------------------------------------- /src/modifaxe/runtime/ModParserError.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.runtime; 2 | 3 | #if !modifaxe_parser_no_error_check 4 | 5 | /** 6 | The error type for `ModParser`. 7 | 8 | Every type of error can be represented with this enum. 9 | **/ 10 | @:using(modifaxe.runtime.ModParserError.ModParserErrorFunctions) 11 | enum ModParserError { 12 | UnexpectedChar(charCode: Int); 13 | 14 | ExpectedChar(charCode: Int); 15 | ExpectedIdentifier; 16 | ExpectedBool; 17 | ExpectedDigit; 18 | 19 | SectionShouldBeStartOfLine; 20 | EntryShouldBeStartOfLine; 21 | UnsupportedEscapeSequence; 22 | } 23 | 24 | /** 25 | Provides functions for `ModParserError`. 26 | **/ 27 | class ModParserErrorFunctions { 28 | /** 29 | Returns the message the `ModParserError` should print. 30 | **/ 31 | public static function getMessage(error: ModParserError) { 32 | return switch(error) { 33 | case UnexpectedChar(String.fromCharCode(_) => charString): 'Unexpected character $charString'; 34 | case ExpectedChar(String.fromCharCode(_) => charString): 'Expected character $charString'; 35 | case ExpectedIdentifier: 'Expected identifier'; 36 | case ExpectedBool: 'Expected true or false'; 37 | case ExpectedDigit: 'Expected number'; 38 | case SectionShouldBeStartOfLine: 'Section should start at the beginning of the line'; 39 | case EntryShouldBeStartOfLine: 'Entry should start at the beginning of the line'; 40 | case UnsupportedEscapeSequence: 'Unsupported escape sequence'; 41 | } 42 | } 43 | } 44 | 45 | #end 46 | -------------------------------------------------------------------------------- /src/modifaxe/BuildMacro.hx: -------------------------------------------------------------------------------- 1 | package modifaxe; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | 8 | import modifaxe.builder.Builder; 9 | import modifaxe.config.Meta; 10 | 11 | /** 12 | The `@:build` macro function called on all types. 13 | **/ 14 | function build() { 15 | // ClassType 16 | final cls = #if macro Context.getLocalClass() #else null #end; 17 | final cls = cls != null ? cls.get() : null; 18 | 19 | // Array 20 | final fields: Array = #if macro Context.getBuildFields() #else [] #end; 21 | 22 | // Create `Builder` if metadata arguments are supplied 23 | var builder = null; 24 | function pushArgs(args: Null>) { 25 | if(builder == null) builder = new Builder(); 26 | builder.setArguments(args ?? []); 27 | } 28 | function popArgs() builder.popArguments(); 29 | 30 | // Check for class `@:modifaxe` args 31 | final processAll = if(cls != null) { 32 | final metaEntries: Array = cls.meta.extract(Meta.Modifaxe); 33 | for(entry in metaEntries) { 34 | pushArgs(entry.params); 35 | } 36 | metaEntries.length > 0; 37 | } else { 38 | false; 39 | } 40 | 41 | // Process fields 42 | for(f in fields) { 43 | // Check for `@:modifaxe` 44 | var hasMeta = false; 45 | if(f.meta != null) { 46 | for(m in f.meta) { 47 | if(m.name == Meta.Modifaxe) { 48 | pushArgs(m.params); 49 | hasMeta = true; 50 | break; 51 | } 52 | } 53 | } 54 | 55 | // Skip if no `@:modifaxe` on function or class 56 | if(!hasMeta && !processAll) { 57 | continue; 58 | } 59 | 60 | // Process the function body 61 | switch(f.kind) { 62 | case FFun(func) if(func.expr != null): { 63 | final newExpr = builder.buildFunctionExpr(cls, f, func.expr); 64 | if(newExpr != null) { 65 | func.expr = newExpr; 66 | } 67 | } 68 | case _: 69 | } 70 | 71 | // Pop function-specific state 72 | if(hasMeta) { 73 | popArgs(); 74 | } 75 | } 76 | 77 | if(builder != null) { 78 | builder.generateLoadFunction(); 79 | for(f in builder.getAdditionalFields()) { 80 | fields.push(f); 81 | } 82 | } 83 | 84 | return fields; 85 | } 86 | 87 | #end 88 | -------------------------------------------------------------------------------- /src/modifaxe/format/Format.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.format; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Expr; 6 | 7 | import modifaxe.builder.File; 8 | 9 | /** 10 | The base class for a Modifaxe format. 11 | 12 | Its abstract functions allow for control over how the "mod" file is 13 | saved and the expressions used to load at runtime. 14 | **/ 15 | abstract class Format { 16 | public static var formats(default, null): Map = []; 17 | 18 | /** 19 | Registers a format for Modifaxe. 20 | 21 | `name` should be a unique identifier for your format. 22 | Assign the `Format` argument `name` in `@:modifaxe` to use the format. 23 | **/ 24 | public static function registerFormat(name: String, format: Format) { 25 | // Ignore capitalization 26 | name = name.toLowerCase(); 27 | 28 | if(formats.exists(name)) { 29 | throw "Cannot register format with same name twice."; 30 | } 31 | formats.set(name, format); 32 | } 33 | 34 | /** 35 | Constructor. No need to create one in child classes, but you can if you want. 36 | **/ 37 | public function new() { 38 | } 39 | 40 | /** 41 | Once all `@:build` macros have run, this is called to save the accumulated `File` 42 | objects in your desired format. This is called in `Context.onAfterTyping`. 43 | 44 | Simply use `modifaxe.Output` `saveContent` or `saveBytes` to save the file. 45 | (If you use `sys.io.File` directly, Modifaxe can't track and delete old files). 46 | **/ 47 | public abstract function saveModFiles(files: Array): Void; 48 | 49 | /** 50 | Generates the `Expr` for a class's `_modifaxe_loadData` function. 51 | 52 | Use this to generate the runtime code to read and parse your desired format. 53 | 54 | This function may be called multiple times, once for each class that uses the format. 55 | **/ 56 | public abstract function generateLoadExpression(files: Array): Expr; 57 | 58 | /** 59 | A helper function that generates en expression that loads an enum given its `Type` 60 | (of `enum` or `enum abstract`) and the expression for the loader argument 61 | 62 | The loader argument (`stringExpr`) should be an expression that results in a `String`. 63 | **/ 64 | function generateEnumLoadingExpr(enumType: haxe.macro.Type, stringExpr: Expr) { 65 | final enumLoadIdent = Output.getFunctionForEnumType(enumType); 66 | return if(enumLoadIdent != null) { 67 | final mn = "Modifaxe_" + enumLoadIdent; 68 | macro modifaxe.enumloaders.$mn.$enumLoadIdent($stringExpr); 69 | } else { 70 | null; 71 | } 72 | } 73 | } 74 | 75 | #end 76 | -------------------------------------------------------------------------------- /src/modifaxe/FileCollection.hx: -------------------------------------------------------------------------------- 1 | package modifaxe; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Context; 6 | 7 | import modifaxe.builder.File; 8 | import modifaxe.builder.Section; 9 | import modifaxe.config.Define; 10 | import modifaxe.format.FormatIdentifier; 11 | 12 | class FileCollection { 13 | /** 14 | A `Map` of the files that should be generated. 15 | 16 | The outer map uses format's identifier as the key. 17 | The inner map uses the absolute file path for the key. 18 | **/ 19 | var files: Map> = []; 20 | 21 | /** 22 | This is set to `true` once a single entry has been added to `files`. 23 | **/ 24 | var hasAnyFiles: Bool = false; 25 | 26 | /** 27 | Constructor. 28 | **/ 29 | public function new() { 30 | } 31 | 32 | /** 33 | Returns `true` if no sections have been added to this collection. 34 | **/ 35 | public function isEmpty() { 36 | return !hasAnyFiles; 37 | } 38 | 39 | /** 40 | Clears the entire collection. 41 | **/ 42 | public function clear() { 43 | files = []; 44 | } 45 | 46 | /** 47 | Adds a section to a file given its path and format. 48 | **/ 49 | public function addSectionToFile(section: Section, format: Null, filePath: Null) { 50 | // Use default file path if `null` 51 | filePath ??= #if macro Context.definedValue(Define.DefaultFilePath) ?? #end "data"; 52 | 53 | // Use default format if `null` 54 | format ??= #if macro Context.definedValue(Define.DefaultFormat) ?? #end "modhx"; 55 | 56 | if(filePath == null || format == null) return; 57 | 58 | if(!files.exists(format)) { 59 | files.set(format, []); 60 | } 61 | 62 | final filePathMap = files.get(format); 63 | if(filePathMap == null) return; 64 | 65 | final absolutePath = filePath.length == 0 ? filePath : sys.FileSystem.absolutePath(filePath); 66 | if(!filePathMap.exists(absolutePath)) { 67 | filePathMap.set(absolutePath, new File(filePath)); 68 | hasAnyFiles = true; 69 | } 70 | 71 | final file = filePathMap.get(absolutePath); 72 | if(file != null) { 73 | file.addSection(section); 74 | } 75 | } 76 | 77 | /** 78 | Generates an `Array` of `File`s for each format. 79 | **/ 80 | public function generateFileList(): Map> { 81 | final result: Map> = []; 82 | 83 | for(format => fileMap in files) { 84 | final fileList: Array = []; 85 | for(_ => fileObj in fileMap) { 86 | fileList.push(fileObj); 87 | } 88 | result.set(format, fileList); 89 | } 90 | 91 | return result; 92 | } 93 | } 94 | 95 | #end 96 | -------------------------------------------------------------------------------- /src/modifaxe/config/Define.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.config; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | /** 6 | A list of all the defines in Modifaxe. 7 | **/ 8 | enum abstract Define(String) from String to String { 9 | /** 10 | -D modifaxe_default_file_path=FILE_PATH 11 | 12 | Default value: "data" 13 | 14 | Configures the default file path the data file is generated at. 15 | **/ 16 | var DefaultFilePath = "modifaxe_default_file_path"; 17 | 18 | /** 19 | -D modifaxe_default_format=FORMAT_NAME 20 | 21 | Default value: "modhx" 22 | 23 | Configures the default format used by mod files. 24 | **/ 25 | var DefaultFormat = "modifaxe_default_format"; 26 | 27 | /** 28 | -D modifaxe_path_filter=PATH 29 | 30 | Default value: "" 31 | 32 | The path filter applied to the `addGlobalMetadata` function that sets up the 33 | `@:build` macro. Use this to optimize the build macro to only check in a 34 | specific package or module. 35 | 36 | Use `--macro Modifaxe.addPath(pathFilter)` to add an additional path. 37 | **/ 38 | var PathFilter = "modifaxe_path_filter"; 39 | 40 | /** 41 | -D modifaxe_dont_delete_old_files 42 | 43 | If defined, old data files will not be deleted and the `.modifaxe` tracking file will 44 | not be generated. 45 | **/ 46 | var DontDeleteOldFiles = "modifaxe_dont_delete_old_files"; 47 | 48 | /** 49 | -D modifaxe_old_file_tracker_name=FILE_PATH 50 | 51 | Default value: ".modifaxe" 52 | 53 | This is the name and path of the file that stores a list of generated files. 54 | **/ 55 | var OldFileTrackerName = "modifaxe_old_file_tracker_name"; 56 | 57 | /** 58 | -D modifaxe_use_relative_path 59 | 60 | By default, the data file path injected into the code is the absolute path of the generated file. 61 | If this is defined, the user-specified path will be used verbatim. 62 | **/ 63 | var UseRelativePath = "modifaxe_use_relative_path"; 64 | 65 | /** 66 | -D modifaxe_make_enum_loaders_reflective 67 | 68 | If defined, the generated enum-loading classes will not have `@:nativeGen` and `@:unreflective`. 69 | **/ 70 | var MakeEnumLoaderReflective = "modifaxe_make_enum_loaders_reflective"; 71 | 72 | /** 73 | -D modifaxe_parser_no_map_cache 74 | 75 | If defined, disables the use of `Map` to cache `.modhx` parsers and their entry positions. 76 | Useful for custom targets with minimal API support. 77 | **/ 78 | var ParserNoMapCache = "modifaxe_parser_no_map_cache"; 79 | 80 | /** 81 | -D modifaxe_no_error_check 82 | 83 | Disables error checking on `.modhx` parsing (improves performance). 84 | **/ 85 | var ParserNoErrorCheck = "modifaxe_parser_no_error_check"; 86 | 87 | /** 88 | -D modifaxe_parser_use_string_concat 89 | 90 | Uses string concatenation to generate the resulting `String` object when parsing 91 | a `String` from `.modhx`. 92 | **/ 93 | var ParserUseStringConcat = "modifaxe_parser_use_string_concat"; 94 | 95 | /** 96 | -D modiflaxe_no_dynamic_functions 97 | 98 | Disables `dynamic` functions in Modifaxe runtime code. 99 | **/ 100 | var NoDynamicFunctions = "modiflaxe_no_dynamic_functions"; 101 | } 102 | 103 | #end 104 | -------------------------------------------------------------------------------- /src/modifaxe/format/HxModFormat.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.format; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Expr; 6 | 7 | import modifaxe.builder.File; 8 | 9 | /** 10 | The implementation of the `.modhx` file format. 11 | **/ 12 | class HxModFormat extends Format { 13 | /** 14 | The file extension used by this format. 15 | **/ 16 | static var extension = "modhx"; 17 | 18 | /** 19 | A `Map` that accumulates the number of entries for `.modhx` files. 20 | 21 | The key is the file path, and the value is the entry count. 22 | **/ 23 | var entryCounter: Map = []; 24 | 25 | /** 26 | Generates default value for `output`. 27 | **/ 28 | inline function generateDefaultOutputObject() { 29 | return { output: new StringBuf(), entries: 0 }; 30 | } 31 | 32 | /** 33 | Generates an expression that loads data from `.modhx` files. 34 | **/ 35 | public function generateLoadExpression(files: Array): Expr { 36 | final blockExpressions = []; 37 | 38 | for(file in files) { 39 | final expressions = []; 40 | 41 | final path = file.getPath(extension); 42 | 43 | if(!entryCounter.exists(path)) { 44 | entryCounter.set(path, 0); 45 | } 46 | var entryCount = entryCounter.get(path) ?? 0; 47 | 48 | #if macro // fix display error with $v{} 49 | expressions.push( 50 | macro final loader = modifaxe.runtime.ModParser.fromEntryCount($v{path}, $v{entryCount}) 51 | ); 52 | #end 53 | 54 | for(section in file.sections) { 55 | for(entry in section.entries) { 56 | 57 | // Get identifier for static variable with the data 58 | final identifier = entry.getUniqueName(); 59 | 60 | // Expression used to load data from `.modhx` parser 61 | final valueExpr = switch(entry.value) { 62 | case EBool(_): macro loader.nextBool(false); 63 | case EInt(_): macro loader.nextInt(0); 64 | case EFloat(_): macro loader.nextFloat(0.0); 65 | case EString(_): macro loader.nextString(""); 66 | case EEnum(_, enumType): generateEnumLoadingExpr(enumType, macro loader.nextEnumIdentifier("")); 67 | } 68 | 69 | // Store expression in list. 70 | if(valueExpr != null) { 71 | expressions.push(macro $i{identifier} = $valueExpr); 72 | } 73 | } 74 | 75 | // Increment entry count 76 | entryCount += section.entries.length; 77 | } 78 | 79 | // Update count 80 | entryCounter.set(path, entryCount); 81 | 82 | blockExpressions.push(macro $b{expressions}); 83 | } 84 | 85 | return macro @:mergeBlock $b{blockExpressions}; 86 | } 87 | 88 | /** 89 | Generates `.modhx` files from the provided `File`s. 90 | This is called after all `@:build` macros in `Context.onAfterTyping`. 91 | **/ 92 | public function saveModFiles(files: Array): Void { 93 | for(file in files) { 94 | final buf = new StringBuf(); 95 | 96 | for(section in file.sections) { 97 | buf.addChar(91); // [ 98 | buf.add(section.name); 99 | buf.addChar(93); // ] 100 | buf.addChar(10); // \n 101 | 102 | for(entry in section.entries) { 103 | buf.addChar(entry.value.toTypeCharCode()); 104 | buf.addChar(46); // . 105 | buf.add(entry.name); 106 | buf.addChar(58); // : 107 | buf.addChar(32); // [space] 108 | buf.add(entry.value.toValueString()); 109 | buf.addChar(10); // \n 110 | } 111 | 112 | buf.addChar(10); // \n 113 | } 114 | 115 | Output.saveContent(file.getPath(extension), buf.toString()); 116 | } 117 | } 118 | } 119 | 120 | #end 121 | -------------------------------------------------------------------------------- /src/modifaxe/tools/ExprTools.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.tools; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Expr; 6 | import haxe.macro.ExprTools; 7 | 8 | /** 9 | A class used to help track the expression mapping history so a descriptive name can be 10 | generated for the constant value entry. 11 | **/ 12 | class ExprMapContext { 13 | var nameStack: Array = []; 14 | var varNameStack: Array = []; 15 | var funcNameStack: Array = []; 16 | 17 | public function new() { 18 | } 19 | 20 | /** 21 | Generates a name given the current context. 22 | **/ 23 | public function generateName() { 24 | if(nameStack.length > 0) { 25 | return nameStack[nameStack.length - 1]; 26 | } 27 | 28 | return "value"; 29 | } 30 | 31 | /** 32 | Pushes a name to the stack and returns itself. 33 | Helpful for pushing a name and passing as an argument. 34 | **/ 35 | public function named(name: String) { 36 | nameStack.push(name); 37 | return this; 38 | } 39 | 40 | public function popName() { 41 | nameStack.pop(); 42 | } 43 | 44 | public function pushVar(name: String) { 45 | varNameStack.push(name); 46 | } 47 | 48 | public function popVar() { 49 | varNameStack.pop(); 50 | } 51 | 52 | public function pushFunction(name: String) { 53 | funcNameStack.push(name); 54 | } 55 | 56 | public function popFunction() { 57 | funcNameStack.pop(); 58 | } 59 | } 60 | 61 | /** 62 | Used internally in `mapWithContext`. 63 | **/ 64 | private inline function opt(e: Null, role: Null, f: (Expr, Null) -> Expr): Null { 65 | return e == null ? null : f(e, role); 66 | } 67 | 68 | /** 69 | Used internally in `mapWithContext`. 70 | **/ 71 | private inline function arrMap(el: Array, f: (Expr, Int) -> Expr): Array { 72 | var ret = []; 73 | for (i in 0...el.length) ret.push(f(el[i], i)); 74 | return ret; 75 | } 76 | 77 | /** 78 | Used internally in `mapWithContext`. 79 | 80 | Generates a descriptive name for an expression used with a Binop. 81 | **/ 82 | private function getBinopName(op: Binop, isRight: Bool) { 83 | final opBaseName = Std.string(op).substring(2); 84 | 85 | if(isRight) { 86 | final result = switch(opBaseName) { 87 | case "Mult": "MultipliedBy"; 88 | case "Div": "DividedBy"; 89 | case "Eq": "EqualTo"; 90 | case "NotEq": "NotEqualTo"; 91 | case "Gte": "GreaterThanOrEqualTo"; 92 | case "Lte": "LessThanOrEqualTo"; 93 | case _: null; 94 | } 95 | 96 | if(result != null) return result; 97 | } 98 | 99 | final base = switch(opBaseName) { 100 | case "Add": "Addition"; 101 | case "Sub": "Subtraction"; 102 | case "Mult": "Multiplication"; 103 | case "Div": "Division"; 104 | case "Eq": "Equals"; 105 | case "NotEq": "NotEquals"; 106 | case "Gt": "GreaterThan"; 107 | case "Gte": "GreaterThanOrEquals"; 108 | case "Lt": "LessThen"; 109 | case "Lte": "LessThanOrEquals"; 110 | case n: n; 111 | } 112 | 113 | return (isRight ? "RightOf" : "LeftOf") + base; 114 | } 115 | 116 | /** 117 | Used internally in `mapWithContext`. 118 | **/ 119 | private inline function getUnopName(op: Unop) { 120 | return Std.string(op).substring(2); 121 | } 122 | 123 | /** 124 | An alternative version of `ExprTools.map` that passes around an `ExprMapContext` and names each `ExprDef`. 125 | **/ 126 | function mapWithContext(e: Expr, context: ExprMapContext, callback: (Expr, ExprMapContext) -> Expr): Expr { 127 | function f(_e, role: Null) { 128 | if(role == null) return callback(_e, context); 129 | 130 | final result = callback(_e, context.named(role)); 131 | context.popName(); 132 | return result; 133 | } 134 | 135 | return { 136 | pos: e.pos, 137 | expr: switch (e.expr) { 138 | case EConst(_): e.expr; 139 | case EArray(e1, e2): EArray(f(e1, "ArrayAccessed"), f(e2, "ArrayAccess")); 140 | case EBinop(op, e1, e2): EBinop(op, f(e1, getBinopName(op, false)), f(e2, getBinopName(op, true))); 141 | case EField(e, field, kind): EField(f(e, "fieldAccessed"), field, kind); 142 | case EParenthesis(e): EParenthesis(f(e, null)); 143 | case EObjectDecl(fields): 144 | var ret = []; 145 | for (field in fields) 146 | ret.push({field: field.field, expr: f(field.expr, "ObjectField_" + field.field), quotes: field.quotes}); 147 | EObjectDecl(ret); 148 | case EArrayDecl(el): EArrayDecl(arrMap(el, (e, i) -> f(e, "ArrayElement" + i))); 149 | case ECall(e, params): ECall(f(e, "Called"), arrMap(params, (e, i) -> f(e, "CallArgument" + i))); 150 | case ENew(tp, params): ENew(tp, arrMap(params, (e, i) -> f(e, "ConstructorArgument" + i))); 151 | case EUnop(op, postFix, e): EUnop(op, postFix, f(e, Std.string(op))); 152 | case EVars(vars): 153 | var ret = []; 154 | for (v in vars) { 155 | context.pushVar(v.name); 156 | var v2:Var = {name: v.name, type: v.type, expr: opt(v.expr, null, f)}; 157 | if (v.isFinal != null) 158 | v2.isFinal = v.isFinal; 159 | ret.push(v2); 160 | context.popVar(); 161 | } 162 | EVars(ret); 163 | case EBlock(el): EBlock(arrMap(el, (e, i) -> f(e, null))); 164 | case EFor(it, expr): EFor(f(it, "ForIterator"), f(expr, "ForExpr")); 165 | case EIf(econd, eif, eelse): EIf(f(econd, "IfCondition"), f(eif, "Elseif"), opt(eelse, "Else", f)); 166 | case EWhile(econd, e, normalWhile): EWhile(f(econd, "WhileCondition"), f(e, "WhileBlock"), normalWhile); 167 | case EReturn(e): EReturn(opt(e, "Returned", f)); 168 | case EUntyped(e): EUntyped(f(e, null)); 169 | case EThrow(e): EThrow(f(e, "Thrown")); 170 | case ECast(e, t): ECast(f(e, "Casted"), t); 171 | case EIs(e, t): EIs(f(e, null), t); 172 | case EDisplay(e, dk): EDisplay(f(e, null), dk); 173 | case ETernary(econd, eif, eelse): ETernary(f(econd, "TernaryCondition"), f(eif, "TernaryIfTrue"), f(eelse, "TernaryIfFalse")); 174 | case ECheckType(e, t): ECheckType(f(e, null), t); 175 | case EContinue, EBreak: 176 | e.expr; 177 | case ETry(e, catches): 178 | var ret = []; 179 | var index = 0; 180 | for (c in catches) 181 | ret.push({name: c.name, type: c.type, expr: f(c.expr, "Catch" + (index++))}); 182 | ETry(f(e, "TryBlock"), ret); 183 | case ESwitch(e, cases, edef): 184 | var ret = []; 185 | var index = 0; 186 | for (c in cases) { 187 | ret.push({expr: opt(c.expr, "Case" + index, f), guard: opt(c.guard, "CaseGuard" + index, f), values: arrMap(c.values, (e, i) -> f(e, "Case" + index + "Pattern" + i))}); 188 | index++; 189 | } 190 | ESwitch(f(e, "SwitchValue"), ret, edef == null || edef.expr == null ? edef : f(edef, "SwitchDefault")); 191 | case EFunction(kind, func): 192 | var ret = []; 193 | var index = 0; 194 | for (arg in func.args) { 195 | ret.push({ 196 | name: arg.name, 197 | opt: arg.opt, 198 | type: arg.type, 199 | value: opt(arg.value, "FuncArg" + (index++) + "Default", f) 200 | }); 201 | } 202 | EFunction(kind, { 203 | args: ret, 204 | ret: func.ret, 205 | params: func.params, 206 | expr: opt(func.expr, null, f) 207 | }); 208 | case EMeta(m, e): EMeta(m, f(e, null)); 209 | } 210 | }; 211 | } 212 | 213 | #end 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WOOO been a while since I made a logo. 2 | 3 | [![Test Workflow](https://github.com/SomeRanDev/modifaxe/actions/workflows/Test_DevEnv.yml/badge.svg)](https://github.com/SomeRanDev/modifaxe/actions) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | Modifaxe Thread 6 | 7 | *A tool for modifying hardcoded values in your post-build Haxe application.* 8 | 9 | Change a value -> recompile -> test -> repeat. Every programmer has experienced this loop before; it's very tempting to "guess and check" when it comes to visually designing something with code. This library seeks to aliviate the tedious "recompile" step by allowing hardcoded values from your code to be modified AFTER compiling. 10 | 11 |   12 |   13 | 14 | ## Table of Contents 15 | 16 | | Topic | Description | 17 | | --- | --- | 18 | | [Installation](#installation) | How to install this library into your project. | 19 | | [Reloading](#reloading) | How to reload values at runtime. | 20 | | [Metadata Configuration](#metadata-configuration) | How to configure the metadata. | 21 | | [Define Configuration](#defines) | A list of defines to set the library preferences. | 22 | | [.modhx Format](#modhx-format) | An explanation of the `.modhx` format. | 23 | | [How it Works](#how-it-works) | How Modifaxe transforms your project to function. | 24 | 25 |   26 |   27 |   28 | 29 | ## Installation 30 | First install Modifaxe using one of the commands below: 31 | ```hxml 32 | # install haxelib release (may not exist atm!!) 33 | haxelib install modifaxe 34 | 35 | # install nightly (recommended!) 36 | haxelib git modifaxe https://github.com/SomeRanDev/modifaxe.git 37 | ``` 38 | 39 | Next add the library to your .hxml or compile command: 40 | ``` 41 | -lib modifaxe 42 | ``` 43 | 44 | Add the `@:modifaxe` metadata to a class or function: 45 | ```haxe 46 | @:modifaxe 47 | function getWindowSize() { 48 | return 800; 49 | } 50 | ``` 51 | 52 | Compile your Haxe project to a `sys` target with file-system access. 53 | 54 | Modify the value(s) in the generated `values.modhx` file: 55 | ```haxe 56 | [Main.getWindowSize] 57 | i.return: 800 58 | ``` 59 | 60 | Aaand run! 61 | 62 |   63 |   64 |   65 | 66 | ## Reloading 67 | 68 | To reload the values at runtime, `Modifaxe.reload` can be called. 69 | 70 | This can be used to generate your own makeshift hot-reloading system, like: 71 | ```haxe 72 | // Update function in some random game engine... 73 | function update() { 74 | if(isDebugReloadKeyPressed()) { 75 | Modifaxe.reload(); 76 | } 77 | } 78 | ``` 79 | 80 |   81 |   82 |   83 | 84 | ## Metadata Configuration 85 | 86 | The `@:mod` metadata can be placed on expressions to specify their name. 87 | ```haxe 88 | @:modifaxe 89 | function getWindowSize() { 90 | return @:mod("my_num") 800; 91 | } 92 | ``` 93 | ```haxe 94 | [Main.getWindowSize] 95 | i.my_num: 800 96 | ``` 97 | 98 | To only use constants that have the `@:mod` metadata, the `ModOnly` argument can be used: 99 | ```haxe 100 | @:modifaxe(ModOnly) 101 | function getWindowSize() { 102 | return @:mod("my_num") 800 + 100; 103 | } 104 | ``` 105 | 106 |   107 | 108 | The `@:mod` metadata must also be used to allow for enum configuration. Use the `Enum` argument to set the path to the enum type. 109 | 110 | ```haxe 111 | enum Color { 112 | Red; 113 | Green; 114 | Blue; 115 | } 116 | 117 | @:modifaxe 118 | function colorWindow() { 119 | window.setColor(@:mod(Enum=Main.Color) Red); 120 | } 121 | ``` 122 | ```haxe 123 | [Main.colorWindow] 124 | i.Argument0: Red 125 | ``` 126 | 127 |   128 | 129 | The `File` argument can be used to specify the filename the entries under a metadata will be placed in. Multiple data files can be generated/loaded from this way: 130 | ```haxe 131 | // Generates data1.modhx file containing one entry 132 | @:modifaxe(File="data1") 133 | function getWindowWidth() { return 800; } 134 | 135 | // Generates data2.modhx file that also contains this one entry 136 | @:modifaxe(File="data2") 137 | function getWindowHeight() { return 400; } 138 | ``` 139 | 140 |   141 | 142 | The `Format` argument can be used to set the format of the file entries are placed into. By default, Modifaxe only has one format supported, `modhx`. However, it is possible for other libraries to add their own formats. Check out the [Modifaxe/JSON](https://github.com/SomeRanDev/modifaxe.JSON) library to see an example of this! 143 | 144 | If Modifaxe/JSON is installed, a `.json` format can be used like so: 145 | ```haxe 146 | // Generates and loads the data in a modifiable .json file 147 | @:modifaxe(Format=Json) 148 | function getWindowWidth() { 149 | return 800; 150 | } 151 | ``` 152 | 153 |   154 |   155 |   156 | 157 | ## Defines 158 | 159 | To specify a specific path this library works on (instead of using a global `@:build` macro which could be slower), the `-D modifaxe_path_filter` define can be used: 160 | ```hxml 161 | -D modifaxe_path_filter=my_package 162 | ``` 163 | 164 | To set the default filename for the generated data file, the `-D modifaxe_default_file_path` define can be used (the extension is added automatically): 165 | ```hxml 166 | -D modifaxe_default_file_path=my_data_file 167 | ``` 168 | 169 | You can view a list of all the `-D` defines you can use to configure the library [here](https://github.com/SomeRanDev/modifaxe/blob/main/src/modifaxe/config/Define.hx). 170 | 171 |   172 |   173 |   174 | 175 | ## .modhx Format 176 | The `.modhx` is a text-based file format designed specifically for this project. It is designed to be both human-readable and easily parsable. 177 | 178 |   179 | 180 | ### Comments 181 | Content after a pound sign (#) is a comment and is ignored during parsing: 182 | ```python 183 | # This is a comment. 184 | # This is also a comment. 185 | Something # Comment after content 186 | ``` 187 | 188 |   189 | 190 | ### Sections and Values 191 | Entries are separated into sections. A section is a unique identifier followed by a colon. 192 | 193 | A list of values should follow with the `.: ` format: 194 | ```haxe 195 | [My.Unique.ID] 196 | b.trueOrFalse=true 197 | i.myNum=123 198 | f.floatNum=6.9 199 | s.string="Insert valid Haxe string here. 200 | They can be multiline." 201 | ``` 202 | 203 | Please note the order of value entries MATTERS. The Haxe code for parsing the custom-made `.modhx` is hardcoded to expect the values in their generated order. The section and value identifiers exist to help humans locate values to modify. 204 | 205 |   206 | 207 | ### Value Declaration Options 208 | 209 | There are four types supported: 210 | * `b` is a boolean. 211 | * `i` is an integer. 212 | * `f` is a float. 213 | * `s` is a string. 214 | 215 | The `name` must be a valid Haxe variable name. 216 | 217 | The `value` must be a valid constant Haxe expression of the specified type. 218 | 219 |   220 |   221 |   222 | 223 | ## How it Works 224 | 225 | Each class that uses `@:modifaxe` is given a static function named `_modifaxe_loadData` and a static var for each constant that can be modified. 226 | 227 | At the start of any function with a changeable constant, `_modifaxe_loadData` checks its internal counter to see if it matches with the `Modifaxe` counter. If it doesn't, it runs the procedrually-generated loading code for all the static variables. Otherwise, nothing happens. 228 | 229 | ```haxe 230 | // Before 231 | @:modifaxe 232 | class MyClass { 233 | public function doSomething() { 234 | trace(123); 235 | } 236 | } 237 | ``` 238 | ```haxe 239 | // After 240 | class MyClass { 241 | static var MyClass_doSomething_Argument0 = 123; 242 | 243 | static function _modifaxe_loadData() { 244 | static var count = 0; 245 | if(count != Modifaxe.refreshCount) count = Modifaxe.refreshCount; 246 | else return; 247 | 248 | final parser = modifaxe.runtime.ModParser.fromEntryCount("data.modhx", 0); 249 | MyClass_doSomething_Argument0 = parser.nextInt(123); 250 | } 251 | 252 | public function doSomething() { 253 | _modifaxe_loadData(); 254 | trace(MyClass_doSomething_Argument0); 255 | } 256 | } 257 | ``` 258 | -------------------------------------------------------------------------------- /src/modifaxe/Output.hx: -------------------------------------------------------------------------------- 1 | package modifaxe; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.MacroStringTools; 8 | import haxe.macro.Type; 9 | 10 | import modifaxe.builder.File; 11 | import modifaxe.builder.Section; 12 | import modifaxe.config.Define; 13 | import modifaxe.format.FormatIdentifier; 14 | 15 | /** 16 | Singleton that manages output. 17 | **/ 18 | class Output { 19 | /** 20 | A collection for all files, sections, and entries accumulated for 21 | the entire project. 22 | **/ 23 | static var allFiles = new FileCollection(); 24 | 25 | /** 26 | An accumulated list of fields to generate on the singleton class that 27 | will load the data at runtime. 28 | **/ 29 | static var loaderFields: Array = []; 30 | 31 | /** 32 | A list of all saved files. 33 | Used to determine if there's any old files that need to be deleted. 34 | **/ 35 | static var savedFiles: Array = []; 36 | 37 | /** 38 | Getter for `loaderFields`. 39 | **/ 40 | public static function getLoaderFields() { 41 | return loaderFields; 42 | } 43 | 44 | /** 45 | Adds a section to the project's complete Modifaxe file collection. 46 | **/ 47 | public static function addSectionToAllFiles(section: Section, format: Null, filePath: Null) { 48 | allFiles.addSectionToFile(section, format, filePath); 49 | } 50 | 51 | /** 52 | Given an `enum` or `enum abstract` `Type`, this adds a function to the data loading singleton 53 | to a case of said type as a `String`. 54 | **/ 55 | public static function addDataEnumLoader(enumOrAbstractType: Type, enumTypeExpressionPos: Position, enumValueExpression: Expr, defaultValueName: String) { 56 | static var isAdded: Map = []; 57 | 58 | if(isAdded.exists(Std.string(enumOrAbstractType))) { 59 | return; 60 | } 61 | 62 | final cases = getCaseDataFromBaseType(enumOrAbstractType, enumTypeExpressionPos, defaultValueName); 63 | if(cases == null) { 64 | return; 65 | } 66 | 67 | final switchExpr = { 68 | expr: ESwitch(macro name, cases.slice(1), cases[0].expr), 69 | pos: enumValueExpression.pos 70 | } 71 | 72 | final name = getFunctionForEnumType(enumOrAbstractType); 73 | if(name == null) { 74 | return; 75 | } 76 | 77 | final functionTD = { 78 | name: "Modifaxe_" + (name : String), 79 | pos: enumTypeExpressionPos, 80 | pack: ["modifaxe", "enumloaders"], 81 | fields: [{ 82 | name: (name : String), 83 | access: [APublic, AStatic], 84 | pos: enumTypeExpressionPos, 85 | kind: FFun({ 86 | args: [{ name: "name", type: macro : String }], 87 | expr: macro return $switchExpr 88 | }) 89 | }], 90 | kind: TDClass(null, null, false, true, false), 91 | 92 | #if !modifaxe_make_enum_loaders_reflective 93 | // This class is used internally, so let's optimize it a little. 94 | meta: [ 95 | { name: ":unreflective", pos: enumTypeExpressionPos }, 96 | { name: ":nativeGen", pos: enumTypeExpressionPos } 97 | ] 98 | #end 99 | }; 100 | 101 | #if macro 102 | Context.defineType(functionTD); 103 | #end 104 | } 105 | 106 | /** 107 | Given a `Type` that's a `TEnum` or `TAbstract`, returns a list of `Case`s for each 108 | enum case or enum abstract variable. 109 | 110 | The case conditional value being the `String` name of the enum case, the return value 111 | being the actual enum value. 112 | **/ 113 | static function getCaseDataFromBaseType(enumOrAbstractType: Type, errorPos: Position, defaultCaseName: String): Array { 114 | var enumType = null; 115 | var abstractType = null; 116 | switch(enumOrAbstractType) { 117 | case TEnum(_.get() => e, []): enumType = e; 118 | case TAbstract(_.get() => absType, []) if(absType.meta.has(":enum")): abstractType = absType; 119 | case t: #if macro Context.error("Invalid Modifaxe enum type " + t, errorPos); #end 120 | } 121 | 122 | var defaultCase = null; 123 | 124 | final identifiers: Null> = if(enumType != null) { 125 | // Get list of enum cases 126 | final result = []; 127 | for(name => field in enumType.constructs) { 128 | final enumTypePath = getBaseTypePathAsExpr(enumType); 129 | switch(field.type) { 130 | case TEnum(_, []): { 131 | final c = { name: name, expr: macro $enumTypePath.$name }; 132 | if(defaultCaseName == name) defaultCase = c; 133 | result.push(c); 134 | } 135 | case _: 136 | } 137 | } 138 | result; 139 | } else if(abstractType != null && abstractType.impl != null) { 140 | // Get list of enum abstract variables 141 | [ 142 | for(f in abstractType.impl.get().statics.get()) { 143 | final abstractTypePath = getBaseTypePathAsExpr(abstractType); 144 | final name = f.name; 145 | final c = { name: name, expr: macro $abstractTypePath.$name }; 146 | if(defaultCaseName == name) defaultCase = c; 147 | c; 148 | } 149 | ]; 150 | } else { 151 | null; 152 | } 153 | 154 | if(defaultCase == null || identifiers == null) { 155 | return []; 156 | } 157 | 158 | return [ 159 | for(ident in [(defaultCase : { name: String, expr: Expr })].concat(identifiers)) { 160 | { values: [#if macro macro $v{ident.name} #end], expr: ident.expr } 161 | } 162 | ]; 163 | } 164 | 165 | /** 166 | Converts a `BaseType` to its dot-path `Expr`. 167 | **/ 168 | static function getBaseTypePathAsExpr(baseType: BaseType) { 169 | final fields = baseType.pack.copy(); 170 | if(baseType.module != baseType.name) { 171 | fields.push(baseType.module); 172 | } 173 | fields.push(baseType.name); 174 | return MacroStringTools.toFieldExpr(fields); 175 | } 176 | 177 | /** 178 | Generates the unique "loader" function name for the `enumType` provided. 179 | **/ 180 | public static function getFunctionForEnumType(enumType: Type): Null { 181 | final baseType: Null = switch(enumType) { 182 | case TEnum(_.get() => e, []): e; 183 | case TAbstract(_.get() => a, []): a; 184 | case _: null; 185 | } 186 | 187 | if(baseType == null) { 188 | return null; 189 | } 190 | 191 | final buf = new StringBuf(); 192 | buf.add("load_"); 193 | for(p in baseType.pack) { 194 | buf.add(p); 195 | buf.add("_"); 196 | } 197 | buf.add(baseType.name); 198 | return buf.toString(); 199 | } 200 | 201 | /** 202 | Returns `true` if files need to be generated for this compilation. 203 | **/ 204 | public static function shouldGenerate() { 205 | return !allFiles.isEmpty(); 206 | } 207 | 208 | /** 209 | Returns result of `generateFileList` for all `File`s. 210 | **/ 211 | public static function generateFileList(): Map> { 212 | return allFiles.generateFileList(); 213 | } 214 | 215 | /** 216 | Returns the path the `.modhx` file should be generated and read from. 217 | **/ 218 | public static function generateOutputPath(file: Null): String { 219 | // Load value for default path once 220 | static var defaultFilePath = #if macro Context.definedValue(Define.DefaultFilePath) #else null #end; 221 | 222 | // Use specified file path if it exists, default otherwise 223 | var path = file ?? (defaultFilePath ?? "data"); 224 | 225 | // Generate absolute path if not using relative paths 226 | final useRelativePath = #if macro Context.defined(Define.UseRelativePath) #else false #end; 227 | if(!useRelativePath) { 228 | path = sys.FileSystem.absolutePath(path); 229 | } 230 | 231 | return path; 232 | } 233 | 234 | /** 235 | A wrapper for `sys.io.File.saveContent`. 236 | Tracks the file so it can be deleted later if necessary. 237 | **/ 238 | public static function saveContent(path: String, content: String) { 239 | savedFiles.push(path); 240 | sys.io.File.saveContent(path, content); 241 | } 242 | 243 | /** 244 | A wrapper for `sys.io.File.saveBytes`. 245 | Tracks the file so it can be deleted later if necessary. 246 | **/ 247 | public static function saveBytes(path: String, bytes: haxe.io.Bytes) { 248 | savedFiles.push(path); 249 | sys.io.File.saveBytes(path, bytes); 250 | } 251 | 252 | /** 253 | Called at the end of Modifaxe. 254 | Checks if there are any old files that weren't regenerated and deletes them. 255 | This function can be disabled with `-D modifaxe_dont_delete_old_files`. 256 | **/ 257 | public static function trackAndDeleteOldFiles() { 258 | #if macro 259 | if(Context.defined(Define.DontDeleteOldFiles)) { 260 | return; 261 | } 262 | #end 263 | 264 | final modifaxeTrackerFilename = #if macro Context.definedValue(Define.OldFileTrackerName) ?? #end ".modifaxe"; 265 | final oldFileList = if(sys.FileSystem.exists(modifaxeTrackerFilename)) { 266 | final content = sys.io.File.getContent(modifaxeTrackerFilename); 267 | content.split("\n").filter(p -> StringTools.trim(p).length > 0); 268 | } else { 269 | []; 270 | } 271 | 272 | final toBeDeleted = []; 273 | final newFiles = []; 274 | 275 | for(file in savedFiles) { 276 | final absolutePath = sys.FileSystem.absolutePath(file); 277 | newFiles.push(absolutePath); 278 | } 279 | 280 | for(oldFile in oldFileList) { 281 | if(!newFiles.contains(oldFile)) { 282 | toBeDeleted.push(oldFile); 283 | } 284 | } 285 | 286 | for(oldFile in toBeDeleted) { 287 | sys.FileSystem.deleteFile(oldFile); 288 | } 289 | 290 | sys.io.File.saveContent(modifaxeTrackerFilename, newFiles.join("\n")); 291 | } 292 | } 293 | 294 | #end 295 | -------------------------------------------------------------------------------- /src/modifaxe/builder/Builder.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.builder; 2 | 3 | #if (macro || modifaxe_runtime) 4 | 5 | import haxe.macro.Context; 6 | import haxe.macro.Expr; 7 | import haxe.macro.PositionTools; 8 | import haxe.macro.Type; 9 | 10 | import modifaxe.config.Meta; 11 | import modifaxe.format.FormatIdentifier; 12 | import modifaxe.tools.ExprTools.ExprMapContext; 13 | import modifaxe.tools.ExprTools.mapWithContext; 14 | 15 | typedef ModifaxeState = { 16 | modOnly: Bool, 17 | file: Null, 18 | format: Null 19 | } 20 | 21 | /** 22 | This processes the AST and records the required entries for the data file. 23 | 24 | An instance of `Builder` is created for each `@:build` macro used to process a class. 25 | **/ 26 | class Builder { 27 | var currentEntries: Array = []; 28 | var currentSection: Null
= null; 29 | var currentNames: Map = []; 30 | 31 | var files = new FileCollection(); 32 | 33 | var state: Array = []; 34 | 35 | var index = 0; 36 | 37 | /** 38 | An accumulated list of fields to generate on the singleton class that 39 | will contain all the data at runtime. 40 | **/ 41 | var staticDataFields: Array = []; 42 | 43 | public function new() { 44 | } 45 | 46 | /** 47 | Generates the default argument state. 48 | **/ 49 | function getDefaultState(): ModifaxeState { 50 | return { 51 | modOnly: false, 52 | file: null, 53 | format: null 54 | } 55 | } 56 | 57 | /** 58 | The current state. This should be an accumulation of the previous states. 59 | **/ 60 | function getState(): ModifaxeState { 61 | return if(state.length == 0) { 62 | getDefaultState(); 63 | } else { 64 | state[state.length - 1]; 65 | } 66 | } 67 | 68 | /** 69 | Takes the arguments from the `@:modifaxe` metadata and applies it to the state stack. 70 | **/ 71 | public function setArguments(args: Array) { 72 | final newState = Reflect.copy(getState()); // modify a copy of the current state 73 | 74 | if(newState == null) throw "Reflect.copy failed."; // Required by null-safety, this can never happen. 75 | 76 | for(arg in args) { 77 | switch(arg) { 78 | case macro ModOnly: { 79 | newState.modOnly = true; 80 | } 81 | case macro File=$path: { 82 | final filePath = switch(path.expr) { 83 | case EConst(CString(path, _)): path; 84 | case _: { 85 | #if macro 86 | Context.error("The 'File' value should be a String expression.", arg.pos); 87 | #else null; #end 88 | } 89 | } 90 | newState.file = filePath != null && filePath.length == 0 ? null : filePath; 91 | } 92 | case macro Format=$formatName: { 93 | final formatIdent = switch(formatName.expr) { 94 | case EConst(CString(name, _) | CIdent(name)): name; 95 | case _: { 96 | #if macro 97 | Context.error("The 'Format' value should be an identifier or String expression.", arg.pos); 98 | #else null; #end 99 | } 100 | } 101 | if(formatIdent != null && formatIdent.length > 0) { 102 | newState.format = formatIdent; 103 | } 104 | } 105 | case _: { 106 | #if macro 107 | Context.error("Unknown argument.", arg.pos); 108 | #end 109 | } 110 | } 111 | } 112 | 113 | state.push(newState); 114 | } 115 | 116 | /** 117 | Pops the state of the `@:modifaxe` arguments. 118 | **/ 119 | public function popArguments() { 120 | state.pop(); 121 | } 122 | 123 | /** 124 | Returns a processed modified version of a function field's expression. 125 | Returns `null` if no modifications were generated. 126 | **/ 127 | public function buildFunctionExpr(cls: Null, field: Field, expr: Expr): Null { 128 | final sectionName = (cls != null ? '${cls.name}.' : "") + field.name; 129 | currentSection = new Section(sectionName); 130 | 131 | final state = getState(); 132 | final e = (state.modOnly ? mapExprModOnly : mapExpr)(expr, new ExprMapContext()); 133 | 134 | if(currentSection.hasEntries()) { 135 | files.addSectionToFile(currentSection, state.format, state.file); 136 | Output.addSectionToAllFiles(currentSection, state.format, state.file); 137 | currentSection = null; 138 | currentNames = []; 139 | 140 | // Prepend loader function to this function 141 | return macro { 142 | $i{getLoadFunctionName()}(); 143 | @:mergeBlock $e; 144 | }; 145 | } 146 | 147 | return null; 148 | } 149 | 150 | /** 151 | Processes an expression. 152 | 153 | Redirects constants to their data holding variable and record them. 154 | **/ 155 | function mapExpr(expr: Expr, context: ExprMapContext): Expr { 156 | final result = processConstant(expr, context); 157 | return result ?? mapWithContext(expr, context, mapExpr); 158 | } 159 | 160 | /** 161 | Works the same as `mapExpr`, but used when `ModOnly` mode is enabled. 162 | **/ 163 | function mapExprModOnly(expr: Expr, context: ExprMapContext): Expr { 164 | final result = switch(expr.expr) { 165 | case EMeta({ name: _ == Meta.Mod => true }, _): { 166 | processConstant(expr, context); 167 | } 168 | case _: null; 169 | } 170 | return result ?? mapWithContext(expr, context, mapExprModOnly); 171 | } 172 | 173 | /** 174 | Checks if the provided `Expr` is a constant that can be modified by Modifaxe. 175 | If so, it is added as an entry and its replacement `Expr` is returned. 176 | Returns `null` otherwise. 177 | **/ 178 | function processConstant(expr: Expr, context: ExprMapContext, overrideName: Null = null): Null { 179 | return switch(expr.expr) { 180 | case EMeta({ name: _ == Meta.Mod => true, params: params }, innerExpr) if(params != null): { 181 | processModMeta(innerExpr, params, context, overrideName); 182 | } 183 | case EConst(CIdent(id)) if(id == "true" || id == "false"): { 184 | makeEntry(0, overrideName, id, expr, context); 185 | } 186 | case EConst(CInt(intString, _)): { 187 | makeEntry(1, overrideName, intString, expr, context); 188 | } 189 | case EConst(CFloat(floatString, _)): { 190 | makeEntry(2, overrideName, floatString, expr, context); 191 | } 192 | case EConst(CString(string, DoubleQuotes)): { 193 | makeEntry(3, overrideName, string, expr, context); 194 | } 195 | case _: { 196 | null; 197 | } 198 | } 199 | } 200 | 201 | /** 202 | Processes a `@:mod EXPR` expression. 203 | **/ 204 | function processModMeta(innerExpr: Expr, params: Array, context: ExprMapContext, name: Null) { 205 | // Parse `@:mod` arguments 206 | var newName = null; 207 | var enumTypeExpr: Null = null; 208 | for(p in params) { 209 | switch(p) { 210 | case { expr: EConst(CIdent(name) | CString(name, _)) }: { 211 | newName = name; 212 | break; 213 | } 214 | case macro Enum=$name: { 215 | enumTypeExpr = name; 216 | } 217 | case _: 218 | } 219 | } 220 | 221 | // If not an Enum, call `processConstant` normally 222 | if(enumTypeExpr == null) { 223 | return processConstant(innerExpr, context, newName); 224 | } 225 | 226 | // Try and determine `Type` from enum path expression 227 | final enumType: Null = #if macro try { Context.getType(dotPathExprToString(enumTypeExpr)); } catch(e) #end { null; } 228 | if(enumType == null) { 229 | return #if macro Context.error("Could not determine type.", enumTypeExpr.pos) #else innerExpr #end; 230 | } 231 | 232 | // Replace enum identifier constant or generate error 233 | return switch(innerExpr.expr) { 234 | case EConst(CIdent(ident)): { 235 | Output.addDataEnumLoader((enumType : Type), enumTypeExpr.pos, innerExpr, ident); 236 | makeEntry(4, name ?? context.generateName(), ident, innerExpr, context, enumType); 237 | } 238 | case _: { 239 | #if macro 240 | Context.error("Enum constant must just be identifier.", innerExpr.pos); 241 | #end 242 | innerExpr; 243 | } 244 | } 245 | } 246 | 247 | /** 248 | Converts an `Expr` of identifiers and field access into a dot-path `String`. 249 | **/ 250 | function dotPathExprToString(e: Expr) { 251 | return switch(e.expr) { 252 | case EConst(CIdent(c)): c; 253 | case EField(e2, field, _): dotPathExprToString(e2) + "." + field; 254 | case EParenthesis(e): dotPathExprToString(e); 255 | case _: ""; 256 | } 257 | } 258 | 259 | /** 260 | Adds an entry and returns its access expression. 261 | **/ 262 | function makeEntry(type: Int, name: Null, content: String, expr: Expr, context: ExprMapContext, enumType: Null = null) { 263 | if(currentSection == null) { 264 | throw "Cannot create entries without section."; 265 | } 266 | 267 | final name = ensureUniqueName(name ?? context.generateName(), expr.pos); 268 | currentNames.set(name, true); 269 | 270 | var complexType; 271 | var entryValue: EntryValue; 272 | 273 | switch(type) { 274 | case 0: { 275 | complexType = macro : Bool; 276 | entryValue = EBool(content == "true"); 277 | } 278 | case 1: { 279 | complexType = macro : Int; 280 | entryValue = EInt(content); 281 | } 282 | case 2: { 283 | complexType = macro : Float; 284 | entryValue = EFloat(content); 285 | } 286 | case 3: { 287 | complexType = macro : String; 288 | entryValue = EString(content); 289 | } 290 | case 4 if(enumType != null): { 291 | complexType = haxe.macro.TypeTools.toComplexType(enumType) ?? macro : Dynamic; 292 | entryValue = EEnum(content, enumType); 293 | } 294 | case _: throw "Invalid type id."; 295 | } 296 | 297 | final entry = currentSection.addEntry(name, entryValue); 298 | final entryUniqueName = entry.getUniqueName(); 299 | 300 | addDataField(entryUniqueName, complexType, expr); 301 | 302 | return macro $i{entryUniqueName}; 303 | } 304 | 305 | function ensureUniqueName(name: String, expressionPos: Position) { 306 | if(currentNames.exists(name)) { 307 | name += "_Line" + #if macro PositionTools.toLocation(expressionPos).range.start.line #else 0 #end; 308 | } 309 | while(currentNames.exists(name)) { 310 | name += "_"; 311 | } 312 | return name; 313 | } 314 | 315 | /** 316 | Adds a field to the runtime data class. 317 | **/ 318 | function addDataField(name: String, complexType: ComplexType, originalExpression: Expr) { 319 | staticDataFields.push({ 320 | name: name, 321 | access: [APublic, AStatic], 322 | pos: originalExpression.pos, 323 | kind: FVar(complexType, originalExpression) 324 | }); 325 | } 326 | 327 | /** 328 | A consistent reference to the name of the static data-loader function generated 329 | on classes. 330 | **/ 331 | static function getLoadFunctionName() { 332 | return "_modifaxe_loadData"; 333 | } 334 | 335 | /** 336 | Generates the data-loader function for the class. 337 | **/ 338 | public function generateLoadFunction() { 339 | final loadExpressions = []; 340 | for(formatIdent => fileList in files.generateFileList()) { 341 | final format = formatIdent.getFormat(); 342 | if(format != null) { 343 | loadExpressions.push(format.generateLoadExpression(fileList)); // Generate loading code 344 | } 345 | } 346 | 347 | final loadFunctionExpr = macro { 348 | static var i = 0; 349 | if(i != Modifaxe.refreshCount) { 350 | i = Modifaxe.refreshCount; 351 | } else { 352 | return; 353 | } 354 | 355 | $b{loadExpressions}; 356 | } 357 | 358 | staticDataFields.push({ 359 | name: getLoadFunctionName(), 360 | access: [AStatic], 361 | pos: loadFunctionExpr.pos, 362 | kind: FFun({ 363 | args: [], 364 | expr: loadFunctionExpr 365 | }) 366 | }); 367 | } 368 | 369 | /** 370 | Returns a list of `Field`s to be added to the class that had this `Builder` 371 | generated in their `@:build` macro. 372 | **/ 373 | public function getAdditionalFields(): Array { 374 | return staticDataFields; 375 | } 376 | } 377 | 378 | #end 379 | -------------------------------------------------------------------------------- /src/modifaxe/runtime/ModParser.hx: -------------------------------------------------------------------------------- 1 | package modifaxe.runtime; 2 | 3 | using StringTools; 4 | 5 | /** 6 | A `.modhx` parser. 7 | 8 | The goal is highest-performance and minimal Haxe API usage. 9 | 10 | This should only use `fastCodeAt` on `String`, AND avoid all concatenation 11 | (except using `substring` to generate the return `String` of `nextEntry`). 12 | **/ 13 | class ModParser { 14 | var pos: Int; 15 | var line: Int; 16 | var lineStart: Int; 17 | var content: String; 18 | 19 | #if !modifaxe_parser_no_map_cache 20 | /** 21 | Used with a parser set up through `fromEntryCount` to set where entries 22 | will be retrieved from the cache. 23 | **/ 24 | var entriesCachePos: Int = 0; 25 | 26 | /** 27 | Stores all the parsed entries. 28 | **/ 29 | var entriesCache: Array = []; 30 | #end 31 | 32 | public function new(filePath: String) { 33 | pos = 0; 34 | line = 0; 35 | lineStart = 0; 36 | content = loadString(filePath); 37 | } 38 | 39 | /** 40 | Creates an instance of `ModParser` starting from a specific entry. 41 | 42 | `startEntryCount` should be the number of entries ignored before parsing begins. 43 | **/ 44 | public static function fromEntryCount(filePath: String, startEntryCount: Int) { 45 | #if modifaxe_parser_no_map_cache 46 | 47 | // If not using a cache, make a fresh `ModParser` and find the starting entry 48 | final result = new ModParser(filePath); 49 | for(i in 0...startEntryCount) { 50 | result.nextEntry(); 51 | } 52 | return result; 53 | 54 | #else 55 | 56 | // Haxe 4.3.2 required for `static var _: Map` 57 | #if (haxe < version("4.3.2")) 58 | #error "Haxe 4.3.2+ required for local static Map. (See #11193, #11301)"; 59 | #end 60 | 61 | // Clear cache if `Modifaxe` has been reloaded 62 | static var cache: Map = []; 63 | static var i = 0; 64 | if(i != Modifaxe.refreshCount) { 65 | i = Modifaxe.refreshCount; 66 | cache = []; 67 | } 68 | 69 | // Generate `.modhx` parser if one for this file doesn't exist 70 | var result = cache.get(filePath); 71 | if(result == null) { 72 | result = new ModParser(filePath); 73 | cache.set(filePath, result); 74 | } 75 | 76 | // Go to starting entry 77 | result.goToEntry(startEntryCount); 78 | return result; 79 | 80 | #end 81 | } 82 | 83 | #if !modifaxe_parser_no_map_cache 84 | 85 | /** 86 | Places the position of the parser directly after the number 87 | **/ 88 | public function goToEntry(entryIndex: Int) { 89 | // Parse entries until `entryIndex`. 90 | while(entryIndex >= entriesCache.length) { 91 | final e = nextEntry(); 92 | if(e == null) { 93 | // TODO: Too many entries expected? Should this generate error?? 94 | break; 95 | } 96 | } 97 | 98 | entriesCachePos = entryIndex; 99 | } 100 | 101 | #end 102 | 103 | /** 104 | Loads the `String` content from a text file at `filePath`. 105 | Can be dynamically overwritten with custom file-loading code. 106 | **/ 107 | public #if modiflaxe_no_dynamic_functions dynamic #end function loadString(filePath: String) { 108 | return sys.io.File.getContent(filePath); 109 | } 110 | 111 | #if !modifaxe_parser_no_error_check 112 | /** 113 | Reports an parsing error. 114 | Can be dynamically overwritten with custom error-reporting code. 115 | **/ 116 | public #if modiflaxe_no_dynamic_functions dynamic #end function onError(error: ModParserError, skipToNextLine: Bool) { 117 | // Print 118 | final lineNumberStr = Std.string(line + 1); 119 | var line1 = "".lpad(" ", lineNumberStr.length + 1) + " |"; 120 | var line2 = ' $lineNumberStr | ${content.substring(lineStart, getEndOfCurrentLine())}'; 121 | var line3 = '$line1${"".lpad(" ", pos - lineStart + 1)}^ ${error.getMessage()}'; 122 | Sys.println('[Modifaxe Parse Error]\n$line1\n$line2\n$line3'); 123 | 124 | // Skip to next line 125 | if(skipToNextLine) { 126 | pos = getEndOfCurrentLine(); 127 | } 128 | } 129 | #end 130 | 131 | /** 132 | Returns the index of the next new line (\n) or end of file. 133 | **/ 134 | function getEndOfCurrentLine() { 135 | var result = pos; 136 | final len = content.length; 137 | while(result < len) { 138 | if(content.fastCodeAt(result) == 10) { 139 | break; 140 | } 141 | result++; 142 | } 143 | return result; 144 | } 145 | 146 | /** 147 | Gets the next value entry as a `String`. 148 | **/ 149 | public function nextEntry(): Null { 150 | #if !modifaxe_parser_no_map_cache 151 | if(entriesCachePos < entriesCache.length) { 152 | return entriesCache[entriesCachePos++]; 153 | } 154 | #end 155 | 156 | final result = nextEntryImpl(); 157 | 158 | #if !modifaxe_parser_no_map_cache 159 | if(result != null) { 160 | entriesCache.push(result); 161 | entriesCachePos++; 162 | } 163 | #end 164 | 165 | return result; 166 | } 167 | 168 | /** 169 | The implementation for `nextEntry`. 170 | **/ 171 | function nextEntryImpl(): Null { 172 | final len = content.length; 173 | 174 | var start = pos; 175 | var end = pos; 176 | 177 | while(pos < len) { 178 | final c = content.fastCodeAt(pos); 179 | switch(c) { 180 | // \n (new line) 181 | case 10: { 182 | start = end = pos; 183 | pos++; 184 | line++; 185 | lineStart = pos; 186 | } 187 | 188 | // whitespace (Based on `StringTools.isSpace`) 189 | case 9 | 11 | 12 | 13 | 32: { 190 | start = pos; 191 | end = pos; 192 | pos++; 193 | } 194 | 195 | // # (pound sign) 196 | case 35: { 197 | if(goToNewLine()) { 198 | // skip rest of loop so `pos` isn't incremented 199 | pos++; 200 | continue; 201 | } else { 202 | // break since end was hit 203 | break; 204 | } 205 | } 206 | 207 | // [ (open square bracket) 208 | case 91: { 209 | #if !modifaxe_parser_no_error_check 210 | if(pos - lineStart > 0) { 211 | onError(SectionShouldBeStartOfLine, false); 212 | } 213 | #end 214 | 215 | // move past [ 216 | pos++; 217 | 218 | // move past identifier 219 | expectIdentifier(true); 220 | 221 | // move past ] 222 | expectChar(93); 223 | } 224 | 225 | // b, i, f, s, or e for the type 226 | case 98 | 105 | 102 | 115 | 101: { 227 | #if !modifaxe_parser_no_error_check 228 | if(pos - lineStart > 0) { 229 | onError(EntryShouldBeStartOfLine, false); 230 | } 231 | #end 232 | 233 | final type = switch(c) { 234 | case 98: 0; // bool 235 | case 105: 1; // int 236 | case 102: 2; // float 237 | case 115: 3; // string 238 | case 101: 4; // enum 239 | case _: 0; // impossible 240 | } 241 | 242 | // move past type char 243 | pos++; 244 | 245 | // move past . 246 | expectChar(46); 247 | 248 | // move past identifier 249 | expectIdentifier(false); 250 | 251 | // move past : 252 | expectChar(58); 253 | 254 | // skip spaces if they exist 255 | while(content.fastCodeAt(pos) == 32) { 256 | pos++; 257 | } 258 | 259 | // start found 260 | start = pos; 261 | 262 | switch(type) { 263 | case 0: expectBool(); 264 | case 1: expectInt(); 265 | case 2: expectFloat(); 266 | case 3: return expectAndGetString(); // special case, String type returns itself 267 | case 4: expectIdentifier(false); 268 | } 269 | 270 | end = pos; 271 | 272 | return content.substring(start, end); 273 | } 274 | 275 | case _: { 276 | #if !modifaxe_parser_no_error_check 277 | onError(UnexpectedChar(c), true); 278 | #end 279 | pos++; 280 | } 281 | } 282 | } 283 | 284 | return null; 285 | } 286 | 287 | /** 288 | Calls `getValueText` and parses it as a `Bool`. 289 | **/ 290 | public function nextBool(defaultValue: Bool): Bool { 291 | final line = nextEntry(); 292 | if(line != null) { 293 | return line == "true"; 294 | } 295 | return defaultValue; 296 | } 297 | 298 | /** 299 | Calls `getValueText` and parses it as an `Int`. 300 | **/ 301 | public function nextInt(defaultValue: Int): Int { 302 | final line = nextEntry(); 303 | if(line != null) { 304 | return Std.parseInt(line) ?? defaultValue; 305 | } 306 | return defaultValue; 307 | } 308 | 309 | /** 310 | Calls `getValueText` and parses it as a `Float`. 311 | **/ 312 | public function nextFloat(defaultValue: Float): Float { 313 | final line = nextEntry(); 314 | if(line != null) { 315 | return Std.parseFloat(line); 316 | } 317 | return defaultValue; 318 | } 319 | 320 | /** 321 | Returns the value of `getValueText`. 322 | This function doesn't do anything at the moment; it exists for consistency. 323 | **/ 324 | public function nextString(defaultValue: String) { 325 | final line = nextEntry(); 326 | return line; 327 | } 328 | 329 | /** 330 | Returns the value of `getValueText`. 331 | This function doesn't do anything at the moment; it exists for consistency. 332 | **/ 333 | public function nextEnumIdentifier(defaultValue: String) { 334 | final line = nextEntry(); 335 | return line; 336 | } 337 | 338 | /** 339 | Moves `pos` to the next "\n". 340 | 341 | Returns `true` if successful. 342 | Returns `false` if there are no "\n" for the rest of the string. 343 | **/ 344 | function goToNewLine() { 345 | final len = content.length; 346 | 347 | while(pos < len) { 348 | switch(content.fastCodeAt(pos)) { 349 | case 10: { 350 | return true; 351 | } 352 | case _: 353 | } 354 | pos++; 355 | } 356 | 357 | return false; 358 | } 359 | 360 | /** 361 | Moves `pos` to the end of the next identifier. 362 | 363 | `pos` should be on the first character of the identifier before calling. 364 | Returns `false` if the current character is not a valid start for an identifier. 365 | 366 | An identifier is a group of alphanumeric and underscore characters following 367 | Haxe's identifier rules. 368 | 369 | If `allowDot` is `true`, "." is also accepted in the identifier (section identifiers can contain "."). 370 | **/ 371 | function nextIdentifier(allowDot: Bool) { 372 | final len = content.length; 373 | 374 | // Check that first character is letter 375 | switch(content.fastCodeAt(pos)) { 376 | case c if((c >= 65 && c <= 90) || (c >= 97 && c <= 122)): { 377 | pos++; 378 | } 379 | case _: { 380 | return false; 381 | } 382 | } 383 | 384 | while(pos < len) { 385 | final c = content.fastCodeAt(pos); 386 | // allow A-Z, a-z, 0-9, _, and . (if `allowDot` is true) 387 | if((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c == 95 || (allowDot && c == 46)) { 388 | pos++; 389 | } else { 390 | break; // end once hit non-identifier character 391 | } 392 | } 393 | 394 | return true; 395 | } 396 | 397 | /** 398 | Checks if the current character is the `char` char code. 399 | 400 | If it is, increment `pos`. 401 | If not, return `false`. 402 | **/ 403 | inline function expectChar(char: Int) { 404 | if(content.fastCodeAt(pos) == char) { 405 | pos++; 406 | return true; 407 | } 408 | #if !modifaxe_parser_no_error_check 409 | onError(ExpectedChar(char), true); 410 | #end 411 | return false; 412 | } 413 | 414 | /** 415 | Checks and skips the current identifier. 416 | 417 | If it's a valid identifier, `pos` is set to the end. 418 | If not, return `false`. 419 | **/ 420 | inline function expectIdentifier(allowDot: Bool) { 421 | if(!nextIdentifier(allowDot)) { 422 | #if !modifaxe_parser_no_error_check 423 | onError(ExpectedIdentifier, true); 424 | #end 425 | } 426 | } 427 | 428 | /** 429 | Expects either `true` or `false`. 430 | NOTE: This function goes hard af. 431 | **/ 432 | function expectBool() { 433 | switch(content.fastCodeAt(pos)) { 434 | case 116: { // t 435 | pos++; 436 | expectChar(114); // r 437 | expectChar(117); // u 438 | expectChar(101); // e 439 | } 440 | case 102: { // f 441 | pos++; 442 | expectChar(97); // a 443 | expectChar(108); // l 444 | expectChar(115); // s 445 | expectChar(101); // e 446 | } 447 | case _: { 448 | #if !modifaxe_parser_no_error_check 449 | onError(ExpectedBool, true); 450 | #end 451 | } 452 | } 453 | } 454 | 455 | /** 456 | Parse the next content under the assumption it is an `Int`. 457 | Generate an error if anything unexpected occurs. 458 | **/ 459 | function expectInt() { 460 | final len = content.length; 461 | 462 | // Check if first character is - (minus) 463 | if(content.fastCodeAt(pos) == 45) { 464 | pos++; 465 | } 466 | 467 | // Check for at least one number 468 | final c = content.fastCodeAt(pos); 469 | if(c >= 48 && c <= 57) { 470 | pos++; 471 | } else { 472 | #if !modifaxe_parser_no_error_check 473 | onError(ExpectedDigit, true); 474 | #end 475 | } 476 | 477 | while(pos < len) { 478 | final c = content.fastCodeAt(pos); 479 | // allow 0-9 480 | if(c >= 48 && c <= 57) { 481 | pos++; 482 | } else { 483 | break; // end once hit non-number character 484 | } 485 | } 486 | } 487 | 488 | /** 489 | Parse the next content under the assumption it is an `Float`. 490 | Generate an error if anything unexpected occurs. 491 | **/ 492 | function expectFloat() { 493 | // Check if first character is - (minus) 494 | if(content.fastCodeAt(pos) == 45) { 495 | pos++; 496 | } 497 | 498 | // Check for at least one number 499 | final c = content.fastCodeAt(pos); 500 | if(c >= 48 && c <= 57) { 501 | pos++; 502 | } else { 503 | #if !modifaxe_parser_no_error_check 504 | onError(ExpectedDigit, true); 505 | #end 506 | } 507 | 508 | final len = content.length; 509 | var processedDot = false; 510 | while(pos < len) { 511 | final c = content.fastCodeAt(pos); 512 | 513 | // allow 0-9 514 | if(c >= 48 && c <= 57) { 515 | pos++; 516 | } else if(!processedDot && c == 46) { 517 | pos++; 518 | processedDot = true; 519 | } else { 520 | break; // end once hit non-number character 521 | } 522 | } 523 | } 524 | 525 | /** 526 | Parse the next content under the assumption it is an `String`. 527 | If parsed successfully, the `String` is returned. 528 | **/ 529 | function expectAndGetString() { 530 | // move past " 531 | expectChar(34); 532 | 533 | final len = content.length; 534 | final result = #if modifaxe_parser_use_string_concat "" #else new StringBuf() #end; 535 | var start = pos; 536 | 537 | while(pos < len) { 538 | final c = content.fastCodeAt(pos); 539 | 540 | switch(c) { 541 | case 92 | 34: { 542 | if(pos > start) { 543 | #if modifaxe_parser_use_string_concat 544 | result += content.substring(start, pos); 545 | #else 546 | result.add(content.substring(start, pos)); 547 | #end 548 | } 549 | 550 | if(c == 92 && (pos + 1) < len) { 551 | // backslash \ 552 | final newChar = switch(content.fastCodeAt(pos + 1)) { 553 | case 34: "\""; 554 | case 92: "\\"; 555 | case 110: "\n"; 556 | case 116: "\t"; 557 | case _: { 558 | #if !modifaxe_parser_no_error_check 559 | onError(UnsupportedEscapeSequence, false); 560 | #end 561 | ""; 562 | } 563 | } 564 | 565 | #if modifaxe_parser_use_string_concat 566 | result += newChar; 567 | #else 568 | result.add(newChar); 569 | #end 570 | 571 | pos++; 572 | start = pos + 1; // increment `start` one more, since `pos++` at end of loop 573 | } else { 574 | // double-quote " 575 | pos++; 576 | break; 577 | } 578 | } 579 | } 580 | 581 | pos++; 582 | } 583 | 584 | return result.toString(); 585 | } 586 | } 587 | --------------------------------------------------------------------------------