├── .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 | [](https://github.com/SneakySpook/Tickompiler/releases)
8 | [](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