├── .github └── logo │ ├── 1024.png │ ├── 256.png │ └── 512.png ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── docs ├── basic_syntax.rst ├── concepts.rst ├── conf.py ├── engines.rst ├── functions.rst └── index.rst ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── kotlin └── rhmodding │ └── tickompiler │ ├── Functions.kt │ ├── Tickompiler.kt │ ├── Utils.kt │ ├── cli │ ├── CompileCommand.kt │ ├── DaemonCommand.kt │ ├── DecompileCommand.kt │ ├── ExtractCommand.kt │ ├── GrabCommand.kt │ ├── NotepadppLangCommand.kt │ ├── PackCommand.kt │ └── UpdatesCheckCommand.kt │ ├── compiler │ ├── Compiler.kt │ └── Parboiled.kt │ ├── decompiler │ └── Decompiler.kt │ ├── gameextractor │ ├── GameExtractor.kt │ └── GameExtractorFunctions.kt │ ├── gameputter │ └── GamePutter.kt │ ├── objectify │ ├── ManifestObj.kt │ └── Objectifier.kt │ ├── output │ └── Outputter.kt │ └── util │ ├── Directories.kt │ ├── StringEscaping.kt │ └── Version.kt └── resources ├── locations.json └── notepadplusplustickflowlang.xml /.github/logo/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhmodding/Tickompiler/b55c44bac118a1b33e710cdc4e6752f2f5a894d6/.github/logo/1024.png -------------------------------------------------------------------------------- /.github/logo/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhmodding/Tickompiler/b55c44bac118a1b33e710cdc4e6752f2f5a894d6/.github/logo/256.png -------------------------------------------------------------------------------- /.github/logo/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhmodding/Tickompiler/b55c44bac118a1b33e710cdc4e6752f2f5a894d6/.github/logo/512.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Java 2 | 3 | *.class 4 | *.war 5 | *.ear 6 | hs_err_pid* 7 | 8 | ## Robovm 9 | robovm-build/ 10 | 11 | ## GWT 12 | war/ 13 | html/war/gwt_bree/ 14 | html/gwt-unitCache/ 15 | .apt_generated/ 16 | html/war/WEB-INF/deploy/ 17 | html/war/WEB-INF/classes/ 18 | .gwt/ 19 | gwt-unitCache/ 20 | www-test/ 21 | .gwt-tmp/ 22 | 23 | ## Android Studio and Intellij and Android in general 24 | android/libs/armeabi/ 25 | android/libs/armeabi-v7a/ 26 | android/libs/arm64-v8a/ 27 | android/libs/x86/ 28 | android/libs/x86_64/ 29 | android/gen/ 30 | .idea/ 31 | *.ipr 32 | *.iws 33 | *.iml 34 | out/ 35 | com_crashlytics_export_strings.xml 36 | 37 | ## Eclipse 38 | .classpath 39 | .project 40 | .metadata 41 | **/bin/ 42 | tmp/ 43 | *.tmp 44 | *.bak 45 | *.swp 46 | *~.nib 47 | local.properties 48 | .settings/ 49 | .loadpath 50 | .externalToolBuilders/ 51 | *.launch 52 | 53 | ## NetBeans 54 | **/nbproject/private/ 55 | build/ 56 | nbbuild/ 57 | dist/ 58 | nbdist/ 59 | nbactions.xml 60 | nb-configuration.xml 61 | 62 | ## Gradle 63 | 64 | .gradle 65 | gradle-app.setting 66 | build/ 67 | 68 | ## OS Specific 69 | .DS_Store 70 | Thumbs.db 71 | /core/assets/palettes/ 72 | /core/assets/customSounds/ 73 | /core/assets/logs/ 74 | /core/assets/tmpMusic/ 75 | /core/assets/localization/temp/ 76 | /core/assets/scripts/examples/ 77 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Tickompiler 4 | 5 | Tickompiler is a compiler/decompiler for Tickflow, a language based on the bytecode format used by the game Rhythm Heaven Megamix to describe its rhythm games. 6 | 7 | [![Downloads](https://img.shields.io/github/downloads/SneakySpook/Tickompiler/total.svg)](https://github.com/SneakySpook/Tickompiler/releases) 8 | [![License](https://img.shields.io/github/license/SneakySpook/Tickompiler.svg)](https://github.com/SneakySpook/Tickompiler/blob/master/LICENSE.txt) 9 | 10 | In-depth documentation for Tickflow can be found [here](https://tickompiler.readthedocs.io/en/latest/). 11 | 12 | Game files extracted and decompiled using this tool can be used in conjunction with [this patch](https://github.com/SneakySpook/RHMPatch). 13 | 14 | # Running the program 15 | Requires Java 8 or newer and for `java` to be in the path. 16 | 17 | Open a terminal in the same directory as `tickompiler.jar` and run: 18 | 19 | Java 15 and older: 20 | ``` 21 | java -jar tickompiler.jar --help 22 | ``` 23 | 24 | Java 16 and newer (due to issue #8): 25 | ``` 26 | java --add-opens java.base/java.lang=ALL-UNNAMED -jar tickompiler.jar --help 27 | ``` 28 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'rhmodding' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | dependencies { 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'kotlin' 17 | apply plugin: 'com.github.johnrengelman.shadow' 18 | 19 | sourceCompatibility = 1.8 20 | 21 | compileKotlin { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | 25 | repositories { 26 | mavenCentral() 27 | jcenter() 28 | } 29 | 30 | dependencies { 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 32 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8" 33 | implementation "org.parboiled:parboiled-java:1.2.0" 34 | implementation "com.google.code.gson:gson:2.8.7" 35 | implementation "info.picocli:picocli:4.6.1" 36 | } 37 | 38 | shadowJar { 39 | baseName = "tickompiler" 40 | classifier = null 41 | version = null 42 | manifest { 43 | attributes "Main-Class": "rhmodding.tickompiler.Tickompiler" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/basic_syntax.rst: -------------------------------------------------------------------------------- 1 | Basic Tickflow Syntax 2 | ===================== 3 | 4 | Every line in tickflow is one of four things: 5 | 6 | - :ref:`function_calls`, which directly correspond to a single bytecode instruction. 7 | These are the only lines of tickflow that translate into bytecode; 8 | 9 | - `Compile-time variable assignments`_, which do not correspond to bytecode, but rather allow you to use constant 10 | variables to replace magic numbers; 11 | 12 | - :ref:`markers`, which allow you to use locations in the tickflow code as variables; 13 | 14 | - and :ref:`directives`, which include custom function aliases and file metadata. 15 | 16 | .. _function_calls: 17 | 18 | Function Calls 19 | -------------- 20 | 21 | Function calls are of the form :: 22 | 23 | op arg1, arg2... 24 | 25 | Here, ``op`` is either a name or a number; it determines what operation is performed. 26 | In bytecode, the opcode is a number between 0 and ``0x3FF``. This is an accepted value for ``op`` in Tickflow, 27 | but there are a number of known operations that have defined names in Tickflow. 28 | Examples include ``rest``, ``if`` and ``case``. 29 | 30 | ```` is an expression enclosed in ``<>``, which is used as a special argument for the operation. 31 | The function of this special argument varies per operation, but it can also be used to differentiate similar operations 32 | in place of creating a separate ``op`` value for it. ```` can be omitted, in which case it defaults to 0. 33 | 34 | Following the special argument is a comma-separated list of arguments. These are all expressions, and their effect and 35 | amount depends on the operation. There can be from 0 up to 15 arguments. 36 | 37 | Expressions 38 | ~~~~~~~~~~~ 39 | Expressions resolve to numbers. They consist of variables, numbers and mathematical operations. The operators you can 40 | use for expressions are multiplication (``*``), addition (``+``), subtraction (``-``), integer division (``/``), 41 | bitwise right shift (``>>``), bitwise left shift (``<<``), bitwise AND (``&``), bitwise OR (``|``), and bitwise XOR (``^``). 42 | 43 | Examples of expressions include ``5``, ``0xFE3``, ``0xFF << 5``, and ``x + 2``. 44 | 45 | .. _Compile-time variable assignments: 46 | 47 | Compile-time Variables 48 | ---------------------- 49 | 50 | Compile-time variables can be used to store numbers for later use in your Tickflow code. 51 | For example, you could save a variable ``beat`` with value ``0x30``, since one beat of music corresponds to this value 52 | in timing-related functions. Variable assignment is of the form :: 53 | 54 | var = expr 55 | 56 | ``var`` denotes the variable name. Variable names must start with a letter and contain only alphanumeric characters and 57 | underscores. ``expr`` is an expression denoting the value you are setting the variable to. 58 | 59 | .. _markers: 60 | 61 | Markers 62 | ------- 63 | 64 | Markers 'mark' locations in your tickflow code, saving the location into a variable so that they can be used in other 65 | parts of the file. These are usually used for functions like ``call``, which execute tickflow at a specific location. 66 | Markers are of the form :: 67 | 68 | name: 69 | 70 | ``name`` is the name of the marker and has the same constraints as variable names. Markers generated by the decompiler 71 | will have a naming scheme ``locXX:``, where the number ``XX`` is based on the order the locations are referenced in 72 | the file. 73 | 74 | .. _directives: 75 | 76 | Directives 77 | ---------- 78 | 79 | Directives carry metadata about the file, but are also used for custom operation aliases. Current directives are: 80 | 81 | - ``#index num`` sets the index of the rhythm game this file will replace when patched into the game. 82 | 83 | - ``#start loc`` sets the location in the file at which tickflow execution will begin. This is often 0. 84 | 85 | - ``#assets loc`` sets the location in the file where certain assets, like the intro screen, are loaded. 86 | This is needed for insertion into the game. 87 | 88 | - ``#alias name num`` creates a custom function alias under the name ``name`` for the operation number ``num``. -------------------------------------------------------------------------------- /docs/concepts.rst: -------------------------------------------------------------------------------- 1 | Key Tickflow Concepts / Glossary 2 | ================================ 3 | 4 | Code execution 5 | Tickflow code consists of a sequence of operations or functions, which are all executed sequentially. 6 | Some Tickflow operations may redirect the execution to some other location. 7 | 8 | Conditional variable 9 | Tickflow keeps track of a particular variable, which is used in several operations. It can be set by some 10 | operations, and is generally used by conditional operations such as ``if`` to determine what Tickflow code 11 | to run. 12 | 13 | Threads 14 | Tickflow is multithreaded. This means that multiple pieces of Tickflow code may be running at the same time. 15 | A thread is the execution of one such piece of Tickflow code. A synchronous Tickflow function call will 16 | change the location at which the current thread of execution is running, while an asynchronous call will 17 | spawn a new thread at the desired location. 18 | 19 | Ticks 20 | A tick is the basic unit of time used in all Tickflow operations. 48 (``0x30``) ticks are generally equal to one beat 21 | of music. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Tickompiler documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 07 17:11:12 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'Tickompiler' 49 | copyright = u'2017-2019, chrislo27, SneakySpook' 50 | author = u'chrislo27, SneakySpook' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = u'1.9.0' 58 | # The full version, including alpha/beta/rc tags. 59 | release = u'1.9.0' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | # If true, `todo` and `todoList` produce output, else they produce nothing. 77 | todo_include_todos = False 78 | 79 | 80 | # -- Options for HTML output ---------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | import sphinx_rtd_theme 86 | 87 | html_theme = "sphinx_rtd_theme" 88 | 89 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | # html_theme_options = {} 96 | 97 | # Add any paths that contain custom static files (such as style sheets) here, 98 | # relative to this directory. They are copied after the builtin static files, 99 | # so a file named "default.css" will overwrite the builtin "default.css". 100 | html_static_path = ['_static'] 101 | 102 | 103 | # -- Options for HTMLHelp output ------------------------------------------ 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'Tickompilerdoc' 107 | 108 | 109 | # -- Options for LaTeX output --------------------------------------------- 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'Tickompiler.tex', u'Tickompiler Documentation', 134 | u'chrislo27, SneakySpook', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output --------------------------------------- 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'tickompiler', u'Tickompiler Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'Tickompiler', u'Tickompiler Documentation', 155 | author, 'Tickompiler', 'A compiler/decompiler for Tickflow, a language based on the bytecode format used by the game Rhythm Heaven Megamix', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /docs/functions.rst: -------------------------------------------------------------------------------- 1 | Known Global Tickflow Operations 2 | ================================ 3 | 4 | This is a list of all Tickflow operations which have known functions and have been given a global alias. 5 | 6 | .. _macro: 7 | 8 | Asynchronous Subroutine (0) 9 | --------------------------- 10 | 11 | The async_sub function finds a subroutine corresponding to an argument, then 12 | calls it asynchronously (i.e. the code runs simultaneously to the Tickflow code already running). 13 | Async_sub calls have the following form:: 14 | 15 | async_sub id, delay, cat 16 | 17 | The ``id`` argument is the ID number assigned to the subroutine. It is first taken from a lookup table of 18 | rhythm game-specific IDs, usually starting at ``0x56``, and then from a global list of subroutines, which starts at 0. 19 | ``delay`` represents the delay in ticks before the macro is executed. 20 | ``cat`` is the category the new thread will belong to. This is often ``0x7D0`` (2000). 21 | The second and third arguments can be omitted, and default to 0 and ``0x7D0`` respectively. 22 | 23 | If the location called by the sub is within the Tickflow file it's called in, ``async_sub`` is replaced with a corresponding 24 | `async_call`_ call. 25 | 26 | .. _get_set_async: 27 | 28 | Get Async/Set Function (1) 29 | -------------------------- 30 | 31 | These two operations share the same operation number, 1. They are differentiated by the special argument. 32 | ``get_async`` corresponds to ``1<0>``, while ``set_func`` corresponds to ``1<1>``. 33 | ``set_func`` stores the location of a function into a slot, which can later be accessed and run asynchronously using 34 | ``get_async``, or synchronously using ``get_sync`` (5). ``set_func`` is of the following form:: 35 | 36 | set_func slot, loc 37 | 38 | It stores the location ``loc`` into the slot ``slot``. ``get_async`` is of the following form:: 39 | 40 | get_async slot, delay 41 | 42 | It calls the location stored into slot ``slot`` as an asynchronous function after ``delay`` ticks. 43 | 44 | .. _async_call: 45 | 46 | Async Call Location (2) 47 | ----------------------- 48 | 49 | ``async_call``, operation number 2, takes a location as an argument and runs the Tickflow code at that location 50 | as an asynchronous function. :: 51 | 52 | async_call loc, delay 53 | 54 | The asynchronous function at ``loc`` is called after a delay of ``delay`` ticks. 55 | 56 | Kill Threads (3) 57 | ---------------- 58 | 59 | Kills Tickflow threads according to several criteria. :: 60 | 61 | kill_all 62 | 63 | Kills all Tickflow threads. :: 64 | 65 | kill_cat c 66 | 67 | Kills all Tickflow threads in category ``c`` . :: 68 | 69 | kill_loc location 70 | 71 | Kills all Tickflow threads currently running inside the function at ``location`` . :: 72 | 73 | kill_sub id 74 | 75 | Kills all Tickflow threads currently running inside the subroutine ``id`` . 76 | 77 | Subroutine (4) 78 | -------------- 79 | 80 | ``sub`` finds a subroutine corresponding to an argument, then calls it synchronously. :: 81 | 82 | sub id 83 | 84 | The ``id`` argument is identical to the one in :ref:`macro`. 85 | 86 | Get Sync (5) 87 | ------------ 88 | 89 | Gets a function set by ``set_func`` and calls it synchronously. :: 90 | 91 | get_sync slot 92 | 93 | Call Location (6) 94 | ----------------- 95 | 96 | ``call`` takes a location as an argument and calls the Tickflow code at that location as a synchronous function. :: 97 | 98 | call loc 99 | 100 | The synchronous function at ``loc`` is called. 101 | 102 | Return (7) 103 | ---------- 104 | 105 | ``return`` takes no arguments, but returns from a synchronous function call. That is, when ``return`` occurs in a 106 | synchronous function, execution is returned to the location the function was called from. 107 | 108 | Stop (8) 109 | -------- 110 | 111 | ``stop`` stops the current thread of execution. 112 | 113 | Set Category (9) 114 | ---------------- 115 | :: 116 | 117 | set_cat c 118 | 119 | Sets the current thread to category ``c`` . 120 | 121 | Set Conditional Variable (0xA) 122 | ------------------------------ 123 | 124 | ``set_condvar`` sets the value of the conditional variable to its first argument. :: 125 | 126 | set_condvar val 127 | 128 | Add Conditional Variable (0xB) 129 | ------------------------------ 130 | 131 | ``add_condvar`` adds its first argument to the value of the conditional variable. :: 132 | 133 | add_condvar val 134 | 135 | Push Conditional Variable (0xC) 136 | ------------------------------- 137 | 138 | The conditional variable is pushed to a stack containing at most 16 values. For more information about stacks, see 139 | Wikipedia_. 140 | 141 | .. _Wikipedia: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) 142 | 143 | :: 144 | 145 | push_condvar 146 | 147 | Pop Conditional Variable (0xD) 148 | ------------------------------ 149 | 150 | The conditional variable is popped from the previously mentioned stack. :: 151 | 152 | pop_condvar 153 | 154 | .. _rest: 155 | 156 | Rest (0xE) 157 | ---------- 158 | :: 159 | 160 | rest duration 161 | 162 | ``duration`` is added to the rest counter. If the rest counter is now greater than zero, it will decrement at a rate 163 | of 48 per beat, pausing Tickflow execution until it reaches zero again. 164 | Note that ``duration`` is actually the special argument for ``rest``, but the syntax is like a regular argument here 165 | for convenience. 166 | 167 | Get/Set Rest (0xF) 168 | ------------------ 169 | 170 | ``getrest`` and ``setrest`` work similarly to :ref:`get_set_async`: ``setrest`` stores a duration in a slot, to later 171 | be used by ``getrest`` to add to the rest counter. :: 172 | 173 | setrest slot, duration 174 | 175 | The duration ``duration`` is stored in slot ``slot``. :: 176 | 177 | getrest slot 178 | 179 | The duration previously stored in ``slot`` is added to the rest counter. 180 | 181 | Reset Rest Counter (0x11) 182 | ------------------------- 183 | :: 184 | 185 | rest_reset 186 | 187 | The rest counter is set to 0. 188 | 189 | Unrest (0x12) 190 | ------------- 191 | :: 192 | 193 | unrest duration 194 | 195 | ``duration`` is subtracted from the rest counter. If the rest counter is negative, no action is undertaken. This effectively 196 | functions as a sort of buffer to subtract a duration from succeeding rests. Like in ``rest``, ``duration`` is actually 197 | a special argument, but the syntax is adjusted for convenience. 198 | 199 | Label (0x14) 200 | ------------ 201 | 202 | A label takes only a special argument, and marks this location for use by ``goto``. Can be positioned after a ``goto``. :: 203 | 204 | label id 205 | 206 | This location in the file is marked as ``id`` for use by ``goto``. 207 | Note that, like in :ref:`rest`, ``id`` is actually a special argument. 208 | 209 | Goto (0x15) 210 | ----------- 211 | 212 | ``goto`` takes only a special argument, and jumps to the corresponding ``label``. It presumably searches for the nearest 213 | label matching the ID. :: 214 | 215 | goto id 216 | 217 | Execution jumps to the label with ID ``id``. 218 | Note that, like in :ref:`rest`, ``id`` is actually a special argument. 219 | 220 | If, Else, Endif (0x16...0x18) 221 | ----------------------------- 222 | 223 | Together, these operations form if-blocks, a popular programming construct. :: 224 | 225 | if arg 226 | // Tickflow code 227 | else 228 | // other Tickflow code 229 | endif 230 | 231 | If the value of the conditional variable is equal to ``arg``, then the first block of Tickflow code is executed. 232 | Otherwise, the second block of Tickflow code is executed. The ``else`` block can be omitted entirely, in which case 233 | it is assumed to be empty. 234 | 235 | There are also several different variants on ``if``:: 236 | 237 | if_neq arg 238 | if_lt arg 239 | if_leq arg 240 | if_gt arg 241 | if_geq arg 242 | 243 | These execute the code if the conditional variable is 244 | not equal, less than, less than or equal, greater than, and greater than or equal to ``arg``, respectively. 245 | 246 | Switch, Case, Break, Default, Endswitch (0x19...0x1D) 247 | ----------------------------------------------------- 248 | 249 | Together, these operations form switch-case statements, another construct commonly found in programming languages. :: 250 | 251 | switch 252 | case arg1 253 | // tickflow code 254 | break 255 | case arg2 256 | // more tickflow code 257 | break 258 | [...] 259 | default 260 | // code 261 | break 262 | endswitch 263 | 264 | If the value of the condition variable is equal to ``arg1``, then the ``case arg1`` block runs. If the value of the 265 | condition variable is equal to ``arg2``, then the ``case arg2`` block runs, etc. If none of the cases match the value 266 | of the condition variable, the ``default`` block runs. If any ``break`` is omitted, then after running the corresponding 267 | code block, the next case will also be run. 268 | 269 | Countdown (0x1E) 270 | ---------------- 271 | 272 | ``countdown`` operations implement a countdown using two internal variables; the initial value of the countdown, and the 273 | "progress" of the countdown, which is subtracted from the initial value. :: 274 | 275 | set_countdown num 276 | 277 | Sets the initial value to ``num`` and sets the progress to 0. Equivalent to ``0x1E<0>``. :: 278 | 279 | set_countdown_condvar 280 | 281 | Sets the initial value to the value of the conditional variable, and sets progress to 0. Equivalent to ``0x1E<1>``. :: 282 | 283 | get_countdown_init 284 | 285 | Sets the conditional variable to the initial value of the countdown. Equivalent to ``0x1E<2>``. :: 286 | 287 | get_countdown_prog 288 | 289 | Sets the conditional variable to the progress of the countdown. Equivalent to ``0x1E<3>``. :: 290 | 291 | get_countdown 292 | 293 | Sets the conditional variable to the countdown value: ``initial - progress``. Equivalent to ``0x1E<4>``. :: 294 | 295 | dec_countdown 296 | 297 | Increments the progress variable by 1, therefore decrementing the countdown value by 1. Equivalent to ``0x1E<5>``. 298 | 299 | Speed (0x24) 300 | ------------ 301 | 302 | ``speed`` sets the speed of the game to a specified fraction of the original speed. This also increases the pitch 303 | of the music. An example of ``speed`` usage can be found in Karate Man Senior, when the game speeds up. :: 304 | 305 | speed val 306 | 307 | The speed is set to ``val/256`` of the original speed. For example, ``speed 0x100`` sets the speed to the original speed, 308 | while ``speed 0x120`` sets the speed to 288/256, or 112.5% of the original speed. 309 | 310 | Relative Speed (0x25) 311 | --------------------- 312 | 313 | This operation operates on the same speed value as ``speed`` (0x24) does, but instead of setting it, it multiplies, 314 | resulting in a relative speed change from the current speed. A lower and upper bound on the resulting overall speed 315 | can also be set. :: 316 | 317 | speed_relative val, lb, ub 318 | 319 | The game speed is multiplied by ``val/256``. The resulting value cannot fall below ``lb/256`` or rise above ``ub/256`` 320 | of the original speed. 321 | 322 | Engine (0x28) 323 | ------------- 324 | 325 | ``engine`` sets the game engine to the one corresponding to the argument ID. :: 326 | 327 | engine id 328 | 329 | The game engine is set to the engine corresponding to ``id``. Game engines have a set of special tickflow functions which 330 | are specific to that game, as well as a set of macros and/or subroutines. 331 | 332 | Set Game to Asset Slot (0x2A) 333 | ----------------------------- 334 | 335 | This is a set of operations all sharing the same operation number, but being distinguished by different special argument 336 | values. :: 337 | 338 | game_model id, slot 339 | game_cellanim id, slot 340 | game_effect id, slot 341 | game_layout id, slot 342 | 343 | These assign a game engine ID to an asset (model, cellanim, effect or layout) slot, to allow the game to load assets 344 | from the correct asset slots when loading a game. 345 | ``game_model`` corresponds to ``0x2A<0>``, ``game_cellanim`` to ``0x2A<2>``, ``game_effect`` to ``0x2A<3>`` and 346 | ``game_layout`` to ``0x2A<4>``. 347 | 348 | .. _model: 349 | 350 | Model Asset Management (0x31) 351 | ----------------------------- 352 | 353 | This is a set of operations differentiated by their special argument, which all share a common theme of being used 354 | to manage the loading of model assets. Model assets are organized into slots starting at slot 1, 355 | where one slot can hold assets for one rhythm game. :: 356 | 357 | set_model slot, str, ??? 358 | 359 | The first argument is a the slot for the model assets to be loaded into, the second argument is a location in memory 360 | that contains a string, namely the filename of the file containing the assets to be loaded. The third argument is unknown, 361 | but seems to always be 1. ``set_model`` corresponds to ``0x31<0>``. :: 362 | 363 | remove_model slot 364 | 365 | Removes the model assets currently loaded into ``slot``. ``remove_model`` corresponds to ``0x31<1>``. :: 366 | 367 | has_model slot 368 | 369 | Seems to set the conditional variable to 1 if ``slot`` contains assets, and 0 otherwise. ``has_model`` corresponds 370 | to ``0x31<2>``. 371 | 372 | Cellanim Asset Management (0x35) 373 | -------------------------------- 374 | 375 | Very similarly to :ref:`model`, this set of operations manages cellanim assets. Cellanim assets consist of 2D sprites 376 | and animations thereof. Cellanim assets, similarly to model assets, are organized into slots starting at slot 2, with 377 | each slot holding assets for one rhythm game. :: 378 | 379 | set_cellanim slot, str, ??? 380 | 381 | The first argument is the slot for the assets to be loaded into, the second argument is a location in memory that contains 382 | the filename of the file to be loaded. The third argument is unknown, but seems to always be ``0xFFFFFFFF``, -1 when 383 | interpreted as a signed integer. ``set_cellanim`` corresponds to ``0x35<0>``. :: 384 | 385 | cellanim_busy slot 386 | 387 | Seems to set the conditional variable to 1 if ``slot`` is currently being written to or deleted from, and 0 otherwise. 388 | ``cellanim_busy`` corresponds to ``0x35<1>``. :: 389 | 390 | remove_cellanim slot 391 | 392 | Removes the cellanim assets currently loaded into ``slot``. ``remove_cellanim`` corresponds to ``0x35<3>``. 393 | 394 | Effect Asset Management (0x39) 395 | ------------------------------ 396 | 397 | Similarly to the previous two entries, this set of operations manages effect assets. Effect assets seem to consist of 398 | particle effects, and are organized into slots starting at slot 2, with each slot holding assets for one rhythm game. :: 399 | 400 | set_effect slot, str, ??? 401 | 402 | This operation has identical functioning to ``set_cellanim``. ``set_effect`` corresponds to ``0x39<0>``. :: 403 | 404 | effect_busy slot 405 | 406 | This operation has identical functioning to ``cellanim_busy``. ``effect_busy`` corresponds to ``0x39<1>``. :: 407 | 408 | remove_effect slot 409 | 410 | This operation has identical functioning to ``remove_cellanim``. ``remove_effect`` corresponds to ``0x39<7>``. 411 | 412 | Layout Asset Management (0x3E) 413 | ------------------------------ 414 | 415 | Similarly to the previous entries, this set of operations manages layout assets. Layout assets are organized into slots 416 | starting at slot 4, though the slots used by stock games and remixes wildly vary. :: 417 | 418 | set_layout slot, str, ??? 419 | 420 | This operation has identical functioning to ``set_effect`` and ``set_cellanim``. ``set_layout`` corresponds to ``0x3E<0>``. :: 421 | 422 | layout_busy slot 423 | 424 | This operation has identical functioning to ``effect_busy`` and ``cellanim_busy``. ``layout_busy`` corresponds to ``0x3E<1>``. :: 425 | 426 | remove_layout slot 427 | 428 | This operation has identical functioning to ``remove_effect`` and ``remove_cellanim``. ``remove_layout`` corresponds to ``0x3E<7>``. 429 | 430 | Play SFX (0x40) 431 | --------------- 432 | 433 | This operation plays a sound effect according to an ID. :: 434 | 435 | play_sfx id 436 | 437 | A sound effect is played according to ``id``. Where these IDs are defined is not yet clear, though the sound effect 438 | may be played after a tempo-dependent delay, suggesting that these IDs encode additional info, and not only the sound 439 | effect itself. 440 | 441 | Set SFX Slot (0x5D) 442 | ------------------- 443 | 444 | This operation loads sound effects into the specified SFX slot. Sound effects in the loaded assets can thereafter be 445 | played at any time. :: 446 | 447 | set_sfx slot, str 448 | 449 | Loads the sound effects corresponding to the group name at the location ``str`` in memory into ``slot``. 450 | 451 | Remove SFX (0x5F) 452 | ----------------- 453 | 454 | This operation removes previously loaded sound effects from the specified SFX slot. :: 455 | 456 | remove_sfx slot 457 | 458 | Removes the SFX assets loaded into ``slot``. 459 | 460 | Enable/Disable Input (0x6A) 461 | --------------------------- 462 | 463 | This operation enables or disables all user input. :: 464 | 465 | input flag 466 | 467 | Disables input if ``flag`` is 0, enables it if it is 1. 468 | 469 | Zoom View (0x7E) 470 | ---------------- 471 | :: 472 | 473 | zoom n, x, y 474 | 475 | Instantaneously sets the X-axis zoom factor for the ``n`` th view to ``x/0x100``, and the Y-axis zoom factor to ``y/0x100``. 476 | It is currently unknown how to determine the correct view number to use, however, it is known to usually be 3 or 4 when 477 | it is used in-game. :: 478 | 479 | zoom_gradual n, i, s, duration, x, y 480 | 481 | Changes the X-axis zoom factor to ``x/0x100`` and the Y-axis zoom factor to ``y/0x100`` over ``duration`` ticks. ``i`` 482 | determines the interpolation method used, and ``s`` determines the intensity of said interpolation's variation. Values for 483 | ``i`` are: 484 | 485 | - 1: Linear 486 | - 2: Faster at the start 487 | - 3: Faster at the end 488 | - 4: Faster in the middle (smooth) 489 | - 5: Slower in the middle 490 | 491 | Pan View (0x7F) 492 | --------------- 493 | :: 494 | 495 | pan n, x, y 496 | 497 | Instantaneously pans the view to the position ``x`` units (pixels?) left and ``y`` units (pixels?) up from the origin. ``n`` is as above. :: 498 | 499 | pan_gradual n, i, s, duration, x, y 500 | 501 | Pans the view to ``x`` units left and ``y`` units up from the origin over ``duration`` ticks. ``i`` and ``s`` are as above. 502 | 503 | Rotate View (0x80) 504 | ------------------ 505 | :: 506 | 507 | rotate n, angle 508 | 509 | Instantaneously rotates the view to ``angle`` degrees clockwise from the default. ``n`` is as above. :: 510 | 511 | rotate_gradual n, i, s, duration, angle 512 | 513 | Rotates the view to ``angle`` degrees clockwise from the default over ``duration`` ticks. ``i`` and ``s`` are as above. 514 | 515 | 516 | Skill Star (0xAE) 517 | ----------------- 518 | :: 519 | 520 | star time 521 | 522 | A skill star appears, to be collected after ``time`` ticks. Glitchy if no input matches the given time. 523 | 524 | Random (0xB8) 525 | ------------- 526 | 527 | This operation generates a random number and stores it in the conditional variable. :: 528 | 529 | random num 530 | 531 | Stores a random number between 0 and ``num`` exclusive in the conditional variable. Note that, like in :ref:`rest`, 532 | ``num`` is actually a special variable. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Tickompiler documentation master file, created by 2 | sphinx-quickstart on Wed Jun 07 17:11:12 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. warning:: 7 | This documentation is severely outdated! Please check out Tox's docs `here ` instead. 8 | 9 | Welcome to Tickompiler's documentation! 10 | ======================================= 11 | 12 | Tickompiler is a compiler and decompiler for Tickflow, a language constructed from the bytecode format 13 | used by the game Rhythm Heaven Megamix for the Nintendo 3DS to create the sequence of events in its rhythm games and 14 | remixes. Tickompiler allows easy editing of these rhythm games in a more human-readable format. 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :caption: Tickflow Documentation 19 | 20 | basic_syntax 21 | concepts 22 | functions 23 | engines 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin_version=1.3.72 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhmodding/Tickompiler/b55c44bac118a1b33e710cdc4e6752f2f5a894d6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 29 21:59:48 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'tickompiler' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/Functions.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler 2 | 3 | import rhmodding.tickompiler.compiler.FunctionCall 4 | import rhmodding.tickompiler.decompiler.CommentType 5 | import rhmodding.tickompiler.decompiler.DecompilerState 6 | import java.util.* 7 | import kotlin.math.abs 8 | 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @Target(AnnotationTarget.CLASS) 11 | annotation class DeprecatedFunction(val value: String) 12 | 13 | abstract class Functions { 14 | 15 | val opcode = OpcodeFunction() 16 | val bytecode = BytecodeFunction() 17 | abstract val allFunctions: MutableList 18 | 19 | val byName: MutableMap 20 | get() = allFunctions.associateBy { it.name }.toMutableMap() 21 | 22 | operator fun get(op: Long): Function { 23 | allFunctions.forEach { if (it.acceptOp(op)) return@get it } 24 | return opcode 25 | } 26 | 27 | operator fun set(op: Long, alias: String) { 28 | val f = op alias alias 29 | allFunctions.add(f) 30 | } 31 | 32 | private fun isNumeric(input: String): Boolean { 33 | return input.toIntOrNull() != null 34 | } 35 | 36 | operator fun get(key: String): Function { 37 | return if (key.startsWith("0x") or isNumeric(key)) { 38 | opcode 39 | } else { 40 | byName[key] ?: throw MissingFunctionError("Failed to find function $key") 41 | } 42 | } 43 | 44 | protected fun alias(opcode: Long, alias: String, argsNeeded: IntRange, indentChange: Int = 0, 45 | currentAdjust: Int = 0): AliasedFunction { 46 | return AliasedFunction(opcode, alias, argsNeeded, indentChange, currentAdjust) 47 | } 48 | 49 | /** 50 | * Assumes 0..0b1111 for args needed. 51 | */ 52 | protected infix fun Long.alias(alias: String): AliasedFunction { 53 | return alias(this, alias, 0..0b1111) 54 | } 55 | 56 | 57 | } 58 | 59 | fun createInts(opcode: Long, special: Long, args: LongArray?): LongArray { 60 | if (args == null) 61 | return longArrayOf(opcode) 62 | 63 | if (args.size > 0b1111) 64 | throw IllegalArgumentException("Args size cannot be more than ${0b1111}") 65 | val firstLong: Long = opcode or ((args.size.toLong() and 0b1111) shl 10) or ((special and 0b111111111111111111) shl 14) 66 | 67 | return longArrayOf(firstLong, *args) 68 | } 69 | 70 | object MegamixFunctions : Functions() { 71 | override val allFunctions = mutableListOf( 72 | opcode, 73 | bytecode, 74 | BytesFunction(), 75 | RestFunction(0xE), 76 | SpecialOnlyFunction(0x12, "unrest"), 77 | SpecialOnlyFunction(0x14, "label"), 78 | SpecialOnlyFunction(0x15, "goto"), 79 | SpecialOnlyFunction(0x1A, "case", indentChange = 1), 80 | SpecialOnlyFunction(0xB8, "random"), 81 | SpecificSpecialFunction(0x1, 0, "get_async", 2..2), 82 | SpecificSpecialFunction(0x1, 1, "set_func", 2..2), 83 | SpecificSpecialFunction(0x3, 0, "kill_all", 0..0), 84 | SpecificSpecialFunction(0x3, 1, "kill_cat", 1..1), 85 | SpecificSpecialFunction(0x3, 2, "kill_loc", 1..1), 86 | SpecificSpecialFunction(0x3, 3, "kill_sub", 1..1), 87 | SpecificSpecialFunction(0xF, 0, "getrest", 1..1), 88 | SpecificSpecialFunction(0xF, 1, "setrest", 2..2), 89 | SpecificSpecialFunction(0x16, 0, "if", 1..1, 1), 90 | SpecificSpecialFunction(0x16, 1, "if_neq", 1..1, 1), 91 | SpecificSpecialFunction(0x16, 2, "if_lt", 1..1, 1), 92 | SpecificSpecialFunction(0x16, 3, "if_leq", 1..1, 1), 93 | SpecificSpecialFunction(0x16, 4, "if_gt", 1..1, 1), 94 | SpecificSpecialFunction(0x16, 5, "if_geq", 1..1, 1), 95 | SpecificSpecialFunction(0x1E, 0, "set_countdown", 1..1), 96 | SpecificSpecialFunction(0x1E, 1, "set_countdown_condvar", 0..0), 97 | SpecificSpecialFunction(0x1E, 2, "get_countdown_init", 0..0), 98 | SpecificSpecialFunction(0x1E, 3, "get_countdown_prog", 0..0), 99 | SpecificSpecialFunction(0x1E, 4, "get_countdown", 0..0), 100 | SpecificSpecialFunction(0x1E, 5, "dec_countdown", 0..0), 101 | SpecificSpecialFunction(0x2A, 0, "game_model", 102 | 2..2), 103 | SpecificSpecialFunction(0x2A, 2, "game_cellanim", 104 | 2..2), 105 | SpecificSpecialFunction(0x2A, 3, "game_effect", 106 | 2..2), 107 | SpecificSpecialFunction(0x2A, 4, "game_layout", 108 | 2..2), 109 | SpecificSpecialFunction(0x31, 0, "set_model", 3..3), 110 | SpecificSpecialFunction(0x31, 1, "remove_model", 111 | 1..1), 112 | SpecificSpecialFunction(0x31, 2, "has_model", 1..1), 113 | SpecificSpecialFunction(0x35, 0, "set_cellanim", 114 | 3..3), 115 | SpecificSpecialFunction(0x35, 1, "cellanim_busy", 116 | 1..1), 117 | SpecificSpecialFunction(0x35, 3, "remove_cellanim", 118 | 1..1), 119 | SpecificSpecialFunction(0x39, 0, "set_effect", 120 | 3..3), 121 | SpecificSpecialFunction(0x39, 1, "effect_busy", 122 | 1..1), 123 | SpecificSpecialFunction(0x39, 7, "remove_effect", 124 | 1..1), 125 | SpecificSpecialFunction(0x3E, 0, "set_layout", 126 | 3..3), 127 | SpecificSpecialFunction(0x3E, 1, "layout_busy", 128 | 1..1), 129 | SpecificSpecialFunction(0x3E, 7, "remove_layout", 130 | 1..1), 131 | SpecificSpecialFunction(0x7E, 0, "zoom", 3..3), 132 | SpecificSpecialFunction(0x7E, 1, "zoom_gradual", 6..6), 133 | SpecificSpecialFunction(0x7F, 0, "pan", 3..3), 134 | SpecificSpecialFunction(0x7F, 1, "pan_gradual", 6..6), 135 | SpecificSpecialFunction(0x80, 0, "rotate", 2..2), 136 | SpecificSpecialFunction(0x80, 1, "rotate_gradual", 5..5), 137 | OptionalArgumentsFunction(0, "async_sub", 3, 0, 2000), 138 | OptionalArgumentsFunction(2, "async_call", 2, 0), 139 | alias(0x4, "sub", 1..1), 140 | alias(0x5, "get_sync", 1..1), 141 | alias(0x6, "call", 1..1), 142 | alias(0x7, "return", 0..0), 143 | alias(0x8, "stop", 0..0), 144 | alias(0x9, "set_cat", 1..1), 145 | alias(0xA, "set_condvar", 1..1), 146 | alias(0xB, "add_condvar", 1..1), 147 | alias(0xC, "push_condvar", 0..0), 148 | alias(0xD, "pop_condvar", 0..0), 149 | alias(0x11, "rest_reset", 0..0), 150 | alias(0x17, "else", 0..0, 0, 151 | -1), // current adjust pushes the else back an indent 152 | alias(0x18, "endif", 0..0, -1, -1), // same here 153 | alias(0x19, "switch", 0..0, 1), 154 | alias(0x1B, "break", 0..0, -1), 155 | alias(0x1C, "default", 0..0, 1), 156 | alias(0x1D, "endswitch", 0..0, -1, -1), 157 | alias(0x24, "speed", 1..1), 158 | alias(0x25, "speed_relative", 3..3), 159 | alias(0x28, "engine", 1..1), 160 | alias(0x40, "play_sfx", 1..1), 161 | alias(0x5D, "set_sfx", 2..2), 162 | alias(0x5F, "remove_sfx", 1..1), 163 | alias(0x6A, "input", 1..1), 164 | alias(0xAE, "star", 1..1), 165 | alias(0xB5, "debug", 1..1), 166 | 0x7DL alias "fade" 167 | ) 168 | } 169 | 170 | object DSFunctions : Functions() { 171 | override val allFunctions = mutableListOf( 172 | RestFunction(1) 173 | ) 174 | } 175 | 176 | abstract class Function(val opCode: Long, val name: String, val argsNeeded: IntRange) { 177 | 178 | open fun acceptOp(op: Long): Boolean { 179 | val opcode = op and 0x3FF 180 | val args = (op and 0x3C00) ushr 10 181 | return opcode == opCode && args in argsNeeded 182 | } 183 | 184 | fun checkArgsNeeded(functionCall: FunctionCall) { 185 | val args = functionCall.args.size.toLong() 186 | if (args !in argsNeeded) { 187 | throw WrongArgumentsError(args, argsNeeded, 188 | "function ${functionCall.func}<${functionCall.specialArg}>, got ${functionCall.args}") 189 | } 190 | } 191 | 192 | abstract fun produceBytecode(funcCall: FunctionCall): LongArray 193 | 194 | abstract fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 195 | comments: CommentType, specialArgStrings: Map): String 196 | 197 | fun argsToTickflowArgs(args: LongArray, specialArgStrings: Map, radix: Int = 16): String { 198 | return args.mapIndexed { index, it -> 199 | if (specialArgStrings.containsKey(index)) { 200 | specialArgStrings[index] 201 | } else { 202 | if (radix == 16) getHex(it) else 203 | it.toInt().toString(radix).toString() 204 | } 205 | }.joinToString(separator = ", ") 206 | } 207 | 208 | fun addSpecialArg(specialArg: Long): String { 209 | return (if (specialArg != 0L) "<${getHex(specialArg)}>" else "") 210 | } 211 | 212 | fun getHex(num: Long): String { 213 | return if (abs(num.toInt()) < 10) 214 | num.toInt().toString(16).toString().toUpperCase(Locale.ROOT) 215 | else 216 | (if (num.toInt() < 0) "-" else "") + "0x" + abs(num.toInt()).toString(16).toString().toUpperCase(Locale.ROOT) 217 | } 218 | 219 | } 220 | 221 | class BytecodeFunction : Function(-1, "bytecode", 1..1) { 222 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 223 | comments: CommentType, specialArgStrings: Map): String { 224 | throw NotImplementedError() 225 | } 226 | 227 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 228 | return longArrayOf(funcCall.args.first()) 229 | } 230 | 231 | } 232 | 233 | class BytesFunction : Function(-1, "bytes", 1..Int.MAX_VALUE) { 234 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 235 | val list = mutableListOf() 236 | var i = 0 237 | while (i < funcCall.args.size) { 238 | var n = 0L 239 | n += funcCall.args[i] 240 | if (i + 1 < funcCall.args.size) 241 | n += funcCall.args[i+1] shl 8 242 | if (i + 2 < funcCall.args.size) 243 | n += funcCall.args[i+2] shl 16 244 | if (i + 3 < funcCall.args.size) 245 | n += funcCall.args[i+3] shl 24 246 | i += 4 247 | list.add(n) 248 | } 249 | return list.toLongArray() 250 | } 251 | 252 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, comments: CommentType, specialArgStrings: Map): String { 253 | return this.name + " " + argsToTickflowArgs(args, specialArgStrings) 254 | } 255 | } 256 | 257 | class OpcodeFunction : Function(-1, "opcode", 0..Integer.MAX_VALUE) { 258 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 259 | comments: CommentType, specialArgStrings: Map): String { 260 | return getHex(opcode) + 261 | addSpecialArg(specialArg) + 262 | " " + argsToTickflowArgs(args, specialArgStrings) 263 | } 264 | 265 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 266 | val opcode = if (funcCall.func.startsWith("0x")) 267 | funcCall.func.substring(2).toLong(16) 268 | else 269 | funcCall.func.toLong() 270 | 271 | return createInts(opcode, funcCall.specialArg, funcCall.args.toLongArray()) 272 | } 273 | 274 | } 275 | 276 | open class OptionalArgumentsFunction(opcode: Long, alias: String, val numArgs: Int, vararg val defaultArgs: Long): AliasedFunction(opcode, alias, (numArgs - defaultArgs.size)..numArgs) { 277 | override fun acceptOp(op: Long): Boolean { 278 | val opcode = op and 0x3FF 279 | val args = (op and 0x3C00) ushr 10 280 | return opcode == opCode && args == numArgs.toLong() 281 | } 282 | 283 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, comments: CommentType, specialArgStrings: Map): String { 284 | val newArgs = args.toMutableList() 285 | while (newArgs.size > argsNeeded.first && newArgs.last() == defaultArgs[newArgs.size - argsNeeded.first - 1]) { 286 | newArgs.removeAt(newArgs.size-1) 287 | } 288 | return super.produceTickflow(state, opcode, specialArg, newArgs.toLongArray(), comments, specialArgStrings) 289 | } 290 | 291 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 292 | val newArgs = funcCall.args.toMutableList() 293 | while (newArgs.size < numArgs) { 294 | newArgs.add(defaultArgs[newArgs.size - argsNeeded.first]) 295 | } 296 | return super.produceBytecode(FunctionCall(funcCall.func, funcCall.specialArg, newArgs)) 297 | } 298 | } 299 | 300 | open class SpecialOnlyFunction(opcode: Long, alias: String, val indentChange: Int = 0, 301 | val currentAdjust: Int = 0) 302 | : Function(opcode, alias, 1..1) { 303 | override fun acceptOp(op: Long): Boolean { 304 | val opcode = op and 0x3FF 305 | val args = (op and 0x3C00) ushr 10 306 | return opcode == opCode && args == 0L 307 | } 308 | 309 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 310 | comments: CommentType, specialArgStrings: Map): String { 311 | state.nextIndentLevel += indentChange 312 | state.currentAdjust = currentAdjust 313 | return "${this.name} ${getHex(specialArg)}" 314 | } 315 | 316 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 317 | val spec: Long = funcCall.args.first() 318 | if (spec !in 0..0b111111111111111111) 319 | throw IllegalArgumentException( 320 | "Special argument out of range: got $spec, needs to be ${0..0b111111111111111111}") 321 | 322 | return longArrayOf((this.opCode or (spec shl 14))) 323 | } 324 | } 325 | 326 | open class SpecificSpecialFunction(opcode: Long, val special: Long, alias: String, 327 | argsNeeded: IntRange = 0..0b1111, 328 | indentChange: Int = 0, currentAdjust: Int = 0) 329 | : AliasedFunction(opcode, alias, argsNeeded, indentChange, currentAdjust) { 330 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 331 | comments: CommentType, specialArgStrings: Map): String { 332 | return super.produceTickflow(state, opcode, 0, args, comments, specialArgStrings) 333 | } 334 | 335 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 336 | return createInts(opCode, special, funcCall.args.toLongArray()) 337 | } 338 | 339 | override fun acceptOp(op: Long): Boolean { 340 | val opcode = op and 0x3FF 341 | val args = (op and 0x3C00) ushr 10 342 | val special = op ushr 14 343 | return opcode == opCode && special == this.special && args in argsNeeded 344 | } 345 | 346 | } 347 | 348 | open class RestFunction(opcode: Long) : SpecialOnlyFunction(opcode, "rest") { 349 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 350 | comments: CommentType, specialArgStrings: Map): String { 351 | return "rest " + getHex(specialArg) + if (comments != CommentType.NONE) "\t// ${specialArg / 48f} beats" else "" 352 | } 353 | } 354 | 355 | open class AliasedFunction(opcode: Long, alias: String, argsNeeded: IntRange, val indentChange: Int = 0, 356 | val currentAdjust: Int = 0) : Function(opcode, alias, argsNeeded) { 357 | override fun produceTickflow(state: DecompilerState, opcode: Long, specialArg: Long, args: LongArray, 358 | comments: CommentType, specialArgStrings: Map): String { 359 | state.nextIndentLevel += indentChange 360 | state.currentAdjust = currentAdjust 361 | return this.name + addSpecialArg(specialArg) + " " + argsToTickflowArgs(args, specialArgStrings) 362 | } 363 | 364 | override fun produceBytecode(funcCall: FunctionCall): LongArray { 365 | return createInts(this.opCode, funcCall.specialArg, funcCall.args.toLongArray()) 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/Tickompiler.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler 2 | 3 | import picocli.CommandLine 4 | import rhmodding.tickompiler.cli.* 5 | import rhmodding.tickompiler.util.Version 6 | 7 | 8 | object Tickompiler { 9 | 10 | val VERSION: Version = Version(1, 10, 0, "") 11 | const val GITHUB: String = "https://github.com/rhmodding/Tickompiler" 12 | 13 | fun createAndParseCommandLine(runnable: Runnable, vararg args: String): CommandLine { 14 | // This is equivalent to the static method CommandLine.run(...) but with the settings desired 15 | return CommandLine(runnable).setToggleBooleanFlags(false).apply { 16 | parseWithHandlers(CommandLine.RunLast().useOut(System.out).useAnsi(CommandLine.Help.Ansi.AUTO), 17 | CommandLine.DefaultExceptionHandler>().useErr(System.err).useAnsi(CommandLine.Help.Ansi.AUTO), 18 | *(if (args.isEmpty()) arrayOf("--help") else args)) 19 | } 20 | } 21 | 22 | @JvmStatic 23 | fun main(args: Array) { 24 | createAndParseCommandLine(TickompilerCommand(), *args) 25 | } 26 | } 27 | 28 | @CommandLine.Command(mixinStandardHelpOptions = true, versionProvider = TickompilerVersionProvider::class, 29 | name = "tickompiler", description = ["A RHM tickflow compiler/decompiler"], 30 | subcommands = [CompileCommand::class, DecompileCommand::class, PackCommand::class, ExtractCommand::class, GrabCommand::class, 31 | NotepadppLangCommand::class, DaemonCommand::class, UpdatesCheckCommand::class]) 32 | class TickompilerCommand : Runnable { 33 | override fun run() { 34 | } 35 | } 36 | 37 | class TickompilerVersionProvider : CommandLine.IVersionProvider { 38 | override fun getVersion(): Array = arrayOf("Tickompiler: A RHM tickflow compiler/decompiler", Tickompiler.VERSION.toString(), Tickompiler.GITHUB, "Licensed under the MIT License") 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/Utils.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler 2 | 3 | interface TickompilerError 4 | 5 | open class CompilerError(message: String) : RuntimeException(message), TickompilerError 6 | 7 | open class DecompilerError(message: String) : RuntimeException(message), TickompilerError 8 | 9 | class MissingFunctionError(message: String) : CompilerError(message) 10 | 11 | class WrongArgumentsError(args: Long, needed: IntRange, msg: String = "") : CompilerError( 12 | "Wrong arg count: got $args, need $needed - $msg") 13 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/CompileCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import kotlinx.coroutines.Deferred 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.runBlocking 7 | import picocli.CommandLine 8 | import rhmodding.tickompiler.DSFunctions 9 | import rhmodding.tickompiler.MegamixFunctions 10 | import rhmodding.tickompiler.compiler.Compiler 11 | import rhmodding.tickompiler.util.getDirectories 12 | import java.io.File 13 | import java.io.FileOutputStream 14 | import java.nio.ByteOrder 15 | 16 | 17 | @CommandLine.Command(name = "compile", aliases = ["c"], description = ["Compile tickflow file(s) and output them as a binary to the file/directory specified.", 18 | "Files must be with the file extension .tickflow", 19 | "Files will be overwritten without warning.", 20 | "Use the --objectify/-o optional parameter to create a .tfobj (tickflow object) file, which contains the compiled data along with the tempo files required.", 21 | "If the output is not specified, the file will be a (little-endian) .bin file with the same name.", 22 | "If the output is not specified AND --objectify was used, the output file MUST be specified!"], 23 | mixinStandardHelpOptions = true) 24 | class CompileCommand : Runnable { 25 | 26 | @CommandLine.Option(names = ["-c"], description = ["Continue even with errors."]) 27 | var continueWithErrors: Boolean = false 28 | 29 | @CommandLine.Option(names = ["-m", "--megamix"], description = ["Compile with Megamix functions. (default true)"]) 30 | var megamixFunctions: Boolean = true 31 | 32 | @CommandLine.Option(names = ["--ds"], description = ["Compile with RHDS functions."]) 33 | var dsFunctions: Boolean = false 34 | 35 | @CommandLine.Option(names = ["-o", "--objectify"], paramLabel = "tempo files directory", 36 | description = ["Compile as a .tfobj (tickflow object) file. Provide the directory where required .tempo files are located."]) 37 | var objectify: File? = null 38 | 39 | @CommandLine.Parameters(index = "0", arity = "1", description = ["Input file or directory."]) 40 | lateinit var inputFile: File 41 | 42 | @CommandLine.Parameters(index = "1", arity = "0..1", description = ["Output file or directory. If --objectify is used, this is NOT optional and must be a file."]) 43 | var outputFile: File? = null 44 | 45 | override fun run() { 46 | val nanoStart: Long = System.nanoTime() 47 | val tempoLoc = objectify 48 | if (tempoLoc != null && !tempoLoc.isDirectory) { 49 | throw IllegalArgumentException("--objectify was used but the path given was not a directory: ${tempoLoc.path}") 50 | } else if (tempoLoc != null) { 51 | if (outputFile == null) { 52 | throw IllegalArgumentException("--objectify was used but the output file was not specified or is not a file. It must be specified and must be a file.") 53 | } 54 | outputFile!!.createNewFile() 55 | } 56 | val objectifying = tempoLoc != null 57 | val dirs = getDirectories(inputFile, outputFile, { s -> s.endsWith(".tickflow") }, if (objectifying) "tfobj" else "bin", objectifying) 58 | val functions = when { 59 | dsFunctions -> DSFunctions 60 | megamixFunctions -> MegamixFunctions 61 | else -> MegamixFunctions 62 | } 63 | val tempoFiles: List = if (tempoLoc != null) tempoLoc.listFiles { f -> f.name.endsWith(".tempo") }!!.toList() else listOf() 64 | 65 | println("Compiling ${dirs.input.size} file(s)${if (objectifying) " with ${tempoFiles.size} tempo files" else ""} ") 66 | 67 | val outputBinFiles: List> = if (!objectifying) { 68 | dirs.input.mapIndexed { i, f -> f to dirs.output[i] } 69 | } else { 70 | dirs.input.map { it to File.createTempFile("Tickompiler_tmp-", ".bin").apply { deleteOnExit() } } 71 | } 72 | val coroutines: MutableList> = mutableListOf() 73 | 74 | dirs.input.forEachIndexed { index, file -> 75 | coroutines += GlobalScope.async { 76 | val compiler = Compiler(file, functions) 77 | 78 | try { 79 | println("Compiling ${file.path}") 80 | val result = compiler.compile(ByteOrder.LITTLE_ENDIAN) 81 | 82 | if (result.success) { 83 | val out = outputBinFiles[index].second 84 | out.createNewFile() 85 | val fos = FileOutputStream(out) 86 | fos.write(result.data.array()) 87 | fos.close() 88 | 89 | println("Compiled ${file.path} -> ${result.timeMs} ms") 90 | return@async true 91 | } 92 | } catch (e: Exception) { 93 | if (continueWithErrors) { 94 | println("FAILED to compile ${file.path}") 95 | e.printStackTrace() 96 | } else { 97 | throw e 98 | } 99 | } 100 | 101 | return@async false 102 | } 103 | } 104 | 105 | runBlocking { 106 | val numSuccessful = coroutines 107 | .map { it.await() } 108 | .count { it } 109 | 110 | if (objectifying) { 111 | if (numSuccessful != dirs.input.size) { 112 | println(""" 113 | +====================+ 114 | | COMPILATION FAILED | 115 | +====================+ 116 | Only $numSuccessful / ${dirs.input.size} were compiled successfully. (Took ${(System.nanoTime() - nanoStart) / 1_000_000.0} ms) 117 | All must compile successfully to build a tickflow object.""") 118 | } else { 119 | val objOut = outputFile!! 120 | println(""" 121 | +========================+ 122 | | COMPILATION SUCCESSFUL | 123 | +========================+ 124 | All $numSuccessful targets were compiled successfully. (Took ${(System.nanoTime() - nanoStart) / 1_000_000.0} ms) 125 | Building object file ${objOut.name}...""") 126 | rhmodding.tickompiler.objectify.objectify(objOut, outputBinFiles.map { it.second }, tempoFiles) 127 | println("Succeeded.") 128 | } 129 | } else { 130 | println(""" 131 | +======================+ 132 | | COMPILATION COMPLETE | 133 | +======================+ 134 | $numSuccessful / ${dirs.input.size} compiled successfully in ${(System.nanoTime() - nanoStart) / 1_000_000.0} ms""") 135 | } 136 | } 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/DaemonCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import picocli.CommandLine 4 | import rhmodding.tickompiler.Tickompiler 5 | import rhmodding.tickompiler.TickompilerCommand 6 | import java.util.regex.Pattern 7 | 8 | @CommandLine.Command(name = "daemon", description = ["Runs Tickompiler in daemon mode.", 9 | "This will provide a continuously running program which makes use of JIT (just-in-time compilation) to speed up future operations.", 10 | "You can optionally provide the first command you want to run as arguments to this command.", 11 | "CTRL+C will kill the program. Typing 'stop' or 'exit' will work too."], 12 | mixinStandardHelpOptions = true) 13 | class DaemonCommand : Runnable { 14 | 15 | @CommandLine.Parameters(index = "0", arity = "0..*", description = ["First command to execute along with its arguments, if any."]) 16 | var firstCommand: List = listOf() 17 | 18 | override fun run() { 19 | println("Running in daemon mode: press CTRL+C or type 'stop' or 'exit' to terminate\nType '-h' for help\n${Tickompiler.VERSION}\n${Tickompiler.GITHUB}\n") 20 | 21 | var input: String = (if (firstCommand.isNotEmpty()) { 22 | // we have a first command to immediately execute 23 | firstCommand.joinToString(" ") 24 | } else readLine())?.trim() ?: return 25 | 26 | while (!input.equals("stop", true) && !input.equals("exit", true)) { 27 | if (input.isEmpty()) 28 | continue 29 | 30 | val list = mutableListOf() 31 | val m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(input) 32 | while (m.find()) 33 | list.add(m.group(1).replace("\"", "")) 34 | 35 | Tickompiler.createAndParseCommandLine(TickompilerCommand(), *list.toTypedArray()) 36 | 37 | println("--------------------------------") 38 | 39 | input = readLine()?.trim() ?: return 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/DecompileCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import kotlinx.coroutines.Deferred 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.runBlocking 7 | import picocli.CommandLine 8 | import rhmodding.tickompiler.DSFunctions 9 | import rhmodding.tickompiler.MegamixFunctions 10 | import rhmodding.tickompiler.decompiler.CommentType 11 | import rhmodding.tickompiler.decompiler.Decompiler 12 | import rhmodding.tickompiler.util.getDirectories 13 | import java.io.File 14 | import java.io.FileOutputStream 15 | import java.nio.ByteOrder 16 | import java.nio.charset.Charset 17 | import java.nio.file.Files 18 | 19 | 20 | @CommandLine.Command(name = "decompile", aliases = ["d"], description = ["Decompile file(s) and output them to the file/directory specified.", 21 | "Files must be with the file extension .bin (little-endian)", 22 | "Files will be overwritten without warning.", 23 | "If the output is not specified, the file will be a .tickflow file with the same name."], 24 | mixinStandardHelpOptions = true) 25 | class DecompileCommand : Runnable { 26 | 27 | @CommandLine.Option(names = ["-c"], description = ["Continue even with errors."]) 28 | var continueWithErrors: Boolean = false 29 | 30 | @CommandLine.Option(names = ["-nc", "--no-comments"], description = ["Don't include comments."]) 31 | var noComments: Boolean = false 32 | 33 | @CommandLine.Option(names = ["--bytecode"], description = ["Have a comment with the bytecode (overridden by --no-comments)."]) 34 | var showBytecode: Boolean = false 35 | 36 | @CommandLine.Option(names = ["-nm", "--no-metadata"], description = ["No metadata (use when decompiling snippets instead of full files)."]) 37 | var noMetadata: Boolean = false 38 | 39 | @CommandLine.Option(names = ["-m", "--megamix"], description = ["Decompile with Megamix functions. (default true)"]) 40 | var megamixFunctions: Boolean = true 41 | 42 | @CommandLine.Option(names = ["--ds"], description = ["Decompile with RHDS functions (also disables Megamix-specific metadata)"]) 43 | var dsFunctions: Boolean = false 44 | 45 | @CommandLine.Parameters(index = "0", arity = "1", description = ["Input file or directory."]) 46 | lateinit var inputFile: File 47 | 48 | @CommandLine.Parameters(index = "1", arity = "0..1", description = ["Output file or directory."]) 49 | var outputFile: File? = null 50 | 51 | override fun run() { 52 | val nanoStart = System.nanoTime() 53 | val dirs = getDirectories(inputFile, outputFile, { s -> s.endsWith(".bin") }, "tickflow") 54 | val functions = when { 55 | dsFunctions -> DSFunctions 56 | megamixFunctions -> MegamixFunctions 57 | else -> MegamixFunctions 58 | } 59 | 60 | val coroutines: MutableList> = mutableListOf() 61 | 62 | println("Decompiling ${dirs.input.size} file(s)") 63 | dirs.input.forEachIndexed { index, file -> 64 | coroutines += GlobalScope.async { 65 | val decompiler = Decompiler(Files.readAllBytes(file.toPath()), 66 | ByteOrder.LITTLE_ENDIAN, functions) 67 | 68 | try { 69 | println("Decompiling ${file.path}") 70 | val result = decompiler.decompile(when { 71 | noComments -> CommentType.NONE 72 | showBytecode -> CommentType.BYTECODE 73 | else -> CommentType.NORMAL 74 | }, !noMetadata && functions == MegamixFunctions) 75 | 76 | dirs.output[index].createNewFile() 77 | val fos = FileOutputStream(dirs.output[index]) 78 | fos.write(result.second.toByteArray(Charset.forName("UTF-8"))) 79 | fos.close() 80 | 81 | println("Decompiled ${file.path} -> ${result.first} ms") 82 | return@async true 83 | } catch (e: RuntimeException) { 84 | if (continueWithErrors) { 85 | println("FAILED to decompile ${file.path}") 86 | e.printStackTrace() 87 | } else { 88 | throw e 89 | } 90 | } 91 | 92 | return@async true 93 | } 94 | } 95 | 96 | runBlocking { 97 | val successful = coroutines 98 | .map { it.await() } 99 | .count { it } 100 | 101 | println(""" 102 | +========================+ 103 | | DECOMPILATION COMPLETE | 104 | +========================+ 105 | $successful / ${dirs.input.size} decompiled successfully in ${(System.nanoTime() - nanoStart) / 1_000_000.0} ms 106 | """) 107 | } 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/ExtractCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import picocli.CommandLine 4 | import rhmodding.tickompiler.MegamixFunctions 5 | import rhmodding.tickompiler.decompiler.CommentType 6 | import rhmodding.tickompiler.decompiler.Decompiler 7 | import rhmodding.tickompiler.gameextractor.* 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | import java.nio.ByteBuffer 11 | import java.nio.ByteOrder 12 | import java.nio.charset.Charset 13 | import java.nio.file.Files 14 | 15 | @CommandLine.Command(name = "extract", aliases = ["e"], description = ["Extract binary files from a decrypted code.bin file and output them to the directory specified.", 16 | "File must be with the file's extension .bin (little-endian)", 17 | "Files will be overwritten without warning.", 18 | "If the output is not specified, the directory will have the same name as the file.", 19 | "A base.bin file will also be created in the same directory as the executable. This is a base C00.bin file."], 20 | mixinStandardHelpOptions = true) 21 | class ExtractCommand : Runnable { 22 | 23 | @CommandLine.Parameters(index = "0", arity = "1", description = ["code file"]) 24 | lateinit var codeFile: File 25 | 26 | @CommandLine.Parameters(index = "1", arity = "0..1", description = ["output dir"]) 27 | var outputDir: File? = null 28 | 29 | @CommandLine.Option(names = ["-a", "--all-subs"], description = ["Extract all subroutines of game engines, as opposed to only ones used by the games.", 30 | "Note that this potentially includes sequels and prequels to the game."]) 31 | var allSubs: Boolean = false 32 | 33 | @CommandLine.Option(names = ["-d", "--decompile"], description = ["Immediately decompile all extracted games, with enhanced features such as meaningful marker names.", 34 | "Will be extracted into a \"decompiled\" directory in the output directory."]) 35 | var decompileImmediately: Boolean = false 36 | 37 | @CommandLine.Option(names = ["-t", "--tempo"], description = ["Extract tempo files. These will be written as .tempo files in a \"tempo\" folder in the output directory."]) 38 | var extractTempo: Boolean = false 39 | 40 | override fun run() { 41 | val codebin = codeFile 42 | val codeBuffer = ByteBuffer.wrap(Files.readAllBytes(codebin.toPath())).order(ByteOrder.LITTLE_ENDIAN) 43 | val folder = outputDir ?: File(codebin.nameWithoutExtension) 44 | folder.mkdirs() 45 | val decompiledFolder = File(folder, "decompiled") 46 | if (decompileImmediately) { 47 | decompiledFolder.mkdirs() 48 | } 49 | for (i in 0 until 104) { 50 | println("Extracting ${codeBuffer.getName(i)}") 51 | val result = GameExtractor(allSubs).extractGame(codeBuffer, i) 52 | val ints = result.second 53 | val byteBuffer = ByteBuffer.allocate(ints.size * 4).order(ByteOrder.LITTLE_ENDIAN) 54 | val intBuf = byteBuffer.asIntBuffer() 55 | intBuf.put(ints.toIntArray()) 56 | val arr = ByteArray(ints.size * 4) 57 | byteBuffer[arr, 0, ints.size * 4] 58 | val fos = FileOutputStream(File(folder, codeBuffer.getName(i) + ".bin")) 59 | fos.write(arr) 60 | fos.close() 61 | if (decompileImmediately) { 62 | val decompiler = Decompiler(arr, ByteOrder.LITTLE_ENDIAN, MegamixFunctions) 63 | println("Decompiling ${codeBuffer.getName(i)}") 64 | val r = decompiler.decompile(CommentType.NORMAL, true, macros = result.first) 65 | val f = FileOutputStream(File(decompiledFolder, codeBuffer.getName(i) + ".tickflow")) 66 | f.write(r.second.toByteArray(Charset.forName("UTF-8"))) 67 | f.close() 68 | println("Decompiled ${codeBuffer.getName(i)} -> ${r.first} ms") 69 | } 70 | } 71 | for (i in 0 until 16) { 72 | println("Extracting ${codeBuffer.getGateName(i)}") 73 | val result = GameExtractor(allSubs).extractGateGame(codeBuffer, i) 74 | val ints = result.second 75 | val byteBuffer = ByteBuffer.allocate(ints.size * 4).order(ByteOrder.LITTLE_ENDIAN) 76 | val intBuf = byteBuffer.asIntBuffer() 77 | intBuf.put(ints.toIntArray()) 78 | val arr = ByteArray(ints.size * 4) 79 | byteBuffer[arr, 0, ints.size * 4] 80 | val fos = FileOutputStream(File(folder, codeBuffer.getGateName(i) + ".bin")) 81 | fos.write(arr) 82 | fos.close() 83 | if (decompileImmediately) { 84 | val decompiler = Decompiler(arr, ByteOrder.LITTLE_ENDIAN, MegamixFunctions) 85 | println("Decompiling ${codeBuffer.getGateName(i)}") 86 | val r = decompiler.decompile(CommentType.NORMAL, true, macros = result.first) 87 | val f = FileOutputStream(File(decompiledFolder, codeBuffer.getGateName(i) + ".tickflow")) 88 | f.write(r.second.toByteArray(Charset.forName("UTF-8"))) 89 | f.close() 90 | println("Decompiled ${codeBuffer.getGateName(i)} -> ${r.first} ms") 91 | } 92 | } 93 | if (extractTempo) { 94 | val tempoFolder = File(folder, "tempo") 95 | tempoFolder.mkdirs() 96 | for (i in 0 until 0x1DD) { 97 | val tempoPair = GameExtractor.extractTempo(codeBuffer, i) 98 | val f = FileOutputStream(File(tempoFolder, tempoPair.first + ".tempo")) 99 | f.write(tempoPair.second.toByteArray(Charset.forName("UTF-8"))) 100 | f.close() 101 | println("Extracted tempo file ${tempoPair.first}") 102 | } 103 | } 104 | val tableList = ByteArray(104 * 53) 105 | codeBuffer.position(TABLE_OFFSET - 0x100000) 106 | codeBuffer.get(tableList, 0, 104 * 53) 107 | val tempoList = ByteArray(16 * 0x1DD) 108 | codeBuffer.position(TEMPO_TABLE - 0x100000) 109 | codeBuffer.get(tempoList, 0, 16 * 0x1DD) 110 | val gateList = ByteArray(16 * 36 + 16 * 4) 111 | codeBuffer.position(GATE_TABLE - 0x100000) 112 | codeBuffer.get(gateList, 0, 16 * 36 + 16 * 4) 113 | val fos = FileOutputStream(File("base.bin")) 114 | fos.write(tableList) 115 | fos.write(tempoList) 116 | fos.write(gateList) 117 | fos.close() 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/GrabCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import picocli.CommandLine 4 | import rhmodding.tickompiler.MegamixFunctions 5 | import rhmodding.tickompiler.decompiler.CommentType 6 | import rhmodding.tickompiler.decompiler.Decompiler 7 | import rhmodding.tickompiler.gameextractor.GameExtractor 8 | import java.io.File 9 | import java.io.FileOutputStream 10 | import java.nio.ByteBuffer 11 | import java.nio.ByteOrder 12 | import java.nio.charset.Charset 13 | import java.nio.file.Files 14 | 15 | @CommandLine.Command(name = "grab", aliases = ["g"], description = ["Extract tickflow code from a specified location in the given code.bin file and output to the specified file.", 16 | "File must have the extension .bin.", 17 | "File will be overwritten without warning.", 18 | "Location is to be specified in hexadecimal without 0x prefix.", 19 | "If the output is not specified, the file will have the given location as name."], 20 | mixinStandardHelpOptions = true) 21 | class GrabCommand : Runnable { 22 | 23 | @CommandLine.Option(names = ["-d", "--decompile"], description = ["Immediately decompile into a .tickflow file with the same name as the output."]) 24 | var decompileImmediately: Boolean = false 25 | 26 | @CommandLine.Parameters(index = "0", arity = "1", description = ["code file"]) 27 | lateinit var codeFile: File 28 | 29 | @CommandLine.Parameters(index = "1", arity = "1", description = ["location"]) 30 | lateinit var location: String 31 | 32 | @CommandLine.Parameters(index = "2", arity = "0..1", description = ["output file"]) 33 | var outputFile: File? = null 34 | 35 | override fun run() { 36 | val codebin = codeFile 37 | val codeBuffer = ByteBuffer.wrap(Files.readAllBytes(codebin.toPath())).order(ByteOrder.LITTLE_ENDIAN) 38 | val location = location.toInt(16) 39 | val o = outputFile ?: File("$location.bin") 40 | val result = GameExtractor(false).extractArbitrary(codeBuffer, location) 41 | val byteBuffer = ByteBuffer.allocate(result.size * 4).order(ByteOrder.LITTLE_ENDIAN) 42 | val intBuf = byteBuffer.asIntBuffer() 43 | intBuf.put(result.toIntArray()) 44 | val arr = ByteArray(result.size * 4) 45 | byteBuffer[arr, 0, result.size * 4] 46 | val fos = FileOutputStream(o) 47 | fos.write(arr) 48 | fos.close() 49 | if (decompileImmediately) { 50 | val decompiler = Decompiler(arr, ByteOrder.LITTLE_ENDIAN, MegamixFunctions) 51 | println("Decompiling ${o.nameWithoutExtension}") 52 | val r = decompiler.decompile(CommentType.NORMAL, true) 53 | val f = FileOutputStream(File(o.absolutePath.dropLastWhile { it != File.separatorChar } + o.nameWithoutExtension + ".tickflow")) 54 | f.write(r.second.toByteArray(Charset.forName("UTF-8"))) 55 | f.close() 56 | println("Decompiled ${o.nameWithoutExtension} -> ${r.first} ms") 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/NotepadppLangCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import picocli.CommandLine 4 | import java.io.File 5 | 6 | 7 | @CommandLine.Command(name = "notepad++", description = ["Outputs a Notepad++-suitable custom user-defined language XML file. If the output directory is not specified, it will be placed next to this executable."], 8 | mixinStandardHelpOptions = true) 9 | class NotepadppLangCommand : Runnable { 10 | 11 | private val FILE_NAME = "tickflow.xml" 12 | 13 | @CommandLine.Option(names = ["-ow", "--overwrite"], description = ["Overwrite even if a file already exists."]) 14 | var overwrite: Boolean = false 15 | 16 | @CommandLine.Parameters(index = "0", arity = "0..1", description = ["output file or directory"]) 17 | var output: File = File("./") 18 | 19 | override fun run() { 20 | output.mkdirs() 21 | val file = if (output.isDirectory) output.resolve(FILE_NAME) else output 22 | if (file.exists() && !overwrite) { 23 | println("Cannot output ${file.name}, already exists in the target directory (${file.parentFile.absolutePath}). Please move, rename, or delete the file first.") 24 | } else { 25 | val internal = NotepadppLangCommand::class.java.getResource("/notepadplusplustickflowlang.xml") 26 | file.createNewFile() 27 | file.writeBytes(internal.readBytes()) 28 | println("Outputted ${file.name} to ${file.parentFile.canonicalPath}\nImport it into Notepad++ via: Language > Define your language... > Import") 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/PackCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import com.google.gson.Gson 4 | import picocli.CommandLine 5 | import rhmodding.tickompiler.gameputter.GamePutter 6 | import rhmodding.tickompiler.objectify.ManifestObj 7 | import rhmodding.tickompiler.objectify.TFOBJ_PACKER_VERSION 8 | import rhmodding.tickompiler.util.getDirectories 9 | import java.io.ByteArrayOutputStream 10 | import java.io.File 11 | import java.io.FileOutputStream 12 | import java.nio.ByteBuffer 13 | import java.nio.ByteOrder 14 | import java.nio.charset.Charset 15 | import java.nio.file.Files 16 | import java.util.zip.ZipFile 17 | 18 | @CommandLine.Command(name = "pack", aliases = ["p"], description = ["Pack binary, tempo, and/or tfobj files from a specified directory into the output file, using the specified base file.", 19 | "The base file can be obtained from extraction.", 20 | "Files must have the file extension .bin (little-endian), .tempo, or .tfobj.", 21 | "The output file will be overwritten without warning.", 22 | "If the output file name is not specified, it will default to \"C00.bin\"."], 23 | mixinStandardHelpOptions = true) 24 | class PackCommand : Runnable { 25 | 26 | @CommandLine.Parameters(index = "0", arity = "1", description = ["input directory"]) 27 | lateinit var inputFile: File 28 | 29 | @CommandLine.Parameters(index = "1", arity = "1", description = ["base file"]) 30 | lateinit var baseFile: File 31 | 32 | @CommandLine.Parameters(index = "2", arity = "0..1", description = ["output file"]) 33 | var outputFile: File? = null 34 | 35 | override fun run() { 36 | val dirs = getDirectories(inputFile, baseFile, { it.endsWith(".bin") || it.endsWith(".tempo") }, "", true).input 37 | val base = Files.readAllBytes(baseFile.toPath()) 38 | var index = base.size 39 | val baseBuffer = ByteBuffer.wrap(base).order(ByteOrder.LITTLE_ENDIAN) 40 | val out = ByteArrayOutputStream() 41 | val putter = GamePutter 42 | 43 | val lookIn = dirs.map { PackTarget(it, it.name) }.toMutableList() 44 | val tmpFiles = mutableListOf() 45 | 46 | val tfObjs: List = inputFile.listFiles { _, name -> name.endsWith(".tfobj") }?.toList() ?: listOf() 47 | if (tfObjs.isNotEmpty()) { 48 | println("Detected ${tfObjs.size} .tfobj files, extracting those first...") 49 | tfObjs.forEach { f -> 50 | println("\tExtracting from tfobj ${f.name}...") 51 | val zipFile = ZipFile(f) 52 | val manifestEntry = zipFile.getEntry("manifest.json") 53 | val manifest = Gson().fromJson(zipFile.getInputStream(manifestEntry).let { 54 | val res = it.readBytes().toString(Charsets.UTF_8) 55 | it.close() 56 | res 57 | }, ManifestObj::class.java) 58 | if (manifest.version <= 0) { 59 | error("${f.path} - Manifest version is invalid (${manifest.version})") 60 | } else if (manifest.version > TFOBJ_PACKER_VERSION) { 61 | error("${f.path} - Manifest version is too high (${manifest.version}, max $TFOBJ_PACKER_VERSION). Update Tickompiler by using the 'updates' command") 62 | } 63 | 64 | // Version 1 parsing 65 | if (manifest.version <= 1) { 66 | for (i in 0 until manifest.bin.size) { 67 | lookIn += PackTarget(File.createTempFile("Tickompiler_tmp-", ".bin").apply { 68 | zipFile.getInputStream(zipFile.getEntry("bin/bin_$i.bin")).also { stream -> 69 | val fos = FileOutputStream(this) 70 | stream.copyTo(fos) 71 | fos.close() 72 | stream.close() 73 | } 74 | deleteOnExit() 75 | tmpFiles += this 76 | }, "${f.name}[bin_$i.bin]") 77 | } 78 | for (i in 0 until manifest.tempo.size) { 79 | lookIn += PackTarget(File.createTempFile("Tickompiler_tmp-", ".tempo").apply { 80 | zipFile.getInputStream(zipFile.getEntry("tempo/tempo_$i.tempo")).also { stream -> 81 | val fos = FileOutputStream(this) 82 | stream.copyTo(fos) 83 | fos.close() 84 | stream.close() 85 | } 86 | deleteOnExit() 87 | tmpFiles += this 88 | }, "${f.name}[tempo_$i.tempo]") 89 | } 90 | } 91 | 92 | zipFile.close() 93 | } 94 | } 95 | 96 | for ((file, descriptor) in lookIn) { 97 | println("Packing $descriptor...") 98 | val contents = Files.readAllBytes(file.toPath()) 99 | val ints = if (file.path.endsWith(".bin")) { 100 | putter.putGame(baseBuffer, ByteBuffer.wrap(contents).order(ByteOrder.LITTLE_ENDIAN), index) 101 | } else { 102 | putter.putTempo(baseBuffer, contents.toString(Charset.forName("UTF-8")), index) 103 | } 104 | index += ints.size * 4 105 | val byteBuffer = ByteBuffer.allocate(ints.size * 4).order(ByteOrder.LITTLE_ENDIAN) 106 | val intBuf = byteBuffer.asIntBuffer() 107 | intBuf.put(ints.toIntArray()) 108 | val arr = ByteArray(ints.size * 4) 109 | byteBuffer[arr, 0, ints.size * 4] 110 | out.write(arr) 111 | } 112 | val file = outputFile ?: File("C00.bin") 113 | val fos = FileOutputStream(file) 114 | fos.write(baseBuffer.array()) 115 | fos.write(out.toByteArray()) 116 | fos.close() 117 | 118 | tmpFiles.forEach { it.delete() } 119 | 120 | println("Done.") 121 | } 122 | 123 | } 124 | 125 | data class PackTarget(val file: File, val descriptor: String) 126 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/cli/UpdatesCheckCommand.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.cli 2 | 3 | import com.google.gson.Gson 4 | import picocli.CommandLine 5 | import rhmodding.tickompiler.Tickompiler 6 | import rhmodding.tickompiler.Tickompiler.GITHUB 7 | import rhmodding.tickompiler.util.Version 8 | import java.net.HttpURLConnection 9 | import java.net.URL 10 | import java.time.LocalDateTime 11 | import java.time.ZoneId 12 | import java.time.ZonedDateTime 13 | import java.time.format.DateTimeFormatter 14 | 15 | 16 | @CommandLine.Command(name = "updates", description = ["Check GitHub for an update to Tickompiler."], 17 | mixinStandardHelpOptions = true) 18 | class UpdatesCheckCommand : Runnable { 19 | 20 | override fun run() { 21 | println("Checking for updates...") 22 | val path = URL("https://api.github.com/repos/${GITHUB.replace("https://github.com/", "")}/releases/latest") 23 | val con = path.openConnection() as HttpURLConnection 24 | con.requestMethod = "GET" 25 | con.setRequestProperty("Accept", "application/vnd.github.v3+json") 26 | if (con.responseCode != 200) { 27 | println("Failed to get version info: got non-200 response code (${con.responseCode} ${con.responseMessage})") 28 | } else { 29 | val inputStream = con.inputStream 30 | val bufferedReader = inputStream.bufferedReader() 31 | val content = bufferedReader.readText() 32 | bufferedReader.close() 33 | con.disconnect() 34 | val release = Gson().fromJson(content, Release::class.java) 35 | val releaseVersion: Version? = Version.fromStringOrNull(release.tag_name ?: "") 36 | if (releaseVersion == null) { 37 | println("Failed to get version info: release version is null? (${release.tag_name})") 38 | } else { 39 | if (releaseVersion > Tickompiler.VERSION) { 40 | println("A new ${if (release.prerelease) "PRE-RELEASE " else ""}version is available: $releaseVersion") 41 | val publishDate: LocalDateTime? = try { 42 | ZonedDateTime.parse(release.published_at, DateTimeFormatter.ISO_DATE_TIME)?.withZoneSameInstant(ZoneId.systemDefault())?.toLocalDateTime() 43 | } catch (ignored: Exception) { null } 44 | println("Published on ${publishDate?.format(DateTimeFormatter.ofPattern("dd MMMM yyyy, HH:mm:ss")) ?: release.published_at}") 45 | println(release.html_url) 46 | } else { 47 | println("No new version found.") 48 | } 49 | } 50 | } 51 | } 52 | 53 | @Suppress("PropertyName") 54 | class Release { 55 | var html_url: String? = "" 56 | var tag_name: String? = "" 57 | var name: String? = "" 58 | var published_at: String? = "" 59 | var prerelease: Boolean = false 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/compiler/Compiler.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.compiler 2 | 3 | import org.parboiled.Parboiled 4 | import org.parboiled.parserunners.RecoveringParseRunner 5 | import rhmodding.tickompiler.* 6 | import rhmodding.tickompiler.Function 7 | import java.io.File 8 | import java.nio.ByteBuffer 9 | import java.nio.ByteOrder 10 | import java.nio.charset.Charset 11 | 12 | class Compiler(val tickflow: String, val functions: Functions) { 13 | private var hasStartedTiming = false 14 | private var startNanoTime: Long = 0 15 | set(value) { 16 | if (!hasStartedTiming) { 17 | field = value 18 | hasStartedTiming = true 19 | } 20 | } 21 | 22 | enum class VariableType { 23 | VARIABLE, 24 | MARKER, 25 | STRING 26 | } 27 | 28 | fun unicodeStringToInts(str: String, ordering: ByteOrder): List { 29 | val result = mutableListOf() 30 | var i = 0 31 | while (i <= str.length) { 32 | var int = 0 33 | if (i < str.length) 34 | int += str[i].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 16 else 0) 35 | if (i + 1 < str.length) 36 | int += str[i + 1].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 0 else 16) 37 | i += 2 38 | result.add(int.toLong()) 39 | } 40 | return result 41 | } 42 | 43 | fun stringToInts(str: String, ordering: ByteOrder): List { 44 | val result = mutableListOf() 45 | var i = 0 46 | while (i <= str.length) { 47 | var int = 0 48 | if (i < str.length) 49 | int += str[i].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 24 else 0) 50 | if (i + 1 < str.length) 51 | int += str[i + 1].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 16 else 8) 52 | if (i + 2 < str.length) 53 | int += str[i + 2].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 8 else 16) 54 | if (i + 3 < str.length) 55 | int += str[i + 3].toByte().toInt() shl (if (ordering == ByteOrder.BIG_ENDIAN) 0 else 24) 56 | i += 4 57 | result.add(int.toLong()) 58 | } 59 | return result 60 | } 61 | 62 | fun compileStatement(statement: Any, longs: MutableList, variables: MutableMap>) { 63 | when (statement) { 64 | is FunctionCallNode -> { 65 | val argAnnotations = mutableListOf>() 66 | val funcCall = FunctionCall(statement.func, 67 | statement.special?.getValue(variables) ?: 0, 68 | statement.args.mapIndexed { index, it -> 69 | if (it.type == ExpType.VARIABLE && variables[it.id as String]?.second == VariableType.MARKER) { 70 | argAnnotations.add(Pair(index, 0)) 71 | } 72 | if (it.type == ExpType.USTRING) { 73 | argAnnotations.add(Pair(index, 1)) 74 | } 75 | if (it.type == ExpType.STRING) { 76 | argAnnotations.add(Pair(index, 2)) 77 | } 78 | it.getValue(variables) 79 | }) 80 | if (statement.func == "bytes") { 81 | argAnnotations.add(Pair(statement.args.size, 3)) 82 | } 83 | 84 | val function: Function = functions[funcCall.func] 85 | 86 | if (argAnnotations.size > 0) { 87 | if (function is SpecialOnlyFunction) { 88 | throw CompilerError("Argument annotations on SpecialOnlyFunction - are you using goto loc?") 89 | } 90 | 91 | longs.add(0xFFFFFFFF) 92 | longs.add(argAnnotations.size.toLong()) 93 | argAnnotations.forEach { 94 | longs.add((it.second + (it.first shl 8)).toLong()) 95 | } 96 | } 97 | 98 | if (function::class.java.isAnnotationPresent(DeprecatedFunction::class.java)) { 99 | println("DEPRECATION WARNING at ${statement.position.line}:${statement.position.column} -> " + 100 | function::class.java.annotations.filterIsInstance().first().value) 101 | } 102 | 103 | function.checkArgsNeeded(funcCall) 104 | function.produceBytecode(funcCall).forEach { longs.add(it) } 105 | } 106 | is VarAssignNode -> { 107 | variables[statement.variable] = statement.expr.getValue(variables) to VariableType.VARIABLE 108 | } 109 | /*is LoopNode -> { 110 | (1..statement.expr.getValue(variables)).forEach { 111 | statement.statements.forEach { 112 | compileStatement(it, longs, variables) 113 | } 114 | } 115 | }*/ 116 | } 117 | } 118 | 119 | constructor(file: File) : this(preProcess(file), 120 | MegamixFunctions) 121 | 122 | constructor(file: File, functions: Functions) : this( 123 | preProcess(file), functions) 124 | 125 | 126 | fun compile(endianness: ByteOrder): CompileResult { 127 | startNanoTime = System.nanoTime() 128 | 129 | // Split tickflow into lines, stripping comments 130 | val commentLess = tickflow.lines().joinToString("\n") { 131 | it.replaceAfter("//", "").replace("//", "").trim() 132 | } 133 | 134 | val parser = Parboiled.createParser(TickflowParser::class.java) 135 | val result = RecoveringParseRunner(parser.TickflowCode()).run(commentLess) 136 | 137 | // println(ParseTreeUtils.printNodeTree(result)) 138 | 139 | val longs: MutableList = mutableListOf() 140 | val variables: MutableMap> = mutableMapOf() 141 | 142 | // result.valueStack.reversed().forEach(::println) 143 | var counter = 0L 144 | val startMetadata = MutableList(3) { 0 } 145 | var hasMetadata = false 146 | val ustrings = mutableListOf() 147 | val strings = mutableListOf() 148 | result.valueStack.reversed().forEach { 149 | when (it) { 150 | is AliasAssignNode -> functions[it.expr.getValue(variables)] = it.alias 151 | is FunctionCallNode -> { 152 | val funcCall = FunctionCall(it.func, 0, 153 | it.args.map { 154 | if (it.type == ExpType.STRING) { 155 | strings.add(it.string as String) 156 | } 157 | if (it.type == ExpType.USTRING) { 158 | ustrings.add(it.string as String) 159 | } 160 | 0L 161 | }) 162 | val function: Function = functions[funcCall.func] 163 | val len = function.produceBytecode(funcCall).size 164 | counter += len * 4 165 | } 166 | is MarkerNode -> { 167 | variables[it.name] = counter to VariableType.MARKER 168 | if (it.name == "start") { 169 | startMetadata[1] = counter 170 | } 171 | if (it.name == "assets") { 172 | startMetadata[2] = counter 173 | } 174 | } 175 | is DirectiveNode -> { 176 | hasMetadata = true 177 | when (it.name) { 178 | "index" -> startMetadata[0] = it.num 179 | "start" -> startMetadata[1] = it.num 180 | "assets" -> startMetadata[2] = it.num 181 | } 182 | } 183 | } 184 | } 185 | 186 | ustrings.forEach { 187 | variables[it] = counter to VariableType.STRING 188 | counter += unicodeStringToInts(it, endianness).size * 4 189 | } 190 | strings.forEach { 191 | variables[it] = counter to VariableType.STRING 192 | counter += stringToInts(it, endianness).size * 4 193 | } 194 | 195 | result.valueStack.reversed().forEach { 196 | compileStatement(it, longs, variables) 197 | } 198 | longs.add(0xFFFFFFFE) 199 | ustrings.forEach { 200 | longs.addAll(unicodeStringToInts(it, endianness)) 201 | } 202 | strings.forEach { 203 | longs.addAll(stringToInts(it, endianness)) 204 | } 205 | val buffer = ByteBuffer.allocate(longs.size * 4 + (if (hasMetadata) 12 else 0)) 206 | buffer.order(endianness) 207 | if (hasMetadata) { 208 | startMetadata.forEach { buffer.putInt(it.toInt()) } 209 | } 210 | longs.forEach { buffer.putInt(it.toInt()) } 211 | 212 | return CompileResult(result.matched, 213 | (System.nanoTime() - startNanoTime) / 1_000_000.0, buffer) 214 | } 215 | 216 | } 217 | 218 | private fun preProcess(file: File): String { 219 | val tickflow = file.readText(Charset.forName("UTF-8")) 220 | val newTickflow = tickflow.lines().joinToString("\n") { 221 | if (it.startsWith("#include")) { 222 | val filename = it.split(" ")[1] 223 | val otherfile = File(file.parentFile, filename) 224 | if (otherfile.exists() && otherfile.isFile) { 225 | otherfile.readText(Charset.forName("UTF-8")) 226 | } else { 227 | throw CompilerError("Included file $filename not found.") 228 | } 229 | } else it 230 | } 231 | return newTickflow 232 | } 233 | 234 | data class CompileResult(val success: Boolean, val timeMs: Double, val data: ByteBuffer) 235 | 236 | data class FunctionCall(val func: String, val specialArg: Long, val args: List) 237 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/compiler/Parboiled.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.compiler 2 | 3 | import org.parboiled.Action 4 | import org.parboiled.BaseParser 5 | import org.parboiled.Rule 6 | import org.parboiled.annotations.BuildParseTree 7 | import org.parboiled.annotations.SuppressNode 8 | import org.parboiled.annotations.SuppressSubnodes 9 | import org.parboiled.support.Position 10 | import org.parboiled.support.StringVar 11 | import org.parboiled.support.Var 12 | import org.parboiled.trees.ImmutableBinaryTreeNode 13 | import org.parboiled.trees.ImmutableTreeNode 14 | import org.parboiled.trees.MutableTreeNodeImpl 15 | import org.parboiled.trees.TreeNode 16 | import rhmodding.tickompiler.CompilerError 17 | import rhmodding.tickompiler.util.unescape 18 | 19 | abstract class StatementNode>(val position: Position) : ImmutableTreeNode() 20 | 21 | class FunctionCallNode(position: Position, val func: String, val special: ExpressionNode?, 22 | val args: List) : StatementNode(position) { 23 | 24 | override fun toString(): String { 25 | return "[$func $special $args]" 26 | } 27 | 28 | } 29 | 30 | class VarAssignNode(position: Position, val variable: String, val expr: ExpressionNode) : StatementNode(position) { 31 | 32 | override fun toString(): String { 33 | return "$variable = $expr" 34 | } 35 | 36 | } 37 | 38 | class AliasAssignNode(position: Position, val alias: String, val expr: ExpressionNode) : StatementNode(position) { 39 | 40 | override fun toString(): String { 41 | return "#alias $alias $expr" 42 | } 43 | 44 | } 45 | 46 | class MarkerNode(position: Position, val name: String) : StatementNode(position) { 47 | override fun toString(): String { 48 | return "$name:" 49 | } 50 | } 51 | 52 | class DirectiveNode(position: Position, val name: String, val num: Long) : StatementNode(position) { 53 | override fun toString(): String { 54 | return "#$name 0x${num.toString(16).toUpperCase()}" 55 | } 56 | } 57 | 58 | class LoopNode(position: Position, val statements: List>, val expr: ExpressionNode) : StatementNode(position) { 59 | override fun toString(): String { 60 | var str = "Loop $expr times {\n" 61 | statements.forEach { 62 | str += it.toString().lines().joinToString("\n") { "\t" + it } + "\n" 63 | } 64 | str += "}" 65 | return str 66 | } 67 | } 68 | 69 | class StatementsNode(val position: Position, val list: MutableList> = mutableListOf()) : MutableTreeNodeImpl() 70 | 71 | class ArgsNode(val position: Position, val list: MutableList = mutableListOf()) : MutableTreeNodeImpl() 72 | 73 | enum class ExpType { 74 | OPERATION, 75 | NUMBER, 76 | VARIABLE, 77 | STRING, 78 | USTRING 79 | } 80 | 81 | class ExpressionNode constructor(val position: Position, rawop: String, left: ExpressionNode?, 82 | right: ExpressionNode?) : ImmutableBinaryTreeNode(left, right) { 83 | val op = rawop.replace("[ \\t\\n]".toRegex(), "") 84 | var id: String? = null 85 | var string: String? = null 86 | var num: Long? = null 87 | var type: ExpType = ExpType.OPERATION 88 | 89 | constructor(position: Position, str: String, type: ExpType = ExpType.VARIABLE) : this(position, "", null, null) { 90 | if (type == ExpType.VARIABLE) { 91 | this.id = str 92 | } else { 93 | this.string = str 94 | } 95 | this.type = type 96 | } 97 | 98 | constructor(position: Position, num: Long) : this(position, "", null, null) { 99 | this.num = num 100 | type = ExpType.NUMBER 101 | } 102 | 103 | fun getValue(variables: Map>): Long { 104 | if (num != null) { 105 | return num as Long 106 | } 107 | if (id != null) { 108 | return variables[id as String]?.first ?: throw CompilerError("Variable $id not initialized") 109 | } 110 | if (string != null) { 111 | return variables[string as String]?.first ?: throw CompilerError("String $string not properly handled. This should never happen.") 112 | } 113 | return when (op) { 114 | "+" -> left().getValue(variables) + right().getValue(variables) 115 | "-" -> left().getValue(variables) - right().getValue(variables) 116 | "*" -> left().getValue(variables) * right().getValue(variables) 117 | "/" -> { 118 | val rightVal = right().getValue(variables) 119 | if (rightVal == 0L) 120 | throw CompilerError("Division by 0") 121 | left().getValue(variables) / right().getValue(variables) 122 | } 123 | "<<" -> left().getValue(variables) shl right().getValue(variables).toInt() 124 | ">>" -> left().getValue(variables) ushr right().getValue(variables).toInt() 125 | "|" -> left().getValue(variables) or right().getValue(variables) 126 | "^" -> left().getValue(variables) xor right().getValue(variables) 127 | "&" -> left().getValue(variables) and right().getValue(variables) 128 | else -> throw CompilerError("This should never happen, please contact devs :(") 129 | } 130 | } 131 | } 132 | 133 | @BuildParseTree 134 | open class TickflowParser : BaseParser() { 135 | 136 | open fun TickflowCode(): Rule { 137 | return Sequence(OneOrMore(Statement()), EOI) 138 | } 139 | 140 | open fun Statement(): Rule { 141 | return Sequence(Optional(Whitespace()), 142 | Optional(Sequence(FirstOf( 143 | Directive(), 144 | Marker(), 145 | VariableAssignment(), 146 | AliasAssignment(), 147 | FunctionCall() 148 | ), Optional(Whitespace()))), 149 | AnyOf(";\n")) 150 | } 151 | 152 | open fun Marker(): Rule { 153 | return Sequence(VariableIdentifier(), Ch(':'), push(MarkerNode(position(), pop() as String))) 154 | } 155 | 156 | open fun Directive(): Rule { 157 | return Sequence(Ch('#'), VariableIdentifier(), 158 | Optional(Whitespace()), IntegerLiteral(), 159 | push(DirectiveNode(position(), pop(1) as String, 160 | (pop() as ExpressionNode).getValue( 161 | emptyMap()))) 162 | ) 163 | } 164 | 165 | open fun AliasAssignment(): Rule { 166 | return Sequence("#alias", Optional(Whitespace()), VariableIdentifier(), Optional(Whitespace()), Expression(), 167 | push(AliasAssignNode(position(), pop(1) as String, 168 | pop() as ExpressionNode))) 169 | } 170 | 171 | @SuppressNode 172 | open fun Whitespace(): Rule { 173 | return OneOrMore(AnyOf(" \t")) 174 | } 175 | 176 | @SuppressSubnodes 177 | open fun DecimalInteger(): Rule { 178 | return Sequence(OneOrMore(CharRange('0', '9')), 179 | push(Integer.parseUnsignedInt(match(), 10).toLong()) 180 | ) 181 | } 182 | 183 | @SuppressSubnodes 184 | open fun HexInteger(): Rule { 185 | return Sequence(String("0x"), 186 | OneOrMore(AnyOf("0123456789abcdefABCDEF")), 187 | push(Integer.parseUnsignedInt(match(), 16).toLong()) 188 | ) 189 | } 190 | 191 | @SuppressSubnodes 192 | open fun BinaryInteger(): Rule { 193 | return Sequence(String("0b"), 194 | OneOrMore(AnyOf("01")), 195 | push(Integer.parseUnsignedInt(match(), 2).toLong()) 196 | ) 197 | } 198 | 199 | @SuppressSubnodes 200 | open fun IntegerLiteral(): Rule { 201 | return FirstOf( 202 | Sequence(FirstOf(HexInteger(), BinaryInteger(), DecimalInteger()), push( 203 | ExpressionNode(position(), pop() as Long))), 204 | Sequence('-', FirstOf(HexInteger(), BinaryInteger(), DecimalInteger()), 205 | push(ExpressionNode(position(), -(pop() as Long))))) 206 | } 207 | 208 | @SuppressSubnodes 209 | open fun SpecialArgument(expr: Var): Rule { 210 | return Sequence(Ch('<'), 211 | Expression(), 212 | Ch('>'), 213 | expr.set(pop() as ExpressionNode)) 214 | } 215 | 216 | @SuppressSubnodes 217 | open fun VariableIdentifier(): Rule { 218 | val name = StringVar() 219 | return Sequence( 220 | AnyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$"), 221 | name.append(match()), 222 | ZeroOrMore(AnyOf("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$")), 223 | name.append(match()), 224 | push(name.get())) 225 | } 226 | 227 | open fun VariableReference(): Rule { 228 | return Sequence( 229 | VariableIdentifier(), 230 | push(ExpressionNode(position(), pop() as String)) 231 | ) 232 | } 233 | 234 | open fun StringContents(): Rule { 235 | val name = StringVar() 236 | return Sequence( 237 | ZeroOrMore(NoneOf("\\\"")), 238 | name.append(match()), 239 | ZeroOrMore("\\", FirstOf("\"", "\\"), ZeroOrMore(NoneOf("\\\""))), 240 | Action {name.append(match().unescape())}, 241 | push(name.get()) 242 | ) 243 | } 244 | 245 | open fun DoubleQuoteString(): Rule { 246 | return Sequence('"', StringContents(), '"', 247 | push(ExpressionNode(position(), pop() as String, ExpType.STRING))) 248 | } 249 | 250 | open fun UnicodeString(): Rule { 251 | return Sequence('u', '"', StringContents(), '"', 252 | push(ExpressionNode(position(), pop() as String, ExpType.USTRING))) 253 | } 254 | 255 | open fun VariableAssignment(): Rule { 256 | return Sequence(VariableIdentifier(), 257 | Optional(Whitespace()).suppressNode(), 258 | Ch('='), 259 | Optional(Whitespace()).suppressNode(), 260 | Expression(), 261 | push(VarAssignNode(position(), pop(1) as String, 262 | pop() as ExpressionNode))) 263 | } 264 | 265 | @SuppressSubnodes 266 | open fun FunctionIdentifier(name: StringVar): Rule { 267 | return Sequence(FirstOf(IntegerLiteral(), VariableIdentifier()), 268 | name.append(match())) 269 | } 270 | 271 | open fun Argument(): Rule { 272 | return Sequence( 273 | Expression(), 274 | (peek(1) as ArgsNode).list.add(pop() as ExpressionNode) 275 | ) 276 | } 277 | 278 | open fun FunctionArgs(): Rule { 279 | return Sequence(push(ArgsNode(position())), 280 | Argument(), 281 | ZeroOrMore( 282 | Sequence( 283 | Optional(Whitespace()), 284 | Ch(',').suppressNode(), 285 | Optional(Whitespace()), 286 | Argument() 287 | ) 288 | ), 289 | push((pop() as ArgsNode).list.toList())) 290 | } 291 | 292 | open fun FunctionCall(): Rule { 293 | val name = StringVar("") 294 | val special = Var() 295 | 296 | return Sequence(FunctionIdentifier(name), 297 | Optional(SpecialArgument(special)), 298 | push(listOf()), 299 | Optional(// more args 300 | FirstOf( 301 | Sequence(OneOrMore(Whitespace()).suppressNode(), FunctionArgs()), 302 | Sequence( 303 | Optional(Whitespace()).suppressNode(), 304 | Ch('(').suppressNode(), 305 | Optional(Whitespace()).suppressNode(), 306 | FunctionArgs(), 307 | Ch(')').suppressNode() 308 | ) 309 | ), 310 | Action { pop(1); true } 311 | ), 312 | push(FunctionCallNode(position(), name.get(), special.get(), 313 | pop() as List)) 314 | ) 315 | } 316 | 317 | open fun OperatorRule(subRule: Rule, operatorRule: Rule): Rule { 318 | val op = Var() 319 | return Sequence(subRule, 320 | ZeroOrMore( 321 | Sequence( 322 | Sequence(Optional(Whitespace()).suppressNode(), operatorRule, 323 | Optional(Whitespace()).suppressNode()), 324 | op.set(match()), 325 | subRule, 326 | push(ExpressionNode(position(), op.get(), 327 | pop(1) as ExpressionNode, 328 | pop() as ExpressionNode)) 329 | ) 330 | ) 331 | ) 332 | } 333 | 334 | open fun BitwiseOp(): Rule { 335 | return FirstOf("&", "|", "^", "<<", ">>") 336 | } 337 | 338 | open fun AddOp(): Rule { 339 | return FirstOf("+", "-") 340 | } 341 | 342 | open fun MultOp(): Rule { 343 | return FirstOf("*", "/") 344 | } 345 | 346 | open fun Expression(): Rule { 347 | return OperatorRule(BitwiseTerm(), BitwiseOp()) 348 | } 349 | 350 | open fun BitwiseTerm(): Rule { 351 | return OperatorRule(AddTerm(), AddOp()) 352 | } 353 | 354 | open fun AddTerm(): Rule { 355 | return OperatorRule(Factor(), MultOp()) 356 | } 357 | 358 | open fun Factor(): Rule { 359 | return FirstOf(IntegerLiteral(), UnicodeString(), VariableReference(), DoubleQuoteString(), Sequence( 360 | Ch('('), 361 | Expression(), 362 | Ch(')') 363 | ) 364 | ) 365 | } 366 | 367 | open fun ListAppendStatement(): Rule { 368 | return Sequence(Statement(), (peek(1) as StatementsNode).list.add(pop() as StatementNode<*>)) 369 | } 370 | 371 | open fun Statements(): Rule { 372 | return Sequence( 373 | push(StatementsNode(position())), 374 | OneOrMore(ListAppendStatement()), 375 | push((pop() as StatementsNode).list) 376 | ) 377 | } 378 | 379 | open fun Loop(): Rule { 380 | return Sequence( 381 | Expression(), 382 | Optional(Whitespace()).suppressNode(), 383 | "times", 384 | Optional(Whitespace()).suppressNode(), 385 | Ch('{'), 386 | Optional(Whitespace()).suppressNode(), 387 | Optional(Ch('\n')), 388 | Statements(), 389 | Ch('}'), 390 | push(LoopNode(position(), pop() as List>, 391 | pop() as ExpressionNode)) 392 | ) 393 | } 394 | 395 | } 396 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/decompiler/Decompiler.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.decompiler 2 | 3 | import rhmodding.tickompiler.BytesFunction 4 | import rhmodding.tickompiler.Function 5 | import rhmodding.tickompiler.Functions 6 | import rhmodding.tickompiler.Tickompiler.GITHUB 7 | import rhmodding.tickompiler.Tickompiler.VERSION 8 | import rhmodding.tickompiler.util.escape 9 | import java.io.ByteArrayInputStream 10 | import java.nio.ByteOrder 11 | import java.util.* 12 | import kotlin.math.max 13 | 14 | class Decompiler(val array: ByteArray, val order: ByteOrder, val functions: Functions) { 15 | var input = ByteArrayInputStream(array) 16 | 17 | private fun read(): Long { 18 | val r = input.read() 19 | if (r == -1) 20 | throw IllegalStateException("End of stream reached") 21 | return r.toLong() 22 | } 23 | 24 | private fun readInt(): Long { 25 | return if (order == ByteOrder.LITTLE_ENDIAN) { 26 | read() or (read() shl 8) or (read() shl 16) or (read() shl 24) 27 | } else { 28 | (read() shl 24) or (read() shl 16) or (read() shl 8) or read() 29 | } 30 | } 31 | 32 | private fun readStringAuto(): Pair { 33 | var result = "" 34 | var i = 2 35 | var r = read() 36 | if (r == 0L) { 37 | return "" to 1 38 | } 39 | var u = false 40 | result += r.toChar() 41 | r = read() 42 | if (r == 0L) { 43 | u = true 44 | r = read() 45 | read() 46 | i += 2 47 | } 48 | while (r != 0L) { 49 | result += r.toChar() 50 | r = read() 51 | i++ 52 | if (u) { 53 | read() 54 | i++ 55 | } 56 | } 57 | while (i % 4 != 0) { 58 | read() 59 | i++ 60 | } 61 | return Pair(result, i) 62 | } 63 | 64 | fun decompile(addComments: CommentType, useMetadata: Boolean, indent: String = "\t", macros: Map = mapOf()): Pair { 65 | val nanoTime = System.nanoTime() 66 | val builder = StringBuilder() 67 | val state = DecompilerState() 68 | 69 | run decompilerInfo@ { 70 | builder.append("// Decompiled using Tickompiler $VERSION\n// $GITHUB\n") 71 | } 72 | val markers = mutableMapOf() 73 | 74 | if (useMetadata) { 75 | builder.append("#index 0x${readInt().toString(16).toUpperCase()}\n") 76 | markers[readInt()] = "start" 77 | markers[readInt()] = "assets" 78 | } 79 | 80 | for ((key, value) in macros) { 81 | markers[key.toLong()] = "sub${value.toString(16).toUpperCase()}" 82 | } 83 | var markerC = 0 84 | val strings = mutableMapOf() 85 | 86 | var counter = 0L 87 | // first pass is to construct a list of markers: 88 | while (input.available() > 0) { 89 | val anns = mutableListOf() 90 | var opint: Long = readInt() 91 | if (opint == 0xFFFFFFFE) { 92 | break 93 | } 94 | if (opint == 0xFFFFFFFF) { 95 | val amount = readInt() 96 | for (i in 1..amount) { 97 | anns.add(readInt()) 98 | } 99 | opint = readInt() 100 | } 101 | val opcode: Long = opint and 0b1111111111 102 | val special: Long = (opint ushr 14) 103 | val argCount: Long = (opint ushr 10) and 0b1111 104 | val args: MutableList = mutableListOf() 105 | 106 | if (argCount > 0) { 107 | for (i in 1..argCount) { 108 | args.add(readInt()) 109 | } 110 | } 111 | 112 | anns.forEach { 113 | val anncode = it and 0xFF 114 | val annArg = (it ushr 8).toInt() 115 | if (anncode == 0L) { 116 | if (!markers.contains(args[annArg])) { 117 | markers[args[annArg]] = "loc${markerC++}" 118 | } 119 | } 120 | } 121 | 122 | counter += 4 * (1 + argCount) 123 | } 124 | 125 | // and also strings 126 | while (input.available() > 0) { 127 | val p = readStringAuto() 128 | strings[counter] = p.first.escape() 129 | counter += p.second 130 | } 131 | 132 | // reset input stream for the second pass: 133 | input = ByteArrayInputStream(array) 134 | if (useMetadata) { 135 | input.skip(12) 136 | } 137 | 138 | counter = 0L 139 | 140 | while (input.available() > 0) { 141 | if (markers.contains(counter)) { 142 | builder.append("${markers[counter]}:\n") 143 | } 144 | 145 | val specialArgStrings: MutableMap = mutableMapOf() 146 | 147 | val anns = mutableListOf() 148 | var opint: Long = readInt() 149 | if (opint == 0xFFFFFFFE) { 150 | break 151 | } 152 | if (opint == 0xFFFFFFFF) { 153 | val amount = readInt() 154 | for (i in 1..amount) { 155 | anns.add(readInt()) 156 | } 157 | } 158 | 159 | var bytes = 0 160 | anns.forEach { 161 | if ((it and 0xFF) == 3L) { 162 | bytes = (it ushr 8).toInt() 163 | } 164 | } 165 | if (bytes > 0) { 166 | val padding = 4 - (bytes % 4) - (if (bytes % 4 == 0) 4 else 0) 167 | counter += bytes + padding 168 | val byteList = mutableListOf() 169 | for (i in 1..bytes) { 170 | byteList.add(read()) 171 | } 172 | for (i in 1..padding) { 173 | read() 174 | } 175 | val function: Function = BytesFunction() 176 | val tickflow = function.produceTickflow(state, 0, 0, byteList.toLongArray(), addComments, specialArgStrings) 177 | for (i in 1..state.nextIndentLevel) { 178 | builder.append(indent) 179 | } 180 | builder.append(tickflow + "\n") 181 | continue 182 | } 183 | 184 | if (opint == 0xFFFFFFFF) { 185 | opint = readInt() 186 | } 187 | val opcode: Long = opint and 0b1111111111 188 | val special: Long = (opint ushr 14) 189 | val argCount: Long = (opint ushr 10) and 0b1111 190 | val function: Function = functions[opint] 191 | 192 | val args: MutableList = mutableListOf() 193 | 194 | if (argCount > 0) { 195 | for (i in 1..argCount) { 196 | args.add(readInt()) 197 | } 198 | } 199 | 200 | anns.forEach { 201 | val anncode = it and 0b11111111 202 | val annArg = (it ushr 8).toInt() 203 | when(anncode) { 204 | 0L -> specialArgStrings[annArg] = markers[args[annArg]] ?: args[annArg].toString() 205 | 1L -> specialArgStrings[annArg] = "u\"" + (strings[args[annArg]] ?: "") + '"' 206 | 2L -> specialArgStrings[annArg] = '"' + (strings[args[annArg]] ?: "") + '"' 207 | } 208 | } 209 | 210 | val oldIndent = state.nextIndentLevel 211 | val tickFlow = function.produceTickflow(state, opcode, special, args.toLongArray(), addComments, 212 | specialArgStrings) 213 | 214 | for (i in 1..(oldIndent + state.currentAdjust)) { 215 | builder.append(indent) 216 | } 217 | 218 | builder.append(tickFlow) 219 | if (addComments == CommentType.BYTECODE) { 220 | fun Int.toLittleEndianHex(): String { 221 | return toString(16).padStart(8, '0').toUpperCase(Locale.ROOT) 222 | } 223 | 224 | builder.append(" // bytecode: ${opint.toInt().toLittleEndianHex()} ${args.joinToString(" "){it.toInt().toLittleEndianHex()}}") 225 | } 226 | builder.append('\n') 227 | state.currentAdjust = 0 228 | state.nextIndentLevel = max(state.nextIndentLevel, 0) 229 | counter += 4 * (1 + argCount) 230 | } 231 | 232 | return ((System.nanoTime() - nanoTime) / 1_000_000.0) to builder.toString() 233 | } 234 | 235 | } 236 | 237 | data class DecompilerState(var nextIndentLevel: Int = 0, var currentAdjust: Int = 0) 238 | 239 | enum class CommentType { 240 | NONE, NORMAL, BYTECODE 241 | } 242 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/gameextractor/GameExtractor.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.gameextractor 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | import java.nio.ByteBuffer 6 | import java.util.* 7 | import kotlin.math.pow 8 | import kotlin.math.roundToLong 9 | 10 | 11 | class GameExtractor(val allSubs: Boolean) { 12 | 13 | private var engine = -1 14 | private var isRemix = false 15 | private var ustrings = mutableListOf>() 16 | private var astrings = mutableListOf>() 17 | 18 | private val USTRING_OPS = mutableMapOf( 19 | 0x31 to 0 to arrayOf(1), 20 | 0x35 to 0 to arrayOf(1), 21 | 0x39 to 0 to arrayOf(1), 22 | 0x3E to 0 to arrayOf(1), 23 | 0x5D to 0 to arrayOf(1), 24 | 0x5D to 2 to arrayOf(0), 25 | 0x61 to 2 to arrayOf(0) 26 | ) 27 | 28 | private val ASTRING_OPS = mutableMapOf( 29 | 0x3B to 0 to arrayOf(2), 30 | 0x67 to 1 to arrayOf(1), 31 | 0x93 to 0 to arrayOf(2, 3), 32 | 0x94 to 0 to arrayOf(1, 2, 3), 33 | 0x95 to 0 to arrayOf(1), 34 | 0xB0 to 4 to arrayOf(1), 35 | 0xB0 to 5 to arrayOf(1), 36 | 0xB0 to 6 to arrayOf(1), 37 | 0x66 to 0 to arrayOf(1), 38 | 0x65 to 1 to arrayOf(1), 39 | 0x68 to 1 to arrayOf(1), 40 | 0xAF to 2 to arrayOf(2), 41 | 0xB5 to 0 to arrayOf(0) 42 | ) 43 | 44 | companion object { 45 | val LOCATIONS: List> by lazy { 46 | Gson().fromJson>>(GameExtractor::class.java.getResource("/locations.json").readText(), object : TypeToken>>(){}.type) 47 | } 48 | 49 | private const val TEMPO_TABLE = 0x53EF54 50 | private const val DECIMALS: Int = 3 51 | 52 | private fun correctlyRoundDouble(value: Double, places: Int): String { 53 | if (places < 0) 54 | error("Places $places cannot be negative") 55 | if (places == 0) 56 | return "$value" 57 | val long: Long = (value * 10.0.pow(places.toDouble())).roundToLong() 58 | val longString = long.toString() 59 | 60 | val str = longString.substring(0, longString.length - places) + "." + longString.substring(longString.length - places).trimEnd('0') 61 | 62 | return if (str.endsWith('.')) "${str}0" else str 63 | } 64 | 65 | fun extractTempo(buffer: ByteBuffer, index: Int): Pair { 66 | val ids = mutableListOf(buffer.getIntAdj(TEMPO_TABLE + 16 * index), 67 | buffer.getIntAdj(TEMPO_TABLE + 16 * index + 4)) 68 | .filter { it != -1 } 69 | val name = (if (ids[0] != -1) ids[0] else ids[1]).toString(16) 70 | var s: String = ids.joinToString(" ") { it.toString(16) } + "\n" 71 | var addr = buffer.getIntAdj(TEMPO_TABLE + 16 * index + 12) 72 | while (true) { 73 | val beats = buffer.getFloat(addr - 0x100000) 74 | val seconds = buffer.getInt(addr - 0x100000 + 4) / 32000.0 // will not work with unsigned but not important 75 | val bpm = 60 * beats / seconds 76 | val loop = buffer.getInt(addr - 0x100000 + 8) 77 | s += "${correctlyRoundDouble(bpm, DECIMALS)} ${correctlyRoundDouble(beats.toDouble(), DECIMALS)} ${loop}\n" 78 | if (buffer.getIntAdj(addr + 8) != 0) 79 | break 80 | addr += 12 81 | } 82 | return name.toUpperCase(Locale.ROOT) to s.toUpperCase(Locale.ROOT) 83 | } 84 | } 85 | 86 | fun unicodeStringToInts(str: String): List { 87 | val result = mutableListOf() 88 | var i = 0 89 | while (i <= str.length) { 90 | var int = 0 91 | if (i < str.length) 92 | int += str[i].toByte().toInt() shl 0 93 | if (i + 1 < str.length) 94 | int += str[i + 1].toByte().toInt() shl 16 95 | i += 2 96 | result.add(int) 97 | } 98 | return result 99 | } 100 | 101 | fun stringToInts(str: String): List { 102 | val result = mutableListOf() 103 | var i = 0 104 | while (i <= str.length) { 105 | var int = 0 106 | if (i < str.length) 107 | int += str[i].toByte().toInt() shl 0 108 | if (i + 1 < str.length) 109 | int += str[i + 1].toByte().toInt() shl 8 110 | if (i + 2 < str.length) 111 | int += str[i + 2].toByte().toInt() shl 16 112 | if (i + 3 < str.length) 113 | int += str[i + 3].toByte().toInt() shl 24 114 | i += 4 115 | result.add(int) 116 | } 117 | return result 118 | } 119 | 120 | fun extractGateGame(buffer: ByteBuffer, index: Int): Pair, List> { 121 | val start = buffer.getGateStart(index) 122 | engine = -1 123 | val funcs = firstPass(buffer, start, buffer.getIntAdj(GATE_TABLE + 36 * index + 8)) 124 | val sorted = listOf(funcs[0]) + funcs.drop(1).sortedBy { it.first } 125 | val returnMap = mutableMapOf() 126 | val map = mutableMapOf() 127 | var i = 0 128 | for ((first, second) in sorted) { 129 | map[first] = i 130 | if (!isRemix && first in LOCATIONS[engine]) { 131 | returnMap[i] = LOCATIONS[engine].indexOf(first) + 0x56 132 | } 133 | i += second.size * 4 134 | } 135 | for ((first, second) in ustrings) { 136 | map[first] = i 137 | i += unicodeStringToInts(second).size * 4 138 | } 139 | for ((first, second) in astrings) { 140 | map[first] = i 141 | i += stringToInts(second).size * 4 142 | } 143 | val meta = mutableListOf() 144 | meta.add(index + 0x100) 145 | meta.add(0) 146 | meta.add(map[buffer.getIntAdj(GATE_TABLE + 36 * index + 8)] ?: 0) 147 | return returnMap to (meta + secondPass(sorted.map { it.second }, map)) 148 | } 149 | 150 | fun extractGame(buffer: ByteBuffer, index: Int): Pair, List> { 151 | val start = buffer.getStart(index) 152 | engine = -1 153 | val funcs = firstPass(buffer, start) 154 | val sorted = listOf(funcs[0]) + funcs.drop(1).sortedBy { it.first } 155 | val returnMap = mutableMapOf() 156 | val map = mutableMapOf() 157 | var i = 0 158 | for ((first, second) in sorted) { 159 | map[first] = i 160 | if (!isRemix && first in LOCATIONS[engine]) { 161 | returnMap[i] = LOCATIONS[engine].indexOf(first) + 0x56 162 | } 163 | i += second.size * 4 164 | } 165 | for ((first, second) in ustrings) { 166 | map[first] = i 167 | i += unicodeStringToInts(second).size * 4 168 | } 169 | for ((first, second) in astrings) { 170 | map[first] = i 171 | i += stringToInts(second).size * 4 172 | } 173 | val meta = mutableListOf() 174 | meta.add(index) 175 | meta.add(0) 176 | meta.add(map[buffer.getIntAdj(TABLE_OFFSET + 52 * index + 8)] ?: 0) 177 | return returnMap to (meta + secondPass(sorted.map { it.second }, map)) 178 | } 179 | 180 | fun extractArbitrary(buffer: ByteBuffer, index: Int): List { 181 | isRemix = true 182 | engine = -1 183 | val funcs = firstPass(buffer, index, nongame=true) 184 | val sorted = listOf(funcs[0]) + funcs.drop(1).sortedBy {it.first} 185 | val map = mutableMapOf() 186 | var i = 0 187 | for ((first, second) in sorted) { 188 | map[first] = i 189 | i += second.size * 4 190 | } 191 | for ((first, second) in ustrings) { 192 | map[first] = i 193 | i += unicodeStringToInts(second).size * 4 194 | } 195 | for ((first, second) in astrings) { 196 | map[first] = i 197 | i += stringToInts(second).size * 4 198 | } 199 | val meta = mutableListOf() 200 | meta.add(-1) 201 | meta.add(0) 202 | meta.add(-1) 203 | return meta + secondPass(sorted.map {it.second}, map) 204 | } 205 | 206 | fun secondPass(funcs: List>, map: Map): List { 207 | val result = mutableListOf() 208 | for (l in funcs) { 209 | var i = 0 210 | while (i < l.size) { 211 | val opint = l[i] 212 | val opcode = opint and 0b1111111111 213 | val special = (opint ushr 14) 214 | val argCount = (opint ushr 10) and 0b1111 215 | val args = l.slice(i + 1..i + argCount).toMutableList() 216 | val annotations = mutableListOf() 217 | i += argCount + 1 218 | if (opcode == 2 || opcode == 6) { 219 | annotations.add(0) 220 | args[0] = map[args[0]] ?: 0 221 | } 222 | if (opcode == 3 && special == 2) { 223 | annotations.add(0) 224 | args[0] = map[args[0]] ?: 0 225 | } 226 | if (opcode == 1 && special == 1) { 227 | annotations.add(0x100) 228 | args[1] = map[args[1]] ?: 0 229 | } 230 | if (opcode to special in USTRING_OPS) { 231 | val n = USTRING_OPS[opcode to special] ?: arrayOf() 232 | for (arg in n) { 233 | annotations.add(1 or (arg shl 8)) 234 | args[arg] = map[args[arg]] ?: args[arg] 235 | } 236 | } 237 | if (opcode to special in ASTRING_OPS) { 238 | val n = ASTRING_OPS[opcode to special] ?: arrayOf() 239 | for (arg in n) { 240 | annotations.add(2 or (arg shl 8)) 241 | args[arg] = map[args[arg]] ?: args[arg] 242 | } 243 | } 244 | if (annotations.size > 0) { 245 | result.add(-1) 246 | result.add(annotations.size) 247 | result.addAll(annotations) 248 | } 249 | result.add(opint) 250 | result.addAll(args) 251 | } 252 | } 253 | result.add(-2) 254 | result.addAll(ustrings.map { unicodeStringToInts(it.second) }.flatten()) 255 | result.addAll(astrings.map { stringToInts(it.second) }.flatten()) 256 | return result 257 | } 258 | 259 | fun firstPass(buf: ByteBuffer, start: Int, assets: Int? = null, nongame: Boolean = false): List>> { 260 | val result = mutableListOf>>() 261 | ustrings = mutableListOf() 262 | 263 | isRemix = buf.getIntAdj(start) and 0b1111111111 == 1 || nongame 264 | val q = ArrayDeque() 265 | q.add(start) 266 | if (assets != null) { 267 | q.add(assets) 268 | } 269 | while (q.isNotEmpty()) { 270 | // compute the size of the function. 271 | val s = q.remove() 272 | var pc = s 273 | var depth = 0 274 | val ints = mutableListOf() 275 | while (true) { 276 | var opint = buf.getIntAdj(pc) 277 | val opcode = opint and 0b1111111111 278 | val special = (opint ushr 14) 279 | val argCount = (opint ushr 10) and 0b1111 280 | val args = mutableListOf() 281 | pc += 4 282 | 283 | for (i in 1..argCount) { 284 | args.add(buf.getIntAdj(pc)) 285 | pc += 4 286 | } 287 | 288 | if (opcode to special in USTRING_OPS) { 289 | val n = USTRING_OPS[opcode to special] ?: arrayOf() 290 | n 291 | .filter { args[it] > 0x100000 } 292 | .forEach { ustrings.add(args[it] to buf.getUnicodeString(args[it])) } 293 | } 294 | 295 | if (opcode to special in ASTRING_OPS) { 296 | val n = ASTRING_OPS[opcode to special] ?: arrayOf() 297 | n 298 | .filter { args[it] > 0x100000 } 299 | .forEach { astrings.add(args[it] to buf.getASCIIString(args[it])) } 300 | } 301 | 302 | if (!isRemix && opcode == 0x28 && special == 0 && engine == -1) { 303 | engine = args[0] 304 | if (allSubs) { 305 | q.addAll( 306 | LOCATIONS[engine].filter { it > 0x100000 }) 307 | } 308 | } 309 | 310 | if (!isRemix && (opcode == 0 || opcode == 4 || (opcode == 3 && special == 3)) && args[0] >= 0x56 && args[0] < 0x56 + LOCATIONS[engine].size) { 311 | // macro/sub detected. 312 | val location = LOCATIONS[engine][args[0] - 0x56] 313 | if (!q.contains(location) && !result.any { it.first == location }) { 314 | q.add(location) 315 | } 316 | args[0] = location 317 | if (opcode == 0) { 318 | opint = 2 or (2 shl 10) 319 | args.removeAt(2) 320 | } else if (opcode == 4) { 321 | opint = 6 or (1 shl 10) 322 | } else { 323 | opint = 3 or (1 shl 10) or (2 shl 14) 324 | } 325 | } 326 | 327 | if (opcode == 2 || opcode == 6) { 328 | val location = args[0] 329 | if (!q.contains(location) && !result.any { it.first == location }) { 330 | q.add(location) 331 | } 332 | } 333 | 334 | if (opcode == 1 && special == 1) { 335 | val location = args[1] 336 | if (!q.contains(location) && !result.any { it.first == location }) { 337 | q.add(location) 338 | } 339 | } 340 | 341 | if (opcode == 0x16 || opcode == 0x19) { 342 | depth++ 343 | } 344 | 345 | if (opcode == 0x18 || opcode == 0x1D) { 346 | depth-- 347 | } 348 | 349 | ints.add(opint) 350 | ints.addAll(args) 351 | 352 | if (opcode in 7..8 && depth == 0) { 353 | break 354 | } 355 | } 356 | result.add(Pair(s, ints)) 357 | } 358 | return result 359 | } 360 | 361 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/gameextractor/GameExtractorFunctions.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.gameextractor 2 | 3 | import java.nio.ByteBuffer 4 | 5 | const val TABLE_OFFSET = 0x52B498 6 | const val TEMPO_TABLE = 0x53EF54 7 | const val GATE_TABLE = 0x52E488 8 | 9 | fun ByteBuffer.getIntAdj(index: Int): Int = 10 | this.getInt(index - 0x100000) 11 | 12 | fun ByteBuffer.getUnicodeString(index: Int): String { 13 | var i: Int = index - 0x100000 14 | val str: StringBuilder = StringBuilder() 15 | while (true) { 16 | val char: Char = this.getChar(i) 17 | if (char == '\u0000') { 18 | return str.toString() 19 | } 20 | str.append(char) 21 | i += 2 22 | } 23 | } 24 | 25 | fun ByteBuffer.getASCIIString(index: Int): String { 26 | var i: Int = index - 0x100000 27 | val str: StringBuilder = StringBuilder() 28 | while (true) { 29 | val char: Char = this.get(i).toChar() 30 | if (char.toByte().toInt() == 0) { 31 | return str.toString() 32 | } 33 | str.append(char) 34 | i++ 35 | } 36 | } 37 | 38 | fun ByteBuffer.getName(index: Int): String { 39 | val pointer: Int = this.getIntAdj(TABLE_OFFSET + 52 * index + 12) 40 | val str: String = this.getUnicodeString(pointer) 41 | return str.slice((str.lastIndexOf('_') + 1) until str.lastIndexOf('.')) 42 | } 43 | 44 | fun ByteBuffer.getStart(index: Int): Int = 45 | getIntAdj(TABLE_OFFSET + 52 * index + 4) 46 | 47 | fun ByteBuffer.getGateStart(index: Int): Int = 48 | getIntAdj(GATE_TABLE + 36 * index + 4) 49 | 50 | fun ByteBuffer.getGateName(index: Int): String { 51 | val pointer: Int = getIntAdj(GATE_TABLE + 36 * index + 16) 52 | val str: String = getASCIIString(pointer) 53 | return str.slice(str.indexOf('_') + 1 until str.lastIndexOf('_')) 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/gameputter/GamePutter.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.gameputter 2 | 3 | import java.nio.ByteBuffer 4 | import kotlin.math.roundToInt 5 | 6 | object GamePutter { 7 | 8 | fun putGame(base: ByteBuffer, gameContents: ByteBuffer, s: Int): List { 9 | val tableIndex = gameContents.getInt(0) 10 | val start = gameContents.getInt(4) 11 | val assets = gameContents.getInt(8) 12 | if (tableIndex >= 0x100) { 13 | base.putInt(0x3358 + 36*(tableIndex-0x100) + 4, 0xC000000 + start + s) 14 | base.putInt(0x3358 + 36*(tableIndex-0x100) + 8, 0xC000000 + assets + s) 15 | } else { 16 | base.putInt(52 * tableIndex + 4, 0xC000000 + start + s) 17 | base.putInt(52 * tableIndex + 8, 0xC000000 + assets + s) 18 | } 19 | 20 | val result = mutableListOf() 21 | gameContents.position(12) 22 | while (gameContents.hasRemaining()) { 23 | var opint = gameContents.int 24 | if (opint == -2) { 25 | break 26 | } 27 | val adjArgs = mutableListOf() 28 | if (opint == -1) { 29 | val amount = gameContents.int 30 | for (i in 1..amount) { 31 | val ann = gameContents.int 32 | val anncode = ann and 0xFF 33 | val annArg = ann ushr 8 34 | if (anncode == 0 || anncode == 1 || anncode == 2) { 35 | adjArgs.add(annArg) 36 | } 37 | } 38 | opint = gameContents.int 39 | } 40 | result.add(opint) 41 | val argCount = (opint ushr 10) and 0b1111 42 | val args: MutableList = (0 until argCount).map { 43 | val arg = gameContents.int 44 | if (it in adjArgs) { 45 | arg + 0xC000000 + s 46 | } else { 47 | arg 48 | } 49 | }.toMutableList() 50 | result.addAll(args) 51 | } 52 | while (gameContents.hasRemaining()) { 53 | result.add(gameContents.int) 54 | } 55 | return result 56 | } 57 | 58 | fun putTempo(base: ByteBuffer, tempoFile: String, s: Int): List { 59 | val tempo = tempoFile.split(Regex("\\r?\\n")).filter { it.isNotBlank() }.map { it.split(" ") } 60 | val ids = tempo[0].map { it.toInt(16) } 61 | val result = mutableListOf() 62 | tempo.drop(1).forEachIndexed { index, list -> 63 | val bpm = list[0].toFloat() 64 | val beats = list[1].toFloat() 65 | val loop = if (list.size > 2){ // Check for loop value 66 | list[2].toInt() 67 | } else { // Special case for tempo files lacking loop value, behaves exactly as older Tickompiler versions 68 | if (index == tempo.size - 2) { 69 | 0x8000 70 | } else { 71 | 0 72 | } 73 | } 74 | val time = 60*beats/bpm 75 | val timeInt = (time * 32000).roundToInt() 76 | result.add(java.lang.Float.floatToIntBits(beats)) 77 | result.add(timeInt) 78 | result.add(loop) 79 | } 80 | for (i in 0 until 0x1DD) { 81 | val one = base.getInt(16*i + 0x1588) 82 | val two = base.getInt(16*i + 0x1588 + 4) 83 | if (one in ids || two in ids) { 84 | base.putInt(16*i + 0x1588 + 12, 0xC000000 + s) 85 | } 86 | } 87 | return result 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/objectify/ManifestObj.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.objectify 2 | 3 | 4 | class ManifestObj { 5 | var version: Int = -1 6 | var bin: BinObject = BinObject() 7 | var tempo: TempoObject = TempoObject() 8 | } 9 | 10 | class BinObject { 11 | var size: Int = 0 12 | } 13 | 14 | class TempoObject { 15 | var size: Int = 0 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/objectify/Objectifier.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.objectify 2 | 3 | import com.google.gson.GsonBuilder 4 | import rhmodding.tickompiler.Tickompiler 5 | import java.io.File 6 | import java.io.FileOutputStream 7 | import java.util.zip.ZipEntry 8 | import java.util.zip.ZipOutputStream 9 | 10 | val TFOBJ_PACKER_VERSION: Int = 1 11 | 12 | fun objectify(outputFile: File, binFiles: List, tempoFiles: List) { 13 | val packerVersion = TFOBJ_PACKER_VERSION 14 | outputFile.createNewFile() 15 | val zipStream = ZipOutputStream(FileOutputStream(outputFile)) 16 | zipStream.setComment("Tickompiler tfobject (tickflow object) file - ${Tickompiler.VERSION} - packer version $packerVersion") 17 | 18 | val manifestObj = ManifestObj().apply { 19 | version = packerVersion 20 | bin.size = binFiles.size 21 | tempo.size = tempoFiles.size 22 | } 23 | zipStream.putNextEntry(ZipEntry("manifest.json")) 24 | zipStream.write(GsonBuilder().setPrettyPrinting().create().toJson(manifestObj).toByteArray()) 25 | zipStream.closeEntry() 26 | 27 | binFiles.forEachIndexed { i, file -> 28 | zipStream.putNextEntry(ZipEntry("bin/bin_$i.bin")) 29 | file.inputStream().apply { 30 | copyTo(zipStream) 31 | close() 32 | } 33 | zipStream.closeEntry() 34 | } 35 | 36 | tempoFiles.forEachIndexed { i, file -> 37 | zipStream.putNextEntry(ZipEntry("tempo/tempo_$i.tempo")) 38 | file.inputStream().apply { 39 | copyTo(zipStream) 40 | close() 41 | } 42 | zipStream.closeEntry() 43 | } 44 | 45 | zipStream.close() 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/output/Outputter.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.output 2 | 3 | 4 | object Outputter { 5 | 6 | fun toHexBytes(array: ByteArray, group4: Boolean): String { 7 | val sb = StringBuilder() 8 | var count = 0 9 | val grouping = if (group4) 4 else 1 10 | array.forEach { 11 | sb.append(String.format("%02x", it)) 12 | if (++count >= grouping) { 13 | count = 0 14 | sb.append(" ") 15 | } 16 | } 17 | 18 | return sb.toString().toUpperCase() 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/util/Directories.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.util 2 | 3 | import java.io.File 4 | 5 | 6 | class Directories(val input: List, val output: List) 7 | 8 | fun getDirectories(inputFile: File?, outputFile: File?, firstFilter: (String) -> Boolean, outputExtension: String, ignoreDir: Boolean = false): Directories { 9 | val input: MutableList = mutableListOf() 10 | val output: MutableList = mutableListOf() 11 | 12 | if (inputFile != null) { 13 | val first: File = inputFile 14 | if (first.isFile) { 15 | input += first 16 | } else if (first.isDirectory) { 17 | input += first.listFiles { _, name -> firstFilter(name) }.filter { it.isFile } 18 | } 19 | 20 | val second: File? = outputFile 21 | if (second?.isFile == true) { 22 | if (input.size > 1 && !ignoreDir) 23 | throw IllegalArgumentException("Output option cannot be a file when the input is a directory!") 24 | 25 | output += second 26 | } else { 27 | second?.mkdirs() 28 | input.mapTo(output) { file -> 29 | File(second, file.nameWithoutExtension + "." + outputExtension) 30 | } 31 | } 32 | } 33 | 34 | return Directories(input, output) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/util/StringEscaping.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.util 2 | 3 | fun String.escape(): String { 4 | return replace("\\", "\\\\") 5 | .replace("\"", "\\\"") 6 | .replace("\n", "\\n") 7 | } 8 | 9 | fun String.unescape(): String { 10 | return replace("\\n", "\n").replace("\\\\(.)".toRegex()) {it.groupValues[1]} 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/rhmodding/tickompiler/util/Version.kt: -------------------------------------------------------------------------------- 1 | package rhmodding.tickompiler.util 2 | 3 | 4 | data class Version(val major: Int, val minor: Int, val patch: Int, val suffix: String = "") : Comparable { 5 | 6 | override fun compareTo(other: Version): Int { 7 | return numericalValue - other.numericalValue 8 | } 9 | 10 | val numericalValue: Int 11 | private val stringRepresentation: String by lazy { 12 | if (isUnknown) { 13 | "" 14 | } else { 15 | "v$major.$minor.$patch${if (!suffix.isBlank()) "-$suffix" else ""}" 16 | } 17 | } 18 | val isUnknown: Boolean 19 | 20 | companion object { 21 | 22 | val UNKNOWN: Version = Version(-1, -1, -1) 23 | 24 | const val MAX_PART_VALUE: Int = 0xFF 25 | val REGEX: Regex = "v(\\d+).(\\d+).(\\d+)(?:-(.+))?".toRegex() 26 | 27 | fun fromNumberOrNull(numerical: Int, suffix: String = ""): Version? { 28 | return Version((numerical ushr 16) and 0xFF, (numerical ushr 8) and 0xFF, numerical and 0xFF, suffix) 29 | } 30 | 31 | fun fromStringOrNull(thing: String): Version? { 32 | val match: MatchResult = REGEX.matchEntire(thing) ?: return null 33 | return try { 34 | Version(Integer.parseInt(match.groupValues[1]), Integer.parseInt(match.groupValues[2]), 35 | Integer.parseInt(match.groupValues[3]), match.groupValues.getOrNull(4) ?: "") 36 | } catch (e: Exception) { 37 | null 38 | } 39 | } 40 | 41 | fun fromNumber(numerical: Int, suffix: String = ""): Version { 42 | return fromNumberOrNull(numerical, suffix) ?: throw IllegalArgumentException("Invalid arguments: $numerical, $suffix") 43 | } 44 | 45 | fun fromString(thing: String): Version { 46 | return fromStringOrNull(thing) ?: throw IllegalArgumentException("Invalid argument: $thing") 47 | } 48 | } 49 | 50 | init { 51 | if ((major == -1 || major == -2) && minor == major && patch == major) { 52 | isUnknown = true 53 | } else { 54 | isUnknown = false 55 | if (major !in 0..MAX_PART_VALUE || minor !in 0..MAX_PART_VALUE || patch !in 0..MAX_PART_VALUE) { 56 | throw IllegalArgumentException("Invalid version. The max part value is $MAX_PART_VALUE. $this") 57 | } 58 | } 59 | 60 | numericalValue = (major shl 16) or (minor shl 8) or (patch) 61 | } 62 | 63 | override fun toString(): String { 64 | return stringRepresentation 65 | } 66 | 67 | override fun equals(other: Any?): Boolean { 68 | if (other is Version) { 69 | if (other.numericalValue == numericalValue && other.suffix == suffix) { 70 | return true 71 | } 72 | return false 73 | } 74 | 75 | return super.equals(other) 76 | } 77 | 78 | override fun hashCode(): Int { 79 | var result = major 80 | result = 31 * result + minor 81 | result = 31 * result + patch 82 | result = 31 * result + suffix.hashCode() 83 | return result 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/resources/locations.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | 3778928, 4 | 3781032, 5 | 3779288, 6 | 3782848 7 | ], 8 | [ 9 | 3791944, 10 | 3791992, 11 | 3792040, 12 | 3792136, 13 | 3792088, 14 | 3792184, 15 | 3792232, 16 | 3792280, 17 | 3792328, 18 | 3792376, 19 | 3794136, 20 | 3792780, 21 | 3788320, 22 | 3784448, 23 | 3795408 24 | ], 25 | [ 26 | 3799048, 27 | 3799260, 28 | 3799472, 29 | 3799684, 30 | 3799896, 31 | 3800108, 32 | 3800320, 33 | 3800532, 34 | 3800744, 35 | 3800956, 36 | 3801152, 37 | 3801348, 38 | 3801544, 39 | 3801740, 40 | 3802716, 41 | 3802112, 42 | 3797072, 43 | 3804588 44 | ], 45 | [ 46 | 3816476, 47 | 3816512, 48 | 3816548, 49 | 3821328, 50 | 3812044, 51 | 3816940, 52 | 3806396, 53 | 3824332, 54 | 3815024 55 | ], 56 | [ 57 | 3825784, 58 | 3825904, 59 | 3826024, 60 | 3826144, 61 | 3826620, 62 | 5436948, 63 | 3828832 64 | ], 65 | [ 66 | 3830284, 67 | 3830356, 68 | 3830428, 69 | 3830492, 70 | 3834168, 71 | 3830992, 72 | 3835724 73 | ], 74 | [ 75 | 3837176, 76 | 3837644, 77 | 3844900, 78 | 3846172 79 | ], 80 | [ 81 | 3846900, 82 | 3846940, 83 | 3847044, 84 | 3848144, 85 | 3848184, 86 | 3848248, 87 | 3848372, 88 | 3848552, 89 | 3848816, 90 | 3849200, 91 | 3849500, 92 | 3849620, 93 | 3849792, 94 | 3850068, 95 | 3850372, 96 | 3850680, 97 | 3851144, 98 | 3853752, 99 | 3857672 100 | ], 101 | [ 102 | 3859180, 103 | 3859236, 104 | 3859292, 105 | 3859412, 106 | 3862284, 107 | 3859824, 108 | 3864980 109 | ], 110 | [ 111 | 3866432, 112 | 3866532, 113 | 3866644, 114 | 3866712, 115 | 3866780, 116 | 3869972, 117 | 3867204, 118 | 3871448 119 | ], 120 | [ 121 | 3872900, 122 | 3872952, 123 | 3873416, 124 | 3873468, 125 | 3874180, 126 | 3874232, 127 | 3874724, 128 | 3874776, 129 | 3875432, 130 | 3875484, 131 | 3875796, 132 | 3875836, 133 | 3876152, 134 | 3876204, 135 | 3877984, 136 | 3878036, 137 | 3878500, 138 | 3878552, 139 | 3879264, 140 | 3879316, 141 | 3879808, 142 | 3879860, 143 | 3880516, 144 | 3880568, 145 | 3880880, 146 | 3880920, 147 | 3881236, 148 | 3881288, 149 | 3884768, 150 | 3883552, 151 | 3886864 152 | ], 153 | [ 154 | 3895840, 155 | 3896028, 156 | 3896216, 157 | 3896436, 158 | 3896504, 159 | 3896572, 160 | 3896728, 161 | 3896796, 162 | 3896864, 163 | 3896956, 164 | 3897428, 165 | 3902796, 166 | 3905428, 167 | 3888472, 168 | 3898180, 169 | 3901160, 170 | 3902324, 171 | 3889204, 172 | 3892368, 173 | 3894020 174 | ], 175 | [ 176 | 3910356, 177 | 3910396, 178 | 3910472, 179 | 3910488, 180 | 3910528, 181 | 3910568, 182 | 3912724, 183 | 3911144, 184 | 3907212, 185 | 3917100 186 | ], 187 | [ 188 | 3918816, 189 | 3919408, 190 | 3921332, 191 | 3923020 192 | ], 193 | [ 194 | 4240752, 195 | 4240816, 196 | 4240920, 197 | 4241036, 198 | 4241120, 199 | 4245992, 200 | 4241568, 201 | 4248952 202 | ], 203 | [ 204 | 4250932, 205 | 4251044, 206 | 4251188, 207 | 4251304, 208 | 4251448, 209 | 4251460, 210 | 4251472, 211 | 4251484, 212 | 4251496, 213 | 4251508, 214 | 4251520, 215 | 4251580, 216 | 4251648, 217 | 4251660, 218 | 4251708, 219 | 4251776, 220 | 4251804, 221 | 4256156, 222 | 4252284, 223 | 4259832 224 | ], 225 | [ 226 | 4262164, 227 | 4261340, 228 | 4261508, 229 | 4261520, 230 | 4261556, 231 | 4261760, 232 | 4261928, 233 | 4261940, 234 | 4261976, 235 | 4261844, 236 | 4262092, 237 | 4261424, 238 | 4261676, 239 | 4264116, 240 | 4262524, 241 | 4266668 242 | ], 243 | [ 244 | 4268176, 245 | 4268216, 246 | 4268312, 247 | 4268404, 248 | 4268448, 249 | 4268492, 250 | 4268744, 251 | 4268996, 252 | 4269292, 253 | 4269584, 254 | 4269892, 255 | 4273744, 256 | 4270492, 257 | 4275608 258 | ], 259 | [ 260 | 4277168, 261 | 4277360, 262 | 4277564, 263 | 4277848, 264 | 4278088, 265 | 4278468, 266 | 4278780, 267 | 4278972, 268 | 4280892, 269 | 4279592, 270 | 4285608 271 | ], 272 | [ 273 | 4294568, 274 | 4297280, 275 | 4291792, 276 | 4295220, 277 | 4287532, 278 | 4299464, 279 | 4293116 280 | ], 281 | [ 282 | 4321496, 283 | 4321548, 284 | 4321568, 285 | 4321596, 286 | 4321616, 287 | 4321644, 288 | 4321664, 289 | 4321692, 290 | 4321712, 291 | 4321740, 292 | 4321768, 293 | 4321788, 294 | 4321812, 295 | 4321836, 296 | 4321876, 297 | 4321916, 298 | 4321980, 299 | 4322084, 300 | 4322108, 301 | 4322168, 302 | 4322228, 303 | 4322340, 304 | 4322452, 305 | 4322588, 306 | 4322652, 307 | 4322716, 308 | 4322776, 309 | 4322828, 310 | 4322880, 311 | 4322956, 312 | 4323008, 313 | 4323096, 314 | 4323124, 315 | 4323152, 316 | 4323196, 317 | 4323224, 318 | 4323304, 319 | 4323352, 320 | 4323428, 321 | 4323560, 322 | 4323684, 323 | 4323788, 324 | 4323840, 325 | 4323944, 326 | 4324000, 327 | 4324084, 328 | 4324228, 329 | 4324420, 330 | 4324360, 331 | 4324524, 332 | 4324628, 333 | 4324676, 334 | 4324752, 335 | 4324884, 336 | 4325008, 337 | 4325112, 338 | 4325164, 339 | 4325268, 340 | 4325316, 341 | 4325392, 342 | 4325516, 343 | 4325716, 344 | 4325616, 345 | 4325820, 346 | 4325924, 347 | 4325972, 348 | 4326040, 349 | 4326152, 350 | 4326244, 351 | 4326348, 352 | 4326440, 353 | 4326544, 354 | 4326588, 355 | 4326632, 356 | 4326696, 357 | 4326740, 358 | 4326828, 359 | 4326848, 360 | 4326868, 361 | 4326892, 362 | 4326912, 363 | 4326992, 364 | 4327028, 365 | 4327096, 366 | 4327204, 367 | 4327292, 368 | 4327400, 369 | 4327488, 370 | 4327596, 371 | 4327632, 372 | 4327692, 373 | 4327788, 374 | 4327876, 375 | 4327984, 376 | 4328072, 377 | 4328180, 378 | 4368100, 379 | 4328700, 380 | 4312676, 381 | 4373720 382 | ], 383 | [ 384 | 4390672, 385 | 4390676, 386 | 4390872, 387 | 4390984, 388 | 4391096, 389 | 4391208, 390 | 4391320, 391 | 4391484, 392 | 4390812, 393 | 4391432, 394 | 4391576, 395 | 4392008, 396 | 4397060, 397 | 4388036, 398 | 4393220, 399 | 4376608, 400 | 4400040, 401 | 4389164, 402 | 4393788, 403 | 4395036, 404 | 4396336, 405 | 4377236, 406 | 4380764, 407 | 4386504 408 | ], 409 | [ 410 | 4401548, 411 | 4404016, 412 | 4401908, 413 | 4406380 414 | ], 415 | [ 416 | 4419660, 417 | 4419864, 418 | 4420068, 419 | 4420272, 420 | 4420476, 421 | 4420516, 422 | 4422060, 423 | 4415996, 424 | 4410732, 425 | 4420968, 426 | 4414068, 427 | 4408272, 428 | 4424588, 429 | 4418188, 430 | 4412176 431 | ], 432 | [ 433 | 4430816, 434 | 4430848, 435 | 4430880, 436 | 4430912, 437 | 4430944, 438 | 4430976, 439 | 4431008, 440 | 4431040, 441 | 4431092, 442 | 4431136, 443 | 4431188, 444 | 4431256, 445 | 4431356, 446 | 4431520, 447 | 4431552, 448 | 4431620, 449 | 4431688, 450 | 4431756, 451 | 4431824, 452 | 4431892, 453 | 4431960, 454 | 4432028, 455 | 4432096, 456 | 4432164, 457 | 4432232, 458 | 4432300, 459 | 4432368, 460 | 4432436, 461 | 4432504, 462 | 4432572, 463 | 4432640, 464 | 4432680, 465 | 4435204, 466 | 4433140, 467 | 4426340, 468 | 4436724 469 | ], 470 | [ 471 | 4445316, 472 | 4445344, 473 | 4445372, 474 | 4445400, 475 | 4445428, 476 | 4445456, 477 | 4445484, 478 | 4445512, 479 | 4445540, 480 | 4445568, 481 | 4445596, 482 | 4445624, 483 | 4445652, 484 | 4445680, 485 | 4445708, 486 | 4445736, 487 | 4445764, 488 | 4445792, 489 | 4445792, 490 | 4445796, 491 | 4445824, 492 | 4445852, 493 | 4445880, 494 | 4445908, 495 | 4445936, 496 | 4445964, 497 | 4445992, 498 | 4446020, 499 | 4446048, 500 | 4446076, 501 | 4446104, 502 | 4446132, 503 | 4446160, 504 | 4446188, 505 | 4446216, 506 | 4446244, 507 | 4446272, 508 | 4446272, 509 | 4450116, 510 | 4446632, 511 | 4438324, 512 | 4452840 513 | ], 514 | [ 515 | 4453648, 516 | 4453748, 517 | 4453844, 518 | 4453936, 519 | 4454000, 520 | 4454112, 521 | 4454284, 522 | 4454384, 523 | 4454476, 524 | 4454544, 525 | 4454656, 526 | 4454828, 527 | 4454924, 528 | 4455016, 529 | 4455076, 530 | 4455188, 531 | 4455364, 532 | 4455388, 533 | 4455440, 534 | 4455528, 535 | 4455704, 536 | 4455744, 537 | 4456156, 538 | 4456384, 539 | 4456684, 540 | 4456888, 541 | 4457168, 542 | 4457500, 543 | 4457792, 544 | 4458672, 545 | 4461220, 546 | 4463272 547 | ], 548 | [ 549 | 4464760, 550 | 4464820, 551 | 4464900, 552 | 4465480, 553 | 4467164, 554 | 4469768 555 | ], 556 | [ 557 | 4620632, 558 | 4620644, 559 | 4620768, 560 | 4620936, 561 | 4621100, 562 | 4621264, 563 | 4621428, 564 | 4621592, 565 | 4621756, 566 | 4621920, 567 | 4622108, 568 | 4622296, 569 | 4622484, 570 | 4622672, 571 | 4622860, 572 | 4623048, 573 | 4623228, 574 | 4623408, 575 | 4623588, 576 | 4623768, 577 | 4623948, 578 | 4624128, 579 | 4624164, 580 | 4624200, 581 | 4624236, 582 | 4624272, 583 | 4624352, 584 | 4624432, 585 | 4624512, 586 | 4624592, 587 | 4624672, 588 | 4624752, 589 | 4624884, 590 | 4625016, 591 | 4625148, 592 | 4625280, 593 | 4625412, 594 | 4625544, 595 | 4625712, 596 | 4625880, 597 | 4626048, 598 | 4626216, 599 | 4626384, 600 | 4626552, 601 | 4627208, 602 | 4627864, 603 | 4628520, 604 | 4629176, 605 | 4629796, 606 | 4630416, 607 | 4631036, 608 | 4631656, 609 | 4632240, 610 | 4632824, 611 | 4633408, 612 | 4633992, 613 | 4634540, 614 | 4635088, 615 | 4635636, 616 | 4636184, 617 | 4636696, 618 | 4637208, 619 | 4637720, 620 | 4638232, 621 | 4638672, 622 | 4639112, 623 | 4639552, 624 | 4639992, 625 | 4640648, 626 | 4641304, 627 | 4641960, 628 | 4642616, 629 | 4643272, 630 | 4643928, 631 | 4644584, 632 | 4645240, 633 | 4645896, 634 | 4646552, 635 | 4647208, 636 | 4647864, 637 | 4647932, 638 | 4648144, 639 | 4651192, 640 | 4648672, 641 | 4653624 642 | ], 643 | [ 644 | 4660992, 645 | 4661092, 646 | 4661128, 647 | 4661184, 648 | 4661228, 649 | 4661268, 650 | 4661312, 651 | 4661344, 652 | 4661408, 653 | 4661432, 654 | 4661472, 655 | 4661512, 656 | 4661552, 657 | 4661576, 658 | 4661616, 659 | 4661656, 660 | 4661724, 661 | 4661792, 662 | 4661860, 663 | 4661928, 664 | 4661996, 665 | 4662064, 666 | 4662132, 667 | 4664188, 668 | 4662620, 669 | 4655232, 670 | 4666384 671 | ], 672 | [ 673 | 4667892, 674 | 4667960, 675 | 4668028, 676 | 4668088, 677 | 4668156, 678 | 4668224, 679 | 4668292, 680 | 4668312, 681 | 4668352, 682 | 4668456, 683 | 4668548, 684 | 4668616, 685 | 4668696, 686 | 4668776, 687 | 4668876, 688 | 4668980, 689 | 4669072, 690 | 4669140, 691 | 4669220, 692 | 4669656, 693 | 5439792, 694 | 4670968 695 | ], 696 | [ 697 | 4672420, 698 | 4672612, 699 | 4672812, 700 | 4672856, 701 | 4672884, 702 | 5441984, 703 | 5442040, 704 | 5442084, 705 | 4672912, 706 | 4672948, 707 | 4672984, 708 | 4680804, 709 | 4673376, 710 | 4683328, 711 | 4680332, 712 | 5442128, 713 | 4680500, 714 | 5442304, 715 | 4680652, 716 | 5442344 717 | ], 718 | [ 719 | 4684836, 720 | 4684868, 721 | 4684900, 722 | 4684944, 723 | 4684988, 724 | 4685032, 725 | 4685076, 726 | 4685120, 727 | 4685164, 728 | 4685208, 729 | 4685252, 730 | 4685296, 731 | 4685340, 732 | 4685512, 733 | 4685692, 734 | 4685872, 735 | 4686052, 736 | 4686232, 737 | 4686428, 738 | 4686644, 739 | 4686860, 740 | 4687076, 741 | 4687292, 742 | 4687388, 743 | 4687576, 744 | 4687628, 745 | 4687680, 746 | 4687732, 747 | 4687784, 748 | 4687836, 749 | 4687888, 750 | 4687964, 751 | 4688040, 752 | 4688116, 753 | 4688168, 754 | 4688220, 755 | 4688272, 756 | 4688324, 757 | 4688376, 758 | 4688428, 759 | 4688444, 760 | 4688460, 761 | 4688476, 762 | 4693624, 763 | 4688884, 764 | 4698432 765 | ], 766 | [ 767 | 4699884, 768 | 4699920, 769 | 4699956, 770 | 4700016, 771 | 4700076, 772 | 4700140, 773 | 4700204, 774 | 4700268, 775 | 4700332, 776 | 4700396, 777 | 4700460, 778 | 4700524, 779 | 4700588, 780 | 4700652, 781 | 4700716, 782 | 4700780, 783 | 4700844, 784 | 4700908, 785 | 4700972, 786 | 4701016, 787 | 4701084, 788 | 4706052, 789 | 4701576, 790 | 4709164 791 | ], 792 | [ 793 | 4710616, 794 | 4710680, 795 | 4710760, 796 | 4710876, 797 | 4710904, 798 | 4714000, 799 | 4711384, 800 | 4718044 801 | ], 802 | [ 803 | 4724684, 804 | 4724688, 805 | 4724740, 806 | 4724780, 807 | 4724860, 808 | 4724952, 809 | 4725076, 810 | 4727612, 811 | 4725976, 812 | 4720568, 813 | 4731200, 814 | 4725644, 815 | 4720236 816 | ], 817 | [ 818 | 4732652, 819 | 4732744, 820 | 4733144, 821 | 4737496, 822 | 4739308 823 | ], 824 | [ 825 | 4746664, 826 | 4746704, 827 | 4750648, 828 | 5443756, 829 | 5443112, 830 | 4752780 831 | ], 832 | [ 833 | 4765756, 834 | 4765776, 835 | 4765812, 836 | 4765848, 837 | 4766032, 838 | 4766052, 839 | 4766072, 840 | 4766092, 841 | 4766112, 842 | 4766132, 843 | 4766156, 844 | 4766180, 845 | 4766204, 846 | 4766212, 847 | 4766256, 848 | 4766356, 849 | 4766396, 850 | 4766492, 851 | 4766972, 852 | 4767596, 853 | 4768092, 854 | 4768192, 855 | 4768832, 856 | 4769184, 857 | 4769320, 858 | 4770104, 859 | 4770600, 860 | 4771528, 861 | 4771952, 862 | 4773252, 863 | 4774620, 864 | 4776056, 865 | 4777560, 866 | 4779284, 867 | 4784080, 868 | 4762308, 869 | 4781496, 870 | 4754652, 871 | 4787832, 872 | 4764304 873 | ], 874 | [ 875 | 4789584, 876 | 4789644, 877 | 4789724, 878 | 4789788, 879 | 4789860, 880 | 4789924, 881 | 4789984, 882 | 4790036, 883 | 4790116, 884 | 4790184, 885 | 5444928, 886 | 5445024, 887 | 5445512, 888 | 5445596, 889 | 5445632, 890 | 4790672, 891 | 4790708, 892 | 4791572, 893 | 5445668, 894 | 4796840 895 | ], 896 | [ 897 | 4798348, 898 | 4798424, 899 | 4798476, 900 | 4798536, 901 | 4798588, 902 | 4798592, 903 | 4798612, 904 | 4798632, 905 | 4798652, 906 | 4798684, 907 | 4798708, 908 | 4798764, 909 | 4798792, 910 | 4798820, 911 | 4798848, 912 | 4798872, 913 | 4798904, 914 | 4798936, 915 | 4799096, 916 | 4799196, 917 | 4799240, 918 | 4799400, 919 | 4799560, 920 | 4799660, 921 | 4799820, 922 | 4799920, 923 | 4799964, 924 | 4800124, 925 | 4798736, 926 | 4800224, 927 | 4800268, 928 | 4800312, 929 | 4800352, 930 | 4800448, 931 | 4800472, 932 | 4800496, 933 | 4800512, 934 | 4800540, 935 | 4800568, 936 | 4800588, 937 | 4800616, 938 | 4800632, 939 | 4800680, 940 | 4800684, 941 | 4800700, 942 | 4800716, 943 | 4800744, 944 | 4800760, 945 | 4800764, 946 | 4800784, 947 | 4800808, 948 | 4800860, 949 | 4800904, 950 | 4800948, 951 | 4800992, 952 | 4801016, 953 | 4801040, 954 | 4801064, 955 | 4806460, 956 | 4832264, 957 | 4854420, 958 | 4868692, 959 | 4801572, 960 | 4809592, 961 | 4819788, 962 | 4835788, 963 | 4859008, 964 | 4807600, 965 | 4833464, 966 | 4857016, 967 | 4871772 968 | ], 969 | [ 970 | 4874752, 971 | 4874792, 972 | 5454888, 973 | 4875212, 974 | 4877680, 975 | 5447332 976 | ], 977 | [ 978 | 4900456, 979 | 4900468, 980 | 4900480, 981 | 4900492, 982 | 4900524, 983 | 4900572, 984 | 4900616, 985 | 4900652, 986 | 4900708, 987 | 4900760, 988 | 4900832, 989 | 4900868, 990 | 4900904, 991 | 4900940, 992 | 4900964, 993 | 4900988, 994 | 4901008, 995 | 4901028, 996 | 4901048, 997 | 4904232, 998 | 4896236, 999 | 4901484, 1000 | 4890260, 1001 | 4879360, 1002 | 4906496, 1003 | 4898932 1004 | ], 1005 | [ 1006 | 4908004, 1007 | 4908096, 1008 | 4908188, 1009 | 4908280, 1010 | 4908372, 1011 | 4908464, 1012 | 4908556, 1013 | 4908664, 1014 | 4908772, 1015 | 4908880, 1016 | 4908988, 1017 | 4909096, 1018 | 4909204, 1019 | 4909288, 1020 | 4909372, 1021 | 4909456, 1022 | 4909540, 1023 | 4909624, 1024 | 4909708, 1025 | 4909808, 1026 | 4909908, 1027 | 4910008, 1028 | 4910108, 1029 | 4910208, 1030 | 4910308, 1031 | 4910344, 1032 | 4910388, 1033 | 4910488, 1034 | 4910592, 1035 | 4910696, 1036 | 4910800, 1037 | 4910900, 1038 | 4911004, 1039 | 4911108, 1040 | 4911212, 1041 | 4911312, 1042 | 4911416, 1043 | 4911520, 1044 | 4911624, 1045 | 4911724, 1046 | 4911828, 1047 | 4911932, 1048 | 4912036, 1049 | 4912136, 1050 | 4912240, 1051 | 4912344, 1052 | 4912448, 1053 | 4912548, 1054 | 4912652, 1055 | 4912756, 1056 | 4912860, 1057 | 4912960, 1058 | 4913064, 1059 | 4913168, 1060 | 4913272, 1061 | 4913372, 1062 | 4913476, 1063 | 4913580, 1064 | 4913684, 1065 | 4913784, 1066 | 4913888, 1067 | 4913992, 1068 | 4914096, 1069 | 4914196, 1070 | 4914300, 1071 | 4914404, 1072 | 4914508, 1073 | 4914608, 1074 | 4914712, 1075 | 4914816, 1076 | 4914920, 1077 | 4915020, 1078 | 4915124, 1079 | 4915228, 1080 | 4915332, 1081 | 4915432, 1082 | 4915536, 1083 | 4915640, 1084 | 4915744, 1085 | 4915844, 1086 | 4915948, 1087 | 4916052, 1088 | 4916156, 1089 | 4916256, 1090 | 4916360, 1091 | 4916464, 1092 | 4916568, 1093 | 4916668, 1094 | 4916772, 1095 | 4916876, 1096 | 4917400, 1097 | 4921436, 1098 | 4925980 1099 | ], 1100 | [ 1101 | 4927472, 1102 | 4927580, 1103 | 4927684, 1104 | 4927708, 1105 | 4927748, 1106 | 4927808, 1107 | 4927868, 1108 | 4927928, 1109 | 4927988, 1110 | 4928040, 1111 | 4928092, 1112 | 4928144, 1113 | 4928196, 1114 | 4928236, 1115 | 4928276, 1116 | 4928316, 1117 | 5463720, 1118 | 4930808, 1119 | 4934500 1120 | ], 1121 | [ 1122 | 4935952, 1123 | 4936040, 1124 | 4936128, 1125 | 4936216, 1126 | 4936304, 1127 | 4938496, 1128 | 4936856, 1129 | 4940668 1130 | ], 1131 | [ 1132 | 4942652, 1133 | 4942704, 1134 | 4942788, 1135 | 4942908, 1136 | 4943056, 1137 | 4947208, 1138 | 4943464, 1139 | 4950216 1140 | ], 1141 | [ 1142 | 4951676, 1143 | 4951712, 1144 | 4951748, 1145 | 4951800, 1146 | 4951852, 1147 | 4951904, 1148 | 4951956, 1149 | 4952008, 1150 | 4952020, 1151 | 4952084, 1152 | 4952148, 1153 | 4952212, 1154 | 4952276, 1155 | 4952340, 1156 | 4952388, 1157 | 4952792, 1158 | 4956076, 1159 | 4957924 1160 | ], 1161 | [ 1162 | 4959376, 1163 | 4959400, 1164 | 4959424, 1165 | 4959864, 1166 | 4963716, 1167 | 4965920 1168 | ], 1169 | [ 1170 | 4967636, 1171 | 4969144, 1172 | 4967996, 1173 | 4971072 1174 | ], 1175 | [ 1176 | 4972540, 1177 | 4972572, 1178 | 4972604, 1179 | 4972620, 1180 | 4972636, 1181 | 4972652, 1182 | 4972692, 1183 | 4972732, 1184 | 4972776, 1185 | 4972804, 1186 | 4972892, 1187 | 4972928, 1188 | 4972944, 1189 | 4972960, 1190 | 4972976, 1191 | 4973004, 1192 | 4973052, 1193 | 4974980, 1194 | 4973440, 1195 | 4976976 1196 | ], 1197 | [ 1198 | 0, 1199 | 4989584, 1200 | 4989636, 1201 | 4989688, 1202 | 4989748, 1203 | 4989900, 1204 | 4990052, 1205 | 4993032, 1206 | 4990452, 1207 | 4995556, 1208 | 4992992 1209 | ], 1210 | [ 1211 | 3944508, 1212 | 3947520, 1213 | 3944872, 1214 | 3949100 1215 | ], 1216 | [ 1217 | 0, 1218 | 3956584, 1219 | 3952312, 1220 | 3955896, 1221 | 3951484, 1222 | 3958876, 1223 | 3953512 1224 | ], 1225 | [ 1226 | 3960392, 1227 | 3960588, 1228 | 3960696, 1229 | 3960804, 1230 | 3967756, 1231 | 3961256, 1232 | 3970660 1233 | ], 1234 | [ 1235 | 4105116, 1236 | 4105192, 1237 | 4105324, 1238 | 4105416, 1239 | 4105564, 1240 | 4105644, 1241 | 4105728, 1242 | 4105864, 1243 | 4106644, 1244 | 4106720, 1245 | 4106800, 1246 | 4107068, 1247 | 4106932, 1248 | 4107216, 1249 | 4106004, 1250 | 4106084, 1251 | 4106168, 1252 | 4106308, 1253 | 4106404, 1254 | 4106488, 1255 | 4107352, 1256 | 4107432, 1257 | 4107520, 1258 | 4107660, 1259 | 4107804, 1260 | 4107884, 1261 | 4107968, 1262 | 4108112, 1263 | 4108252, 1264 | 4108332, 1265 | 4108416, 1266 | 4108556, 1267 | 4108700, 1268 | 4108788, 1269 | 4108920, 1270 | 4109068, 1271 | 4109200, 1272 | 4109348, 1273 | 4109416, 1274 | 4112620, 1275 | 4109904, 1276 | 4100092, 1277 | 4115496 1278 | ], 1279 | [ 1280 | 4123932, 1281 | 4123936, 1282 | 4123972, 1283 | 4124008, 1284 | 4124064, 1285 | 4124152, 1286 | 4124240, 1287 | 4124276, 1288 | 4124312, 1289 | 4124368, 1290 | 4124420, 1291 | 4124472, 1292 | 4124544, 1293 | 4124596, 1294 | 4124648, 1295 | 4124720, 1296 | 4124788, 1297 | 4124856, 1298 | 4127216, 1299 | 4120908, 1300 | 4125336, 1301 | 4117384, 1302 | 4129832, 1303 | 4122464, 1304 | 4126272, 1305 | 4118992 1306 | ], 1307 | [ 1308 | 0, 1309 | 4139260, 1310 | 4139280, 1311 | 4144676, 1312 | 4139768, 1313 | 4131512, 1314 | 4146996 1315 | ], 1316 | [ 1317 | 4148448, 1318 | 4148552, 1319 | 4148680, 1320 | 4149004, 1321 | 4149316, 1322 | 4151180, 1323 | 4149828, 1324 | 4153472 1325 | ], 1326 | [ 1327 | 4161684, 1328 | 4165592, 1329 | 4162044, 1330 | 4155052, 1331 | 4169532 1332 | ], 1333 | [ 1334 | 4171040, 1335 | 4171044, 1336 | 4171096, 1337 | 4171132, 1338 | 4171184, 1339 | 4171296, 1340 | 4171408, 1341 | 4171520, 1342 | 4171632, 1343 | 4171696, 1344 | 4171784, 1345 | 4171888, 1346 | 4172004, 1347 | 4172100, 1348 | 4172160, 1349 | 4172212, 1350 | 4172256, 1351 | 4172352, 1352 | 4172412, 1353 | 4172464, 1354 | 4172508, 1355 | 4172604, 1356 | 4172664, 1357 | 4172716, 1358 | 4172760, 1359 | 4172856, 1360 | 4172916, 1361 | 4172968, 1362 | 4173012, 1363 | 4173052, 1364 | 4173100, 1365 | 4180592, 1366 | 4176324, 1367 | 4174400 1368 | ], 1369 | [ 1370 | 0, 1371 | 4182328, 1372 | 4182388, 1373 | 4182448, 1374 | 4182508, 1375 | 4183520, 1376 | 4182568, 1377 | 4182640, 1378 | 4182712, 1379 | 4182784, 1380 | 4182856, 1381 | 4182928, 1382 | 4183000, 1383 | 4183072, 1384 | 4183144, 1385 | 4183216, 1386 | 4183288, 1387 | 4183360, 1388 | 4183432, 1389 | 4184036, 1390 | 4186968, 1391 | 4186852, 1392 | 4186892, 1393 | 4186936, 1394 | 4191208 1395 | ], 1396 | [ 1397 | 4193188, 1398 | 4193352, 1399 | 4193684, 1400 | 4193792, 1401 | 4193520, 1402 | 4196268, 1403 | 4195800, 1404 | 4195924, 1405 | 4196096, 1406 | 4194316, 1407 | 4199144 1408 | ], 1409 | [ 1410 | 4211200, 1411 | 4215784, 1412 | 4211884, 1413 | 4208352, 1414 | 4201136, 1415 | 4218012, 1416 | 4209692, 1417 | 4215236, 1418 | 4207104 1419 | ], 1420 | [ 1421 | 3924220, 1422 | 3924548, 1423 | 3924868, 1424 | 3925176, 1425 | 3925472, 1426 | 3925800, 1427 | 3926120, 1428 | 3926428, 1429 | 3926756, 1430 | 3927076, 1431 | 3927404, 1432 | 3932172, 1433 | 3934948, 1434 | 3941712, 1435 | 3930824, 1436 | 3930952, 1437 | 3931080, 1438 | 3930800, 1439 | 3931208, 1440 | 3931328, 1441 | 3931448, 1442 | 3929504 1443 | ], 1444 | [ 1445 | 4300472, 1446 | 4300640, 1447 | 4300668, 1448 | 4300804, 1449 | 4301404, 1450 | 4301644, 1451 | 4301884, 1452 | 4302124, 1453 | 4302364, 1454 | 4302604, 1455 | 4302844, 1456 | 4303064, 1457 | 4303076, 1458 | 4303088, 1459 | 4303100, 1460 | 4303112, 1461 | 4303124, 1462 | 4303136, 1463 | 4303148, 1464 | 4303160, 1465 | 4303172, 1466 | 4303184, 1467 | 4303236, 1468 | 4303276, 1469 | 4303328, 1470 | 4303480, 1471 | 4301076, 1472 | 4301104, 1473 | 4301132, 1474 | 4301524, 1475 | 4301764, 1476 | 4302004, 1477 | 4302244, 1478 | 4302484, 1479 | 4302724, 1480 | 4304092, 1481 | 4307828, 1482 | 4309576, 1483 | 4310840, 1484 | 4303544, 1485 | 4307140, 1486 | 4308888, 1487 | 4310620, 1488 | 4303740, 1489 | 4307336, 1490 | 4309084, 1491 | 4310832, 1492 | 4305824, 1493 | 4305952, 1494 | 4306080, 1495 | 4305800, 1496 | 4306208, 1497 | 4306328, 1498 | 4306448, 1499 | 4304580 1500 | ], 1501 | [ 1502 | 4977984, 1503 | 4978056, 1504 | 4978328, 1505 | 4978600, 1506 | 4978872, 1507 | 4979144, 1508 | 4979416, 1509 | 4979688, 1510 | 4979960, 1511 | 4980232, 1512 | 4980504, 1513 | 4980712, 1514 | 4980920, 1515 | 4981128, 1516 | 4981336, 1517 | 4983816, 1518 | 4985156, 1519 | 4986160, 1520 | 4982500, 1521 | 4982628, 1522 | 4982756, 1523 | 4982476, 1524 | 4982884, 1525 | 4983004, 1526 | 4983124, 1527 | 4981784 1528 | ], 1529 | [ 1530 | 3971788, 1531 | 3971792, 1532 | 3971896, 1533 | 3972228, 1534 | 3972804, 1535 | 3972840, 1536 | 3978888, 1537 | 3982636, 1538 | 4095688, 1539 | 3977452, 1540 | 3977580, 1541 | 3977708, 1542 | 3977428, 1543 | 3977836, 1544 | 3977956, 1545 | 3978076, 1546 | 3975896 1547 | ] 1548 | ] -------------------------------------------------------------------------------- /src/main/resources/notepadplusplustickflowlang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 00// 01 02 03 04 9 | 10 | 0x 11 | 0 1 2 3 4 5 6 7 8 9 a b c d e f A B C D E F 12 | 13 | 14 | 15 | 16 | rest async_call stop call label async_sub engine fade switch case return break default endswitch ace random speed input 17 | sub goto 18 | 19 | 20 | 21 | if 22 | 23 | endif 24 | 25 | 26 | 27 | game_ play_ set_ 28 | 29 | if_ 30 | 31 | 32 | 33 | 34 | 35 | 00< 01 02> 03 04 05 06 07 08, 09 10 11 12" 13 14" 15 16 17 18 19 20 21 22 23 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | --------------------------------------------------------------------------------