├── .dockerignore ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── README.md ├── RESOLVER.md ├── RESOURCES.md ├── cli ├── .gitignore ├── dub.json └── source │ └── zrenderer │ └── cli │ └── package.d ├── configgenerator ├── .gitignore ├── dub.json ├── dub.selections.json └── source │ └── zrenderer │ └── configgenerator │ └── package.d ├── dub.json ├── examples ├── 0_93.png ├── 1001_0.png ├── 1870_16_10.png ├── 1_32.png ├── 4012_17_2.png ├── 4075_0.png └── none_7.png ├── resolver_data ├── imf_names.txt ├── job_names.txt ├── job_pal_names.txt ├── job_weapon_names.txt └── shield_names.txt ├── server ├── .gitignore ├── api-doc │ ├── .gitignore │ ├── README.md │ └── index.html ├── api-spec │ ├── 1.1.json │ ├── 1.2.json │ └── 1.3.yaml ├── dub.json └── source │ └── zrenderer │ └── server │ ├── auth.d │ ├── dto │ ├── accesstoken.d │ ├── package.d │ ├── renderrequest.d │ └── renderresponse.d │ ├── globals.d │ ├── package.d │ ├── routes │ ├── admin.d │ ├── package.d │ ├── render.d │ └── token.d │ └── worker.d ├── source ├── app.d ├── config.d ├── draw │ ├── canvas.d │ ├── color.d │ ├── drawobject.d │ ├── package.d │ └── rawimage.d ├── filehelper.d ├── imageformats │ └── png.d ├── linearalgebra │ ├── box.d │ ├── matrix.d │ ├── operations.d │ ├── package.d │ ├── transform.d │ └── vector.d ├── logging.d ├── luamanager.d ├── renderer.d ├── resolver.d ├── resource │ ├── act.d │ ├── base.d │ ├── empty_body_sprite.d │ ├── imf.d │ ├── lua.d │ ├── manager.d │ ├── package.d │ ├── palette.d │ └── spr.d ├── sprite.d ├── uniqueid.d └── validation.d ├── zrenderer.docker.conf └── zrenderer.example.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | output -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | inputs: 11 | gitref: 12 | description: 'Git ref object (e.g. Tag, Branch, Commit)' 13 | 14 | jobs: 15 | release-windows-x64: 16 | runs-on: windows-2019 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.inputs.gitref || github.ref }} 23 | submodules: recursive 24 | 25 | - name: Extract version 26 | id: extract_version 27 | shell: powershell 28 | run: | 29 | $GIT_REF_TAG = ${env:GITHUB_REF} -replace 'refs/tags/','' 30 | $INPUT_TAG = "${{ github.event.inputs.gitref }}" 31 | $VERSION = if ($INPUT_TAG) { $INPUT_TAG } else { $GIT_REF_TAG } 32 | echo GIT_REF_TAG=$GIT_REF_TAG 33 | echo INPUT_TAG=$INPUT_TAG 34 | echo VERSION=$VERSION 35 | echo "VERSION=$VERSION" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append 36 | echo "Checking if the provided VERSION is valid: git rev-parse --verify $VERSION" 37 | git rev-parse --verify "$VERSION" 38 | 39 | - name: Setup dlang 40 | uses: dlang-community/setup-dlang@v1.4.0 41 | with: 42 | compiler: ldc-latest 43 | 44 | - name: Compile 45 | shell: cmd 46 | run: | 47 | call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" 48 | dub build --build=release --compiler=ldc2 :cli 49 | dub build --build=release --compiler=ldc2 :server 50 | dub run --build=release --compiler=ldc2 :configgenerator 51 | move "bin\zrenderer.exe" "zrenderer.exe" 52 | move "bin\zrenderer-server.exe" "zrenderer-server.exe" 53 | move "bin\libcrypto-1_1-x64.dll" "libcrypto-1_1-x64.dll" 54 | move "bin\libssl-1_1-x64.dll" "libssl-1_1-x64.dll" 55 | move "server\api-spec" "api-spec" 56 | move "server\api-doc" "api-doc" 57 | 58 | - name: Package 59 | shell: powershell 60 | run: | 61 | Compress-Archive -Path zrenderer.exe,zrenderer-server.exe,libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,resolver_data,RESOLVER.md,README.md,RESOURCES.md,zrenderer.example.conf,zrenderer.docker.conf,api-spec,api-doc -DestinationPath zrenderer-windows-x64-${{ env.VERSION }}.zip 62 | 63 | - name: Create Release 64 | uses: ncipollo/release-action@v1.14.0 65 | with: 66 | artifactErrorsFailBuild: true 67 | artifacts: zrenderer-windows-x64-${{ env.VERSION }}.zip 68 | omitBody: true 69 | generateReleaseNotes: true 70 | name: ${{ env.VERSION }} 71 | tag: ${{ env.VERSION }} 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | dub.selections.json 3 | docs.json 4 | __dummy.html 5 | docs/ 6 | /zrenderer 7 | zrenderer.so 8 | zrenderer.dylib 9 | zrenderer.dll 10 | zrenderer.a 11 | zrenderer.lib 12 | zrenderer-test-* 13 | *.exe 14 | *.o 15 | *.obj 16 | *.lst 17 | bin/ 18 | zrenderer.conf 19 | output/ 20 | accesstokens.conf 21 | secrets/ 22 | 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "LuaD"] 2 | path = LuaD 3 | url = https://github.com/zhad3/LuaD.git 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 AS build 2 | 3 | RUN apk update && \ 4 | apk add --no-cache build-base autoconf libtool zlib-dev openssl-dev ldc dub && \ 5 | mkdir /zrenderer 6 | 7 | WORKDIR /zrenderer 8 | COPY . . 9 | RUN dub clean && dub build --build=release --config=docker --force :server 10 | 11 | 12 | FROM alpine:3.19 13 | 14 | EXPOSE 11011 15 | 16 | RUN apk update && \ 17 | apk add --no-cache zlib openssl llvm-libunwind && \ 18 | mkdir /zren && \ 19 | addgroup --gid 5000 zren && \ 20 | adduser -D -h /zren -s /bin/sh -u 5000 -G zren zren 21 | 22 | WORKDIR /zren 23 | COPY --from=build --chown=zren:zren /zrenderer/bin/zrenderer-server . 24 | COPY --from=build --chown=zren:zren /zrenderer/resolver_data ./resolver_data 25 | 26 | USER zren 27 | 28 | CMD ["./zrenderer-server"] 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 zhad3 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 | # zrenderer 2 | 3 | Tool to render sprites from the game Ragnarok Online. This tool is available as either a webservice or as a CLI tool. 4 | 5 | ##### Table of contents 6 | * [Required Resources](#required-resources) 7 | * [Usage](#usage) 8 | * [CLI](#cli) 9 | * [Example](#example) 10 | * [Server](#server) 11 | * [Docker/Podman](#docker-podman) 12 | * [Dependencies when building](#dependencies-when-building) 13 | * [Linux](#linux) 14 | * [Windows](#windows) 15 | * [Building](#building) 16 | * [CLI](#cli-1) 17 | * [Server](#server-1) 18 | * [Extra step for Windows](#extra-step-for-windows) 19 | 20 | ## Required Resources 21 | Please see [RESOURCES.md](https://github.com/zhad3/zrenderer/blob/main/RESOURCES.md). 22 | 23 | ## Usage 24 | ### CLI 25 | `./zrenderer -h` 26 | ``` 27 | A tool to render sprites from Ragnarok Online 28 | -c --config Specific config file to use instead of the default. Default: zrenderer.conf 29 | -o --outdir Output directory where all rendered sprites will be saved to. Default: output 30 | --resourcepath Path to the resource directory. All resources are tried to be found within this directory. Default: 31 | -j --job Job id(s) which should be rendered. Can contain multiple comma separated values. Default: 32 | -g --gender Gender of the player character. Possible values are: 'male' (1) or 'female' (0). Default: male 33 | --head Head id which should be used when drawing a player. Default: 1 34 | --outfit The alternative outfit for player characters. Not all characters have alternative outfits. In these cases the default character will be rendered instead. Value of 0 means no outfit. Default: 0 35 | --headgear Headgears which should be attached to the players head. Can contain up to 3 comma separated values. Default: 36 | --garment Garment which should be attached to the players body. Default: 0 37 | --weapon Weapon which should be attached to the players body. Default: 0 38 | --shield Shield which should be attached to the players body. Default: 0 39 | -a --action Action of the job which should be drawn. Default: 0 40 | -f --frame Frame of the action which should be drawn. Set to -1 to draw all frames. Default: -1 41 | --bodyPalette Palette for the body sprite. Set to -1 to use the standard palette. Default: -1 42 | --headPalette Palette for the head sprite. Set to -1 to use the standard palette. Default: -1 43 | --headdir Direction in which the head should turn. This is only applied to player sprites and only to the stand and sit action. Possible values are: straight, left, right or all. If 'all' is set then this direction system is ignored and all frames are interpreted like any other one. Default: all 44 | --madogearType The alternative madogear sprite for player characters. Only applicable to madogear jobs. Possible values are 'robot' (0) and 'suit' (2). Default: robot 45 | --enableShadow Draw shadow underneath the sprite. Default: true 46 | --singleframes Generate single frames of an animation. Default: false 47 | --enableUniqueFilenames If enabled the output filenames will be the checksum of input parameters. This will ensure that each request creates a filename that is unique to the input parameters and no overlapping for the same job occurs. Default: false 48 | --returnExistingFiles Whether to return already existing sprites (true) or always re-render it (false). You should only use this option in conjuction with 'enableUniqueFilenames=true'. Default: false 49 | --canvas Sets a canvas onto which the sprite should be rendered. The canvas requires two options: its size and an origin point inside the canvas where the sprite should be placed. The format is as following: x±±. An origin point of +0+0 is equal to the top left corner. Example: 200x250+100+125. This would create a canvas and place the sprite in the center. Default: 50 | --outputFormat Defines the output format. Possible values are 'png' (0) or 'zip' (1). If zip is chosen the zip will contain png files. Default: png 51 | --loglevel Log level. Defines the minimum level at which logs will be shown. Possible values are: all, trace, info, warning, error, critical, fatal or off. Default: info 52 | --hosts Hostnames of the server. Can contain multiple comma separated values. Default: localhost 53 | --port Port of the server. Default: 11011 54 | --logfile Log file to write to. E.g. /var/log/zrenderer.log. Leaving it empty will log to stdout. Default: 55 | --tokenfile Access tokens file. File in which access tokens will be stored in. If the file does not exist it will be generated. Default: accesstokens.conf 56 | --enableCORS Setting this to true will add CORS headers to all responses as well as adding an additional OPTIONS route that returns the CORS headers. Default: false 57 | --allowCORSOrigin Comma separated list of origins that are allowed access through CORS. Set this to a single '*' to allow access from any origin. Example: https://example.com. Default: 58 | --enableSSL Whether to use TLS/SSL to secure the connection. You will also need to set the certificate and private key when enabling this setting. We recommend not enabling this feature but instead use a reverse proxy that handles HTTPS for you. Default: false 59 | --certificateChainFile Path to the certificate chain file used by TLS/SSL. Default: 60 | --privateKeyFile Path to the private key file used by TLS/SSL. Default: 61 | -h --help This help information. 62 | ``` 63 | Options _hosts_, _port_, _logfile_ and _tokenfile_ are ignored for the CLI tool. 64 | ### Example 65 | If not otherwise specified the requested sprites will be renderered as an APNG animation of the first action (0, Stand). 66 | 67 | **Render monster with id 1001 (Scorpion) with action 0 (Stand, default)** 68 | `./zrenderer --job=1001` 69 | Result: ![Scorpion](examples/1001_0.png) 70 | 71 | **Render frame 10 of the monster with id 1870 (Necromancer) of action 16 (Attack)** 72 | `./zrenderer --job=1870 --action=16 --frame=10` 73 | Result: ![Necromancer](examples/1870_16_10.png) 74 | 75 | **Render character with id 4012 (Sniper), action 17 (Sit) while looking to the left (indicated by frame 2)** 76 | `./zrenderer --job=4012 --action=17 --frame=2` 77 | Result: ![Sniper](examples/4012_17_2.png) 78 | 79 | **Render character with id 1 (Swordman), action 32 (Ready) with headgears 4 (Flower), 125 (Blush), garment 1 (Wings), weapon 1 (Sword), head 4 and gender female.** 80 | `./zrenderer --job=1 --headgear=4,125 --garment=1 --weapon=2 --head=4 --gender=female --action=32` 81 | Result: ![Swordman](examples/1_32.png) 82 | 83 | **Render character with id 0 (Novice), action 93 (Attack) with garment 1 (Wings), weapon 1 (Sword), head 15, gender male** 84 | **and place the sprite in a canvas of size 200x200px at x=75 and y=175.** 85 | `./zrenderer --job=0 --head=15 --bodyPalette=1 --weapon=2 --garment=1 --gender=male --action=93 --canvas=200x200+75+175` 86 | Result: 87 | 88 | | Image border | 89 | | --- | 90 | | ![Novice](examples/0_93.png) | 91 | 92 | **Render only the head of a player: Action 7 (Stand) with head 18, headgear 311 (Baby Chick), palette 3, gender female and look straight.** 93 | `./zrenderer --job=none --action=7 --head=18 --headgear=311 --headPalette=3 --gender=female` 94 | Result: ![Head](examples/none_7.png) 95 | 96 | 97 | If you're wondering how these IDs get mapped to the actual sprites take a look at [RESOLVER.md](RESOLVER.md). 98 | ## Server 99 | `./zrenderer-server -h` 100 | ``` 101 | Same as CLI 102 | ``` 103 | The server will listen on the _hosts_, bind to _port_, write its logs to _logfile_ and read the access tokens from the _tokenfile_. 104 | 105 | When running the server for the first time and no access token file has been specified the server will automatically generate one 106 | and print the token to the console. You will need that token to make requests to the server. 107 | 108 | You can find the openApi specifications here: [OpenAPI specifications](https://github.com/zhad3/zrenderer/tree/main/server/api-spec). 109 | 110 | And documentation here: https://github.com/zhad3/zrenderer/tree/main/server/api-doc. 111 | The html can be viewed directly here: https://z0q.neocities.org/ragnarok-online-tools/zrenderer/api/ 112 | 113 | ## Docker/Podman 114 | You can use the pre-built and published images to run the server. 115 | 116 | In a terminal run the following command to get up and running: 117 | 118 |
119 | Docker (click to view) 120 | 121 | ``` 122 | mkdir secrets # The auto-generated accesstokens will be stored here 123 | 124 | docker run -d --name zrenderer \ 125 | -v ./zrenderer.docker.conf:/zren/zrenderer.conf \ 126 | -v ./output:/zren/output \ 127 | -v ./my-resources:/zren/resources \ 128 | -v ./secrets:/zren/secrets \ 129 | -p 11011:11011 \ 130 | --user root \ 131 | zhade/zrenderer:latest 132 | ``` 133 |
134 |
135 | Podman (click to view) 136 | 137 | ``` 138 | mkdir secrets # The auto-generated accesstokens will be stored here 139 | 140 | podman run -d --name zrenderer \ 141 | -v ./zrenderer.docker.conf:/zren/zrenderer.conf:z \ 142 | -v ./output:/zren/output:z \ 143 | -v ./my-resources:/zren/resources:z \ 144 | -v ./secrets:/zren/secrets:z \ 145 | -p 11011:11011 \ 146 | --userns keepid:uid=5000,gid=5000 \ 147 | zhade/zrenderer:latest 148 | ``` 149 |
150 | 151 | You will need to provide three directory/files: 152 | - A configuration file (see the example `zrenderer.docker.conf`) 153 | - The output directory where the server will store the images 154 | - The resource directory where all the assets from Ragnarok Online reside in 155 | 156 | ## Dependencies when building 157 | When building for the first time libpng and lua5.1 will be compiled which require a c-compiler. 158 | 159 | ## Linux 160 | `gcc`, `binutils`, `autoconf`, `libtool`, `zlib`, `openssl`. 161 | 162 | When available choose the dev versions of the packages. 163 | 164 | #### OpenSSL 3.x.x 165 | If you are using `openssl version 1.x.x` and receive errors try to downgrade `vibe-d`. 166 | 167 | Modify `/server/dub.json` 168 | and downgrade the version to 0.9.3: 169 | ```diff 170 | "dependencies": { 171 | "zconfig": "~>1.0", 172 | "libpng-apng": "~>1.0", 173 | "luad": {"path": "../LuaD/"}, 174 | "zencoding:windows949": "~>1.0", 175 | - "vibe-d:http": "==0.9.6" 176 | + "vibe-d:http": "==0.9.3" 177 | }, 178 | ``` 179 | Make sure to delete the existing `dub.selections.json` and try to rebuild. 180 | ``` 181 | $ rm /server/dub.selections.json 182 | $ dub build :server 183 | ``` 184 | 185 | ## Windows 186 | [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019) 187 | 188 | ## Building 189 | First clone the repository: 190 | Run: 191 | ``` 192 | git clone https://github.com/zhad3/zrenderer.git 193 | cd zrenderer 194 | git submodule update --init 195 | ``` 196 | 197 | Then you can build the CLI and the Server using `dub`. 198 | 199 | The automatic releases are build using LDC2. DMD seems to have issues linking (at least on Windows). 200 | You can download LDC2 binaries on their Github: https://github.com/ldc-developers/ldc/releases 201 | 202 | ### CLI 203 | Run `dub build --compiler=ldc2 :cli`. 204 | Release build: 205 | Run `dub build --compiler=ldc2 --build=release :cli`. 206 | ### Server 207 | Run `dub build --compiler=ldc2 :server`. 208 | Release build: 209 | Run `dub build --compiler=ldc2 --build=release :server`. 210 | 211 | To add outlines to the sprites boxes for debugging purposes add `--debug=outline` to the build command: 212 | `dub build --compiler=ldc2 --debug=outline :cli` 213 | 214 | Example output: 215 | ![Clown](examples/4075_0.png) 216 | 217 | ### Extra step for Windows: 218 | The above commands must be executed in the [Visual Studio Developer Command Prompt](https://docs.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell) 219 | which will be available when installing the Build Tools for Visual Studio (see above). 220 | 221 | Depending on your system the command prompt will be available for x86 and x64. Whichever you choose will build zrenderer for the same architecture. 222 | 223 | --- 224 | All Ragnarok Online related media and content are copyrighted © by Gravity Co., Ltd & Lee Myoungjin(studio DTDS) and have all rights reserved. 225 | 226 | -------------------------------------------------------------------------------- /RESOLVER.md: -------------------------------------------------------------------------------- 1 | # Resolver 2 | ## About this document 3 | Details about how the resolver is looking up the resources to draw the sprite from the input variables. 4 | 5 | Almost everything written here is taken directly from the source code [resolver.d](https://github.com/zhad3/zrenderer/blob/main/source/resolver.d). 6 | 7 | ## Sprite types 8 | As of this writing there are the following sprite types that can be composed to a fully rendered image: 9 | 10 | - Job (or "Body", this includes Monsters, NPCs, Homunculus and Mercenaries) 11 | - Head 12 | - Headgear 13 | - Garment 14 | - Weapon 15 | - Weapon Slash 16 | - Shield 17 | - Shadow 18 | - ~~Cart~~ (Currently not supported) 19 | - ~~Companion (e.g. Falcon)~~ (Currently not supported) 20 | 21 | ## Input to Sprite type mapping and resource resolving 22 | 23 | Preface: Gender is used in a lot of places and is interpreted as following: 24 | | Gender (-\-gender) | Name | 25 | | --- | --- | 26 | | 0 | 여 | 27 | | 1 | 남 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 49 | 50 | 51 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 91 | 92 | 104 | 105 | 106 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
Sprite typeInputsResource
Job--job
--gender
--outfit
42 | Player
43 | jobname=job_names.txt (Line #<job>, if the job id is greater than 4000 then 3950 is subtracted)

44 | Player: data/sprite/인간족/몸통/<gender>/<job>_<gender>.{spr,act}
45 | Player (doram): data/sprite/도람족/몸통/<gender>/<jobname>_<gender>.{spr,act}
46 | Player (outfit): data/sprite/인간족/몸통/<gender>/costume_<outfit>/<jobname>_<gender>_<outfit>.{spr,act}
47 | Player (doram,outfit): data/sprite/도람족/몸통/<gender>/costume_<outfit>/<jobname>_<gender>_<outfit>.{spr,act} 48 |
52 | Non player
53 | jobname=data/luafiles514/lua files/datainfo/jobname_f.lub#ReqJobName(<job>) (function takes value from jobname.lub.

54 | Monster: data/sprite/몬스터/<jobname>.{spr,act}
55 | NPC: data/sprite/npc/<jobname>.{spr,act}
56 | Mercenary: data/sprite/인간족/몸통/<jobname>.{spr,act}
57 | Homunculus: data/sprite/homun/<jobname>.{spr,act}

58 | Hint: For the differentiation between the nonplayer job ids have a look at resolver.d. 59 |
Head--job
--head
--gender
65 | Human: data/sprite/인간족/머리통/<gender>/<head>_<gender>.{spr,act}
66 | Doram: data/sprite/도람족/머리통/<gender>/<head>_<gender>.{spr,act} 67 |
Headgear--headgear
--gender
73 | name=data/luafiles514/lua files/datainfo/accname_f.lub#ReqAccName(<headgear>) (function takes value from accname.lub.

74 | Headgear: data/sprite/악세사리/<gender>/<gender><name>.{spr,act}

75 | Hint: For a list of human readable ids take a look at the lua file accessoryid.lua 76 |
Garment--job
--gender
--garment
82 | name=data/luafiles514/lua files/datainfo/spriterobename_f.lub#ReqRobSprName_V2(<garment>) (function takes value from spriterobename.lub.
83 | jobname=job_names.txt (Line #<job>, if the job id is greater than 4000 then 3950 is subtracted)

84 | Garment: data/sprite/로브/<name>/<gender>/<jobname>_<gender>.{spr,act}
85 | Garment (fallback): data/sprite/로브/<name>/<name>.{spr,act}

86 | Hint: For a list of human readable ids take a look at the lua file spriterobeid.lua 87 |
Weapon--job
--gender
--weapon
93 | Player
94 | name=data/luafiles514/lua files/datainfo/weapontable_f.lub#ReqWeaponName(<weapon>) (function takes value from weapontable.lub.
95 | If no name is found then try again with a new weapon id: 96 | weapon=data/luafiles514/lua files/datainfo/weapontable_f.lub#GetRealWeaponId(<weapon>)
97 | If there is still no name then just use the weapon id itself as name: 98 | name=_<weapon>
99 | jobname=job_weapon_names.txt (Line #<job>, if the job id is greater than 4000 then 3950 is subtracted)

100 | Human: data/sprite/인간족/<jobname>_<gender><name>.{spr,act}
101 | Doram: data/sprite/도람족/<jobname>_<gender><name>.{spr,act}

102 | Hint: For a list of human readable ids take a look at the lua file weapontable.lua 103 |
107 | Mercenary
108 | Archer: data/sprite/인간족/용병/활용병_활.{spr,act}
109 | Lancer: data/sprite/인간족/용병/창용병_창.{spr,act}
110 | Swordsman: data/sprite/인간족/용병/검용병_검.{spr,act} 111 |
Weapon SlashSame as Weapon but with a _검광 suffix.
Shield--job
--gender
--shield
121 | name=shield_names.txt (Line #<shield>)
122 | name(fallback)=_<shield>_방패
123 | jobname=job_names.txt (Line #<job>, if the job id is greater than 4000 then 3950 is subtracted)

124 | Shield: data/sprite/방패/<jobname>/<jobname>_<gender><name>.{spr,act} 125 |
Shadow--enableShadowShadow: data/sprite/shadow.{spr,act}
134 | 135 | ## Palettes 136 | Body (+outfits) and head sprites can be colored via palettes. These are resolved as following. 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 155 | 156 | 157 | 158 | 159 | 163 | 164 | 165 |
Palette typeInputsResource
Job--job
--gender
--bodyPalette
--outfit
149 | jobname=job_pal_names.txt (Line #<bodyPalette>)

150 | Human: data/palette/몸/<jobname>_<gender>_<bodyPalette>.pal
151 | Doram: data/palette/도람족/body/<jobname>_<gender>_<bodyPalette>.pal
152 | Human (outfit): data/palette/몸/costume_<outfit>/<jobname>_<gender>_<bodyPalette>_<outfit>.pal
153 | Doram (outfit): data/palette/도람족/body/costume_<outfit>/<jobname>_<gender>_<bodyPalette>_<outfit>.pal 154 |
Head--job
--gender
--head
--headPalette
160 | Human: data/palette/머리/머리<head>_<gender>_<headPalette>.pal
161 | Doram: data/palette/도람족/머리/머리<head>_<gender>_<headPalette>.pal
162 |
166 | 167 | -------------------------------------------------------------------------------- /RESOURCES.md: -------------------------------------------------------------------------------- 1 | # Resources required to run zrenderer 2 | 3 | The renderer just as the official client requires a certain amount of files to run which are included in the GRF files. 4 | 5 | The files need to be **unencrypted** and **uncompressed**. It was decided to not directly reference the GRF because it is easier to implement. It also reduces the load on memory usage (but increases disk usage). 6 | 7 | You can use https://github.com/zhad3/zextractor to extract the required files. It allows setting filters which extract only the required files. 8 | 9 | The following files are used by the renderer. Please note that the lua files are **mandatory**. Obviously so are the sprite files. In case where you do not want to render e.g. NPCs or monsters you could omit these. 10 | 11 | ``` 12 | data\sprite\인간족\* 13 | data\sprite\도람족\* 14 | data\sprite\방패\* 15 | data\sprite\로브\* 16 | data\sprite\악세사리\* 17 | data\sprite\몬스터\* 18 | data\sprite\homun\* 19 | data\sprite\npc\* 20 | data\palette\* 21 | data\imf\* 22 | data\luafiles514\lua files\datainfo\accessoryid.* 23 | data\luafiles514\lua files\datainfo\accname.* 24 | data\luafiles514\lua files\datainfo\accname_f.* 25 | data\luafiles514\lua files\datainfo\spriterobeid.* 26 | data\luafiles514\lua files\datainfo\spriterobename.* 27 | data\luafiles514\lua files\datainfo\spriterobename_f.* 28 | data\luafiles514\lua files\datainfo\weapontable.* 29 | data\luafiles514\lua files\datainfo\weapontable_f.* 30 | data\luafiles514\lua files\datainfo\jobidentity.* 31 | data\luafiles514\lua files\datainfo\npcidentity.* 32 | data\luafiles514\lua files\datainfo\jobname.* 33 | data\luafiles514\lua files\datainfo\jobname_f.* 34 | data\luafiles514\lua files\datainfo\shadowtable.* 35 | data\luafiles514\lua files\datainfo\shadowtable_f.* 36 | data\luafiles514\lua files\skillinfoz\jobinheritlist.* 37 | data\luafiles514\lua files\spreditinfo\2dlayerdir_f.* 38 | data\luafiles514\lua files\spreditinfo\biglayerdir_female.* 39 | data\luafiles514\lua files\spreditinfo\biglayerdir_male.* 40 | data\luafiles514\lua files\spreditinfo\_new_2dlayerdir_f.* 41 | data\luafiles514\lua files\spreditinfo\_new_biglayerdir_female.* 42 | data\luafiles514\lua files\spreditinfo\_new_biglayerdir_male.* 43 | data\luafiles514\lua files\spreditinfo\_new_smalllayerdir_female.* 44 | data\luafiles514\lua files\spreditinfo\_new_smalllayerdir_male.* 45 | data\luafiles514\lua files\spreditinfo\smalllayerdir_female.* 46 | data\luafiles514\lua files\spreditinfo\smalllayerdir_male.* 47 | data\luafiles514\lua files\offsetitempos\offsetitempos_f.* 48 | data\luafiles514\lua files\offsetitempos\offsetitempos.* 49 | data\sprite\shadow.* 50 | ``` 51 | The format is ready to be used for the aforementioned zextractor. 52 | 53 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | dub.selections.json 3 | docs.json 4 | __dummy.html 5 | docs/ 6 | /cli 7 | cli.so 8 | cli.dylib 9 | cli.dll 10 | cli.a 11 | cli.lib 12 | cli-test-* 13 | *.exe 14 | *.o 15 | *.obj 16 | *.lst 17 | bin/ -------------------------------------------------------------------------------- /cli/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A command line interface for zrenderer.", 3 | "name": "cli", 4 | "targetName": "zrenderer", 5 | "targetPath": "../bin", 6 | "targetType": "executable", 7 | "dependencies": { 8 | "zconfig": "~>1.0", 9 | "libpng-apng": "~>1.0", 10 | "luad": {"path": "../LuaD/"}, 11 | "zencoding:windows949": "~>1.0" 12 | }, 13 | "sourcePaths": ["../source/"], 14 | "importPaths": ["../source/"], 15 | "mainSourceFile": "source/zrenderer/cli/package.d", 16 | "configurations": [ 17 | { "name": "default" }, 18 | { 19 | "name": "docker", 20 | "dflags": ["-link-defaultlib-shared=false"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /cli/source/zrenderer/cli/package.d: -------------------------------------------------------------------------------- 1 | module zrenderer.cli; 2 | 3 | import config : Config; 4 | import zconfig : initializeConfig, getConfigArguments; 5 | import logging : LogLevel, BasicLogger; 6 | 7 | enum usage = "A tool to render sprites from Ragnarok Online"; 8 | 9 | int main(string[] args) 10 | { 11 | string[] configArgs = getConfigArguments!Config("zrenderer.conf", args); 12 | 13 | if (configArgs.length > 0) 14 | { 15 | import std.array : insertInPlace; 16 | 17 | args.insertInPlace(1, configArgs); 18 | } 19 | import std.getopt : GetOptException; 20 | import std.conv : ConvException; 21 | 22 | Config config; 23 | bool helpWanted = false; 24 | 25 | try 26 | { 27 | config = initializeConfig!(Config, usage)(args, helpWanted); 28 | 29 | import std.exception : enforce; 30 | import validation : isJobArgValid, isCanvasArgValid; 31 | 32 | enforce!GetOptException(isJobArgValid(config.job), "job ids are not valid."); 33 | enforce!GetOptException(isCanvasArgValid(config.canvas), "canvas is not valid."); 34 | } 35 | catch (GetOptException e) 36 | { 37 | import std.stdio : stderr; 38 | 39 | stderr.writefln("Error parsing options: %s", e.msg); 40 | return 1; 41 | } 42 | catch (ConvException e) 43 | { 44 | import std.stdio : stderr; 45 | 46 | stderr.writefln("Error parsing options: %s", e.msg); 47 | return 1; 48 | } 49 | 50 | if (helpWanted) 51 | { 52 | return 0; 53 | } 54 | 55 | import app : run, createOutputDirectory; 56 | 57 | createOutputDirectory(config.outdir); 58 | 59 | auto logger = BasicLogger.get(config.loglevel); 60 | 61 | void consoleLogger(LogLevel logLevel, string msg) 62 | { 63 | logger.log(logLevel,msg); 64 | } 65 | 66 | run(cast(immutable) config, &consoleLogger); 67 | 68 | return 0; 69 | } 70 | -------------------------------------------------------------------------------- /configgenerator/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | /configgenerator 6 | configgenerator.so 7 | configgenerator.dylib 8 | configgenerator.dll 9 | configgenerator.a 10 | configgenerator.lib 11 | configgenerator-test-* 12 | *.exe 13 | *.o 14 | *.obj 15 | *.lst 16 | -------------------------------------------------------------------------------- /configgenerator/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Generates an example config.", 3 | "name": "configgenerator", 4 | "targetType": "executable", 5 | "targetName": "zrenderer-configgenerator", 6 | "targetPath": "../bin", 7 | "dependencies": { 8 | "zconfig": ">=1.0.0" 9 | }, 10 | "sourceFiles": ["../source/logging.d", "../source/config.d"] 11 | } 12 | -------------------------------------------------------------------------------- /configgenerator/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "zconfig": "1.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /configgenerator/source/zrenderer/configgenerator/package.d: -------------------------------------------------------------------------------- 1 | import config; 2 | import zconfig : writeExampleConfigFile; 3 | 4 | int main() 5 | { 6 | import std.file : FileException; 7 | 8 | try 9 | { 10 | writeExampleConfigFile!Config("zrenderer.example.conf"); 11 | } 12 | catch (FileException e) 13 | { 14 | import std.stdio : stderr; 15 | 16 | stderr.writeln(e.msg); 17 | return e.errno; 18 | } 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "zhad3" 4 | ], 5 | "copyright": "Copyright © 2021, zhad3", 6 | "description": "Renders a sprite given a set of parameters", 7 | "license": "MIT", 8 | "name": "zrenderer", 9 | "targetPath": "bin", 10 | "targetType": "none", 11 | "subPackages": [ 12 | "./cli/", 13 | "./server/", 14 | "./configgenerator" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/0_93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/0_93.png -------------------------------------------------------------------------------- /examples/1001_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/1001_0.png -------------------------------------------------------------------------------- /examples/1870_16_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/1870_16_10.png -------------------------------------------------------------------------------- /examples/1_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/1_32.png -------------------------------------------------------------------------------- /examples/4012_17_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/4012_17_2.png -------------------------------------------------------------------------------- /examples/4075_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/4075_0.png -------------------------------------------------------------------------------- /examples/none_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhad3/zrenderer/25a2567e466956aa274fa03dbe73efc627a6f9cd/examples/none_7.png -------------------------------------------------------------------------------- /resolver_data/imf_names.txt: -------------------------------------------------------------------------------- 1 | 초보자 2 | 검사 3 | 마법사 4 | 궁수 5 | 성직자 6 | 상인 7 | 도둑 8 | 기사 9 | 프리스트 10 | 위저드 11 | 제철공 12 | 헌터 13 | 어세신 14 | 페코페코_기사 15 | 크루세이더 16 | 몽크 17 | 세이지 18 | 로그 19 | 연금술사 20 | 바드 21 | 무희 22 | 신페코크루세이더 23 | 결혼 24 | 초보자 25 | 건너 26 | 닌자 27 | 결혼 28 | 결혼 29 | 결혼 30 | 결혼 31 | 결혼 32 | 초보자 33 | 초보자 34 | 초보자 35 | 초보자 36 | 초보자 37 | 초보자 38 | 초보자 39 | 초보자 40 | 초보자 41 | 초보자 42 | 초보자 43 | 초보자 44 | 초보자 45 | 초보자 46 | 초보자 47 | 초보자 48 | 초보자 49 | 초보자 50 | 초보자 51 | 초보자 52 | 초보자 53 | 검사 54 | 마법사 55 | 궁수 56 | 성직자 57 | 상인 58 | 도둑 59 | 기사 60 | 프리스트 61 | 하이위저드 62 | 제철공 63 | 헌터 64 | 어세신크로스 65 | 페코페코_기사 66 | 크루세이더 67 | 몽크 68 | 세이지 69 | 로그 70 | 연금술사 71 | 바드 72 | 무희 73 | 신페코크루세이더 74 | 초보자 75 | 검사 76 | 마법사 77 | 궁수 78 | 성직자 79 | 상인 80 | 도둑 81 | 기사 82 | 프리스트 83 | 위저드 84 | 제철공 85 | 헌터 86 | 어세신 87 | 페코페코_기사 88 | 크루세이더 89 | 몽크 90 | 세이지 91 | 로그 92 | 연금술사 93 | 바드 94 | 무희 95 | 신페코크루세이더 96 | 초보자 97 | 태권소년 98 | 태권소년 99 | 태권소년 100 | 위저드 101 | 성직자 102 | 기사 103 | 세이지 104 | 초보자 105 | 기사 106 | 워록 107 | 헌터 108 | 프리스트 109 | 마도기어 110 | 어세신크로스 111 | 기사 112 | 워록 113 | 헌터 114 | 프리스트 115 | 마도기어 116 | 어세신크로스 117 | 크루세이더 118 | 프로페서 119 | 바드 120 | 무희 121 | 몽크 122 | 연금술사 123 | 로그 124 | 가드 125 | 프로페서 126 | 바드 127 | 무희 128 | 몽크 129 | 연금술사 130 | 로그 131 | 페코페코_기사 132 | 페코페코_기사 133 | 신페코크루세이더 134 | 신페코크루세이더 135 | 레인져늑대 136 | 레인져늑대 137 | 마도기어 138 | 마도기어 139 | 페코페코_기사 140 | 페코페코_기사 141 | 페코페코_기사 142 | 페코페코_기사 143 | 페코페코_기사 144 | 페코페코_기사 145 | 페코페코_기사 146 | 페코페코_기사 147 | 기사 148 | 위저드 149 | 헌터 150 | 프리스트 151 | 마도기어 152 | 어세신크로스 153 | 가드 154 | 프로페서 155 | 바드 156 | 무희 157 | 몽크 158 | 연금술사 159 | 로그 160 | 페코페코_기사 161 | 신페코크루세이더 162 | 레인져늑대 163 | 마도기어 164 | 초보자 165 | 두꺼비닌자 166 | 페코건너 167 | 페코페코_기사 168 | 두꺼비소울링커 169 | 화이트스미스멧돼지 170 | 상인멧돼지 171 | 제네릭멧돼지 172 | 크리에이터멧돼지 173 | 타조궁수 174 | 권성포링 175 | 노비스포링 176 | 몽크알파카 177 | 복사알파카 178 | 슈라알파카 179 | 슈퍼노비스포링 180 | 아크비숍알파카 181 | 여우마법사 182 | 여우세이지 183 | 여우소서러 184 | 여우워록 185 | 여우위저드 186 | 여우프로페서 187 | 여우하이위저드 188 | 연금술사멧돼지 189 | 제철공멧돼지 190 | 챔피온알파카 191 | 켈베로스길로틴크로스 192 | 켈베로스도둑 193 | 켈베로스로그 194 | 켈베로스쉐도우체이서 195 | 켈베로스스토커 196 | 켈베로스어쎄신 197 | 켈베로스어쎄신크로스 198 | 타조무희 199 | 타조민스트럴 200 | 타조바드 201 | 타조스나이퍼 202 | 타조원더러 203 | 타조짚시 204 | 타조크라운 205 | 타조헌터 206 | 태권소년포링 207 | 프리스트알파카 208 | 하이프리스트알파카 209 | 노비스포링 210 | 페코페코_기사 211 | 여우마법사 212 | 타조궁수 213 | 복사알파카 214 | 상인멧돼지 215 | 타조헌터 216 | 켈베로스어쎄신 217 | 몽크알파카 218 | 여우세이지 219 | 켈베로스로그 220 | 연금술사멧돼지 221 | 타조바드 222 | 타조무희 223 | 슈퍼노비스포링 224 | 여우워록 225 | 아크비숍알파카 226 | 켈베로스길로틴크로스 227 | 여우소서러 228 | 타조민스트럴 229 | 타조원더러 230 | 슈라알파카 231 | 제네릭멧돼지 232 | 켈베로스도둑 233 | 켈베로스쉐도우체이서 234 | 노비스포링 235 | 페코페코_기사 236 | 여우마법사 237 | 타조궁수 238 | 복사알파카 239 | 상인멧돼지 240 | 켈베로스도둑 241 | 초보자 242 | 초보자 243 | 슈퍼노비스포링 244 | 슈퍼노비스포링 245 | 프리스트알파카 246 | 여우위저드 247 | 제철공멧돼지 248 | 마도기어 249 | 타조레인져 250 | 사자기사 251 | 사자로드나이트 252 | 사자로얄가드 253 | 사자룬나이트 254 | 사자크루세이더 255 | 사자팔라딘 256 | 마도기어 257 | 타조레인져 258 | 사자기사 259 | 사자로얄가드 260 | 사자룬나이트 261 | 사자크루세이더 262 | 닌자 263 | 닌자 264 | frog_kagerou 265 | frog_oboro 266 | rebellion 267 | peco_rebellion 268 | 초보자 269 | summoner 270 | cart_summoner 271 | summoner 272 | cart_summoner 273 | 닌자 274 | 닌자 275 | 닌자 276 | 태권소년 277 | 태권소년 278 | 위저드 279 | 건너 280 | rebellion 281 | 두꺼비닌자 282 | frog_kagerou 283 | frog_oboro 284 | 태권소년포링 285 | 권성포링 286 | 두꺼비소울링커 287 | 페코건너 288 | peco_rebellion 289 | 태권소년 290 | 태권소년 291 | 위저드 292 | 태권소년 293 | 위저드 294 | 태권소년 295 | 태권소년 296 | 태권소년 297 | 태권소년 298 | 태권소년 299 | 태권소년 300 | 초보자 301 | 초보자 302 | 초보자 303 | DRAGON_KNIGHT 304 | MEISTER 305 | SHADOW_CROSS 306 | 워록 307 | CARDINAL 308 | WINDHAWK 309 | IMPERIAL_GUARD 310 | BIOLO 311 | ABYSS_CHASER 312 | 프로페서 313 | INQUISITOR 314 | TROUBADOUR 315 | TROUVERE 316 | DRAGON_KNIGHT_RIDING 317 | MEISTER_RIDING 318 | SHADOW_CROSS_RIDING 319 | 워록 320 | CARDINAL_RIDING 321 | WINDHAWK_RIDING 322 | IMPERIAL_GUARD_RIDING 323 | BIOLO_RIDING 324 | ABYSS_CHASER_RIDING 325 | 프로페서 326 | INQUISITOR_RIDING 327 | TROUBADOUR_RIDING 328 | TROUVERE_RIDING 329 | WOLF_WINDHAWK 330 | MEISTER_MADOGEAR1 331 | DRAGON_KNIGHT_CHICKEN 332 | IMPERIAL_GUARD_CHICKEN 333 | 초보자 334 | 초보자 335 | 초보자 336 | 초보자 337 | 초보자 338 | 초보자 339 | 초보자 340 | 초보자 341 | 초보자 342 | 초보자 343 | 초보자 344 | 초보자 345 | 초보자 346 | 초보자 347 | 초보자 348 | 초보자 349 | 초보자 350 | 초보자 351 | 초보자 352 | 초보자 353 | SKY_EMPEROR 354 | SOUL_ASCETIC 355 | SHINKIRO 356 | SHIRANUI 357 | NIGHT_WATCH 358 | HYPER_NOVICE 359 | SPIRIT_HANDLER 360 | SKY_EMPEROR_RIDING 361 | SOUL_ASCETIC_RIDING 362 | SHINKIRO_RIDING 363 | SHIRANUI_RIDING 364 | NIGHT_WATCH_RIDING 365 | HYPER_NOVICE_RIDING 366 | SPIRIT_HANDLER_RIDING 367 | SKY_EMPEROR2 368 | -------------------------------------------------------------------------------- /resolver_data/job_names.txt: -------------------------------------------------------------------------------- 1 | 초보자 2 | 검사 3 | 마법사 4 | 궁수 5 | 성직자 6 | 상인 7 | 도둑 8 | 기사 9 | 프리스트 10 | 위저드 11 | 제철공 12 | 헌터 13 | 어세신 14 | 페코페코_기사 15 | 크루세이더 16 | 몽크 17 | 세이지 18 | 로그 19 | 연금술사 20 | 바드 21 | 무희 22 | 신페코크루세이더 23 | 결혼 24 | 슈퍼노비스 25 | 건너 26 | 닌자 27 | 산타 28 | 여름 29 | 한복 30 | 옥토버패스트 31 | 여름2 32 | 초보자 33 | 초보자 34 | 초보자 35 | 초보자 36 | 초보자 37 | 초보자 38 | 초보자 39 | 초보자 40 | 초보자 41 | 초보자 42 | 초보자 43 | 초보자 44 | 초보자 45 | 초보자 46 | 초보자 47 | 초보자 48 | 초보자 49 | 초보자 50 | 초보자 51 | 초보자 52 | 초보자 53 | 검사 54 | 마법사 55 | 궁수 56 | 성직자 57 | 상인 58 | 도둑 59 | 로드나이트 60 | 하이프리 61 | 하이위저드 62 | 화이트스미스 63 | 스나이퍼 64 | 어쌔신크로스 65 | 로드페코 66 | 팔라딘 67 | 챔피온 68 | 프로페서 69 | 스토커 70 | 크리에이터 71 | 클라운 72 | 집시 73 | 페코팔라딘 74 | 초보자 75 | 검사 76 | 마법사 77 | 궁수 78 | 성직자 79 | 상인 80 | 도둑 81 | 기사 82 | 프리스트 83 | 위저드 84 | 제철공 85 | 헌터 86 | 어세신 87 | 페코페코_기사 88 | 크루세이더 89 | 몽크 90 | 세이지 91 | 로그 92 | 연금술사 93 | 바드 94 | 무희바지 95 | 구페코크루세이더 96 | 슈퍼노비스 97 | 태권소년 98 | 권성 99 | 권성융합 100 | 소울링커 101 | 성직자 102 | 기사 103 | 세이지 104 | 초보자 105 | 룬나이트 106 | 워록 107 | 레인져 108 | 아크비숍 109 | 미케닉 110 | 길로틴크로스 111 | 룬나이트 112 | 워록 113 | 레인져 114 | 아크비숍 115 | 미케닉 116 | 길로틴크로스 117 | 가드 118 | 소서러 119 | 민스트럴 120 | 원더러 121 | 슈라 122 | 제네릭 123 | 쉐도우체이서 124 | 가드 125 | 소서러 126 | 민스트럴 127 | 원더러 128 | 슈라 129 | 제네릭 130 | 쉐도우체이서 131 | 룬나이트쁘띠 132 | 룬나이트쁘띠 133 | 그리폰가드 134 | 그리폰가드 135 | 레인져늑대 136 | 레인져늑대 137 | 마도기어 138 | 마도기어 139 | 룬나이트쁘띠2 140 | 룬나이트쁘띠2 141 | 룬나이트쁘띠3 142 | 룬나이트쁘띠3 143 | 룬나이트쁘띠4 144 | 룬나이트쁘띠4 145 | 룬나이트쁘띠5 146 | 룬나이트쁘띠5 147 | 룬나이트 148 | 워록 149 | 레인져 150 | 아크비숍 151 | 미케닉 152 | 길로틴크로스 153 | 가드 154 | 소서러 155 | 민스트럴 156 | 원더러 157 | 슈라 158 | 제네릭 159 | 쉐도우체이서 160 | 룬나이트쁘띠 161 | 그리폰가드 162 | 레인져늑대 163 | 마도기어 164 | 초보자 165 | 두꺼비닌자 166 | 페코건너 167 | 페코검사 168 | 두꺼비소울링커 169 | 화이트스미스멧돼지 170 | 상인멧돼지 171 | 제네릭멧돼지 172 | 크리에이터멧돼지 173 | 타조궁수 174 | 권성포링 175 | 노비스포링 176 | 몽크알파카 177 | 복사알파카 178 | 슈라알파카 179 | 슈퍼노비스포링 180 | 아크비숍알파카 181 | 여우마법사 182 | 여우세이지 183 | 여우소서러 184 | 여우워록 185 | 여우위저드 186 | 여우프로페서 187 | 여우하이위저드 188 | 연금술사멧돼지 189 | 제철공멧돼지 190 | 챔피온알파카 191 | 켈베로스길로틴크로스 192 | 켈베로스도둑 193 | 켈베로스로그 194 | 켈베로스쉐도우체이서 195 | 켈베로스스토커 196 | 켈베로스어쎄신 197 | 켈베로스어쎄신크로스 198 | 타조무희 199 | 타조민스트럴 200 | 타조바드 201 | 타조스나이퍼 202 | 타조원더러 203 | 타조짚시 204 | 타조크라운 205 | 타조헌터 206 | 태권소년포링 207 | 프리스트알파카 208 | 하이프리스트알파카 209 | 노비스포링 210 | 페코검사 211 | 여우마법사 212 | 타조궁수 213 | 복사알파카 214 | 상인멧돼지 215 | 타조헌터 216 | 켈베로스어쎄신 217 | 몽크알파카 218 | 여우세이지 219 | 켈베로스로그 220 | 연금술사멧돼지 221 | 타조바드 222 | 타조무희 223 | 슈퍼노비스포링 224 | 여우워록 225 | 아크비숍알파카 226 | 켈베로스길로틴크로스 227 | 여우소서러 228 | 타조민스트럴 229 | 타조원더러 230 | 슈라알파카 231 | 제네릭멧돼지 232 | 켈베로스도둑 233 | 켈베로스쉐도우체이서 234 | 노비스포링 235 | 페코검사 236 | 여우마법사 237 | 타조궁수 238 | 복사알파카 239 | 상인멧돼지 240 | 켈베로스도둑 241 | 슈퍼노비스 242 | 슈퍼노비스 243 | 슈퍼노비스포링 244 | 슈퍼노비스포링 245 | 프리스트알파카 246 | 여우위저드 247 | 제철공멧돼지 248 | 미케닉멧돼지 249 | 타조레인져 250 | 사자기사 251 | 사자로드나이트 252 | 사자로얄가드 253 | 사자룬나이트 254 | 사자크루세이더 255 | 사자팔라딘 256 | 미케닉멧돼지 257 | 타조레인져 258 | 사자기사 259 | 사자로얄가드 260 | 사자룬나이트 261 | 사자크루세이더 262 | kagerou 263 | oboro 264 | frog_kagerou 265 | frog_oboro 266 | rebellion 267 | peco_rebellion 268 | 초보자 269 | summoner 270 | cart_summoner 271 | summoner 272 | cart_summoner 273 | 닌자 274 | kagerou 275 | oboro 276 | 태권소년 277 | 권성 278 | 소울링커 279 | 건너 280 | rebellion 281 | 두꺼비닌자 282 | frog_kagerou 283 | frog_oboro 284 | 태권소년포링 285 | 권성포링 286 | 두꺼비소울링커 287 | 페코건너 288 | peco_rebellion 289 | 권성융합 290 | 성제 291 | 소울리퍼 292 | 성제 293 | 소울리퍼 294 | 성제융합 295 | 성제융합 296 | 해태성제 297 | 해태소울리퍼 298 | 해태성제 299 | 해태소울리퍼 300 | 초보자 301 | 초보자 302 | 초보자 303 | DRAGON_KNIGHT 304 | MEISTER 305 | SHADOW_CROSS 306 | ARCH_MAGE 307 | CARDINAL 308 | WINDHAWK 309 | IMPERIAL_GUARD 310 | BIOLO 311 | ABYSS_CHASER 312 | ELEMETAL_MASTER 313 | INQUISITOR 314 | TROUBADOUR 315 | TROUVERE 316 | DRAGON_KNIGHT_RIDING 317 | MEISTER_RIDING 318 | SHADOW_CROSS_RIDING 319 | ARCH_MAGE_RIDING 320 | CARDINAL_RIDING 321 | WINDHAWK_RIDING 322 | IMPERIAL_GUARD_RIDING 323 | BIOLO_RIDING 324 | ABYSS_CHASER_RIDING 325 | ELEMETAL_MASTER_RIDING 326 | INQUISITOR_RIDING 327 | TROUBADOUR_RIDING 328 | TROUVERE_RIDING 329 | WOLF_WINDHAWK 330 | MEISTER_MADOGEAR1 331 | DRAGON_KNIGHT_CHICKEN 332 | IMPERIAL_GUARD_CHICKEN 333 | 초보자 334 | 초보자 335 | 초보자 336 | 초보자 337 | 초보자 338 | 초보자 339 | 초보자 340 | 초보자 341 | 초보자 342 | 초보자 343 | 초보자 344 | 초보자 345 | 초보자 346 | 초보자 347 | 초보자 348 | 초보자 349 | 초보자 350 | 초보자 351 | 초보자 352 | 초보자 353 | SKY_EMPEROR 354 | SOUL_ASCETIC 355 | SHINKIRO 356 | SHIRANUI 357 | NIGHT_WATCH 358 | HYPER_NOVICE 359 | SPIRIT_HANDLER 360 | SKY_EMPEROR_RIDING 361 | SOUL_ASCETIC_RIDING 362 | SHINKIRO_RIDING 363 | SHIRANUI_RIDING 364 | NIGHT_WATCH_RIDING 365 | HYPER_NOVICE_RIDING 366 | SPIRIT_HANDLER_RIDING 367 | SKY_EMPEROR2 368 | -------------------------------------------------------------------------------- /resolver_data/job_pal_names.txt: -------------------------------------------------------------------------------- 1 | 초보자 2 | 검사 3 | 마법사 4 | 궁수 5 | 성직자 6 | 상인 7 | 도둑 8 | 기사 9 | 프리스트 10 | 위저드 11 | 제철공 12 | 헌터 13 | 어세신 14 | 페코페코_기사 15 | 크루 16 | 몽크 17 | 세이지 18 | 로그 19 | 연금술사 20 | 바드 21 | 댄서 22 | 페코페코_크루 23 | 결혼 24 | 슈퍼노비스 25 | 건너 26 | 닌자 27 | SANTA 28 | SUMMER 29 | HANBOK 30 | OKTOBERFEST 31 | SUMMER2 32 | 초보자 33 | 초보자 34 | 초보자 35 | 초보자 36 | 초보자 37 | 초보자 38 | 초보자 39 | 초보자 40 | 초보자 41 | 초보자 42 | 초보자 43 | 초보자 44 | 초보자 45 | 초보자 46 | 초보자 47 | 초보자 48 | 초보자 49 | 초보자 50 | 초보자 51 | 초보자 52 | 초보자 53 | 검사 54 | 마법사 55 | 궁수 56 | 성직자 57 | 상인 58 | 도둑 59 | 로드나이트 60 | 하이프리스트 61 | 하이위저드 62 | 화이트스미스 63 | 스나이퍼 64 | 어세신크로스 65 | 페코로나 66 | 팔라딘 67 | 챔피온 68 | 프로페서 69 | 스토커 70 | 크리에이터 71 | 크라운 72 | 집시 73 | 페코팔라 74 | 초보자 75 | 검사 76 | 마법사 77 | 궁수 78 | 성직자 79 | 상인 80 | 도둑 81 | 기사 82 | 프리스트 83 | 위저드 84 | 제철공 85 | 헌터 86 | 어세신 87 | 페코페코_기사 88 | 크루 89 | 몽크 90 | 세이지 91 | 로그 92 | 연금술사 93 | 바드 94 | 댄서 95 | 페코페코_크루 96 | 슈퍼노비스 97 | 태권소년 98 | 권성 99 | 권성 100 | 소울링커 101 | 성직자 102 | 기사 103 | 위저드 104 | 초보자 105 | 룬나이트 106 | 워록 107 | 레인저 108 | 아크비숍 109 | 미케닉 110 | 길로틴크로스 111 | 룬나이트 112 | 워록 113 | 레인저 114 | 아크비숍 115 | 미케닉 116 | 길로틴크로스 117 | 로얄가드 118 | 소서러 119 | 민스트럴 120 | 원더러 121 | 슈라 122 | 제네릭 123 | 쉐도우체이서 124 | 로얄가드 125 | 소서러 126 | 민스트럴 127 | 원더러 128 | 슈라 129 | 제네릭 130 | 쉐도우체이서 131 | 룬드래곤 132 | 룬드래곤 133 | 그리폰로얄 134 | 그리폰로얄 135 | 울프레인저 136 | 울프레인저 137 | 미케닉_마도기어 138 | 미케닉_마도기어 139 | 룬드래곤 140 | 룬드래곤 141 | 룬드래곤 142 | 룬드래곤 143 | 룬드래곤 144 | 룬드래곤 145 | 룬드래곤 146 | 룬드래곤 147 | 룬나이트 148 | 워록 149 | 레인저 150 | 아크비숍 151 | 미케닉 152 | 길로틴크로스 153 | 로얄가드 154 | 소서러 155 | 민스트럴 156 | 원더러 157 | 슈라 158 | 제네릭 159 | 쉐도우체이서 160 | 룬드래곤 161 | 그리폰로얄 162 | 울프레인저 163 | 미케닉_마도기어 164 | 초보자 165 | 닌자 166 | 건너 167 | 검사 168 | 태권소년 169 | 화이트스미스 170 | 상인 171 | 제네릭멧돼지 172 | 크리에이터 173 | 궁수 174 | 권성 175 | 초보자 176 | 몽크 177 | 성직자 178 | 슈라알파카 179 | 슈퍼노비스 180 | 아크비숍알파카 181 | 마법사 182 | 세이지 183 | 여우소서러 184 | 여우워록 185 | 위저드 186 | 프로페서 187 | 하이위저드 188 | 연금술사 189 | 제철공 190 | 챔피온 191 | 켈베로스길로틴크로스 192 | 도둑 193 | 로그 194 | 켈베로스쉐도우체이서 195 | 스토커 196 | 어세신 197 | 어세신크로스 198 | 댄서 199 | 타조민스트럴 200 | 바드 201 | 스나이퍼 202 | 타조원더러 203 | 집시 204 | 크라운 205 | 헌터 206 | 태권소년 207 | 프리스트 208 | 하이프리스트 209 | 초보자 210 | 검사 211 | 마법사 212 | 궁수 213 | 성직자 214 | 상인 215 | 헌터 216 | 어세신 217 | 몽크 218 | 세이지 219 | 로그 220 | 연금술사 221 | 바드 222 | 댄서 223 | 슈퍼노비스 224 | 여우워록 225 | 아크비숍알파카 226 | 켈베로스길로틴크로스 227 | 여우소서러 228 | 타조민스트럴 229 | 타조원더러 230 | 슈라알파카 231 | 제네릭멧돼지 232 | 도둑 233 | 켈베로스쉐도우체이서 234 | 초보자 235 | 검사 236 | 마법사 237 | 궁수 238 | 성직자 239 | 상인 240 | 도둑 241 | 슈퍼노비스 242 | 슈퍼노비스 243 | 슈퍼노비스 244 | 슈퍼노비스 245 | 프리스트 246 | 위저드 247 | 제철공 248 | 미케닉멧돼지 249 | 타조레인져 250 | 기사 251 | 기사 252 | 사자로얄가드 253 | 사자룬나이트 254 | 크루 255 | 팔라딘 256 | 미케닉멧돼지 257 | 타조레인져 258 | 기사 259 | 사자로얄가드 260 | 사자룬나이트 261 | 크루 262 | KAGEROU 263 | OBORO 264 | KAGEROU 265 | OBORO 266 | 리벨리온 267 | 리벨리온 268 | 초보자 269 | 묘족 270 | 고양이카트 271 | 묘족 272 | 고양이카트 273 | 닌자 274 | KAGEROU 275 | OBORO 276 | 태권소년 277 | 권성 278 | 소울링커 279 | 건너 280 | 리벨리온 281 | 닌자 282 | KAGEROU 283 | OBORO 284 | 태권소년 285 | 권성 286 | 태권소년 287 | 건너 288 | 리벨리온 289 | 권성 290 | 성제 291 | 소울리퍼 292 | 성제 293 | 소울리퍼 294 | 성제 295 | 성제 296 | 해태성제 297 | 해태소울리퍼 298 | 해태성제 299 | 해태소울리퍼 300 | 초보자 301 | 초보자 302 | 초보자 303 | DRAGON_KNIGHT 304 | MEISTER 305 | SHADOW_CROSS 306 | ARCH_MAGE 307 | CARDINAL 308 | WINDHAWK 309 | IMPERIAL_GUARD 310 | BIOLO 311 | ABYSS_CHASER 312 | ELEMETAL_MASTER 313 | INQUISITOR 314 | TROUBADOUR 315 | TROUVERE 316 | DRAGON_KNIGHT_RIDING 317 | MEISTER_RIDING 318 | SHADOW_CROSS_RIDING 319 | ARCH_MAGE_RIDING 320 | CARDINAL_RIDING 321 | WINDHAWK_RIDING 322 | IMPERIAL_GUARD_RIDING 323 | BIOLO_RIDING 324 | ABYSS_CHASER_RIDING 325 | ELEMETAL_MASTER_RIDING 326 | INQUISITOR_RIDING 327 | TROUBADOUR_RIDING 328 | TROUVERE_RIDING 329 | WOLF_WINDHAWK 330 | MEISTER_MADOGEAR1 331 | DRAGON_KNIGHT_CHICKEN 332 | IMPERIAL_GUARD_CHICKEN 333 | 초보자 334 | 초보자 335 | 초보자 336 | 초보자 337 | 초보자 338 | 초보자 339 | 초보자 340 | 초보자 341 | 초보자 342 | 초보자 343 | 초보자 344 | 초보자 345 | 초보자 346 | 초보자 347 | 초보자 348 | 초보자 349 | 초보자 350 | 초보자 351 | 초보자 352 | 초보자 353 | SKY_EMPEROR 354 | SOUL_ASCETIC 355 | SHINKIRO 356 | SHIRANUI 357 | NIGHT_WATCH 358 | HYPER_NOVICE 359 | SPIRIT_HANDLER 360 | SKY_EMPEROR_RIDING 361 | SOUL_ASCETIC_RIDING 362 | SHINKIRO_RIDING 363 | SHIRANUI_RIDING 364 | NIGHT_WATCH_RIDING 365 | HYPER_NOVICE_RIDING 366 | SPIRIT_HANDLER_RIDING 367 | SKY_EMPEROR2 368 | -------------------------------------------------------------------------------- /resolver_data/job_weapon_names.txt: -------------------------------------------------------------------------------- 1 | 초보자\초보자 2 | 검사\검사 3 | 마법사\마법사 4 | 궁수\궁수 5 | 성직자\성직자 6 | 상인\상인 7 | 도둑\도둑 8 | 기사\기사 9 | 프리스트\프리스트 10 | 위저드\위저드 11 | 제철공\제철공 12 | 헌터\헌터 13 | 어세신\어세신 14 | 페코페코_기사\페코페코_기사 15 | 크루세이더\크루세이더 16 | 몽크\몽크 17 | 세이지\세이지 18 | 로그\로그 19 | 연금술사\연금술사 20 | 바드\바드 21 | 무희\무희 22 | 신페코크루세이더\신페코크루세이더 23 | 결혼\결혼 24 | 초보자\초보자 25 | 건너\건너 26 | 닌자\닌자 27 | 결혼\결혼 28 | 결혼\결혼 29 | 결혼\결혼 30 | 결혼\결혼 31 | 결혼\결혼 32 | 초보자\초보자 33 | 초보자\초보자 34 | 초보자\초보자 35 | 초보자\초보자 36 | 초보자\초보자 37 | 초보자\초보자 38 | 초보자\초보자 39 | 초보자\초보자 40 | 초보자\초보자 41 | 초보자\초보자 42 | 초보자\초보자 43 | 초보자\초보자 44 | 초보자\초보자 45 | 초보자\초보자 46 | 초보자\초보자 47 | 초보자\초보자 48 | 초보자\초보자 49 | 초보자\초보자 50 | 초보자\초보자 51 | 초보자\초보자 52 | 초보자\초보자 53 | 검사\검사 54 | 마법사\마법사 55 | 궁수\궁수 56 | 성직자\성직자 57 | 상인\상인 58 | 도둑\도둑 59 | 기사\기사 60 | 프리스트\프리스트 61 | 위저드\위저드 62 | 제철공\제철공 63 | 헌터\헌터 64 | 어세신\어세신 65 | 페코페코_기사\페코페코_기사 66 | 크루세이더\크루세이더 67 | 몽크\몽크 68 | 세이지\세이지 69 | 로그\로그 70 | 연금술사\연금술사 71 | 바드\바드 72 | 무희\무희 73 | 신페코크루세이더\신페코크루세이더 74 | 초보자\초보자 75 | 검사\검사 76 | 마법사\마법사 77 | 궁수\궁수 78 | 성직자\성직자 79 | 상인\상인 80 | 도둑\도둑 81 | 기사\기사 82 | 프리스트\프리스트 83 | 위저드\위저드 84 | 제철공\제철공 85 | 헌터\헌터 86 | 어세신\어세신 87 | 페코페코_기사\페코페코_기사 88 | 크루세이더\크루세이더 89 | 몽크\몽크 90 | 세이지\세이지 91 | 로그\로그 92 | 연금술사\연금술사 93 | 바드\바드 94 | 무희\무희 95 | 구페코크루세이더\구페코크루세이더 96 | 초보자\초보자 97 | 태권소년\태권소년 98 | 태권소년\태권소년 99 | 태권소년\태권소년 100 | 위저드\위저드 101 | 성직자\성직자 102 | 기사\기사 103 | 세이지\세이지 104 | 초보자\초보자 105 | 기사\기사 106 | 위저드\위저드 107 | 헌터\헌터 108 | 프리스트\프리스트 109 | 제철공\제철공 110 | 어세신\어세신 111 | 기사\기사 112 | 위저드\위저드 113 | 헌터\헌터 114 | 프리스트\프리스트 115 | 제철공\제철공 116 | 어세신\어세신 117 | 크루세이더\크루세이더 118 | 세이지\세이지 119 | 바드\바드 120 | 무희\무희 121 | 몽크\몽크 122 | 연금술사\연금술사 123 | 로그\로그 124 | 크루세이더\크루세이더 125 | 세이지\세이지 126 | 바드\바드 127 | 무희\무희 128 | 몽크\몽크 129 | 연금술사\연금술사 130 | 로그\로그 131 | 페코페코_기사\페코페코_기사 132 | 페코페코_기사\페코페코_기사 133 | 신페코크루세이더\신페코크루세이더 134 | 신페코크루세이더\신페코크루세이더 135 | 레인져늑대\레인져늑대 136 | 레인져늑대\레인져늑대 137 | 마도기어\마도기어 138 | 마도기어\마도기어 139 | 페코페코_기사\페코페코_기사 140 | 페코페코_기사\페코페코_기사 141 | 페코페코_기사\페코페코_기사 142 | 페코페코_기사\페코페코_기사 143 | 페코페코_기사\페코페코_기사 144 | 페코페코_기사\페코페코_기사 145 | 페코페코_기사\페코페코_기사 146 | 페코페코_기사\페코페코_기사 147 | 기사\기사 148 | 위저드\위저드 149 | 헌터\헌터 150 | 프리스트\프리스트 151 | 제철공\제철공 152 | 어세신\어세신 153 | 크루세이더\크루세이더 154 | 세이지\세이지 155 | 바드\바드 156 | 무희\무희 157 | 몽크\몽크 158 | 연금술사\연금술사 159 | 로그\로그 160 | 페코페코_기사\페코페코_기사 161 | 신페코크루세이더\신페코크루세이더 162 | 레인져늑대\레인져늑대 163 | 마도기어\마도기어 164 | 초보자\초보자 165 | 닌자\닌자 166 | 건너\건너 167 | 검사\검사 168 | 위저드\위저드 169 | 제철공\제철공 170 | 상인\상인 171 | 연금술사\연금술사 172 | 연금술사\연금술사 173 | 궁수\궁수 174 | 태권소년\태권소년 175 | 초보자\초보자 176 | 몽크\몽크 177 | 성직자\성직자 178 | 몽크\몽크 179 | 초보자\초보자 180 | 프리스트\프리스트 181 | 마법사\마법사 182 | 세이지\세이지 183 | 세이지\세이지 184 | 위저드\위저드 185 | 위저드\위저드 186 | 세이지\세이지 187 | 위저드\위저드 188 | 연금술사\연금술사 189 | 제철공\제철공 190 | 몽크\몽크 191 | 어세신\어세신 192 | 도둑\도둑 193 | 로그\로그 194 | 로그\로그 195 | 로그\로그 196 | 어세신\어세신 197 | 어세신\어세신 198 | 무희\무희 199 | 바드\바드 200 | 바드\바드 201 | 헌터\헌터 202 | 무희\무희 203 | 무희\무희 204 | 바드\바드 205 | 헌터\헌터 206 | 태권소년\태권소년 207 | 프리스트\프리스트 208 | 프리스트\프리스트 209 | 초보자\초보자 210 | 검사\검사 211 | 마법사\마법사 212 | 궁수\궁수 213 | 성직자\성직자 214 | 상인\상인 215 | 헌터\헌터 216 | 어세신\어세신 217 | 몽크\몽크 218 | 세이지\세이지 219 | 로그\로그 220 | 연금술사\연금술사 221 | 바드\바드 222 | 무희\무희 223 | 초보자\초보자 224 | 위저드\위저드 225 | 프리스트\프리스트 226 | 어세신\어세신 227 | 세이지\세이지 228 | 바드\바드 229 | 무희\무희 230 | 몽크\몽크 231 | 연금술사\연금술사 232 | 도둑\도둑 233 | 로그\로그 234 | 초보자\초보자 235 | 검사\검사 236 | 마법사\마법사 237 | 궁수\궁수 238 | 성직자\성직자 239 | 상인\상인 240 | 도둑\도둑 241 | 초보자\초보자 242 | 초보자\초보자 243 | 초보자\초보자 244 | 초보자\초보자 245 | 프리스트\프리스트 246 | 위저드\위저드 247 | 제철공\제철공 248 | 제철공\제철공 249 | 헌터\헌터 250 | 기사\기사 251 | 기사\기사 252 | 크루세이더\크루세이더 253 | 기사\기사 254 | 크루세이더\크루세이더 255 | 크루세이더\크루세이더 256 | 제철공\제철공 257 | 헌터\헌터 258 | 기사\기사 259 | 크루세이더\크루세이더 260 | 기사\기사 261 | 크루세이더\크루세이더 262 | kagerou\kagerou 263 | oboro\oboro 264 | kagerou\kagerou 265 | oboro\oboro 266 | rebellion\rebellion 267 | 초보자\초보자 268 | 초보자\초보자 269 | summoner\summoner 270 | summoner\cart_summoner 271 | summoner\summoner 272 | summoner\cart_summoner 273 | 닌자\닌자 274 | kagerou\kagerou 275 | oboro\oboro 276 | 태권소년\태권소년 277 | 태권소년\태권소년 278 | 위저드\위저드 279 | 건너\건너 280 | rebellion\rebellion 281 | 닌자\닌자 282 | kagerou\kagerou 283 | oboro\oboro 284 | 태권소년\태권소년 285 | 태권소년\태권소년 286 | 위저드\위저드 287 | 건너\건너 288 | rebellion\rebellion 289 | 태권소년\태권소년 290 | 태권소년\태권소년 291 | 위저드\위저드 292 | 태권소년\태권소년 293 | 위저드\위저드 294 | 태권소년\태권소년 295 | 태권소년\태권소년 296 | 태권소년\태권소년 297 | 위저드\위저드 298 | 태권소년\태권소년 299 | 위저드\위저드 300 | 초보자\초보자 301 | 초보자\초보자 302 | 초보자\초보자 303 | DRAGON_KNIGHT\DRAGON_KNIGHT 304 | MEISTER\MEISTER 305 | ABYSS_CHASER\ABYSS_CHASER 306 | 위저드\위저드 307 | CARDINAL\CARDINAL 308 | WINDHAWK\WINDHAWK 309 | IMPERIAL_GUARD\IMPERIAL_GUARD 310 | BIOLO\BIOLO 311 | 초보자\초보자 312 | 세이지\세이지 313 | INQUISITOR\INQUISITOR 314 | TROUBADOUR\TROUBADOUR 315 | TROUVERE\TROUVERE 316 | DRAGON_KNIGHT_RIDING\DRAGON_KNIGHT_RIDING 317 | MEISTER_RIDING\MEISTER_RIDING 318 | SHADOW_CROSS\SHADOW_CROSS_RIDING 319 | 위저드\위저드 320 | CARDINAL\CARDINAL_RIDING 321 | WINDHAWK\WINDHAWK_RIDING 322 | IMPERIAL_GUARD_RIDING\IMPERIAL_GUARD_RIDING 323 | BIOLO_RIDING\BIOLO_RIDING 324 | ABYSS_CHASER\ABYSS_CHASER_RIDING 325 | 세이지\세이지 326 | INQUISITOR\INQUISITOR_RIDING 327 | TROUBADOUR\TROUBADOUR_RIDING 328 | TROUVERE\TROUVERE_RIDING 329 | WINDHAWK\WOLF_WINDHAWK 330 | MEISTER_MADOGEAR\MEISTER_MADOGEAR1 331 | DRAGON_KNIGHT_CHICKEN\DRAGON_KNIGHT_CHICKEN 332 | IMPERIAL_GUARD_CHICKEN\IMPERIAL_GUARD_CHICKEN 333 | 초보자\초보자 334 | 초보자\초보자 335 | 초보자\초보자 336 | 초보자\초보자 337 | 초보자\초보자 338 | 초보자\초보자 339 | 초보자\초보자 340 | 초보자\초보자 341 | 초보자\초보자 342 | 초보자\초보자 343 | 초보자\초보자 344 | 초보자\초보자 345 | 초보자\초보자 346 | 초보자\초보자 347 | 초보자\초보자 348 | 초보자\초보자 349 | 초보자\초보자 350 | 초보자\초보자 351 | 초보자\초보자 352 | 초보자\초보자 353 | SKY_EMPEROR\SKY_EMPEROR 354 | SOUL_ASCETIC\SOUL_ASCETIC 355 | SHINKIRO\SHINKIRO 356 | SHIRANUI\SHIRANUI 357 | NIGHT_WATCH\NIGHT_WATCH 358 | HYPER_NOVICE\HYPER_NOVICE 359 | SPIRIT_HANDLER\SPIRIT_HANDLER 360 | SKY_EMPEROR\SKY_EMPEROR_RIDING 361 | SOUL_ASCETIC\SOUL_ASCETIC_RIDING 362 | SHINKIRO\SHINKIRO_RIDING 363 | SHIRANUI\SHIRANUI_RIDING 364 | NIGHT_WATCH\NIGHT_WATCH_RIDING 365 | HYPER_NOVICE\HYPER_NOVICE_RIDING 366 | SPIRIT_HANDLER\SPIRIT_HANDLER_RIDING 367 | SKY_EMPEROR\SKY_EMPEROR2 368 | -------------------------------------------------------------------------------- /resolver_data/shield_names.txt: -------------------------------------------------------------------------------- 1 | 2 | _가드 3 | _버클러 4 | _쉴드 5 | _미러쉴드 6 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | dub.selections.json 3 | docs.json 4 | __dummy.html 5 | docs/ 6 | /server 7 | server.so 8 | server.dylib 9 | server.dll 10 | server.a 11 | server.lib 12 | server-test-* 13 | *.exe 14 | *.o 15 | *.obj 16 | *.lst 17 | -------------------------------------------------------------------------------- /server/api-doc/.gitignore: -------------------------------------------------------------------------------- 1 | .openapi-generator 2 | .openapi-generator-ignore 3 | openapitools.json 4 | -------------------------------------------------------------------------------- /server/api-doc/README.md: -------------------------------------------------------------------------------- 1 | # api-docs 2 | Documentation is generated via https://openapi-generator.tech/. 3 | 4 | E.g.: 5 | `openapi-generator-cli generate -i ../api-spec/1.3.yaml -g html` 6 | -------------------------------------------------------------------------------- /server/api-spec/1.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "zrenderer API", 5 | "version": "1.1", 6 | "description": "API for the zrenderer service (https://github.com/zhad3/zrenderer)." 7 | }, 8 | "paths": { 9 | "/render": { 10 | "summary": "Endpoint for requesting to render sprites", 11 | "post": { 12 | "requestBody": { 13 | "$ref": "#/components/requestBodies/renderRequestBody" 14 | }, 15 | "responses": { 16 | "200": { 17 | "$ref": "#/components/responses/200" 18 | }, 19 | "400": { 20 | "$ref": "#/components/responses/400" 21 | }, 22 | "500": { 23 | "$ref": "#/components/responses/500" 24 | } 25 | }, 26 | "operationId": "render", 27 | "summary": "Send a request to render sprites" 28 | } 29 | } 30 | }, 31 | "components": { 32 | "schemas": { 33 | "renderRequest": { 34 | "required": [ 35 | "job" 36 | ], 37 | "type": "object", 38 | "properties": { 39 | "job": { 40 | "description": "Job id(s) which should be rendered. Can contain multiple comma separated values as well as ranges (e.g. \"1001-1999\").", 41 | "type": "array", 42 | "items": { 43 | "type": "string" 44 | } 45 | }, 46 | "action": { 47 | "format": "int32", 48 | "description": "Action of the job which should be drawn.", 49 | "minimum": 0, 50 | "type": "integer" 51 | }, 52 | "frame": { 53 | "format": "int32", 54 | "description": "Frame of the action which should be drawn. Set to -1 to draw all frames.", 55 | "minimum": -1, 56 | "type": "integer" 57 | }, 58 | "gender": { 59 | "description": "Gender of the player character. Possible values are: 1=male or 0=female.", 60 | "enum": [ 61 | 0, 62 | 1 63 | ], 64 | "format": "int32" 65 | }, 66 | "head": { 67 | "format": "int32", 68 | "description": "Head id which should be used when drawing a player.", 69 | "minimum": 0, 70 | "type": "integer" 71 | }, 72 | "outfit": { 73 | "format": "int32", 74 | "description": "The alternative outfit for player characters. Not all characters have alternative outfits. In these cases the default character will be rendered instead.", 75 | "minimum": 0, 76 | "type": "integer" 77 | }, 78 | "headgear": { 79 | "description": "Headgears which should be attached to the players head.", 80 | "type": "array", 81 | "items": { 82 | "format": "int32", 83 | "minimum": 0, 84 | "type": "integer" 85 | } 86 | }, 87 | "garment": { 88 | "format": "int32", 89 | "description": "Garment which should be attached to the players body.", 90 | "minimum": 0, 91 | "type": "integer" 92 | }, 93 | "weapon": { 94 | "format": "int32", 95 | "description": "Weapon which should be attached to the players body.", 96 | "minimum": 0, 97 | "type": "integer" 98 | }, 99 | "shield": { 100 | "format": "int32", 101 | "description": "Shield which should be attached to the players body.", 102 | "minimum": 0, 103 | "type": "integer" 104 | }, 105 | "bodyPalette": { 106 | "format": "int32", 107 | "description": "Palette for the body sprite. Set to -1 to use the standard palette.", 108 | "minimum": -1, 109 | "type": "integer" 110 | }, 111 | "headPalette": { 112 | "format": "int32", 113 | "description": "Palette for the head sprite. Set to -1 to use the standard palette.", 114 | "minimum": -1, 115 | "type": "integer" 116 | }, 117 | "headdir": { 118 | "description": "Direction in which the head should turn. This is only applied to player sprites and only to the stand and sit action. Possible values are: 0=straight, 1=left, 2=right or 3=all. If 'all' is set then this direction system is ignored and all frames are interpreted like any other one.", 119 | "enum": [ 120 | 0, 121 | 1, 122 | 2, 123 | 3 124 | ], 125 | "format": "int32" 126 | }, 127 | "enableShadow": { 128 | "description": "Draw shadow underneath the sprite.", 129 | "type": "boolean" 130 | }, 131 | "canvas": { 132 | "description": "Sets a canvas onto which the sprite should be rendered. The canvas requires two options: its size and an origin point inside the canvas where the sprite should be placed. The format is as following: x±±. An origin point of +0+0 is equal to the top left corner. Example: 200x250+100+125. This would create a canvas and place the sprite in the center.", 133 | "type": "string" 134 | }, 135 | "outputFormat": { 136 | "description": "Defines the output format. Possible values are 0=png or 1=zip. If zip is chosen the zip will contain png files.", 137 | "enum": [ 138 | 0, 139 | 1 140 | ], 141 | "format": "int32" 142 | } 143 | } 144 | }, 145 | "commonError": { 146 | "required": [ 147 | "statusMessage" 148 | ], 149 | "type": "object", 150 | "properties": { 151 | "statusMessage": { 152 | "type": "string" 153 | } 154 | } 155 | }, 156 | "renderResponse": { 157 | "required": [ 158 | "output" 159 | ], 160 | "type": "object", 161 | "properties": { 162 | "output": { 163 | "type": "array", 164 | "items": { 165 | "type": "string" 166 | } 167 | } 168 | } 169 | } 170 | }, 171 | "responses": { 172 | "200": { 173 | "content": { 174 | "application/json": { 175 | "schema": { 176 | "$ref": "#/components/schemas/renderResponse" 177 | } 178 | } 179 | }, 180 | "description": "Ok" 181 | }, 182 | "400": { 183 | "content": { 184 | "application/json": { 185 | "schema": { 186 | "$ref": "#/components/schemas/commonError" 187 | } 188 | } 189 | }, 190 | "description": "Illegal input for operation." 191 | }, 192 | "500": { 193 | "content": { 194 | "application/json": { 195 | "schema": { 196 | "$ref": "#/components/schemas/commonError" 197 | } 198 | } 199 | }, 200 | "description": "Server Error" 201 | } 202 | }, 203 | "requestBodies": { 204 | "renderRequestBody": { 205 | "content": { 206 | "application/json": { 207 | "schema": { 208 | "$ref": "#/components/schemas/renderRequest" 209 | } 210 | } 211 | }, 212 | "required": true 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /server/api-spec/1.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "zrenderer API", 5 | "version": "1.2", 6 | "description": "API for the zrenderer service (https://github.com/zhad3/zrenderer)." 7 | }, 8 | "paths": { 9 | "/render": { 10 | "summary": "Endpoint for requesting to render sprites", 11 | "post": { 12 | "requestBody": { 13 | "$ref": "#/components/requestBodies/renderRequestBody" 14 | }, 15 | "parameters": [ 16 | { 17 | "deprecated": false, 18 | "name": "downloadimage", 19 | "description": "If provided, the request will return the first rendered image.", 20 | "in": "query", 21 | "required": false, 22 | "allowEmptyValue": true 23 | } 24 | ], 25 | "responses": { 26 | "200": { 27 | "$ref": "#/components/responses/200" 28 | }, 29 | "400": { 30 | "$ref": "#/components/responses/400" 31 | }, 32 | "401": { 33 | "$ref": "#/components/responses/401" 34 | }, 35 | "500": { 36 | "$ref": "#/components/responses/500" 37 | } 38 | }, 39 | "operationId": "render", 40 | "summary": "Send a request to render sprites" 41 | } 42 | } 43 | }, 44 | "components": { 45 | "schemas": { 46 | "commonError": { 47 | "required": [ 48 | "statusMessage" 49 | ], 50 | "type": "object", 51 | "properties": { 52 | "statusMessage": { 53 | "type": "string" 54 | } 55 | } 56 | }, 57 | "renderResponse": { 58 | "required": [ 59 | "output" 60 | ], 61 | "type": "object", 62 | "properties": { 63 | "output": { 64 | "type": "array", 65 | "items": { 66 | "type": "string" 67 | } 68 | } 69 | } 70 | }, 71 | "renderRequest": { 72 | "required": [ 73 | "job" 74 | ], 75 | "type": "object", 76 | "properties": { 77 | "job": { 78 | "description": "Job id(s) which should be rendered. Can contain multiple comma separated values as well as ranges (e.g. \"1001-1999\").", 79 | "type": "array", 80 | "items": { 81 | "type": "string" 82 | } 83 | }, 84 | "action": { 85 | "format": "int32", 86 | "description": "Action of the job which should be drawn.", 87 | "minimum": 0, 88 | "type": "integer" 89 | }, 90 | "frame": { 91 | "format": "int32", 92 | "description": "Frame of the action which should be drawn. Set to -1 to draw all frames.", 93 | "minimum": -1, 94 | "type": "integer" 95 | }, 96 | "gender": { 97 | "format": "int32", 98 | "description": "Gender of the player character. Possible values are: 1=male or 0=female.", 99 | "enum": [ 100 | 0, 101 | 1 102 | ] 103 | }, 104 | "head": { 105 | "format": "int32", 106 | "description": "Head id which should be used when drawing a player.", 107 | "minimum": 0, 108 | "type": "integer" 109 | }, 110 | "outfit": { 111 | "format": "int32", 112 | "description": "The alternative outfit for player characters. Not all characters have alternative outfits. In these cases the default character will be rendered instead.", 113 | "minimum": 0, 114 | "type": "integer" 115 | }, 116 | "headgear": { 117 | "description": "Headgears which should be attached to the players head.", 118 | "type": "array", 119 | "items": { 120 | "format": "int32", 121 | "minimum": 0, 122 | "type": "integer" 123 | } 124 | }, 125 | "garment": { 126 | "format": "int32", 127 | "description": "Garment which should be attached to the players body.", 128 | "minimum": 0, 129 | "type": "integer" 130 | }, 131 | "weapon": { 132 | "format": "int32", 133 | "description": "Weapon which should be attached to the players body.", 134 | "minimum": 0, 135 | "type": "integer" 136 | }, 137 | "shield": { 138 | "format": "int32", 139 | "description": "Shield which should be attached to the players body.", 140 | "minimum": 0, 141 | "type": "integer" 142 | }, 143 | "bodyPalette": { 144 | "format": "int32", 145 | "description": "Palette for the body sprite. Set to -1 to use the standard palette.", 146 | "minimum": -1, 147 | "type": "integer" 148 | }, 149 | "headPalette": { 150 | "format": "int32", 151 | "description": "Palette for the head sprite. Set to -1 to use the standard palette.", 152 | "minimum": -1, 153 | "type": "integer" 154 | }, 155 | "headdir": { 156 | "format": "int32", 157 | "description": "Direction in which the head should turn. This is only applied to player sprites and only to the stand and sit action. Possible values are: 0=straight, 1=left, 2=right or 3=all. If 'all' is set then this direction system is ignored and all frames are interpreted like any other one.", 158 | "enum": [ 159 | 0, 160 | 1, 161 | 2, 162 | 3 163 | ] 164 | }, 165 | "enableShadow": { 166 | "description": "Draw shadow underneath the sprite.", 167 | "type": "boolean" 168 | }, 169 | "canvas": { 170 | "description": "Sets a canvas onto which the sprite should be rendered. The canvas requires two options: its size and an origin point inside the canvas where the sprite should be placed. The format is as following: {width}x{height}±{x}±{y}. An origin point of +0+0 is equal to the top left corner. Example: 200x250+100+125. This would create a canvas and place the sprite in the center.", 171 | "type": "string" 172 | }, 173 | "outputFormat": { 174 | "format": "int32", 175 | "description": "Defines the output format. Possible values are 0=png or 1=zip. If zip is chosen the zip will contain png files.", 176 | "enum": [ 177 | 0, 178 | 1 179 | ] 180 | } 181 | } 182 | }, 183 | "renderResponseZip": { 184 | "format": "binary", 185 | "description": "Returns application/zip.", 186 | "type": "string" 187 | }, 188 | "renderResponseImage": { 189 | "format": "binary", 190 | "description": "Returns image/png.", 191 | "type": "string" 192 | } 193 | }, 194 | "responses": { 195 | "200": { 196 | "content": { 197 | "application/json": { 198 | "schema": { 199 | "$ref": "#/components/schemas/renderResponse" 200 | } 201 | }, 202 | "image/png": { 203 | "schema": { 204 | "$ref": "#/components/schemas/renderResponseImage" 205 | } 206 | }, 207 | "text/*": { 208 | "schema": { 209 | "$ref": "#/components/schemas/renderResponseZip" 210 | } 211 | } 212 | }, 213 | "description": "Ok" 214 | }, 215 | "400": { 216 | "content": { 217 | "application/json": { 218 | "schema": { 219 | "$ref": "#/components/schemas/commonError" 220 | } 221 | } 222 | }, 223 | "description": "Illegal input for operation." 224 | }, 225 | "401": { 226 | "content": { 227 | "application/json": { 228 | "schema": { 229 | "$ref": "#/components/schemas/commonError" 230 | } 231 | } 232 | }, 233 | "description": "Unauthorized." 234 | }, 235 | "500": { 236 | "content": { 237 | "application/json": { 238 | "schema": { 239 | "$ref": "#/components/schemas/commonError" 240 | } 241 | } 242 | }, 243 | "description": "Server Error" 244 | } 245 | }, 246 | "requestBodies": { 247 | "renderRequestBody": { 248 | "content": { 249 | "application/json": { 250 | "schema": { 251 | "$ref": "#/components/schemas/renderRequest" 252 | } 253 | } 254 | }, 255 | "required": true 256 | } 257 | }, 258 | "securitySchemes": { 259 | "Access Token": { 260 | "type": "apiKey", 261 | "description": "Token required to make requests.", 262 | "name": "accesstoken", 263 | "in": "query" 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /server/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "REST server interface for zrenderer.", 3 | "name": "server", 4 | "targetName": "zrenderer-server", 5 | "targetPath": "../bin", 6 | "targetType": "executable", 7 | "dependencies": { 8 | "zconfig": "~>1.0", 9 | "libpng-apng": "~>1.0", 10 | "luad": {"path": "../LuaD/"}, 11 | "zencoding:windows949": "~>1.0", 12 | "vibe-d:http": "==0.9.6" 13 | }, 14 | "versions": ["VibeDisableCommandLineParsing"], 15 | "sourcePaths": ["../source/", "./source/"], 16 | "importPaths": ["../source/", "./source/"], 17 | "mainSourceFile": "source/zrenderer/server/package.d", 18 | "configurations": [ 19 | { "name": "default" }, 20 | { 21 | "name": "docker", 22 | "dflags": ["-link-defaultlib-shared=false"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/dto/accesstoken.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.dto.accesstoken; 2 | 3 | import std.typecons : Nullable; 4 | import vibe.data.serialization : optional; 5 | import zrenderer.server.auth : Capabilities, Properties; 6 | 7 | struct AccessTokenData 8 | { 9 | @optional Nullable!uint id; 10 | @optional Nullable!string description; 11 | @optional Nullable!bool isAdmin; 12 | @optional Nullable!CapabilitiesData capabilities; 13 | @optional Nullable!PropertiesData properties; 14 | } 15 | 16 | struct CapabilitiesData 17 | { 18 | mixin(optionalStructOf!Capabilities); 19 | } 20 | 21 | struct PropertiesData 22 | { 23 | mixin(optionalStructOf!Properties); 24 | } 25 | 26 | template optionalStructOf(T) 27 | { 28 | string optionalStructOf() 29 | { 30 | import std.meta : AliasSeq; 31 | import std.traits : Fields, FieldNameTuple; 32 | 33 | string output; 34 | alias Types = Fields!T; 35 | alias Names = FieldNameTuple!T; 36 | static foreach (i, name; Names) 37 | { 38 | output ~= "@optional Nullable!" ~ Types[i].stringof ~ " " ~ name ~ ";"; 39 | } 40 | 41 | return output; 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/dto/package.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.dto; 2 | 3 | public import zrenderer.server.dto.accesstoken; 4 | public import zrenderer.server.dto.renderrequest; 5 | public import zrenderer.server.dto.renderresponse; 6 | 7 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/dto/renderrequest.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.dto.renderrequest; 2 | 3 | import vibe.data.serialization : optional; 4 | import std.typecons : Nullable; 5 | import config : Gender, HeadDirection, OutputFormat, MadogearType; 6 | 7 | struct RenderRequestData 8 | { 9 | @optional Nullable!uint action; 10 | @optional Nullable!int frame; 11 | @optional Nullable!Gender gender; 12 | @optional Nullable!uint head; 13 | @optional Nullable!uint outfit; 14 | @optional Nullable!uint garment; 15 | @optional Nullable!uint weapon; 16 | @optional Nullable!uint shield; 17 | @optional Nullable!int bodyPalette; 18 | @optional Nullable!int headPalette; 19 | @optional Nullable!HeadDirection headdir; 20 | @optional Nullable!MadogearType madogearType; 21 | @optional Nullable!bool enableShadow; 22 | @optional Nullable!string canvas; 23 | @optional Nullable!OutputFormat outputFormat; 24 | @optional Nullable!(uint[]) headgear; 25 | string[] job; 26 | } 27 | 28 | string toString(const scope RenderRequestData data) pure @safe 29 | { 30 | import std.array : appender; 31 | import std.range : ElementType; 32 | import std.traits : isArray, isSomeString; 33 | import std.algorithm.iteration : joiner, map; 34 | import std.conv : to, ConvException; 35 | 36 | auto app = appender!string; 37 | 38 | void putArray(T)(T member, const scope string name) 39 | { 40 | app.put(name); 41 | app.put("="); 42 | static if (isSomeString!(T)) 43 | { 44 | app.put("["); 45 | app.put(member.joiner(",")); 46 | app.put("]"); 47 | } 48 | else 49 | { 50 | app.put("["); 51 | try 52 | { 53 | app.put(member.map!(a => a.to!string).joiner(",")); 54 | } 55 | catch (ConvException err) 56 | { 57 | app.put("?"); 58 | } 59 | app.put("]"); 60 | } 61 | app.put(", "); 62 | } 63 | 64 | void putSingle(T)(T member, const scope string name) 65 | { 66 | app.put(name); 67 | app.put("="); 68 | static if (isSomeString!(T)) 69 | { 70 | app.put(member); 71 | } 72 | else 73 | { 74 | try 75 | { 76 | app.put(member.to!string); 77 | } 78 | catch (ConvException err) 79 | { 80 | app.put("?"); 81 | } 82 | } 83 | app.put(", "); 84 | } 85 | 86 | app.put("RenderRequestData { "); 87 | 88 | putArray(data.job, "job"); 89 | if (!data.action.isNull) putSingle(data.action.get, "action"); 90 | if (!data.frame.isNull) putSingle(data.frame.get, "frame"); 91 | if (!data.gender.isNull) putSingle(data.gender.get, "gender"); 92 | if (!data.head.isNull) putSingle(data.head.get, "head"); 93 | if (!data.outfit.isNull) putSingle(data.outfit.get, "outfit"); 94 | if (!data.garment.isNull) putSingle(data.garment.get, "garment"); 95 | if (!data.weapon.isNull) putSingle(data.weapon.get, "weapon"); 96 | if (!data.shield.isNull) putSingle(data.shield.get, "shield"); 97 | if (!data.bodyPalette.isNull) putSingle(data.bodyPalette.get, "bodyPalette"); 98 | if (!data.headPalette.isNull) putSingle(data.headPalette.get, "headPalette"); 99 | if (!data.headdir.isNull) putSingle(data.headdir.get, "headdir"); 100 | if (!data.madogearType.isNull) putSingle(data.madogearType.get, "madogearType"); 101 | if (!data.enableShadow.isNull) putSingle(data.enableShadow.get, "enableShadow"); 102 | if (!data.canvas.isNull) putSingle(data.canvas.get, "canvas"); 103 | if (!data.outputFormat.isNull) putSingle(data.outputFormat.get, "outputFormat"); 104 | if (!data.headgear.isNull) putArray(data.headgear.get, "headgear"); 105 | 106 | return app.data[0 .. $ - 2] ~ " }"; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/dto/renderresponse.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.dto.renderresponse; 2 | 3 | struct RenderResponseData 4 | { 5 | /// Contains one or more paths to the rendered sprites. 6 | immutable(string)[] output; 7 | } 8 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/globals.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.globals; 2 | 3 | import config : Config; 4 | import zrenderer.server.auth : AccessToken, AccessTokenDB; 5 | 6 | __gshared Config defaultConfig; 7 | __gshared AccessTokenDB accessTokens; 8 | 9 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/package.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server; 2 | 3 | import app : createOutputDirectory; 4 | import config : Config; 5 | import logging : zLogLevel = LogLevel; 6 | import std.conv : ConvException; 7 | import std.getopt : GetOptException; 8 | import std.stdio : stderr; 9 | import vibe.core.core; 10 | import vibe.core.log : LogLevel; 11 | import vibe.http.router; 12 | import vibe.http.server; 13 | import vibe.http.log : HTTPLogger; 14 | import zconfig : initializeConfig, getConfigArguments; 15 | import zrenderer.server.auth; 16 | import zrenderer.server.globals : defaultConfig, accessTokens; 17 | import zrenderer.server.routes; 18 | 19 | enum usage = "A REST server to render sprites from Ragnarok Online"; 20 | 21 | int main(string[] args) 22 | { 23 | string[] configArgs = getConfigArguments!Config("zrenderer.conf", args); 24 | if (configArgs.length > 0) 25 | { 26 | import std.array : insertInPlace; 27 | 28 | args.insertInPlace(1, configArgs); 29 | } 30 | 31 | Config config; 32 | bool helpWanted = false; 33 | 34 | try 35 | { 36 | config = initializeConfig!(Config, usage)(args, helpWanted); 37 | 38 | import std.exception : enforce; 39 | import validation : isJobArgValid, isCanvasArgValid; 40 | 41 | enforce!GetOptException(isJobArgValid(config.job), "job ids are not valid."); 42 | enforce!GetOptException(isCanvasArgValid(config.canvas), "canvas is not valid."); 43 | } 44 | catch (GetOptException e) 45 | { 46 | stderr.writefln("Error parsing options: %s", e.msg); 47 | return 1; 48 | } 49 | catch (ConvException e) 50 | { 51 | stderr.writefln("Error parsing options: %s", e.msg); 52 | return 1; 53 | } 54 | 55 | if (helpWanted) 56 | { 57 | return 0; 58 | } 59 | 60 | defaultConfig = config; 61 | 62 | createOutputDirectory(config.outdir); 63 | 64 | if (config.logfile.length > 0) 65 | { 66 | import vibe.core.log : registerLogger, FileLogger; 67 | 68 | auto filelogger = cast(shared) new FileLogger(config.logfile); 69 | filelogger.minLevel = config.loglevel.toVibeLogLevel(); 70 | 71 | registerLogger(filelogger); 72 | } 73 | 74 | if (!createOrLoadAccessTokens(config.tokenfile)) 75 | { 76 | return 1; 77 | } 78 | 79 | auto router = new URLRouter; 80 | 81 | if (defaultConfig.enableCORS) 82 | { 83 | router.any("*", &addCORSOriginHeader); 84 | } 85 | router.post("/render", &handleRenderRequest); 86 | router.get("/token/info", &getAccessTokenInfo); 87 | router.get("/admin/tokens", &getAccessTokens); 88 | router.post("/admin/tokens", &newAccessToken); 89 | router.post("/admin/tokens/:id", &modifyAccessToken); 90 | router.delete_("/admin/tokens/:id", &revokeAccessToken); 91 | router.get("/admin/health", &getHealth); 92 | 93 | if (defaultConfig.enableCORS) 94 | { 95 | router.corsOptionsRoute!("/render", "POST"); 96 | router.corsOptionsRoute!("/token/info", "GET"); 97 | router.corsOptionsRoute!("/admin/tokens", "GET, POST"); 98 | router.corsOptionsRoute!("/admin/tokens/:id", "POST, DELETE"); 99 | router.corsOptionsRoute!("/admin/health", "GET"); 100 | } 101 | 102 | auto settings = new HTTPServerSettings; 103 | settings.bindAddresses = config.hosts; 104 | settings.port = config.port; 105 | settings.accessLogFormat = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{x-token-desc}i\""; 106 | settings.accessLogger = new MaskedConsoleLogger(settings, settings.accessLogFormat); 107 | 108 | if (defaultConfig.enableSSL) 109 | { 110 | import vibe.stream.tls : createTLSContext, TLSContextKind; 111 | 112 | settings.tlsContext = createTLSContext(TLSContextKind.server); 113 | settings.tlsContext.useCertificateChainFile(defaultConfig.certificateChainFile); 114 | settings.tlsContext.usePrivateKeyFile(defaultConfig.privateKeyFile); 115 | } 116 | 117 | auto listener = listenHTTP(settings, router); 118 | 119 | import vibe.core.args : finalizeCommandLineOptions; 120 | 121 | finalizeCommandLineOptions(null); 122 | 123 | try 124 | { 125 | runApplication(); 126 | } 127 | catch (Throwable e) 128 | { 129 | import vibe.core.log : logError; 130 | 131 | logError("%s in %s:%d", e.msg, e.file, e.line); 132 | } 133 | 134 | listener.stopListening(); 135 | 136 | return 0; 137 | } 138 | 139 | bool createOrLoadAccessTokens(const scope string tokenfilename) 140 | { 141 | import std.file : exists, FileException; 142 | 143 | if (!exists(tokenfilename)) 144 | { 145 | accessTokens = new AccessTokenDB; 146 | 147 | AccessToken accessToken = accessTokens.generateAccessToken(); 148 | accessToken.isAdmin = true; 149 | accessToken.isValid = true; 150 | accessToken.description = "Auto-generated Admin Token"; 151 | 152 | try 153 | { 154 | import std.string : join; 155 | import std.stdio : File; 156 | 157 | auto f = File(tokenfilename, "w"); 158 | f.writeln(accessTokens.lastId); 159 | f.writeln(serializeAccessToken(accessToken)); 160 | } 161 | catch (FileException err) 162 | { 163 | stderr.writeln(err.message); 164 | return false; 165 | } 166 | 167 | import std.stdio : writefln; 168 | 169 | writefln("Created access token file including a randomly generated admin token: %s", accessToken.token); 170 | accessTokens.storeToken(accessToken); 171 | } 172 | else 173 | { 174 | accessTokens = parseAccessTokensFile(tokenfilename); 175 | } 176 | 177 | return true; 178 | } 179 | 180 | LogLevel toVibeLogLevel(zLogLevel loglevel) pure nothrow @safe @nogc 181 | { 182 | switch (loglevel) 183 | { 184 | case zLogLevel.all: 185 | return LogLevel.min; 186 | case zLogLevel.trace: 187 | return LogLevel.trace; 188 | case zLogLevel.info: 189 | return LogLevel.info; 190 | case zLogLevel.warning: 191 | return LogLevel.warn; 192 | case zLogLevel.error: 193 | return LogLevel.error; 194 | case zLogLevel.critical: 195 | return LogLevel.critical; 196 | case zLogLevel.fatal: 197 | return LogLevel.fatal; 198 | case zLogLevel.off: 199 | return LogLevel.none; 200 | default: 201 | return LogLevel.min; 202 | } 203 | } 204 | 205 | class MaskedConsoleLogger : HTTPLogger 206 | { 207 | import std.regex : regex, replaceAll; 208 | 209 | auto accesstokenRegex = regex(r"(accesstoken=)([^&\n\r\t\s]+)"); 210 | 211 | this(HTTPServerSettings settings, string format) 212 | { 213 | super(settings, format); 214 | } 215 | 216 | override void writeLine(const(char)[] ln) 217 | { 218 | import vibe.core.log : logInfo; 219 | 220 | logInfo("%s", replaceAll(ln, accesstokenRegex, "$1***")); 221 | } 222 | } 223 | 224 | void corsOptionsRoute(string path, string methods)(URLRouter router) 225 | { 226 | router.match(HTTPMethod.OPTIONS, path, 227 | delegate void(HTTPServerRequest req, HTTPServerResponse res) @safe 228 | { 229 | const allowHeaders = req.headers.get("Access-Control-Request-Headers", string.init); 230 | if (allowHeaders != string.init) 231 | { 232 | res.headers.addField("Access-Control-Allow-Headers", allowHeaders); 233 | const varyHeader = res.headers.get("Vary", string.init); 234 | if (varyHeader != string.init) 235 | { 236 | res.headers["Vary"] = varyHeader ~ ", Access-Control-Request-Headers"; 237 | } 238 | else 239 | { 240 | res.headers.addField("Vary", "Access-Control-Request-Headers"); 241 | } 242 | } 243 | res.headers.addField("Access-Control-Allow-Methods", methods); 244 | res.statusCode = HTTPStatus.noContent; 245 | res.writeBody(""); 246 | }); 247 | } 248 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/routes/admin.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.routes.admin; 2 | 3 | import vibe.data.json; 4 | import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 5 | import vibe.http.status; 6 | import zrenderer.server.auth : AccessToken, checkAuth, isAllowedToSetTokenData; 7 | import zrenderer.server.globals : accessTokens, defaultConfig; 8 | import zrenderer.server.routes : setErrorResponse, setOkResponse, mergeStruct, unauthorized; 9 | 10 | void getAccessTokens(HTTPServerRequest req, HTTPServerResponse res) @trusted 11 | { 12 | immutable accessToken = checkAuth(req, accessTokens); 13 | if (accessToken.isNull() || (!accessToken.get.isAdmin && 14 | !accessToken.get.capabilities.readAccessTokens)) 15 | { 16 | unauthorized(res); 17 | return; 18 | } 19 | 20 | import std.algorithm.iteration : filter, map; 21 | import std.range : tee; 22 | import std.array : array; 23 | 24 | res.writeJsonBody(accessTokens.tokenMap 25 | .byValue() 26 | .filter!(token => accessToken.get.isAdmin || !token.isAdmin) 27 | .map!((token) { 28 | auto t = serializeToJson(token); 29 | t.remove("isValid"); 30 | if (!accessToken.get.isAdmin) 31 | t.remove("isAdmin"); 32 | return t; 33 | }) 34 | .array); 35 | } 36 | 37 | void newAccessToken(HTTPServerRequest req, HTTPServerResponse res) @trusted 38 | { 39 | immutable accessToken = checkAuth(req, accessTokens); 40 | if (accessToken.isNull() || (!accessToken.get.isAdmin && !accessToken.get 41 | .capabilities.createAccessTokens)) 42 | { 43 | unauthorized(res); 44 | return; 45 | } 46 | 47 | if (req.json == Json.undefined) 48 | { 49 | setErrorResponse(res, HTTPStatus.badRequest, "Expected json input"); 50 | return; 51 | } 52 | 53 | import zrenderer.server.dto : AccessTokenData; 54 | 55 | AccessTokenData tokenData; 56 | 57 | try 58 | { 59 | tokenData = deserializeJson!AccessTokenData(req.json); 60 | } 61 | catch (Exception err) 62 | { 63 | setErrorResponse(res, HTTPStatus.badRequest, err.msg); 64 | return; 65 | } 66 | 67 | if (!tokenData.description.isNull) 68 | { 69 | import std.array : replace; 70 | 71 | tokenData.description = tokenData.description.get.replace(",", ""); 72 | if (tokenData.description.get.length == 0) 73 | { 74 | tokenData.description.nullify(); 75 | } 76 | } 77 | 78 | if (tokenData.description.isNull) 79 | { 80 | setErrorResponse(res, HTTPStatus.badRequest, "Mandatory 'description' is missing"); 81 | return; 82 | } 83 | 84 | if (!accessToken.get.isAdmin && !isAllowedToSetTokenData(accessToken.get, tokenData)) 85 | { 86 | setErrorResponse(res, HTTPStatus.badRequest, "Not allowed to set token capability/property"); 87 | return; 88 | } 89 | 90 | accessTokens.mtx.lock(); 91 | scope (exit) 92 | accessTokens.mtx.unlock(); 93 | 94 | import std.stdio : File; 95 | import std.exception : ErrnoException; 96 | import zrenderer.server.auth : serializeAccessToken; 97 | 98 | try 99 | { 100 | auto tokenfile = File(defaultConfig.tokenfile, "w+"); 101 | tokenfile.lock(); 102 | scope (exit) 103 | tokenfile.unlock(); 104 | 105 | auto newToken = mergeStruct(accessTokens.generateAccessToken(), tokenData); 106 | 107 | accessTokens.storeToken(newToken); 108 | tokenfile.write(accessTokens.serialize()); 109 | res.writeJsonBody(Json(["token": Json(newToken.token), "id": Json(newToken.id)])); 110 | } 111 | catch (ErrnoException err) 112 | { 113 | setErrorResponse(res, HTTPStatus.internalServerError, "Failed to persist tokens file"); 114 | } 115 | } 116 | 117 | void modifyAccessToken(HTTPServerRequest req, HTTPServerResponse res) @trusted 118 | { 119 | immutable accessToken = checkAuth(req, accessTokens); 120 | if (accessToken.isNull() || (!accessToken.get.isAdmin && !accessToken.get 121 | .capabilities.modifyAccessTokens)) 122 | { 123 | unauthorized(res); 124 | return; 125 | } 126 | 127 | if (req.json == Json.undefined) 128 | { 129 | setErrorResponse(res, HTTPStatus.badRequest, "Expected json input"); 130 | return; 131 | } 132 | 133 | import std.exception : ifThrown; 134 | import std.conv : to; 135 | 136 | const tokenId = req.params["id"].to!uint.ifThrown(uint.max); 137 | 138 | if (tokenId == uint.max) 139 | { 140 | setErrorResponse(res, HTTPStatus.badRequest, "Invalid token id provided"); 141 | return; 142 | } 143 | 144 | import zrenderer.server.dto : AccessTokenData; 145 | 146 | AccessTokenData tokenData; 147 | 148 | try 149 | { 150 | tokenData = deserializeJson!AccessTokenData(req.json); 151 | } 152 | catch (Exception err) 153 | { 154 | setErrorResponse(res, HTTPStatus.badRequest, err.msg); 155 | return; 156 | } 157 | 158 | if (!accessToken.get.isAdmin && !isAllowedToSetTokenData(accessToken.get, tokenData)) 159 | { 160 | setErrorResponse(res, HTTPStatus.badRequest, "Not allowed to set token capability/property"); 161 | return; 162 | } 163 | 164 | if (!tokenData.description.isNull) 165 | { 166 | import std.array : replace; 167 | 168 | tokenData.description = tokenData.description.get.replace(",", ""); 169 | if (tokenData.description.get.length == 0) 170 | { 171 | tokenData.description.nullify(); 172 | } 173 | } 174 | 175 | accessTokens.mtx.lock(); 176 | scope (exit) 177 | accessTokens.mtx.unlock(); 178 | 179 | auto existingToken = accessTokens.getById(tokenId); 180 | if (existingToken.isNull) 181 | { 182 | setErrorResponse(res, HTTPStatus.notFound, "Token doesn't exist"); 183 | return; 184 | } 185 | else if (existingToken.get.isAdmin) 186 | { 187 | setErrorResponse(res, HTTPStatus.badRequest, "Cannot change token"); 188 | return; 189 | } 190 | 191 | import std.stdio : File; 192 | import std.exception : ErrnoException; 193 | import zrenderer.server.auth : serializeAccessToken; 194 | 195 | try 196 | { 197 | auto tokenfile = File(defaultConfig.tokenfile, "w+"); 198 | tokenfile.lock(); 199 | scope (exit) 200 | tokenfile.unlock(); 201 | 202 | auto updatedToken = mergeStruct(existingToken.get, tokenData); 203 | 204 | accessTokens.storeToken(updatedToken); 205 | tokenfile.write(accessTokens.serialize()); 206 | setOkResponse(res); 207 | } 208 | catch (ErrnoException err) 209 | { 210 | setErrorResponse(res, HTTPStatus.internalServerError, "Failed to persist tokens file"); 211 | } 212 | } 213 | 214 | void revokeAccessToken(HTTPServerRequest req, HTTPServerResponse res) @trusted 215 | { 216 | immutable accessToken = checkAuth(req, accessTokens); 217 | if (accessToken.isNull() || (!accessToken.get.isAdmin && !accessToken.get 218 | .capabilities.revokeAccessTokens)) 219 | { 220 | unauthorized(res); 221 | return; 222 | } 223 | 224 | import std.exception : ifThrown; 225 | import std.conv : to; 226 | 227 | const tokenId = req.params["id"].to!uint.ifThrown(uint.max); 228 | 229 | if (tokenId == uint.max) 230 | { 231 | setErrorResponse(res, HTTPStatus.badRequest, "Invalid token id provided"); 232 | return; 233 | } 234 | 235 | accessTokens.mtx.lock(); 236 | scope (exit) 237 | accessTokens.mtx.unlock(); 238 | 239 | auto existingToken = accessTokens.getById(tokenId); 240 | if (existingToken.isNull) 241 | { 242 | setErrorResponse(res, HTTPStatus.notFound, "Token doesn't exist"); 243 | return; 244 | } 245 | else if (existingToken.get.isAdmin) 246 | { 247 | setErrorResponse(res, HTTPStatus.badRequest, "Cannot revoke token"); 248 | return; 249 | } 250 | 251 | import std.stdio : File; 252 | import std.exception : ErrnoException; 253 | import zrenderer.server.auth : serializeAccessToken; 254 | 255 | try 256 | { 257 | auto tokenfile = File(defaultConfig.tokenfile, "w+"); 258 | tokenfile.lock(); 259 | scope (exit) 260 | tokenfile.unlock(); 261 | 262 | accessTokens.removeById(tokenId); 263 | tokenfile.write(accessTokens.serialize()); 264 | setOkResponse(res); 265 | } 266 | catch (ErrnoException err) 267 | { 268 | setErrorResponse(res, HTTPStatus.internalServerError, "Failed to persist tokens file"); 269 | } 270 | } 271 | 272 | void getHealth(HTTPServerRequest req, HTTPServerResponse res) @trusted 273 | { 274 | immutable accessToken = checkAuth(req, accessTokens); 275 | if (accessToken.isNull() || (!accessToken.get.isAdmin && !accessToken.get 276 | .capabilities.readHealth)) 277 | { 278 | unauthorized(res); 279 | return; 280 | } 281 | 282 | auto reply = Json(["up": Json(true)]); 283 | 284 | if (accessToken.get.isAdmin) 285 | { 286 | import core.memory : GC; 287 | 288 | reply["gc"] = Json(["usedSize": Json(GC.stats.usedSize), "freeSize": Json(GC.stats.freeSize)]); 289 | } 290 | 291 | res.writeJsonBody(reply); 292 | } 293 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/routes/package.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.routes; 2 | 3 | public import zrenderer.server.routes.admin; 4 | public import zrenderer.server.routes.render; 5 | public import zrenderer.server.routes.token; 6 | 7 | import std.typecons : Nullable; 8 | import vibe.data.json; 9 | import vibe.http.server : HTTPServerResponse, HTTPServerRequest; 10 | import vibe.http.status; 11 | import zrenderer.server.auth : AccessToken; 12 | import zrenderer.server.globals : defaultConfig; 13 | 14 | void setErrorResponse(ref HTTPServerResponse res, HTTPStatus httpStatus, const scope string message = string.init) 15 | { 16 | res.statusCode = httpStatus; 17 | auto jsonResponse = Json(["statusMessage": Json(message)]); 18 | res.writeJsonBody(jsonResponse); 19 | } 20 | 21 | void setOkResponse(ref HTTPServerResponse res, const scope string message = string.init) 22 | { 23 | res.statusCode = HTTPStatus.ok; 24 | if (message == string.init) 25 | { 26 | res.writeJsonBody(Json(["statusMesage": Json("Ok")])); 27 | } 28 | else 29 | { 30 | res.writeJsonBody(Json(["statusMessage": Json(message)])); 31 | } 32 | } 33 | 34 | void unauthorized(HTTPServerResponse res) 35 | { 36 | setErrorResponse(res, HTTPStatus.unauthorized, "Unauthorized"); 37 | } 38 | 39 | void addCORSOriginHeader(HTTPServerRequest req, HTTPServerResponse res) 40 | { 41 | // If Origin is not set we assume that this is not a CORS request 42 | immutable origin = req.headers.get("Origin", string.init); 43 | if (origin != string.init) 44 | { 45 | import std.algorithm.searching : canFind; 46 | 47 | const allowOrigins = defaultConfig.allowCORSOrigin; 48 | 49 | if (allowOrigins.length > 1) 50 | { 51 | res.headers.addField("Vary", "Origin"); 52 | } 53 | 54 | if (allowOrigins.length > 0) 55 | { 56 | if (allowOrigins[0] == "*") 57 | { 58 | res.headers.addField("Access-Control-Allow-Origin", "*"); 59 | } 60 | else if (allowOrigins.canFind(origin)) 61 | { 62 | res.headers.addField("Access-Control-Allow-Origin", origin); 63 | } 64 | else 65 | { 66 | res.headers.addField("Access-Control-Allow-Origin", defaultConfig.allowCORSOrigin[0]); 67 | } 68 | } 69 | else 70 | { 71 | res.headers.addField("Access-Control-Allow-Origin", "*"); 72 | } 73 | } 74 | } 75 | 76 | void logCustomRequest(HTTPServerRequest req, const scope string message, 77 | Nullable!AccessToken accessToken = Nullable!AccessToken.init) @safe 78 | { 79 | import std.exception : ifThrown; 80 | import std.format : format; 81 | import vibe.core.log : logInfo; 82 | 83 | auto remoteHost = req.headers["X-REAL-IP"].ifThrown(req.peer); 84 | 85 | logInfo("%s - %s %s \"%s\" -- Token: %s", 86 | remoteHost, 87 | req.username.length > 0 ? req.username : "-", 88 | req.timeCreated.toSimpleString(), 89 | message, 90 | accessToken.isNull ? "-" : format("Id: %u Desc: %s", 91 | accessToken.get.id, 92 | accessToken.get.description)); 93 | 94 | } 95 | 96 | T mergeStruct(T, S)(T target, S source) pure nothrow @safe 97 | { 98 | T mergedStruct = target; 99 | 100 | static foreach (memberName; __traits(allMembers, S)) 101 | { 102 | static if (__traits(hasMember, mergedStruct, memberName)) 103 | { 104 | static if (__traits(compiles, (__traits(getMember, source, memberName)).isNull)) 105 | { 106 | if (!(__traits(getMember, source, memberName)).isNull) 107 | { 108 | static if (is(typeof(__traits(getMember, source, memberName).get) == struct) && is(typeof(__traits(getMember, target, memberName)) == struct)) 109 | { 110 | __traits(getMember, mergedStruct, memberName) = mergeStruct!(typeof(__traits(getMember, target, memberName)), typeof(__traits(getMember, source, memberName).get))(__traits(getMember, target, memberName), __traits(getMember, source, memberName).get); 111 | } 112 | else 113 | { 114 | __traits(getMember, mergedStruct, memberName) = __traits(getMember, source, memberName).get(); 115 | } 116 | } 117 | } 118 | else 119 | { 120 | static if (is(typeof(__traits(getMember, source, memberName).get) == struct) && is(typeof(__traits(getMember, target, memberName)) == struct)) 121 | { 122 | __traits(getMember, mergedStruct, memberName) = mergeStruct!(typeof(__traits(getMember, target, memberName)), typeof(__traits(getMember, source, memberName)))(__traits(getMember, target, memberName), __traits(getMember, source, memberName)); 123 | } 124 | else 125 | { 126 | __traits(getMember, mergedStruct, memberName) = __traits(getMember, source, memberName); 127 | } 128 | } 129 | } 130 | } 131 | 132 | return mergedStruct; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/routes/render.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.routes.render; 2 | 3 | import config; 4 | import std.datetime : seconds; 5 | import std.typecons : Nullable; 6 | import std.zip : ArchiveMember, ZipArchive; 7 | import validation : isJobArgValid, isCanvasArgValid; 8 | import vibe.core.concurrency : send, receiveTimeout, OwnerTerminated; 9 | import vibe.core.core : runWorkerTaskH; 10 | import vibe.core.log : logInfo, logError; 11 | import vibe.core.task : Task; 12 | import vibe.data.json; 13 | import vibe.data.serialization; 14 | import vibe.http.common : HTTPStatusException; 15 | import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 16 | import vibe.http.status; 17 | import zrenderer.server.auth : AccessToken, checkAuth; 18 | import zrenderer.server.dto : RenderRequestData, RenderResponseData, toString; 19 | import zrenderer.server.globals : defaultConfig, accessTokens; 20 | import zrenderer.server.routes : setErrorResponse, mergeStruct, unauthorized, logCustomRequest; 21 | import zrenderer.server.worker : renderWorker; 22 | 23 | void handleRenderRequest(HTTPServerRequest req, HTTPServerResponse res) @trusted 24 | { 25 | immutable accessToken = checkAuth(req, accessTokens); 26 | 27 | if (accessToken.isNull() || !accessToken.get.isValid) 28 | { 29 | unauthorized(res); 30 | return; 31 | } 32 | 33 | if (req.json == Json.undefined) 34 | { 35 | setErrorResponse(res, HTTPStatus.badRequest, "Expected json input"); 36 | return; 37 | } 38 | 39 | RenderRequestData requestData; 40 | 41 | try 42 | { 43 | requestData = deserializeJson!RenderRequestData(req.json); 44 | } 45 | catch (Exception err) 46 | { 47 | setErrorResponse(res, HTTPStatus.badRequest, err.msg); 48 | return; 49 | } 50 | 51 | logCustomRequest(req, requestData.toString, accessToken); 52 | 53 | const(Config) mergedConfig = mergeStruct(defaultConfig, requestData); 54 | 55 | if (!isJobArgValid(mergedConfig.job, accessToken.get.properties.maxJobIdsPerRequest)) 56 | { 57 | setErrorResponse(res, HTTPStatus.badRequest, "Invalid job element"); 58 | return; 59 | } 60 | 61 | if (!isCanvasArgValid(mergedConfig.canvas)) 62 | { 63 | setErrorResponse(res, HTTPStatus.badRequest, "Invalid canvas element"); 64 | return; 65 | } 66 | 67 | auto worker = runWorkerTaskH(&renderWorker, Task.getThis); 68 | send(worker, cast(immutable Config) mergedConfig); 69 | 70 | RenderResponseData response; 71 | bool renderingSucceeded = false; 72 | 73 | try 74 | { 75 | receiveTimeout(5.seconds, 76 | (immutable(string)[] filenames) { 77 | response.output = filenames; 78 | renderingSucceeded = true; 79 | }, 80 | (bool failed) { 81 | renderingSucceeded = !failed; 82 | } 83 | ); 84 | } 85 | catch (OwnerTerminated e) 86 | { 87 | setErrorResponse(res, HTTPStatus.internalServerError, "Rendering timed out / was aborted"); 88 | return; 89 | } 90 | 91 | if (!renderingSucceeded) 92 | { 93 | setErrorResponse(res, HTTPStatus.internalServerError, "Error during rendering process"); 94 | return; 95 | } 96 | 97 | import std.file : read, FileException; 98 | 99 | if (mergedConfig.outputFormat == OutputFormat.zip) 100 | { 101 | 102 | if (response.output.length == 0) 103 | { 104 | setErrorResponse(res, HTTPStatus.noContent, "Nothing rendered"); 105 | return; 106 | } 107 | 108 | res.contentType("application/zip"); 109 | try 110 | { 111 | res.writeBody(cast(ubyte[]) read(response.output[$-1])); 112 | } 113 | catch (FileException err) 114 | { 115 | logError(err.message); 116 | setErrorResponse(res, HTTPStatus.internalServerError, "Error when writing response"); 117 | return; 118 | } 119 | } 120 | else 121 | { 122 | import std.exception : ifThrown; 123 | 124 | bool downloadImage = (req.query["downloadimage"].length >= 0).ifThrown(false); 125 | 126 | if (downloadImage) 127 | { 128 | if (response.output.length == 0) 129 | { 130 | setErrorResponse(res, HTTPStatus.noContent, "Nothing rendered"); 131 | return; 132 | } 133 | 134 | res.contentType("image/png"); 135 | try 136 | { 137 | res.writeBody(cast(ubyte[]) read(response.output[0])); 138 | } 139 | catch (FileException err) 140 | { 141 | logError(err.message); 142 | setErrorResponse(res, HTTPStatus.internalServerError, "Error when writing response"); 143 | return; 144 | } 145 | } 146 | else 147 | { 148 | res.writeJsonBody(serializeToJson(response)); 149 | } 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/routes/token.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.routes.token; 2 | 3 | import vibe.data.json; 4 | import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 5 | import zrenderer.server.auth : AccessToken, checkAuth; 6 | import zrenderer.server.globals : accessTokens; 7 | import zrenderer.server.routes : unauthorized; 8 | 9 | void getAccessTokenInfo(HTTPServerRequest req, HTTPServerResponse res) @trusted 10 | { 11 | immutable accessToken = checkAuth(req, accessTokens); 12 | if (accessToken.isNull()) 13 | { 14 | unauthorized(res); 15 | return; 16 | } 17 | 18 | auto jsonToken = Json(["capabilities": serializeToJson(accessToken.get.capabilities), 19 | "properties": serializeToJson(accessToken.get.properties)]); 20 | 21 | res.writeJsonBody(jsonToken); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /server/source/zrenderer/server/worker.d: -------------------------------------------------------------------------------- 1 | module zrenderer.server.worker; 2 | 3 | import vibe.core.log; 4 | import vibe.core.task : Task; 5 | import vibe.core.concurrency : send; 6 | 7 | import zrenderer.server.dto : RenderRequestData; 8 | import config : Config; 9 | import resource : ResourceManager, ResourceException; 10 | import resolver : Resolver; 11 | import luamanager : loadRequiredLuaFiles; 12 | import luad.state : LuaState; 13 | import logging : LogDg; 14 | 15 | /// Throws ResourceException 16 | static ResourceManager createAndInitResourceManager(LuaState L, string resourcePath, LogDg log) 17 | { 18 | auto resManager = new ResourceManager(resourcePath); 19 | 20 | loadRequiredLuaFiles(L, resManager, log); 21 | 22 | return resManager; 23 | } 24 | 25 | static void renderWorker(Task caller) nothrow 26 | { 27 | import logging : LogLevel; 28 | 29 | void logger (LogLevel logLevel, string msg) 30 | { 31 | switch (logLevel) 32 | { 33 | case LogLevel.trace: 34 | logTrace(msg); 35 | break; 36 | default: 37 | case LogLevel.info: 38 | logInfo(msg); 39 | break; 40 | case LogLevel.warning: 41 | logWarn(msg); 42 | break; 43 | case LogLevel.error: 44 | logError(msg); 45 | break; 46 | case LogLevel.critical: 47 | logCritical(msg); 48 | break; 49 | case LogLevel.fatal: 50 | logFatal(msg); 51 | break; 52 | } 53 | } 54 | 55 | try 56 | { 57 | static isInitialized = false; 58 | static LuaState L; 59 | static ResourceManager resManager; 60 | static Resolver resolver; 61 | 62 | import vibe.core.concurrency : receiveOnly; 63 | 64 | immutable config = receiveOnly!(immutable Config)(); 65 | 66 | if (!isInitialized) 67 | { 68 | L = new LuaState; 69 | L.openLibs(); 70 | resManager = createAndInitResourceManager(L, config.resourcepath, &logger); 71 | resolver = new Resolver(L); 72 | isInitialized = true; 73 | } 74 | 75 | import app : run; 76 | immutable(string)[] filenames = cast(immutable(string)[]) run(config, &logger, L, resManager, resolver); 77 | 78 | send(caller, filenames); 79 | } 80 | catch (Throwable e) 81 | { 82 | logError("%s in %s:%d", e.msg, e.file, e.line); 83 | try 84 | { 85 | send(caller, true); // Don't make the caller wait and tell it we failed 86 | } 87 | catch (Throwable e2) 88 | { 89 | // I have no clue what is supposed to be thrown here. Docs say nothing. 90 | // Not even gonna bother logging anything, because at this point the whole 91 | // service is dead anyway 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /source/config.d: -------------------------------------------------------------------------------- 1 | module config; 2 | 3 | import logging : LogLevel; 4 | import zconfig : Section, Desc, Short, ConfigFile, Required; 5 | 6 | enum NoJobId = uint.max - 1; 7 | 8 | enum Gender 9 | { 10 | female, 11 | male 12 | } 13 | 14 | string toString(Gender gender) pure nothrow @safe @nogc 15 | { 16 | switch (gender) 17 | { 18 | case Gender.male: 19 | return "남"; 20 | case Gender.female: 21 | default: 22 | return "여"; 23 | } 24 | } 25 | 26 | int toInt(Gender gender) pure nothrow @safe @nogc 27 | { 28 | switch (gender) 29 | { 30 | case Gender.male: 31 | return 1; 32 | case Gender.female: 33 | default: 34 | return 0; 35 | } 36 | } 37 | 38 | enum HeadDirection 39 | { 40 | straight, 41 | left, 42 | right, 43 | all 44 | } 45 | 46 | int toInt(HeadDirection headdir) pure nothrow @safe @nogc 47 | { 48 | switch (headdir) 49 | { 50 | case HeadDirection.straight: 51 | return 0; 52 | case HeadDirection.right: 53 | return 1; 54 | case HeadDirection.left: 55 | return 2; 56 | default: 57 | return 0; 58 | } 59 | } 60 | 61 | enum MadogearType 62 | { 63 | robot, 64 | unused, 65 | suit 66 | } 67 | 68 | int toInt(MadogearType type) pure nothrow @safe @nogc 69 | { 70 | switch (type) 71 | { 72 | case MadogearType.robot: 73 | return 0; 74 | case MadogearType.unused: 75 | return 1; 76 | case MadogearType.suit: 77 | return 2; 78 | default: 79 | return 0; 80 | } 81 | } 82 | 83 | enum OutputFormat 84 | { 85 | png, 86 | zip 87 | } 88 | 89 | int toInt(OutputFormat format) pure nothrow @safe @nogc 90 | { 91 | switch (format) 92 | { 93 | case OutputFormat.png: 94 | return 0; 95 | case OutputFormat.zip: 96 | return 1; 97 | default: 98 | return 0; 99 | } 100 | } 101 | 102 | struct Config 103 | { 104 | @ConfigFile @Short("c") @Desc("Specific config file to use instead of the default.") 105 | string config = "zrenderer.conf"; 106 | 107 | @Short("o") @Desc("Output directory where all rendered sprites will be saved to.") 108 | string outdir = "output"; 109 | 110 | @Desc("Path to the resource directory. All resources are tried to be found within " ~ 111 | "this directory.") 112 | string resourcepath = ""; 113 | 114 | @Short("j") @Desc("Job id(s) which should be rendered. Can contain multiple comma " ~ 115 | "separated values as well as ranges (e.g. '1001-1999'). Providing a single value " ~ 116 | "of 'none' will not render the body, only the head with headgers.") 117 | string[] job; 118 | 119 | @Short("g") @Desc("Gender of the player character. Possible values are: 'male' (1) or 'female' (0).") 120 | Gender gender = Gender.male; 121 | 122 | @Desc("Head id which should be used when drawing a player.") 123 | uint head = 1; 124 | 125 | @Desc("The alternative outfit for player characters. Not all characters have alternative outfits. " ~ 126 | "In these cases the default character will be rendered instead. Value of 0 means no outfit.") 127 | uint outfit = 0; 128 | 129 | @Desc("Headgears which should be attached to the players head. Can contain up to 3 " ~ 130 | "comma separated values.") 131 | uint[] headgear; 132 | 133 | @Desc("Garment which should be attached to the players body.") 134 | uint garment; 135 | 136 | @Desc("Weapon which should be attached to the players body.") 137 | uint weapon; 138 | 139 | @Desc("Shield which should be attached to the players body.") 140 | uint shield; 141 | 142 | @Short("a") @Desc("Action of the job which should be drawn.") 143 | uint action = 0; 144 | 145 | @Short("f") @Desc("Frame of the action which should be drawn. Set to -1 to draw all frames.") 146 | int frame = -1; 147 | 148 | @Desc("Palette for the body sprite. Set to -1 to use the standard palette.") 149 | int bodyPalette = -1; 150 | 151 | @Desc("Palette for the head sprite. Set to -1 to use the standard palette.") 152 | int headPalette = -1; 153 | 154 | @Desc("Direction in which the head should turn. This is only applied to player sprites and only to the stand " ~ 155 | "and sit action. Possible values are: straight, left, right or all. If 'all' is set then this direction " ~ 156 | "system is ignored and all frames are interpreted like any other one.") 157 | HeadDirection headdir = HeadDirection.all; 158 | 159 | @Desc("The alternative madogear sprite for player characters. Only applicable to madogear jobs. Possible values " ~ 160 | "are 'robot' (0) and 'suit' (2).") 161 | MadogearType madogearType = MadogearType.robot; 162 | 163 | @Desc("Draw shadow underneath the sprite.") 164 | bool enableShadow = true; 165 | 166 | @Desc("Generate single frames of an animation.") 167 | bool singleframes = false; 168 | 169 | @Desc("If enabled the output filenames will be the checksum of input parameters. This will ensure that each " ~ 170 | "request creates a filename that is unique to the input parameters and no overlapping for the same " ~ 171 | "job occurs.") 172 | bool enableUniqueFilenames = false; 173 | 174 | @Desc("Whether to return already existing sprites (true) or always re-render it (false). You should only use " ~ 175 | "this option in conjuction with 'enableUniqueFilenames=true'.") 176 | bool returnExistingFiles = false; 177 | 178 | @Desc("Sets a canvas onto which the sprite should be rendered. The canvas requires two options: its size and " ~ 179 | "an origin point inside the canvas where the sprite should be placed. The format is as following: " ~ 180 | "x±±. An origin point of +0+0 is equal to the top left corner. " ~ 181 | "Example: 200x250+100+125. This would create a canvas and place the sprite in the center.") 182 | string canvas = ""; 183 | 184 | @Desc("Defines the output format. Possible values are 'png' (0) or 'zip' (1). If zip is chosen the zip will contain png " ~ 185 | "files.") 186 | OutputFormat outputFormat = OutputFormat.png; 187 | 188 | @Desc("Log level. Defines the minimum level at which logs will be shown. Possible values are: " ~ 189 | "all, trace, info, warning, error, critical, fatal or off.") 190 | LogLevel loglevel = LogLevel.info; 191 | 192 | @Section("server") 193 | { 194 | @Desc("Hostnames of the server. Can contain multiple comma separated values.") 195 | string[] hosts = ["localhost"]; 196 | 197 | @Desc("Port of the server.") 198 | ushort port = 11011; 199 | 200 | @Desc("Log file to write to. E.g. /var/log/zrenderer.log. Leaving it empty will log to stdout.") 201 | string logfile = ""; 202 | 203 | @Desc("Access tokens file. File in which access tokens will be stored in. If the file does not exist it will be generated.") 204 | string tokenfile = "accesstokens.conf"; 205 | 206 | @Desc("Setting this to true will add CORS headers to all responses as well as adding an additional OPTIONS route " ~ 207 | "that returns the CORS headers.") 208 | bool enableCORS = false; 209 | 210 | @Desc("Comma separated list of origins that are allowed access through CORS. Set this to a single '*' to allow access " ~ 211 | "from any origin. Example: https://example.com.") 212 | string[] allowCORSOrigin = []; 213 | 214 | @Desc("Whether to use TLS/SSL to secure the connection. You will also need to set the certificate and private key when " ~ 215 | "enabling this setting. We recommend not enabling this feature but instead use a reverse proxy that handles HTTPS " ~ 216 | "for you.") 217 | bool enableSSL = false; 218 | 219 | @Desc("Path to the certificate chain file used by TLS/SSL.") 220 | string certificateChainFile = ""; 221 | 222 | @Desc("Path to the private key file used by TLS/SSL.") 223 | string privateKeyFile = ""; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /source/draw/canvas.d: -------------------------------------------------------------------------------- 1 | module draw.canvas; 2 | 3 | struct Canvas 4 | { 5 | uint width; 6 | uint height; 7 | int originx; 8 | int originy; 9 | } 10 | 11 | immutable(Canvas) canvasFromString(const scope string canvasArg) pure @safe 12 | { 13 | if (canvasArg.length == 0) 14 | { 15 | return Canvas.init; 16 | } 17 | 18 | import std.regex : matchFirst; 19 | import std.conv : to; 20 | import validation : CanvasRegex; 21 | 22 | auto matchFound = matchFirst(canvasArg, CanvasRegex); 23 | 24 | return Canvas( 25 | matchFound[1].to!uint, 26 | matchFound[2].to!uint, 27 | matchFound[3].to!int, 28 | matchFound[4].to!int 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /source/draw/color.d: -------------------------------------------------------------------------------- 1 | module draw.color; 2 | 3 | struct Color 4 | { 5 | uint color = 0x00000000; 6 | 7 | ubyte a() const pure nothrow @safe @nogc 8 | { 9 | return cast(ubyte)((this.color >> 24) & 0xFF); 10 | } 11 | 12 | void a(ubyte alpha) pure nothrow @safe @nogc 13 | { 14 | this.color = ((alpha << 24) & 0xFF000000) | (this.color & 0x00FFFFFF); 15 | } 16 | 17 | ubyte b() const pure nothrow @safe @nogc 18 | { 19 | return cast(ubyte)((this.color >> 16) & 0xFF); 20 | } 21 | 22 | void b(ubyte blue) pure nothrow @safe @nogc 23 | { 24 | this.color = ((blue << 16) & 0x00FF0000) | (this.color & 0xFF00FFFF); 25 | } 26 | 27 | ubyte g() const pure nothrow @safe @nogc 28 | { 29 | return cast(ubyte)((this.color >> 8) & 0xFF); 30 | } 31 | 32 | void g(ubyte green) pure nothrow @safe @nogc 33 | { 34 | this.color = ((green << 8) & 0x0000FF00) | (this.color & 0xFFFF00FF); 35 | } 36 | 37 | ubyte r() const pure nothrow @safe @nogc 38 | { 39 | return cast(ubyte)(this.color & 0xFF); 40 | } 41 | 42 | void r(ubyte red) pure nothrow @safe @nogc 43 | { 44 | this.color = (red & 0x000000FF) | (this.color & 0xFFFFFF00); 45 | } 46 | 47 | Color reverse() const pure nothrow @safe @nogc 48 | { 49 | import std.bitmanip : swapEndian; 50 | 51 | return cast(Color) this.color.swapEndian; 52 | } 53 | 54 | alias color this; 55 | } 56 | 57 | Color fromAABBGGRR(uint color) pure nothrow @safe @nogc 58 | { 59 | Color newcolor; 60 | return newcolor; 61 | } 62 | -------------------------------------------------------------------------------- /source/draw/drawobject.d: -------------------------------------------------------------------------------- 1 | module draw.drawobject; 2 | 3 | import draw.color; 4 | import draw.rawimage; 5 | import linearalgebra : Vector3, Box, TransformMatrix; 6 | import sprite : Sprite; 7 | 8 | struct DrawObject 9 | { 10 | Color tint; 11 | Vector3 offset; 12 | Box boundingBox; 13 | TransformMatrix transform; 14 | DrawObject[] children; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /source/draw/package.d: -------------------------------------------------------------------------------- 1 | module draw; 2 | 3 | public import draw.color; 4 | public import draw.rawimage; 5 | public import draw.drawobject; 6 | public import draw.canvas; 7 | -------------------------------------------------------------------------------- /source/draw/rawimage.d: -------------------------------------------------------------------------------- 1 | module draw.rawimage; 2 | 3 | import draw.color; 4 | 5 | struct RawImage 6 | { 7 | uint width; 8 | uint height; 9 | Color[] pixels; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /source/filehelper.d: -------------------------------------------------------------------------------- 1 | module filehelper; 2 | 3 | import config : Config, OutputFormat; 4 | import draw : RawImage; 5 | import logging : LogLevel, LogDg; 6 | import std.zip : ZipArchive; 7 | 8 | string[] storeImages(const RawImage[] images, int requestFrame, immutable(Config) config, 9 | const scope string outputFilename, const scope string zipFilename, LogDg log, 10 | float animationInterval, ZipArchive archive) 11 | { 12 | import imageformats.png : saveToPngFile, saveToApngFile; 13 | import std.file : mkdirRecurse, FileException, read; 14 | import std.format : format; 15 | import std.path : buildPath; 16 | import std.zip : ArchiveMember, CompressionMethod; 17 | 18 | bool shouldPutInZip = config.outputFormat == OutputFormat.zip; 19 | string[] filenames; 20 | 21 | try 22 | { 23 | if (shouldPutInZip) 24 | { 25 | mkdirRecurse(buildPath(config.outdir, zipFilename, outputFilename)); 26 | } 27 | else 28 | { 29 | mkdirRecurse(buildPath(config.outdir, outputFilename)); 30 | } 31 | } 32 | catch (FileException err) 33 | { 34 | log(LogLevel.error, err.msg); 35 | return []; 36 | } 37 | 38 | if (config.singleframes) 39 | { 40 | foreach (i, image; images) 41 | { 42 | auto names = storeImage(image, cast(int) i, outputFilename, config, zipFilename, archive); 43 | filenames ~= names.filename; 44 | } 45 | } 46 | 47 | if (images.length > 1) 48 | { 49 | auto names = storeAnimation(images, outputFilename, config, animationInterval, zipFilename, archive); 50 | filenames ~= names.filename; 51 | } 52 | else 53 | { 54 | if (requestFrame < 0) 55 | { 56 | auto names = storeImage(images[0], -1, outputFilename, config, zipFilename, archive); 57 | filenames ~= names.filename; 58 | } 59 | else 60 | { 61 | auto names = storeImage(images[0], requestFrame, outputFilename, config, zipFilename, archive); 62 | filenames ~= names.filename; 63 | } 64 | } 65 | 66 | return filenames; 67 | } 68 | 69 | private auto storeImage(const scope RawImage image, int frame, const scope string outputFilename, 70 | immutable(Config) config, lazy const scope string zipFilename, lazy ZipArchive archive) 71 | { 72 | import imageformats.png : saveToPngFile; 73 | 74 | auto names = getFilenames(outputFilename, config, zipFilename, config.outputFormat, config.action, frame); 75 | 76 | saveToPngFile(image, names.filename); 77 | 78 | if (config.outputFormat == OutputFormat.zip) 79 | { 80 | import std.path : buildPath; 81 | 82 | putFileInZip(archive, names.filename, buildPath(outputFilename, names.basename)); 83 | } 84 | 85 | return names; 86 | } 87 | 88 | private auto storeAnimation(const scope RawImage[] images, const scope string outputFilename, 89 | immutable(Config) config, float animationInterval, lazy const scope string zipFilename, 90 | lazy ZipArchive archive) 91 | { 92 | import imageformats.png : saveToApngFile; 93 | import std.conv : to; 94 | 95 | auto names = getFilenames(outputFilename, config, zipFilename, config.outputFormat, config.action, -1); 96 | 97 | saveToApngFile(images, names.filename, (25 * animationInterval).to!ushort); 98 | 99 | if (config.outputFormat == OutputFormat.zip) 100 | { 101 | import std.path : buildPath; 102 | 103 | putFileInZip(archive, names.filename, buildPath(outputFilename, names.basename)); 104 | } 105 | 106 | return names; 107 | } 108 | 109 | private auto getFilenames(const scope string outputFilename, immutable(Config) config, 110 | lazy const scope string zipFilename, OutputFormat outputFormat, int action, int frame) 111 | { 112 | import std.path : buildPath; 113 | import std.format : format; 114 | 115 | struct Filenames 116 | { 117 | string basename; 118 | string filename; 119 | } 120 | 121 | Filenames names; 122 | 123 | names.basename = (frame < 0) ? format("%d.png", action) : format("%d-%d.png", action, frame); 124 | names.filename = (outputFormat == OutputFormat.zip) 125 | ? buildPath(config.outdir, zipFilename, outputFilename, names.basename) 126 | : buildPath(config.outdir, outputFilename, names.basename); 127 | 128 | return names; 129 | } 130 | 131 | private void putFileInZip(ZipArchive archive, const scope string filename, const scope string nameInZip) 132 | { 133 | import std.file : read; 134 | import std.zip : ArchiveMember, CompressionMethod; 135 | 136 | auto member = new ArchiveMember(); 137 | member.compressionMethod = CompressionMethod.none; 138 | member.name = nameInZip; 139 | member.expandedData(cast(ubyte[]) read(filename)); 140 | archive.addMember(member); 141 | } 142 | 143 | -------------------------------------------------------------------------------- /source/imageformats/png.d: -------------------------------------------------------------------------------- 1 | module imageformats.png; 2 | 3 | import std.stdio : File; 4 | import draw : RawImage; 5 | import libpng.png; 6 | 7 | void saveToPngFile(const scope RawImage image, string filename) 8 | { 9 | auto file = File(filename, "wb"); 10 | if (!file.tryLock()) 11 | { 12 | file.close(); 13 | return; 14 | } 15 | scope (exit) 16 | { 17 | file.unlock(); 18 | file.close(); 19 | } 20 | 21 | png_bytep[] png_row_pointers = new png_bytep[image.height]; 22 | 23 | for (auto rowIndex = 0; rowIndex < image.height; ++rowIndex) 24 | { 25 | png_row_pointers[rowIndex] = cast(png_bytep) &image.pixels[rowIndex * image.width]; 26 | } 27 | 28 | png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, null, null, null); 29 | png_infop png_info_ptr = png_create_info_struct(png_ptr); 30 | scope(failure) 31 | { 32 | png_destroy_write_struct(&png_ptr, &png_info_ptr); 33 | } 34 | 35 | png_set_IHDR(png_ptr, 36 | png_info_ptr, 37 | image.width, 38 | image.height, 39 | 8, 40 | PNG_COLOR_TYPE_RGB_ALPHA, 41 | PNG_INTERLACE_NONE, 42 | PNG_COMPRESSION_TYPE_BASE, 43 | PNG_FILTER_TYPE_BASE); 44 | 45 | png_init_io(png_ptr, file.getFP()); 46 | png_set_rows(png_ptr, png_info_ptr, cast(ubyte**) png_row_pointers); 47 | png_write_png(png_ptr, png_info_ptr, PNG_TRANSFORM_IDENTITY, null); 48 | 49 | png_destroy_write_struct(&png_ptr, &png_info_ptr); 50 | } 51 | 52 | void saveToApngFile(const scope RawImage[] images, string filename, ushort delay) 53 | { 54 | auto file = File(filename, "wb"); 55 | if (!file.tryLock()) 56 | { 57 | file.close(); 58 | return; 59 | } 60 | scope (exit) 61 | { 62 | file.unlock(); 63 | file.close(); 64 | } 65 | 66 | png_bytep[] png_row_pointers = new png_bytep[images[0].height]; 67 | 68 | png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, null, null, null); 69 | png_infop png_info_ptr = png_create_info_struct(png_ptr); 70 | scope(failure) 71 | { 72 | png_destroy_write_struct(&png_ptr, &png_info_ptr); 73 | } 74 | 75 | png_set_IHDR(png_ptr, 76 | png_info_ptr, 77 | images[0].width, 78 | images[0].height, 79 | 8, 80 | PNG_COLOR_TYPE_RGB_ALPHA, 81 | PNG_INTERLACE_NONE, 82 | PNG_COMPRESSION_TYPE_BASE, 83 | PNG_FILTER_TYPE_BASE); 84 | 85 | png_set_acTL(png_ptr, png_info_ptr, cast(uint) images.length, 0); 86 | 87 | png_init_io(png_ptr, file.getFP()); 88 | 89 | png_write_info(png_ptr, png_info_ptr); 90 | 91 | foreach (image; images) { 92 | for (auto rowIndex = 0; rowIndex < image.height; ++rowIndex) 93 | { 94 | png_row_pointers[rowIndex] = cast(png_bytep) &image.pixels[rowIndex * image.width]; 95 | } 96 | 97 | png_write_frame_head(png_ptr, png_info_ptr, null, image.width, image.height, 0, 0, delay, 1000, 98 | PNG_DISPOSE_OP_NONE, PNG_BLEND_OP_SOURCE); 99 | png_write_image(png_ptr, cast(ubyte**) png_row_pointers); 100 | png_write_frame_tail(png_ptr, png_info_ptr); 101 | } 102 | 103 | png_write_end(png_ptr, png_info_ptr); 104 | //png_set_rows(png_ptr, png_info_ptr, cast(ubyte**) png_row_pointers); 105 | //png_write_png(png_ptr, png_info_ptr, PNG_TRANSFORM_IDENTITY, null); 106 | 107 | png_destroy_write_struct(&png_ptr, &png_info_ptr); 108 | } 109 | -------------------------------------------------------------------------------- /source/linearalgebra/box.d: -------------------------------------------------------------------------------- 1 | module linearalgebra.box; 2 | 3 | import std.math : abs, ceil, floor, round, fmin, fmax, isInfinity; 4 | 5 | struct Box 6 | { 7 | // We use int instead of float because the actual game engine does the same. 8 | // All kind of floats will be truncated to 0 (deliberately). 9 | int x1 = 0, y1 = 0; 10 | int x2 = 0, y2 = 0; 11 | 12 | uint width() const pure nothrow @safe @nogc 13 | { 14 | if (x1 == int.max || x2 == int.max || x1 == int.min || x2 == int.min) 15 | { 16 | return 0; 17 | } 18 | return cast(uint) (abs(x2 - x1)); 19 | } 20 | 21 | uint height() const pure nothrow @safe @nogc 22 | { 23 | if (y1 == int.max || y2 == int.max || y1 == int.min || y2 == int.min) 24 | { 25 | return 0; 26 | } 27 | return cast(uint) (abs(y2 - y1)); 28 | } 29 | 30 | Box toInfinity() pure nothrow @safe @nogc 31 | { 32 | this.x1 = int.max; 33 | this.y1 = int.max; 34 | this.x2 = int.min; 35 | this.y2 = int.min; 36 | 37 | return this; 38 | } 39 | 40 | bool isInfinite() const pure nothrow @safe @nogc 41 | { 42 | return (this.x1 == int.max || this.y1 == int.max || 43 | this.x2 == int.min || this.y2 == int.min); 44 | } 45 | 46 | Box updateBounds(int x1, int y1, int x2, int y2) pure nothrow @safe @nogc 47 | { 48 | import std.algorithm : max, min; 49 | 50 | this.x1 = min(this.x1, min(x1, x2)); 51 | this.y1 = min(this.y1, min(y1, y2)); 52 | this.x2 = max(this.x2, max(x1, x2)); 53 | this.y2 = max(this.y2, max(y1, y2)); 54 | 55 | return this; 56 | } 57 | 58 | Box updateBounds(float x1, float y1, float x2, float y2) pure nothrow @safe @nogc 59 | { 60 | this.x1 = cast(int) (fmin(this.x1, fmin(x1, x2))); 61 | this.y1 = cast(int) (fmin(this.y1, fmin(y1, y2))); 62 | this.x2 = cast(int) (fmax(this.x2, fmax(x1, x2))); 63 | this.y2 = cast(int) (fmax(this.y2, fmax(y1, y2))); 64 | 65 | return this; 66 | } 67 | 68 | Box updateBounds(const scope Box box) pure nothrow @safe @nogc 69 | { 70 | return this.updateBounds(box.x1, box.y1, box.x2, box.y2); 71 | } 72 | 73 | import linearalgebra.vector : Vector2; 74 | 75 | Box updateBounds(const scope Vector2 pointA, const scope Vector2 pointB) pure nothrow @safe @nogc 76 | { 77 | return this.updateBounds(pointA.x, pointA.y, pointB.x, pointB.y); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /source/linearalgebra/matrix.d: -------------------------------------------------------------------------------- 1 | module linearalgebra.matrix; 2 | 3 | import std.traits : isNumeric; 4 | 5 | struct Matrix2 6 | { 7 | float m11, m12; 8 | float m21, m22; 9 | 10 | this(float m11, float m12, float m21, float m22) pure nothrow @safe @nogc 11 | { 12 | this.m11 = m11; 13 | this.m12 = m12; 14 | this.m21 = m21; 15 | this.m22 = m22; 16 | } 17 | 18 | Matrix2 opUnary(string op)() const pure nothrow @safe @nogc 19 | { 20 | return mixin( 21 | "Matrix2(" ~ 22 | op ~ "this.m11, " ~ op ~ "this.m12," ~ 23 | op ~ "this.m21, " ~ op ~ "this.m22" ~ 24 | ")"); 25 | } 26 | 27 | unittest 28 | { 29 | auto matrix = Matrix2(1, 2, 3, 4); 30 | 31 | assert(-matrix == Matrix2(-1, -2, -3, -4)); 32 | } 33 | 34 | Matrix2 opBinary(string op, R)(const scope R rhs) const pure nothrow @safe @nogc 35 | if (isNumeric!R && (op == "*" || op == "/")) 36 | { 37 | return mixin( 38 | "Matrix2(" ~ 39 | "this.m11 " ~ op ~ " rhs, this.m12 " ~ op ~ " rhs," ~ 40 | "this.m21 " ~ op ~ " rhs, this.m22 " ~ op ~ " rhs" ~ 41 | ")"); 42 | } 43 | 44 | unittest 45 | { 46 | auto matrix = Matrix2(1, 2, 4, 8); 47 | 48 | assert(matrix * 6 == Matrix2(6, 12, 24, 48)); 49 | assert(matrix / 2 == Matrix2(0.5, 1, 2, 4)); 50 | } 51 | 52 | Matrix2 opBinaryRight(string op, R)(const scope R lhs) const pure nothrow @safe @nogc 53 | if (isNumeric!R && (op == "*")) 54 | { 55 | return this.opBinary!(op, R)(lhs); 56 | } 57 | 58 | Matrix2 opBinary(string op)(const scope Matrix2 rhs) const pure nothrow @safe @nogc 59 | if (op == "*") 60 | { 61 | return Matrix2( 62 | this.m11 * rhs.m11 + this.m12 * rhs.m21, 63 | this.m11 * rhs.m12 + this.m12 * rhs.m22, 64 | this.m21 * rhs.m11 + this.m22 * rhs.m21, 65 | this.m21 * rhs.m12 + this.m22 * rhs.m22); 66 | } 67 | 68 | unittest 69 | { 70 | auto matrixA = Matrix2(1, 2, 3, 4); 71 | auto matrixB = Matrix2(5, 6, 7, 8); 72 | 73 | assert(matrixA * matrixB == Matrix2(19, 22, 43, 50)); 74 | assert(matrixB * matrixA == Matrix2(23, 34, 31, 46)); 75 | } 76 | 77 | Matrix2 opOpAssign(string op, R)(const scope R rhs) pure nothrow @safe @nogc 78 | if (isNumeric!R && (op == "*" || op == "/")) 79 | { 80 | return mixin("this " ~ op ~ " rhs"); 81 | } 82 | 83 | Matrix2 opOpAssign(string op)(const scope Matrix2 rhs) pure nothrow @safe @nogc 84 | if (op == "*") 85 | { 86 | return mixin("this " ~ op ~ " rhs"); 87 | } 88 | } 89 | 90 | struct Matrix3 91 | { 92 | float m11, m12, m13; 93 | float m21, m22, m23; 94 | float m31, m32, m33; 95 | 96 | this(float m11, float m12, float m13, 97 | float m21, float m22, float m23, 98 | float m31, float m32, float m33) pure nothrow @safe @nogc 99 | { 100 | this.m11 = m11; 101 | this.m12 = m12; 102 | this.m13 = m13; 103 | this.m21 = m21; 104 | this.m22 = m22; 105 | this.m23 = m23; 106 | this.m31 = m31; 107 | this.m32 = m32; 108 | this.m33 = m33; 109 | } 110 | 111 | Matrix3 opUnary(string op)() const pure nothrow @safe @nogc 112 | { 113 | return mixin( 114 | "Matrix3(" ~ 115 | op ~ "this.m11, " ~ op ~ "this.m12, " ~ op ~ "this.m13, " ~ 116 | op ~ "this.m21, " ~ op ~ "this.m22, " ~ op ~ "this.m23, " ~ 117 | op ~ "this.m31, " ~ op ~ "this.m32, " ~ op ~ "this.m33" ~ 118 | ")"); 119 | } 120 | 121 | unittest 122 | { 123 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 124 | 125 | assert(-matrix == Matrix3(-1, -2, -3, -4, -5, -6, -7, -8, -9)); 126 | } 127 | 128 | Matrix3 opBinary(string op, R)(const scope R rhs) const pure nothrow @safe @nogc 129 | if (isNumeric!R && (op == "*" || op == "/")) 130 | { 131 | return mixin( 132 | "Matrix3(" ~ 133 | "this.m11 " ~ op ~ " rhs, this.m12 " ~ op ~ " rhs, this.m13 " ~ op ~ " rhs," ~ 134 | "this.m21 " ~ op ~ " rhs, this.m22 " ~ op ~ " rhs, this.m23 " ~ op ~ " rhs," ~ 135 | "this.m31 " ~ op ~ " rhs, this.m32 " ~ op ~ " rhs, this.m33 " ~ op ~ " rhs" ~ 136 | ")"); 137 | } 138 | 139 | unittest 140 | { 141 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 142 | 143 | assert(matrix * 6 == Matrix3(6, 12, 18, 24, 30, 36, 42, 48, 54)); 144 | assert(matrix / 2 == Matrix3(0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5)); 145 | } 146 | 147 | Matrix3 opBinaryRight(string op, R)(const scope R lhs) const pure nothrow @safe @nogc 148 | if (isNumeric!R && (op == "*")) 149 | { 150 | return this.opBinary!(op, R)(lhs); 151 | } 152 | 153 | Matrix3 opBinary(string op)(const scope Matrix3 rhs) const pure nothrow @safe @nogc 154 | if (op == "*") 155 | { 156 | return Matrix3( 157 | this.m11 * rhs.m11 + this.m12 * rhs.m21 + this.m13 * rhs.m31, 158 | this.m11 * rhs.m12 + this.m12 * rhs.m22 + this.m13 * rhs.m32, 159 | this.m11 * rhs.m13 + this.m12 * rhs.m23 + this.m13 * rhs.m33, 160 | this.m21 * rhs.m11 + this.m22 * rhs.m21 + this.m23 * rhs.m31, 161 | this.m21 * rhs.m12 + this.m22 * rhs.m22 + this.m23 * rhs.m32, 162 | this.m21 * rhs.m13 + this.m22 * rhs.m23 + this.m23 * rhs.m33, 163 | this.m31 * rhs.m11 + this.m32 * rhs.m21 + this.m33 * rhs.m31, 164 | this.m31 * rhs.m12 + this.m32 * rhs.m22 + this.m33 * rhs.m32, 165 | this.m31 * rhs.m13 + this.m32 * rhs.m23 + this.m33 * rhs.m33); 166 | } 167 | 168 | unittest 169 | { 170 | auto matrixA = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 171 | auto matrixB = Matrix3(9, 8, 7, 6, 5, 4, 3, 2, 1); 172 | 173 | assert(matrixA * matrixB == Matrix3(30, 24, 18, 84, 69, 54, 138, 114, 90)); 174 | assert(matrixB * matrixA == Matrix3(90, 114, 138, 54, 69, 84, 18, 24, 30)); 175 | } 176 | 177 | Matrix3 opOpAssign(string op, R)(const scope R rhs) pure nothrow @safe @nogc 178 | if (isNumeric!R && (op == "*" || op == "/")) 179 | { 180 | return mixin("this " ~ op ~ " rhs"); 181 | } 182 | 183 | Matrix3 opOpAssign(string op)(const scope Matrix3 rhs) pure nothrow @safe @nogc 184 | if (op == "*") 185 | { 186 | return mixin("this " ~ op ~ " rhs"); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /source/linearalgebra/operations.d: -------------------------------------------------------------------------------- 1 | module linearalgebra.operations; 2 | 3 | import linearalgebra.vector; 4 | import linearalgebra.matrix; 5 | 6 | ref Vector2 set(return ref Vector2 vector, float x, float y) pure nothrow @safe @nogc 7 | { 8 | vector.x = x; 9 | vector.y = y; 10 | return vector; 11 | } 12 | /// 13 | unittest 14 | { 15 | auto vector = Vector2(0, 0); 16 | vector.set(1, 2); 17 | 18 | assert(vector == Vector2(1, 2)); 19 | } 20 | 21 | ref Vector3 set(return ref Vector3 vector, float x, float y, float z) pure nothrow @safe @nogc 22 | { 23 | vector.x = x; 24 | vector.y = y; 25 | vector.z = z; 26 | return vector; 27 | } 28 | /// 29 | unittest 30 | { 31 | auto vector = Vector3(0, 0, 0); 32 | vector.set(1, 2, 3); 33 | 34 | assert(vector == Vector3(1, 2, 3)); 35 | } 36 | 37 | ref Matrix2 set(return ref Matrix2 matrix, float m11, float m12, float m21, float m22) pure nothrow @safe @nogc 38 | { 39 | matrix.m11 = m11; 40 | matrix.m12 = m12; 41 | matrix.m21 = m21; 42 | matrix.m22 = m22; 43 | return matrix; 44 | } 45 | /// 46 | unittest 47 | { 48 | auto matrix = Matrix2(0, 0, 0, 0); 49 | matrix.set(1, 2, 3, 4); 50 | 51 | assert(matrix == Matrix2(1, 2, 3, 4)); 52 | } 53 | 54 | ref Matrix2 setRow(uint row)(return ref Matrix2 matrix, float x, float y) pure nothrow @safe @nogc 55 | if (row < 2) 56 | { 57 | import std.format : format; 58 | 59 | mixin(format("matrix.m%d1 = x;", row + 1)); 60 | mixin(format("matrix.m%d2 = y;", row + 1)); 61 | 62 | return matrix; 63 | } 64 | /// 65 | unittest 66 | { 67 | auto matrix = Matrix2(0, 0, 0, 0); 68 | matrix.setRow!1(1, 2); 69 | assert(matrix == Matrix2(0, 0, 1, 2)); 70 | 71 | matrix.setRow!0(3, 4); 72 | assert(matrix == Matrix2(3, 4, 1, 2)); 73 | } 74 | 75 | ref Matrix2 setCol(uint col)(return ref Matrix2 matrix, float x, float y) pure nothrow @safe @nogc 76 | if (col < 2) 77 | { 78 | import std.format : format; 79 | 80 | mixin(format("matrix.m1%d = x;", col + 1)); 81 | mixin(format("matrix.m2%d = y;", col + 1)); 82 | 83 | return matrix; 84 | } 85 | /// 86 | unittest 87 | { 88 | auto matrix = Matrix2(0, 0, 0, 0); 89 | matrix.setCol!1(1, 2); 90 | assert(matrix == Matrix2(0, 1, 0, 2)); 91 | 92 | matrix.setCol!0(3, 4); 93 | assert(matrix == Matrix2(3, 1, 4, 2)); 94 | } 95 | 96 | float determinant(const scope Matrix2 matrix) pure nothrow @safe @nogc 97 | { 98 | return matrix.m11 * matrix.m22 - matrix.m21 * matrix.m12; 99 | } 100 | /// 101 | unittest 102 | { 103 | auto matrix = Matrix2(1, 2, 3, 4); 104 | 105 | assert(matrix.determinant == -2); 106 | } 107 | 108 | Matrix2 inverse(const scope Matrix2 matrix) pure @safe @nogc 109 | { 110 | auto det = matrix.determinant; 111 | 112 | import std.math : isClose; 113 | 114 | if (isClose(det, 0f)) 115 | { 116 | det = float.epsilon; 117 | } 118 | 119 | return (1 / det) * Matrix2(matrix.m22, -matrix.m12, -matrix.m21, matrix.m11); 120 | } 121 | /// 122 | unittest 123 | { 124 | auto matrix = Matrix2(1, 2, 3, 4); 125 | auto inverseMatrix = Matrix2(-2, 1, 3/2f, -0.5); 126 | 127 | assert(matrix.inverse == inverseMatrix); 128 | assert(inverseMatrix * matrix == Matrix2(1, 0, 0, 1)); 129 | } 130 | 131 | void invert(out Matrix2 matrix) pure @safe 132 | { 133 | matrix = matrix.inverse; 134 | } 135 | 136 | ref Matrix3 set(return ref Matrix3 matrix, 137 | float m11, float m12, float m13, 138 | float m21, float m22, float m23, 139 | float m31, float m32, float m33) pure nothrow @safe @nogc 140 | { 141 | matrix.m11 = m11; 142 | matrix.m12 = m12; 143 | matrix.m13 = m13; 144 | matrix.m21 = m21; 145 | matrix.m22 = m22; 146 | matrix.m23 = m23; 147 | matrix.m31 = m31; 148 | matrix.m32 = m32; 149 | matrix.m33 = m33; 150 | return matrix; 151 | } 152 | /// 153 | unittest 154 | { 155 | auto matrix = Matrix3(0, 0, 0, 0, 0, 0, 0, 0, 0); 156 | matrix.set(1, 2, 3, 4, 5, 6, 7, 8, 9); 157 | 158 | assert(matrix == Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9)); 159 | } 160 | 161 | ref Matrix3 setRow(uint row)(return ref Matrix3 matrix, float x, float y, float z) pure nothrow @safe @nogc 162 | if (row < 3) 163 | { 164 | import std.format : format; 165 | 166 | mixin(format("matrix.m%d1 = x;", row + 1)); 167 | mixin(format("matrix.m%d2 = y;", row + 1)); 168 | mixin(format("matrix.m%d3 = z;", row + 1)); 169 | 170 | return matrix; 171 | } 172 | /// 173 | unittest 174 | { 175 | auto matrix = Matrix3(0, 0, 0, 0, 0, 0, 0, 0, 0); 176 | matrix.setRow!1(1, 2, 3); 177 | assert(matrix == Matrix3(0, 0, 0, 1, 2, 3, 0, 0, 0)); 178 | 179 | matrix.setRow!0(3, 4, 5); 180 | assert(matrix == Matrix3(3, 4, 5, 1, 2, 3, 0, 0, 0)); 181 | } 182 | 183 | ref Matrix3 setCol(uint col)(return ref Matrix3 matrix, float x, float y, float z) pure nothrow @safe @nogc 184 | if (col < 3) 185 | { 186 | import std.format : format; 187 | 188 | mixin(format("matrix.m1%d = x;", col + 1)); 189 | mixin(format("matrix.m2%d = y;", col + 1)); 190 | mixin(format("matrix.m3%d = z;", col + 1)); 191 | 192 | return matrix; 193 | } 194 | /// 195 | unittest 196 | { 197 | auto matrix = Matrix3(0, 0, 0, 0, 0, 0, 0, 0, 0); 198 | matrix.setCol!1(1, 2, 3); 199 | assert(matrix == Matrix3(0, 1, 0, 0, 2, 0, 0, 3, 0)); 200 | 201 | matrix.setCol!0(3, 4, 5); 202 | assert(matrix == Matrix3(3, 1, 0, 4, 2, 0, 5, 3, 0)); 203 | } 204 | 205 | float determinant(const scope Matrix3 matrix) pure nothrow @safe @nogc 206 | { 207 | return matrix.m11 * matrix.m22 * matrix.m33 + 208 | matrix.m12 * matrix.m23 * matrix.m31 + 209 | matrix.m13 * matrix.m21 * matrix.m32 - 210 | matrix.m31 * matrix.m22 * matrix.m13 - 211 | matrix.m32 * matrix.m23 * matrix.m11 - 212 | matrix.m33 * matrix.m21 * matrix.m12; 213 | } 214 | /// 215 | unittest 216 | { 217 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 218 | 219 | assert(matrix.determinant == 0); 220 | } 221 | 222 | Matrix3 inverse(const scope Matrix3 matrix) pure @safe @nogc 223 | { 224 | auto det = matrix.determinant; 225 | 226 | import std.math : isClose; 227 | 228 | if (isClose(det, 0f)) 229 | { 230 | det = float.epsilon; 231 | } 232 | 233 | return (1 / det) * Matrix3( 234 | matrix.m22 * matrix.m33 - matrix.m23 * matrix.m32, 235 | matrix.m13 * matrix.m32 - matrix.m12 * matrix.m33, 236 | matrix.m12 * matrix.m23 - matrix.m13 * matrix.m22, 237 | matrix.m23 * matrix.m31 - matrix.m21 * matrix.m33, 238 | matrix.m11 * matrix.m33 - matrix.m13 * matrix.m31, 239 | matrix.m13 * matrix.m21 - matrix.m11 * matrix.m23, 240 | matrix.m21 * matrix.m32 - matrix.m22 * matrix.m31, 241 | matrix.m12 * matrix.m31 - matrix.m11 * matrix.m32, 242 | matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21); 243 | } 244 | /// 245 | unittest 246 | { 247 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 10); 248 | auto inverseMatrix = Matrix3(-2/3f, -4/3f, 1, -2/3f, 11/3f, -2, 1, -2, 1); 249 | 250 | assert(matrix.inverse == inverseMatrix); 251 | assert(inverseMatrix * matrix == Matrix3(1, 0, 0, 0, 1, 0, 0, 0, 1)); 252 | } 253 | 254 | void invert(out Matrix3 matrix) pure @safe 255 | { 256 | matrix = matrix.inverse; 257 | } 258 | 259 | Vector3 fromVector2(const scope Vector2 vector) pure nothrow @safe @nogc 260 | { 261 | return Vector3(vector.x, vector.y, 1); 262 | } 263 | 264 | Vector2 toVector2(const scope Vector3 vector) pure nothrow @safe @nogc 265 | { 266 | return Vector2(vector.x, vector.y); 267 | } 268 | 269 | Matrix3 fromMatrix2(const scope Matrix2 matrix) pure nothrow @safe @nogc 270 | { 271 | return Matrix3(matrix.m11, matrix.m12, 0, matrix.m21, matrix.m22, 0, 0, 0, 1); 272 | } 273 | 274 | Vector2 truncate(const scope Vector2 vector) pure nothrow @safe @nogc 275 | { 276 | import std.math : trunc; 277 | 278 | return Vector2(trunc(vector.x), trunc(vector.y)); 279 | } 280 | 281 | Vector3 truncate(const scope Vector3 vector) pure nothrow @safe @nogc 282 | { 283 | import std.math : trunc; 284 | 285 | return Vector3(trunc(vector.x), trunc(vector.y), trunc(vector.z)); 286 | } 287 | 288 | Vector2 apply(alias fun)(const scope Vector2 vector) nothrow @safe @nogc 289 | { 290 | return Vector2(fun(vector.x), fun(vector.y)); 291 | } 292 | 293 | Vector3 apply(alias fun)(const scope Vector3 vector) nothrow @safe @nogc 294 | { 295 | return Vector3(fun(vector.x), fun(vector.y), fun(vector.z)); 296 | } 297 | -------------------------------------------------------------------------------- /source/linearalgebra/package.d: -------------------------------------------------------------------------------- 1 | module linearalgebra; 2 | 3 | public import linearalgebra.vector; 4 | public import linearalgebra.matrix; 5 | public import linearalgebra.box; 6 | public import linearalgebra.operations; 7 | public import linearalgebra.transform; 8 | -------------------------------------------------------------------------------- /source/linearalgebra/transform.d: -------------------------------------------------------------------------------- 1 | module linearalgebra.transform; 2 | 3 | import linearalgebra.vector; 4 | import linearalgebra.matrix; 5 | import std.math : PI; 6 | 7 | enum PI_180 = PI / 180f; 8 | 9 | struct TransformMatrix 10 | { 11 | Vector2 size = Vector2(0, 0); 12 | Vector2 origin = Vector2(0, 0); 13 | Vector2 translation = Vector2(0, 0); 14 | Vector2 scaling = Vector2(1, 1); 15 | float rotation = 0f; 16 | Matrix3 transformation = identityMatrix(); 17 | 18 | void setSize(float x, float y) pure nothrow @safe @nogc 19 | { 20 | this.size.x = x; 21 | this.size.y = y; 22 | } 23 | 24 | void setOrigin(float x, float y) pure nothrow @safe @nogc 25 | { 26 | this.origin.x = x; 27 | this.origin.y = y; 28 | } 29 | 30 | void translate(float x, float y) pure nothrow @safe @nogc 31 | { 32 | this.translation.x = x; 33 | this.translation.y = y; 34 | } 35 | 36 | void scale(float x, float y) pure nothrow @safe @nogc 37 | { 38 | this.scaling.x = x; 39 | this.scaling.y = y; 40 | } 41 | 42 | void rotate(float radians) pure nothrow @safe @nogc 43 | { 44 | this.rotation = radians; 45 | } 46 | 47 | Matrix3 calculate() nothrow @safe @nogc 48 | { 49 | import linearalgebra.operations : truncate, apply; 50 | import std.math : round; 51 | 52 | Matrix3 transformationMatrix = identityMatrix(); 53 | transformationMatrix = transformationMatrix * translationMatrix(this.translation); 54 | transformationMatrix = transformationMatrix * rotationMatrix(this.rotation); 55 | transformationMatrix = transformationMatrix * scaleMatrix(this.scaling); 56 | transformationMatrix = transformationMatrix * translationMatrix((-this.size * this.origin).apply!round); 57 | 58 | this.transformation = transformationMatrix; 59 | 60 | return this.transformation; 61 | } 62 | unittest 63 | { 64 | TransformMatrix t; 65 | t.setOrigin(0.5, 0.5); 66 | t.setSize(10, 10); 67 | t.translate(5, 5); 68 | t.scale(2, 2); 69 | t.rotate(PI/2f); 70 | 71 | t.calculate(); 72 | 73 | import linearalgebra.operations : inverse; 74 | 75 | assert(t * t.inverse == identityMatrix()); 76 | 77 | Vector3 p = Vector3(10, 5, 1); 78 | Vector3 p2 = t * p; 79 | 80 | import std.stdio : writeln; 81 | 82 | writeln(p2); 83 | writeln(t.inverse * p2, p); 84 | 85 | assert(t.inverse * p2 == p); 86 | } 87 | 88 | alias transformation this; 89 | } 90 | 91 | Matrix3 identityMatrix() pure nothrow @safe @nogc 92 | { 93 | return Matrix3( 94 | 1, 0, 0, 95 | 0, 1, 0, 96 | 0, 0, 1); 97 | } 98 | 99 | Matrix3 translationMatrix(float x = 0, float y = 0) pure nothrow @safe @nogc 100 | { 101 | return Matrix3( 102 | 1, 0, x, 103 | 0, 1, y, 104 | 0, 0, 1); 105 | } 106 | 107 | Matrix3 translationMatrix(const scope Vector2 vector) pure nothrow @safe @nogc 108 | { 109 | return Matrix3( 110 | 1, 0, vector.x, 111 | 0, 1, vector.y, 112 | 0, 0, 1); 113 | } 114 | 115 | Matrix3 scaleMatrix(float x = 1, float y = 1) pure nothrow @safe @nogc 116 | { 117 | return Matrix3( 118 | x, 0, 0, 119 | 0, y, 0, 120 | 0, 0, 1); 121 | } 122 | 123 | Matrix3 scaleMatrix(const scope Vector2 vector) pure nothrow @safe @nogc 124 | { 125 | return Matrix3( 126 | vector.x, 0, 0, 127 | 0, vector.y, 0, 128 | 0, 0, 1); 129 | } 130 | 131 | Matrix3 rotationMatrix(float radians = 0) pure nothrow @safe @nogc 132 | { 133 | import std.math : sin, cos; 134 | 135 | return Matrix3( 136 | cos(radians), -sin(radians), 0, 137 | sin(radians), cos(radians), 0, 138 | 0, 0, 1); 139 | } 140 | -------------------------------------------------------------------------------- /source/linearalgebra/vector.d: -------------------------------------------------------------------------------- 1 | module linearalgebra.vector; 2 | 3 | import std.traits : isNumeric; 4 | import linearalgebra.matrix; 5 | 6 | struct Vector2 7 | { 8 | float x = 0, y = 0; 9 | 10 | this(float x, float y) pure nothrow @safe @nogc 11 | { 12 | this.x = x; 13 | this.y = y; 14 | } 15 | 16 | Vector2 opUnary(string op)() const pure nothrow @safe @nogc 17 | { 18 | return mixin("Vector2(" ~ op ~ "this.x, " ~ op ~ "this.y)"); 19 | } 20 | 21 | unittest 22 | { 23 | auto vector = Vector2(1, 2); 24 | assert(-vector == Vector2(-1, -2)); 25 | } 26 | 27 | Vector2 opBinary(string op, R)(const scope R rhs) const pure nothrow @safe @nogc 28 | if (isNumeric!R) 29 | { 30 | return mixin("Vector2(this.x " ~ op ~ " rhs, this.y " ~ op ~ " rhs)"); 31 | } 32 | 33 | unittest 34 | { 35 | auto vectorA = Vector2(2, 3); 36 | auto scalar = 6; 37 | 38 | assert(vectorA + scalar == Vector2(8, 9)); 39 | assert(vectorA - scalar == Vector2(-4, -3)); 40 | assert(vectorA * scalar == Vector2(12, 18)); 41 | assert(vectorA / scalar == Vector2(1 / 3f, 0.5)); 42 | } 43 | 44 | Vector2 opBinaryRight(string op, R)(const scope R lhs) const pure nothrow @safe @nogc 45 | if (isNumeric!R && (op == "*" || op == "+")) 46 | { 47 | return this.opBinary!(op, R)(lhs); 48 | } 49 | 50 | unittest 51 | { 52 | auto vector = Vector2(2, 3); 53 | 54 | assert(6 * vector == Vector2(12, 18)); 55 | assert(2 + vector == Vector2(4, 5)); 56 | } 57 | 58 | Vector2 opBinary(string op)(const scope Vector2 rhs) const pure nothrow @safe @nogc 59 | { 60 | return mixin("Vector2(this.x " ~ op ~ " rhs.x, this.y " ~ op ~ " rhs.y)"); 61 | } 62 | 63 | unittest 64 | { 65 | auto vectorA = Vector2(2, 3); 66 | auto vectorB = Vector2(6, 2); 67 | 68 | assert(vectorA + vectorB == Vector2(8, 5)); 69 | assert(vectorA - vectorB == Vector2(-4, 1)); 70 | assert(vectorA * vectorB == Vector2(12, 6)); 71 | assert(vectorA / vectorB == Vector2(1 / 3f, 1.5)); 72 | assert(vectorB / vectorA == Vector2(3, 2 / 3f)); 73 | } 74 | 75 | Vector2 opBinary(string op)(const scope Matrix2 rhs) const pure nothrow @safe @nogc 76 | if (op == "*") 77 | { 78 | return Vector2(this.x * rhs.m11 + this.y * rhs.m21, this.x * rhs.m12 + this.y * rhs.m22); 79 | } 80 | 81 | unittest 82 | { 83 | auto vector = Vector2(1, 2); 84 | auto matrix = Matrix2(1, 2, 3, 4); 85 | 86 | assert(vector * matrix == Vector2(7, 10)); 87 | } 88 | 89 | Vector2 opBinaryRight(string op)(const scope Matrix2 lhs) const pure nothrow @safe @nogc 90 | if (op == "*") 91 | { 92 | return Vector2(lhs.m11 * this.x + lhs.m12 * y, lhs.m21 * this.x + lhs.m22 * this.y); 93 | } 94 | 95 | unittest 96 | { 97 | auto vector = Vector2(1, 2); 98 | auto matrix = Matrix2(1, 2, 3, 4); 99 | 100 | assert(matrix * vector == Vector2(5, 11)); 101 | } 102 | 103 | Vector2 opOpAssign(string op, R)(const scope R rhs) pure nothrow @safe @nogc 104 | if (isNumeric!R) 105 | { 106 | return mixin("this " ~ op ~ " rhs"); 107 | } 108 | 109 | Vector2 opOpAssign(string op)(const scope Vector2 rhs) pure nothrow @safe @nogc 110 | { 111 | return mixin("this " ~ op ~ " rhs"); 112 | } 113 | 114 | Vector2 opOpAssign(string op)(const scope Matrix2 rhs) pure nothrow @safe @nogc 115 | if (op == "*") 116 | { 117 | return mixin("this " ~ op ~ " rhs"); 118 | } 119 | } 120 | 121 | struct Vector3 122 | { 123 | float x = 0, y = 0, z = 0; 124 | 125 | this(float x, float y, float z) pure nothrow @safe @nogc 126 | { 127 | this.x = x; 128 | this.y = y; 129 | this.z = z; 130 | } 131 | 132 | Vector3 opUnary(string op)() const pure nothrow @safe @nogc 133 | { 134 | return mixin("Vector3(" ~ op ~ "this.x, " ~ op ~ "this.y, " ~ op ~ "this.z)"); 135 | } 136 | 137 | unittest 138 | { 139 | auto vector = Vector3(1, 2, 3); 140 | assert(-vector == Vector3(-1, -2, -3)); 141 | } 142 | 143 | Vector3 opBinary(string op, R)(const scope R rhs) const pure nothrow @safe @nogc 144 | if (isNumeric!R) 145 | { 146 | return mixin("Vector3(this.x " ~ op ~ " rhs, this.y " ~ op ~ " rhs, this.z " ~ op ~ " rhs)"); 147 | } 148 | 149 | unittest 150 | { 151 | auto vectorA = Vector3(2, 3, 4); 152 | auto scalar = 6; 153 | 154 | assert(vectorA + scalar == Vector3(8, 9, 10)); 155 | assert(vectorA - scalar == Vector3(-4, -3, -2)); 156 | assert(vectorA * scalar == Vector3(12, 18, 24)); 157 | assert(vectorA / scalar == Vector3(1 / 3f, 0.5, 2 / 3f)); 158 | } 159 | 160 | Vector3 opBinaryRight(string op, R)(const scope R lhs) const pure nothrow @safe @nogc 161 | if (isNumeric!R && (op == "*" || op == "+")) 162 | { 163 | return this.opBinary!(op, R)(lhs); 164 | } 165 | 166 | unittest 167 | { 168 | auto vector = Vector3(2, 3, 4); 169 | 170 | assert(6 * vector == Vector3(12, 18, 24)); 171 | assert(2 + vector == Vector3(4, 5, 6)); 172 | } 173 | 174 | Vector3 opBinary(string op)(const scope Vector3 rhs) const pure nothrow @safe @nogc 175 | { 176 | return mixin("Vector3(this.x " ~ op ~ " rhs.x, this.y " ~ op ~ " rhs.y, this.z " ~ op ~ " rhs.z)"); 177 | } 178 | 179 | unittest 180 | { 181 | auto vectorA = Vector3(2, 3, 4); 182 | auto vectorB = Vector3(6, 2, 4); 183 | 184 | assert(vectorA + vectorB == Vector3(8, 5, 8)); 185 | assert(vectorA - vectorB == Vector3(-4, 1, 0)); 186 | assert(vectorA * vectorB == Vector3(12, 6, 16)); 187 | assert(vectorA / vectorB == Vector3(1 / 3f, 1.5, 1)); 188 | assert(vectorB / vectorA == Vector3(3, 2 / 3f, 1)); 189 | } 190 | 191 | Vector3 opBinary(string op)(const scope Matrix3 rhs) const pure nothrow @safe @nogc 192 | if (op == "*") 193 | { 194 | return Vector3( 195 | this.x * rhs.m11 + this.y * rhs.m21 + this.z * rhs.m31, 196 | this.x * rhs.m12 + this.y * rhs.m22 + this.z * rhs.m32, 197 | this.x * rhs.m13 + this.y * rhs.m23 + this.z * rhs.m33); 198 | } 199 | 200 | unittest 201 | { 202 | auto vector = Vector3(1, 2, 3); 203 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 204 | 205 | assert(vector * matrix == Vector3(30, 36, 42)); 206 | } 207 | 208 | Vector3 opBinaryRight(string op)(const scope Matrix3 lhs) const pure nothrow @safe @nogc 209 | if (op == "*") 210 | { 211 | return Vector3( 212 | lhs.m11 * this.x + lhs.m12 * this.y + lhs.m13 * this.z, 213 | lhs.m21 * this.x + lhs.m22 * this.y + lhs.m23 * this.z, 214 | lhs.m31 * this.x + lhs.m32 * this.y + lhs.m33 * this.z); 215 | } 216 | 217 | unittest 218 | { 219 | auto vector = Vector3(1, 2, 3); 220 | auto matrix = Matrix3(1, 2, 3, 4, 5, 6, 7, 8, 9); 221 | 222 | assert(matrix * vector == Vector3(14, 32, 50)); 223 | } 224 | 225 | Vector3 opOpAssign(string op, R)(const scope R rhs) pure nothrow @safe @nogc 226 | if (isNumeric!R) 227 | { 228 | return mixin("this " ~ op ~ " rhs"); 229 | } 230 | 231 | Vector3 opOpAssign(string op)(const scope Vector3 rhs) pure nothrow @safe @nogc 232 | { 233 | return mixin("this " ~ op ~ " rhs"); 234 | } 235 | 236 | Vector3 opOpAssign(string op)(const scope Matrix3 rhs) pure nothrow @safe @nogc 237 | if (op == "*") 238 | { 239 | return mixin("this " ~ op ~ " rhs"); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /source/logging.d: -------------------------------------------------------------------------------- 1 | module logging; 2 | 3 | import core.sync.mutex : Mutex; 4 | import std.datetime : SysTime; 5 | import std.range : Appender, appender; 6 | import std.format : formattedWrite; 7 | 8 | enum LogLevel : ubyte 9 | { 10 | all = 1, 11 | trace = 32, 12 | info = 64, 13 | warning = 96, 14 | error = 128, 15 | critical = 160, 16 | fatal = 192, 17 | off = ubyte.max 18 | } 19 | 20 | string toString(LogLevel ll) pure nothrow @safe 21 | { 22 | final switch (ll) 23 | { 24 | case LogLevel.all: 25 | return "ALL"; 26 | case LogLevel.trace: 27 | return "TRACE"; 28 | case LogLevel.info: 29 | return "INFO"; 30 | case LogLevel.warning: 31 | return "WARNING"; 32 | case LogLevel.error: 33 | return "ERROR"; 34 | case LogLevel.critical: 35 | return "CRITICAL"; 36 | case LogLevel.fatal: 37 | return "FATAL"; 38 | case LogLevel.off: 39 | return "OFF"; 40 | } 41 | } 42 | 43 | class BasicLogger 44 | { 45 | private this(LogLevel ll) { 46 | _ll = ll; 47 | } 48 | 49 | private __gshared BasicLogger _instance; 50 | private static bool _instantiated; 51 | private static Mutex _mutex; 52 | 53 | private LogLevel _ll; 54 | 55 | static BasicLogger get(LogLevel ll) 56 | { 57 | if (!_instantiated) 58 | { 59 | synchronized (BasicLogger.classinfo) 60 | { 61 | if (!_instance) 62 | { 63 | _instance = new BasicLogger(ll); 64 | _mutex = new Mutex(); 65 | } 66 | 67 | _instantiated = true; 68 | } 69 | } 70 | 71 | return _instance; 72 | } 73 | 74 | void log(A...)(LogLevel logLevel, lazy A args) const @trusted 75 | { 76 | if (logLevel >= _ll) 77 | { 78 | synchronized (_mutex) 79 | { 80 | auto msg = appender!string(); 81 | 82 | msg.formattedWrite("[%s] ", logLevel.toString()); 83 | foreach (arg; args) 84 | { 85 | msg.formattedWrite("%s", arg); 86 | } 87 | 88 | import std.stdio : stdout, stderr; 89 | 90 | if (logLevel > logLevel.warning) 91 | { 92 | stderr.writeln(msg.data); 93 | } 94 | else 95 | { 96 | stdout.writeln(msg.data); 97 | } 98 | } 99 | } 100 | } 101 | 102 | template log(LogLevel logLevel) 103 | { 104 | void log(A...)(lazy bool condition, lazy A args) const @trusted 105 | { 106 | if (condition) 107 | { 108 | log(line, file, func, prettyFunc, mod)(args); 109 | } 110 | } 111 | 112 | void log (A...)(lazy A args) const @trusted 113 | { 114 | log(logLevel, args); 115 | } 116 | } 117 | 118 | alias trace = log!(LogLevel.trace); 119 | alias info = log!(LogLevel.info); 120 | alias warning = log!(LogLevel.warning); 121 | alias error = log!(LogLevel.error); 122 | alias critical = log!(LogLevel.critical); 123 | alias fatal = log!(LogLevel.fatal); 124 | } 125 | 126 | alias LogDg = void delegate(LogLevel, string); 127 | 128 | -------------------------------------------------------------------------------- /source/luamanager.d: -------------------------------------------------------------------------------- 1 | module luamanager; 2 | 3 | import std.typecons : Flag, No, Yes; 4 | import luad.state : LuaState; 5 | import resource : ResourceManager; 6 | import linearalgebra : Vector2; 7 | import logging : LogLevel, LogDg; 8 | import config : Gender; 9 | 10 | bool[string] luaFunctionAvailability; 11 | 12 | void loadRequiredLuaFiles(ref LuaState L, ResourceManager resManager, LogDg log) 13 | { 14 | luaFunctionAvailability = [ 15 | "ReqshadowFactor": true, 16 | "OffsetItemPos_GetOffsetForDoram": true, 17 | "ReqJobName": true, 18 | "ReqWeaponName": true, 19 | "GetRealWeaponId": true, 20 | "ReqAccName": true, 21 | "ReqRobSprName_V2": true, 22 | "_New_DrawOnTop": true, 23 | "IsTopLayer": true 24 | ]; 25 | 26 | luaLoader("datainfo/accessoryid", resManager, L, log); 27 | luaLoader("datainfo/accname", resManager, L, log); 28 | luaLoader("datainfo/accname_f", resManager, L, log); 29 | luaLoader("datainfo/spriterobeid", resManager, L, log); 30 | luaLoader("datainfo/spriterobename", resManager, L, log); 31 | luaLoader("datainfo/spriterobename_f", resManager, L, log); 32 | luaLoader("datainfo/weapontable", resManager, L, log); 33 | luaLoader("datainfo/weapontable_f", resManager, L, log); 34 | luaLoader("datainfo/npcidentity", resManager, L, log); 35 | luaLoader("datainfo/jobidentity", resManager, L, log); 36 | luaLoader("datainfo/jobname", resManager, L, log); 37 | luaLoader("datainfo/jobname_f", resManager, L, log); 38 | luaLoader("datainfo/shadowtable", resManager, L, log, Yes.optional); 39 | luaLoader("datainfo/shadowtable_f", resManager, L, log, Yes.optional); 40 | luaLoader("skillinfoz/jobinheritlist", resManager, L, log); 41 | luaLoader("spreditinfo/2dlayerdir_f", resManager, L, log); 42 | luaLoader("spreditinfo/biglayerdir_female", resManager, L, log); 43 | luaLoader("spreditinfo/biglayerdir_male", resManager, L, log); 44 | luaLoader("spreditinfo/_new_2dlayerdir_f", resManager, L, log); 45 | luaLoader("spreditinfo/_new_biglayerdir_female", resManager, L, log); 46 | luaLoader("spreditinfo/_new_biglayerdir_male", resManager, L, log); 47 | luaLoader("spreditinfo/_new_smalllayerdir_female", resManager, L, log); 48 | luaLoader("spreditinfo/_new_smalllayerdir_male", resManager, L, log); 49 | luaLoader("spreditinfo/smalllayerdir_female", resManager, L, log); 50 | luaLoader("spreditinfo/smalllayerdir_male", resManager, L, log); 51 | luaLoader("offsetitempos/offsetitempos_f", resManager, L, log); 52 | luaLoader("offsetitempos/offsetitempos", resManager, L, log); 53 | 54 | import luad.error : LuaErrorException; 55 | import luad.lfunction : LuaFunction; 56 | import std.format : format; 57 | 58 | foreach (functionName; luaFunctionAvailability.keys) 59 | { 60 | try 61 | { 62 | L.get!LuaFunction(functionName); 63 | } 64 | catch (LuaErrorException err) 65 | { 66 | log(LogLevel.info, format("Lua function \"%s\" is not available. Rendered output might not be correct", functionName)); 67 | luaFunctionAvailability[functionName] = false; 68 | } 69 | } 70 | } 71 | 72 | import resource.lua : LuaResource; 73 | 74 | private void luaLoader(string luaFilename, ResourceManager resManager, ref LuaState L, LogDg log, Flag!"optional" optional = No.optional) 75 | { 76 | import resource : ResourceException; 77 | try 78 | { 79 | auto luaRes = resManager.get!LuaResource(luaFilename); 80 | luaRes.load(); 81 | 82 | import std.exception : enforce; 83 | import std.format : format; 84 | 85 | enforce!ResourceException(luaRes.usable, 86 | format("Lua resource (%s) is not usable. This usually happens when " ~ 87 | "the resource has not been loaded yet.", luaRes.name)); 88 | 89 | luaRes.loadIntoLuaState(L); 90 | } 91 | catch (ResourceException err) 92 | { 93 | if (optional) 94 | { 95 | log(LogLevel.info, err.msg); 96 | } 97 | else 98 | { 99 | throw err; 100 | } 101 | } 102 | } 103 | 104 | T executeLuaFunctionOrElse(T, U...)(ref LuaState L, string functionName, T fallback, U args) 105 | { 106 | bool* available = functionName in luaFunctionAvailability; 107 | 108 | if (available is null || !(*available)) 109 | { 110 | return fallback; 111 | } 112 | 113 | import luad.lfunction : LuaFunction; 114 | 115 | auto func = L.get!LuaFunction(functionName); 116 | return func.call!(T)(args); 117 | } 118 | 119 | float shadowfactor(uint jobid, ref LuaState L) 120 | { 121 | return executeLuaFunctionOrElse(L, "ReqshadowFactor", 1, jobid); 122 | } 123 | 124 | auto headgearOffsetForDoram(uint headgear, uint direction, const Gender gender, ref LuaState L) 125 | { 126 | import luad.lfunction : LuaFunction; 127 | import luad.base : LuaObject; 128 | import config : toInt; 129 | 130 | struct Point 131 | { 132 | int x; 133 | int y; 134 | } 135 | 136 | bool* available = "OffsetItemPos_GetOffsetForDoram" in luaFunctionAvailability; 137 | if (available is null || !(*available)) 138 | { 139 | return Point.init; 140 | } 141 | 142 | auto getOffsetForDoram = L.get!LuaFunction("OffsetItemPos_GetOffsetForDoram"); 143 | 144 | scope LuaObject[] returnValues = getOffsetForDoram(headgear, direction, gender.toInt()); 145 | 146 | if (returnValues.length == 2) 147 | { 148 | int x = returnValues[0].isNil() ? 0 : returnValues[0].to!int; 149 | int y = returnValues[1].isNil() ? 0 : returnValues[1].to!int; 150 | return Point(x, y); 151 | } 152 | 153 | return Point.init; 154 | } 155 | 156 | -------------------------------------------------------------------------------- /source/resource/act.d: -------------------------------------------------------------------------------- 1 | module resource.act; 2 | 3 | import resource.base; 4 | import draw : Color; 5 | 6 | struct ActAction 7 | { 8 | float interval = 4; 9 | ActFrame[] frames; 10 | } 11 | 12 | struct ActFrame 13 | { 14 | int eventId; 15 | ActSprite[] sprites; 16 | ActAttachPoint[] attachPoints; 17 | } 18 | 19 | struct ActSprite 20 | { 21 | int x; 22 | int y; 23 | int sprId; 24 | uint flags; 25 | Color tint; 26 | float xScale; 27 | float yScale; 28 | int rotation; 29 | int sprType; 30 | int width; 31 | int height; 32 | } 33 | 34 | struct ActAttachPoint 35 | { 36 | int x; 37 | int y; 38 | int attr; 39 | } 40 | 41 | private enum MinActSize = 18 + 2 + 10; 42 | 43 | class ActResource : BaseResource 44 | { 45 | private ubyte[] _buffer; 46 | private ActAction[] _actions; 47 | private string[] _events; 48 | private ushort _ver; 49 | 50 | static immutable(string[]) fileExtensions = ["act"]; 51 | static immutable(string) filePath = "sprite"; 52 | 53 | this(string filename, string resourcePath) 54 | { 55 | super(filename, resourcePath, filePath, fileExtensions); 56 | } 57 | 58 | /** 59 | Loads the file specified by filename. If the file does not exist 60 | an ResourceException is thrown. If there is an error during the file parsing 61 | a ResourceException is thrown. 62 | Throws: ResourceException 63 | */ 64 | override void load() 65 | { 66 | if (this.filename.length == 0) 67 | { 68 | return; 69 | } 70 | 71 | import std.stdio : File; 72 | import std.exception : collectException, ErrnoException, enforce; 73 | 74 | File fileHandle; 75 | auto err = collectException!ErrnoException( 76 | File(this.filename, "rb"), 77 | fileHandle); 78 | 79 | enforce!ResourceException(!err, err.msg); // Re-throw ErrnoException as ResourceException 80 | 81 | this._buffer = fileHandle.rawRead(new ubyte[fileHandle.size()]); 82 | 83 | this.readData(this._buffer); 84 | this._usable = true; 85 | } 86 | 87 | override void load(const(ubyte)[] buffer) 88 | { 89 | this.readData(buffer); 90 | this._usable = true; 91 | } 92 | 93 | ushort ver() const pure nothrow @safe @nogc 94 | { 95 | return this._ver; 96 | } 97 | 98 | ulong numberOfActions() const pure nothrow @safe @nogc 99 | { 100 | return this._actions.length; 101 | } 102 | 103 | ulong numberOfEvents() const pure nothrow @safe @nogc 104 | { 105 | return this._events.length; 106 | } 107 | 108 | ulong numberOfFrames(uint action) const pure nothrow @safe @nogc 109 | { 110 | if (action >= this.numberOfActions) 111 | { 112 | return 0; 113 | } 114 | 115 | return this._actions[action].frames.length; 116 | } 117 | 118 | ulong numberOfSprites(uint action, uint frame) const pure nothrow @safe @nogc 119 | { 120 | if (frame >= this.numberOfFrames(action)) 121 | { 122 | return 0; 123 | } 124 | 125 | return this._actions[action].frames[frame].sprites.length; 126 | } 127 | 128 | ulong numberOfAttachpoints(uint action, uint frame) const pure nothrow @safe @nogc 129 | { 130 | if (frame >= this.numberOfFrames(action)) 131 | { 132 | return 0; 133 | } 134 | 135 | return this._actions[action].frames[frame].attachPoints.length; 136 | } 137 | 138 | const(ActAction) action(uint action) const pure nothrow @safe @nogc 139 | { 140 | if (action >= this.numberOfActions) 141 | { 142 | return ActAction.init; 143 | } 144 | 145 | return this._actions[action]; 146 | } 147 | 148 | const(ActAction)[] actions() const pure nothrow @safe @nogc 149 | { 150 | return this._actions; 151 | } 152 | 153 | const(ActFrame) frame(uint action, uint frame) const pure nothrow @safe @nogc 154 | { 155 | if (frame >= this.numberOfFrames(action)) 156 | { 157 | return ActFrame.init; 158 | } 159 | 160 | return this._actions[action].frames[frame]; 161 | } 162 | 163 | const(ActFrame)[] frames(uint action) const pure nothrow @safe @nogc 164 | { 165 | if (action >= this.numberOfActions) 166 | { 167 | return ActFrame[].init; 168 | } 169 | 170 | return this._actions[action].frames; 171 | } 172 | 173 | const(ActSprite) sprite(uint action, uint frame, uint sprite) const pure nothrow @safe @nogc 174 | { 175 | if (sprite >= this.numberOfSprites(action, frame)) 176 | { 177 | return ActSprite.init; 178 | } 179 | 180 | return this._actions[action].frames[frame].sprites[sprite]; 181 | } 182 | 183 | const(ActSprite)[] sprites(uint action, uint frame) const pure nothrow @safe @nogc 184 | { 185 | if (frame >= this.numberOfFrames(action)) 186 | { 187 | return ActSprite[].init; 188 | } 189 | 190 | return this._actions[action].frames[frame].sprites; 191 | } 192 | 193 | const(ActAttachPoint) attachpoint(uint action, uint frame, uint attachpoint) const pure nothrow @safe @nogc 194 | { 195 | if (attachpoint >= this.numberOfAttachpoints(action, frame)) 196 | { 197 | return ActAttachPoint.init; 198 | } 199 | 200 | return this._actions[action].frames[frame].attachPoints[attachpoint]; 201 | } 202 | 203 | const(ActAttachPoint)[] attachpoints(uint action, uint frame) const pure nothrow @safe @nogc 204 | { 205 | if (frame >= this.numberOfFrames(action)) 206 | { 207 | return ActAttachPoint[].init; 208 | } 209 | 210 | return this._actions[action].frames[frame].attachPoints; 211 | } 212 | 213 | void modifySprite(string prop, ValueType)(uint action, uint frame, uint sprite, 214 | ValueType value) pure nothrow @safe @nogc 215 | if (canSetProp!(prop, ActSprite, ValueType)) 216 | { 217 | __traits(getMember, this._actions[action].frames[frame].sprites[sprite], prop) = value; 218 | } 219 | 220 | void modifyAttachpoint(string prop, ValueType)(uint action, uint frame, uint attachpoint, 221 | ValueType value) pure nothrow @safe @nogc 222 | if (canSetProp!(prop, ActAttachPoint, ValueType)) 223 | { 224 | __traits(getMember, this._actions[action].frames[frame].attachPoints[attachpoint], prop) = value; 225 | } 226 | 227 | int opApply(scope int delegate(const(ActAction)) dg) const 228 | { 229 | int result = 0; 230 | foreach (const action; this._actions) 231 | { 232 | result = dg(action); 233 | if (result) 234 | { 235 | break; 236 | } 237 | } 238 | return result; 239 | } 240 | 241 | int opApply(scope int delegate(ulong, const(ActAction)) dg) const 242 | { 243 | int result = 0; 244 | foreach (i, const action; this._actions) 245 | { 246 | result = dg(i, action); 247 | if (result) 248 | { 249 | break; 250 | } 251 | } 252 | return result; 253 | } 254 | 255 | /** 256 | Extracts act data from the previously loaded buffer 257 | If the buffer does not contain a valid act file. A ResourceException 258 | is thrown. 259 | Throws: ResourceException 260 | */ 261 | private void readData(const(ubyte)[] buffer) 262 | { 263 | import std.conv : to; 264 | import std.exception : enforce; 265 | 266 | enforce!ResourceException(buffer.length >= MinActSize, 267 | "Act file: '" ~ this.filename ~ "' does not have enough bytes to be " ~ 268 | "valid. Has: " ~ buffer.length.to!string ~ " bytes. " ~ 269 | "Should have: " ~ MinActSize.to!string ~ " bytes."); 270 | 271 | enforce!ResourceException(buffer[0 .. 2] == ['A', 'C'], 272 | "Act file: '" ~ this.filename ~ "' does not have a valid signature."); 273 | 274 | ulong offset = 2; 275 | this._ver = buffer.peekLE!ushort(&offset); 276 | const numberOfActions = buffer.peekLE!ushort(&offset); 277 | offset += 10; // skip reserved bytes 278 | 279 | this._actions = new ActAction[numberOfActions]; 280 | 281 | foreach (ref action; this._actions) 282 | { 283 | const numberOfFrames = buffer.peekLE!uint(&offset); 284 | if (numberOfFrames > 0) 285 | { 286 | action.frames = new ActFrame[numberOfFrames]; 287 | 288 | foreach (ref frame; action.frames) 289 | { 290 | this.readFrame(buffer, frame, offset); 291 | } 292 | } 293 | } 294 | 295 | if (this._ver >= 0x201) 296 | { 297 | const numberOfEvents = buffer.peekLE!uint(&offset); 298 | if (numberOfEvents > 0) 299 | { 300 | this._events = new string[numberOfEvents]; 301 | 302 | foreach (ref event; this._events) 303 | { 304 | import std.string : fromStringz; 305 | 306 | event = fromStringz(cast(char*)&buffer[offset]).dup; 307 | offset += 40; 308 | } 309 | } 310 | 311 | if (this._ver >= 0x202) 312 | { 313 | foreach (ref action; this._actions) 314 | { 315 | action.interval = buffer.peekLE!float(&offset); 316 | } 317 | } 318 | } 319 | 320 | assert(offset == buffer.length, "Offset of act file is not EOF"); 321 | } 322 | 323 | private void readFrame(const(ubyte)[] buffer, ref ActFrame frame, ref ulong offset) pure nothrow 324 | { 325 | // Skip attackRange and fitRange 326 | offset += uint.sizeof * 8; 327 | const numberOfSprites = buffer.peekLE!uint(&offset); 328 | if (numberOfSprites > 0) 329 | { 330 | frame.sprites = new ActSprite[numberOfSprites]; 331 | 332 | foreach (ref sprite; frame.sprites) 333 | { 334 | this.readSprite(buffer, sprite, offset); 335 | } 336 | } 337 | 338 | if (this._ver >= 0x200) 339 | { 340 | frame.eventId = buffer.peekLE!int(&offset); 341 | 342 | if (this._ver >= 0x203) 343 | { 344 | const numberOfAttachPoints = buffer.peekLE!uint(&offset); 345 | if (numberOfAttachPoints > 0) 346 | { 347 | frame.attachPoints = new ActAttachPoint[numberOfAttachPoints]; 348 | 349 | foreach (ref attachpoint; frame.attachPoints) 350 | { 351 | this.readAttachPoint(buffer, attachpoint, offset); 352 | } 353 | } 354 | } 355 | } 356 | } 357 | 358 | private void readSprite(const(ubyte)[] buffer, ref ActSprite sprite, ref ulong offset) pure nothrow @nogc 359 | { 360 | sprite.x = buffer.peekLE!int(&offset); 361 | sprite.y = buffer.peekLE!int(&offset); 362 | sprite.sprId = buffer.peekLE!int(&offset); 363 | sprite.flags = buffer.peekLE!uint(&offset); 364 | sprite.tint = buffer.peekLE!uint(&offset); 365 | sprite.xScale = buffer.peekLE!float(&offset); 366 | if (this._ver >= 0x204) 367 | { 368 | sprite.yScale = buffer.peekLE!float(&offset); 369 | } 370 | else 371 | { 372 | sprite.yScale = sprite.xScale; 373 | } 374 | sprite.rotation = buffer.peekLE!int(&offset); 375 | sprite.sprType = buffer.peekLE!int(&offset); 376 | if (this._ver >= 0x205) 377 | { 378 | sprite.width = buffer.peekLE!int(&offset); 379 | sprite.height = buffer.peekLE!int(&offset); 380 | } 381 | } 382 | 383 | private void readAttachPoint(const(ubyte)[] buffer, ref ActAttachPoint attachpoint, ref ulong offset) pure nothrow @nogc 384 | { 385 | offset += ubyte.sizeof * 4; 386 | attachpoint.x = buffer.peekLE!int(&offset); 387 | attachpoint.y = buffer.peekLE!int(&offset); 388 | attachpoint.attr = buffer.peekLE!int(&offset); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /source/resource/base.d: -------------------------------------------------------------------------------- 1 | module resource.base; 2 | 3 | class ResourceException : Exception 4 | { 5 | this(string msg, string file = __FILE__, size_t line = __LINE__) 6 | { 7 | super(msg, file, line); 8 | } 9 | } 10 | 11 | private enum PathPrefix = "data"; 12 | 13 | string buildFilepath(immutable(string) resourcePath, immutable(string) filePath, 14 | immutable(string) filename, immutable(string) fileExtension) pure nothrow @safe 15 | { 16 | import std.path : buildPath, setExtension; 17 | 18 | return buildPath(resourcePath, PathPrefix, filePath, filename).setExtension(fileExtension); 19 | } 20 | 21 | string buildFilepath(immutable(string) resourcePath, immutable(string) filePath, 22 | immutable(string) filename) pure nothrow @safe 23 | { 24 | import std.path : buildPath, setExtension; 25 | 26 | return buildPath(resourcePath, PathPrefix, filePath, filename); 27 | } 28 | 29 | package class BaseResource 30 | { 31 | protected: 32 | string _filename; 33 | string _name; 34 | bool _usable = false; 35 | 36 | public: 37 | static immutable(string[]) fileExtensions = []; 38 | static immutable(string) filePath = ""; 39 | 40 | this(string filename, string resourcePath, immutable(string) filePath, immutable(string[]) fileExtensions) 41 | { 42 | import std.path : buildPath, setExtension; 43 | 44 | this._name = filename; 45 | 46 | if (fileExtensions.length == 1) 47 | { 48 | this._filename = buildFilepath(resourcePath, filePath, filename, fileExtensions[0]); 49 | } 50 | else 51 | { 52 | this._filename = buildFilepath(resourcePath, filePath, filename); 53 | } 54 | } 55 | 56 | abstract void load(); 57 | abstract void load(const(ubyte)[] buffer); 58 | 59 | bool usable() const pure nothrow @safe @nogc 60 | { 61 | return this._usable; 62 | } 63 | 64 | const(string) filename() const pure nothrow @safe @nogc 65 | { 66 | return this._filename; 67 | } 68 | 69 | const(string) name() const pure nothrow @safe @nogc 70 | { 71 | return this._name; 72 | } 73 | } 74 | 75 | import std.bitmanip : peek; 76 | import std.system : Endian; 77 | 78 | T peekLE(T, R)(R range) 79 | { 80 | return peek!(T, Endian.littleEndian, R)(range); 81 | } 82 | 83 | T peekLE(T, R)(R range, size_t index) 84 | { 85 | return peek!(T, Endian.littleEndian, R)(range, index); 86 | } 87 | 88 | T peekLE(T, R)(R range, size_t* index) 89 | { 90 | return peek!(T, Endian.littleEndian, R)(range, index); 91 | } 92 | 93 | import std.traits : isImplicitlyConvertible; 94 | 95 | package template canSetProp(string prop, DataStruct, ValueType) 96 | { 97 | enum canSetProp = __traits(hasMember, DataStruct, prop) && 98 | (is(typeof(__traits(getMember, DataStruct, prop) == ValueType)) || 99 | isImplicitlyConvertible!(ValueType, typeof(__traits(getMember, DataStruct, prop)))); 100 | } 101 | 102 | -------------------------------------------------------------------------------- /source/resource/imf.d: -------------------------------------------------------------------------------- 1 | module resource.imf; 2 | 3 | import resource.base; 4 | 5 | struct ImfFrame 6 | { 7 | int priority; 8 | // We don't use these 9 | //int cx; 10 | //int cy; 11 | } 12 | 13 | class ImfResource : BaseResource 14 | { 15 | private 16 | { 17 | ubyte[] _buffer; 18 | float _ver; 19 | enum MAX_LAYER = 20; 20 | enum MAX_ACTION = 600; 21 | ImfFrame[][][] _data; 22 | } 23 | 24 | static immutable(string[]) fileExtensions = ["imf"]; 25 | static immutable(string) filePath = "imf"; 26 | 27 | this(string filename, string resourcePath) 28 | { 29 | super(filename, resourcePath, filePath, fileExtensions); 30 | } 31 | 32 | int layer(uint priority, uint action, uint frame) const pure nothrow @safe @nogc 33 | { 34 | if (priority >= this._data.length || 35 | action >= this._data[priority].length || 36 | frame >= this._data[priority][action].length) 37 | { 38 | return -1; 39 | } 40 | 41 | for (auto layer = 0; layer < this._data.length; ++layer) 42 | { 43 | if (this._data[layer][action][frame].priority == priority) 44 | { 45 | return layer; 46 | } 47 | } 48 | 49 | return -1; 50 | } 51 | 52 | int priority(uint layer, uint action, uint frame) const pure nothrow @safe @nogc 53 | { 54 | if (layer >= this._data.length || 55 | action >= this._data[layer].length || 56 | frame >= this._data[layer][action].length) 57 | { 58 | return -1; 59 | } 60 | 61 | return this._data[layer][action][frame].priority; 62 | } 63 | 64 | /** 65 | Loads the file specified by filename. If the file does not exist 66 | an ResourceException is thrown. If there is an error during the file parsing 67 | a ResourceException is thrown. 68 | Throws: ResourceException 69 | */ 70 | override void load() 71 | { 72 | if (this.filename.length == 0) 73 | { 74 | return; 75 | } 76 | 77 | import std.stdio : File; 78 | import std.exception : collectException, ErrnoException, enforce; 79 | 80 | File fileHandle; 81 | auto err = collectException!ErrnoException( 82 | File(this.filename, "rb"), 83 | fileHandle); 84 | 85 | enforce!ResourceException(!err, err.msg); // Re-throw ErrnoException as ResourceException 86 | 87 | this._buffer = fileHandle.rawRead(new ubyte[fileHandle.size()]); 88 | 89 | this.readData(this._buffer); 90 | this._usable = true; 91 | } 92 | 93 | override void load(const(ubyte)[] buffer) 94 | { 95 | this.readData(buffer); 96 | this._usable = true; 97 | } 98 | 99 | float ver() const pure nothrow @safe @nogc 100 | { 101 | return this._ver; 102 | } 103 | 104 | private void readData(const(ubyte)[] buffer) 105 | { 106 | ulong offset = 0; 107 | this._ver = buffer.peekLE!float(&offset); 108 | 109 | // skip checksum (int) because it is unused 110 | offset += 4; 111 | 112 | uint maxLayer = buffer.peekLE!uint(&offset); 113 | 114 | this._data = new ImfFrame[][][maxLayer + 1]; 115 | 116 | for (auto layer = 0; layer <= maxLayer; ++layer) 117 | { 118 | uint numActions = buffer.peekLE!uint(&offset); 119 | this._data[layer] = new ImfFrame[][numActions]; 120 | 121 | for (auto action = 0; action < numActions; ++action) 122 | { 123 | uint numFrames = buffer.peekLE!uint(&offset); 124 | this._data[layer][action] = new ImfFrame[numFrames]; 125 | 126 | for (auto frame = 0; frame < numFrames; ++frame) 127 | { 128 | auto frameData = &this._data[layer][action][frame]; 129 | frameData.priority = buffer.peekLE!int(&offset); 130 | 131 | // We skip cx and cy because we have no use for it 132 | //frameData.cx = buffer.peekLE!int(&offset); 133 | //frameData.cy = buffer.peekLE!int(&offset); 134 | offset += 8; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /source/resource/lua.d: -------------------------------------------------------------------------------- 1 | module resource.lua; 2 | 3 | import resource.base; 4 | 5 | class LuaResource : BaseResource 6 | { 7 | private ubyte[] _buffer; 8 | 9 | static immutable(string[]) fileExtensions = ["lub", "lua"]; 10 | static immutable(string) filePath = "luafiles514/lua files"; 11 | 12 | this(string filename, string resourcePath) 13 | { 14 | super(filename, resourcePath, filePath, fileExtensions); 15 | } 16 | 17 | override void load() 18 | { 19 | if (this._filename.length == 0) 20 | { 21 | return; 22 | } 23 | 24 | bool fileFound = false; 25 | 26 | import std.exception : ErrnoException; 27 | import std.file : FileException; 28 | 29 | foreach (extension; this.fileExtensions) 30 | { 31 | import std.path : setExtension; 32 | 33 | string filename = setExtension(this._filename, extension); 34 | 35 | import std.file : getSize; 36 | 37 | try 38 | { 39 | if (filename.getSize() == 0) 40 | { 41 | continue; 42 | } 43 | fileFound = true; 44 | this._filename = filename; 45 | } 46 | catch (ErrnoException err) 47 | { 48 | // Skip 49 | } 50 | catch (FileException err) 51 | { 52 | // Skip 53 | } 54 | } 55 | 56 | import std.exception : enforce; 57 | import std.format : format; 58 | 59 | enforce!ResourceException(fileFound, format("LuaResource (%s) does not exist or is empty", this.name)); 60 | 61 | import std.stdio : File; 62 | import std.exception : collectException, ErrnoException; 63 | 64 | File fileHandle; 65 | auto err = collectException!ErrnoException( 66 | File(this.filename, "rb"), 67 | fileHandle); 68 | 69 | enforce!ResourceException(!err, err.msg); // Re-throw ErrnoException as ResourceException 70 | 71 | this._buffer = fileHandle.rawRead(new ubyte[fileHandle.size()]); 72 | 73 | this._usable = true; 74 | } 75 | 76 | override void load(const(ubyte)[] buffer) 77 | { 78 | this._buffer = buffer.dup; 79 | this._usable = true; 80 | } 81 | 82 | import luad.state : LuaState; 83 | 84 | void loadIntoLuaState(ref LuaState L) 85 | { 86 | if (this._buffer.length == 0) 87 | { 88 | return; 89 | } 90 | 91 | import luad.error : LuaErrorException; 92 | 93 | try 94 | { 95 | import luad.c.lua : lua_gettop, lua_pop, lua_pcall, lua_error, LUA_MULTRET; 96 | import luad.c.lauxlib : luaL_loadbuffer; 97 | import std.exception : enforce; 98 | 99 | const top = lua_gettop(L.state); 100 | 101 | auto ret = luaL_loadbuffer(L.state, cast(char*) this._buffer.ptr, this._buffer.length, 102 | this._filename.ptr); 103 | 104 | if (ret > 0) 105 | { 106 | lua_error(L.state); 107 | } 108 | 109 | //enforce!ResourceException(lua_pcall(L.state, 0, LUA_MULTRET, 0) == 0, "Error loading lua resource."); 110 | ret = lua_pcall(L.state, 0, LUA_MULTRET, 0); 111 | if (ret > 0) 112 | { 113 | lua_error(L.state); 114 | } 115 | 116 | const ntop = lua_gettop(L.state); 117 | lua_pop(L.state, ntop - top); 118 | } 119 | catch (LuaErrorException err) 120 | { 121 | throw new ResourceException("While loading lua resource \"" ~ this.filename ~ "\": " ~ err.msg); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /source/resource/manager.d: -------------------------------------------------------------------------------- 1 | module resource.manager; 2 | 3 | import resource.base : BaseResource, ResourceException; 4 | import sprite; 5 | 6 | class ResourceManager 7 | { 8 | private string _resourcePath; 9 | 10 | this(string resourcePath) pure nothrow @safe 11 | { 12 | this._resourcePath = resourcePath; 13 | } 14 | 15 | ResourceType get(ResourceType)(string filename) 16 | if (is(ResourceType : BaseResource)) 17 | { 18 | return new ResourceType(filename, this._resourcePath); 19 | } 20 | 21 | Sprite getSprite(string filename, SpriteType type = SpriteType.standard) 22 | { 23 | return getSprite(filename, filename, type); 24 | } 25 | 26 | Sprite getSprite(string actfilename, string sprfilename, SpriteType type = SpriteType.standard) 27 | { 28 | import resource.act : ActResource; 29 | import resource.spr : SprResource; 30 | 31 | auto act = this.get!ActResource(actfilename); 32 | auto spr = this.get!SprResource(sprfilename); 33 | 34 | act.load(); 35 | spr.load(); 36 | 37 | auto sprite = new Sprite(act, spr); 38 | sprite.filename = actfilename; 39 | sprite.type = type; 40 | 41 | import std.exception : enforce; 42 | import std.format : format; 43 | 44 | enforce!ResourceException(sprite.usable, format("Sprite's act or spr resource is not usable. " ~ 45 | "Making this sprite (ACT: %s, SPR: %s) also unusable.", actfilename, sprfilename)); 46 | 47 | return sprite; 48 | } 49 | 50 | Sprite getSprite(ActResource act, SprResource spr, SpriteType type = SpriteType.standard) 51 | { 52 | auto sprite = new Sprite(act, spr); 53 | sprite.filename = act.filename(); 54 | sprite.type = type; 55 | 56 | return sprite; 57 | } 58 | 59 | bool exists(ResourceType)(string filename) 60 | if (is(ResourceType : BaseResource)) 61 | { 62 | import std.file : exists; 63 | import resource.base : buildFilepath; 64 | 65 | bool resourceExists = false; 66 | 67 | static if (ResourceType.fileExtensions.length > 1) 68 | { 69 | foreach (ext; ResourceType.fileExtensions) 70 | { 71 | scope path = buildFilepath(this._resourcePath, ResourceType.filePath, filename, ext); 72 | resourceExists = exists(path); 73 | if (resourceExists) 74 | { 75 | break; 76 | } 77 | } 78 | } 79 | else static if (ResourceType.fileExtensions.length == 0) 80 | { 81 | resourceExists = exists(buildFilepath(this._resourcePath, ResourceType.filePath, filename)); 82 | } 83 | else 84 | { 85 | scope path = buildFilepath(this._resourcePath, ResourceType.filePath, filename, 86 | ResourceType.fileExtensions[0]); 87 | resourceExists = exists(path); 88 | } 89 | 90 | return resourceExists; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /source/resource/package.d: -------------------------------------------------------------------------------- 1 | module resource; 2 | 3 | public import resource.base : ResourceException, buildFilepath; 4 | public import resource.manager; 5 | public import resource.spr; 6 | public import resource.act; 7 | public import resource.palette; 8 | public import resource.lua; 9 | public import resource.imf; 10 | -------------------------------------------------------------------------------- /source/resource/palette.d: -------------------------------------------------------------------------------- 1 | module resource.palette; 2 | 3 | import resource.base; 4 | import draw : Color; 5 | 6 | alias Palette = Color[]; 7 | 8 | class PaletteResource : BaseResource 9 | { 10 | private Palette _palette; 11 | 12 | static immutable(string[]) fileExtensions = ["pal"]; 13 | static immutable(string) filePath = "palette"; 14 | 15 | this(string filename, string resourcePath) 16 | { 17 | super(filename, resourcePath, filePath, fileExtensions); 18 | } 19 | 20 | override void load() 21 | { 22 | if (this.filename.length == 0) 23 | { 24 | return; 25 | } 26 | 27 | import std.stdio : File; 28 | import std.exception : collectException, ErrnoException, enforce; 29 | 30 | File fileHandle; 31 | auto err = collectException!ErrnoException( 32 | File(this.filename, "rb"), 33 | fileHandle); 34 | 35 | enforce!ResourceException(!err, err.msg); // Re-throw ErrnoException as ResourceException 36 | 37 | this._palette = fileHandle.rawRead(new Color[256]); 38 | 39 | this._usable = true; 40 | } 41 | 42 | override void load(const(ubyte)[] buffer) 43 | { 44 | if (buffer.length < 256) 45 | { 46 | return; 47 | } 48 | 49 | this._palette = cast(Color[]) buffer[0 .. 256].dup; 50 | this._usable = true; 51 | } 52 | 53 | const(Palette) palette() const pure nothrow @safe @nogc 54 | { 55 | return this._palette; 56 | } 57 | 58 | int opApply(scope int delegate(const(Color)) dg) 59 | { 60 | int result = 0; 61 | foreach (const color; this._palette) 62 | { 63 | result = dg(color); 64 | if (result) 65 | { 66 | break; 67 | } 68 | } 69 | return result; 70 | } 71 | 72 | int opApply(scope int delegate(ulong, const(Color)) dg) 73 | { 74 | int result = 0; 75 | foreach (i, const color; this._palette) 76 | { 77 | result = dg(i, color); 78 | if (result) 79 | { 80 | break; 81 | } 82 | } 83 | return result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /source/resource/spr.d: -------------------------------------------------------------------------------- 1 | module resource.spr; 2 | 3 | import resource.base; 4 | import resource.palette : Palette; 5 | import draw : Color, RawImage; 6 | 7 | private enum MinSprSize = 2 + 6 + 1024; 8 | 9 | class SprResource : BaseResource 10 | { 11 | private ubyte[] _buffer; 12 | private Palette _palette; 13 | private RawImage[][2] _images; 14 | private ulong[][2] _imageDataOffsets; 15 | private ushort _ver; 16 | 17 | static immutable(string[]) fileExtensions = ["spr"]; 18 | static immutable(string) filePath = "sprite"; 19 | 20 | this(string filename, string resourcePath) 21 | { 22 | super(filename, resourcePath, filePath, fileExtensions); 23 | } 24 | 25 | /** 26 | Loads the file specified by filename. If the file does not exist 27 | an ResourceException is thrown. If there is an error during the file parsing 28 | a ResourceException is thrown. 29 | Throws: ResourceException 30 | */ 31 | override void load() 32 | { 33 | if (this.filename.length == 0) 34 | { 35 | return; 36 | } 37 | 38 | import std.stdio : File; 39 | import std.exception : collectException, ErrnoException, enforce; 40 | 41 | File fileHandle; 42 | auto err = collectException!ErrnoException( 43 | File(this.filename, "rb"), 44 | fileHandle); 45 | 46 | enforce!ResourceException(!err, err.msg); // Re-throw ErrnoException as ResourceException 47 | 48 | this._buffer = fileHandle.rawRead(new ubyte[fileHandle.size()]); 49 | 50 | this.readData(this._buffer); 51 | this._usable = true; 52 | } 53 | 54 | override void load(const(ubyte)[] buffer) 55 | { 56 | this._buffer = buffer.dup; 57 | this.readData(this._buffer); 58 | this._usable = true; 59 | } 60 | 61 | /** 62 | Returns the image corresponding to the given sprite type and index. 63 | If the type or index are out of bounds an empty image is instead returned. 64 | Returns: const(RawImage) or const(RawImage.init) 65 | */ 66 | const(RawImage) image(uint index, uint sprtype) const pure nothrow @safe @nogc 67 | in (sprtype < 2, "Spr type can only be 0 or 1") 68 | { 69 | if (sprtype > 1 || index >= this._images[sprtype].length) 70 | { 71 | return RawImage.init; 72 | } 73 | 74 | const img = this._images[sprtype][index]; 75 | if (img.pixels == img.pixels.init) 76 | { 77 | return RawImage.init; 78 | } 79 | return this._images[sprtype][index]; 80 | } 81 | 82 | ulong numberOfImages() const pure nothrow @safe @nogc 83 | { 84 | return this._images[0].length + this._images[1].length; 85 | } 86 | 87 | ulong numberOfPalImages() const pure nothrow @safe @nogc 88 | { 89 | return this._images[0].length; 90 | } 91 | 92 | ulong numberOfRgbaImages() const pure nothrow @safe @nogc 93 | { 94 | return this._images[1].length; 95 | } 96 | 97 | ushort ver() const pure nothrow @safe @nogc 98 | { 99 | return this._ver; 100 | } 101 | 102 | const(Palette) palette() const pure nothrow @safe @nogc 103 | { 104 | return this._palette; 105 | } 106 | 107 | int opApply(scope int delegate(const(RawImage)) dg) const 108 | { 109 | int result = 0; 110 | foreach (const img; this._images[0]) 111 | { 112 | result = dg(img); 113 | if (result) 114 | { 115 | break; 116 | } 117 | } 118 | foreach (const img; this._images[1]) 119 | { 120 | result = dg(img); 121 | if (result) 122 | { 123 | break; 124 | } 125 | } 126 | return result; 127 | } 128 | 129 | int opApply(scope int delegate(ulong, const(RawImage)) dg) const 130 | { 131 | int result = 0; 132 | foreach (i, const img; this._images[0]) 133 | { 134 | result = dg(i, img); 135 | if (result) 136 | { 137 | break; 138 | } 139 | } 140 | foreach (i, const img; this._images[1]) 141 | { 142 | result = dg(i, img); 143 | if (result) 144 | { 145 | break; 146 | } 147 | } 148 | return result; 149 | } 150 | 151 | /** 152 | Extracts spr data from the previously loaded buffer 153 | If the buffer does not contain a valid spr file. A ResourceException 154 | is thrown. 155 | Throws: ResourceException 156 | */ 157 | private void readData(const(ubyte)[] buffer) 158 | { 159 | import std.conv : to; 160 | import std.exception : enforce; 161 | 162 | enforce!ResourceException(buffer.length >= MinSprSize, 163 | "Spr file: '" ~ this.filename ~ "' does not have enough bytes to be " ~ 164 | "valid. Has: " ~ buffer.length.to!string ~ " bytes. " ~ 165 | "Should have: " ~ MinSprSize.to!string ~ " bytes."); 166 | 167 | enforce!ResourceException(buffer[0 .. 2] == ['S', 'P'], 168 | "Spr file: '" ~ this.filename ~ "' does not have a valid signature."); 169 | 170 | import std.bitmanip : littleEndianToNative, peek; 171 | import std.system : Endian; 172 | 173 | this._palette = cast(Color[]) buffer[$ - 1024 .. $]; 174 | 175 | ulong offset = 2; 176 | 177 | this._ver = buffer.peekLE!ushort(&offset); 178 | const palImages = buffer.peekLE!ushort(&offset); 179 | auto rgbaImages = 0; 180 | if (this._ver >= 0x200) 181 | { 182 | rgbaImages = buffer.peekLE!ushort(&offset); 183 | } 184 | 185 | this._images[0] = new RawImage[palImages]; 186 | this._images[1] = new RawImage[rgbaImages]; 187 | this._imageDataOffsets[0] = new ulong[palImages]; 188 | this._imageDataOffsets[1] = new ulong[rgbaImages]; 189 | 190 | for (auto i = 0; i < palImages; ++i) 191 | { 192 | RawImage img; 193 | img.width = buffer.peekLE!ushort(&offset); 194 | img.height = buffer.peekLE!ushort(&offset); 195 | 196 | ulong size = img.width * img.height; 197 | 198 | this._imageDataOffsets[0][i] = offset; 199 | 200 | if (this._ver >= 0x201) 201 | { 202 | size = buffer.peekLE!ushort(&offset); 203 | } 204 | 205 | this._images[0][i] = img; 206 | 207 | offset += size; 208 | } 209 | 210 | if (this._ver >= 0x200) 211 | { 212 | for (auto i = 0; i < rgbaImages; ++i) 213 | { 214 | RawImage img; 215 | img.width = buffer.peekLE!ushort(&offset); 216 | img.height = buffer.peekLE!ushort(&offset); 217 | 218 | ulong size = img.width * img.height; 219 | 220 | this._imageDataOffsets[1][i] = offset; 221 | 222 | this._images[1][i] = img; 223 | 224 | offset += size * 4; 225 | } 226 | } 227 | } 228 | 229 | void loadImageData(uint index, uint sprtype, const scope Palette palette = Palette.init) 230 | { 231 | if (sprtype > 1 || index >= this._images[sprtype].length) 232 | { 233 | return; 234 | } 235 | 236 | auto img = &this._images[sprtype][index]; 237 | 238 | if (img.pixels != img.pixels.init) 239 | { 240 | return; 241 | } 242 | 243 | const Palette pal = (palette == Palette.init) ? this._palette : palette; 244 | 245 | auto offset = this._imageDataOffsets[sprtype][index]; 246 | ulong size; 247 | 248 | img.pixels = new Color[img.width * img.height]; 249 | 250 | if (this._ver >= 0x201 && sprtype == 0) 251 | { 252 | size = this._buffer.peekLE!ushort(&offset); 253 | } 254 | else 255 | { 256 | size = img.width * img.height; 257 | } 258 | 259 | if (sprtype == 0) 260 | { 261 | for (auto j = 0, p = 0; j < size; ++j, ++p) 262 | { 263 | const palid = this._buffer[offset + j]; 264 | if (palid == 0 && this._ver >= 0x201) 265 | { 266 | const len = this._buffer[offset + j + 1]; 267 | Color color = pal[palid]; 268 | color.a(0x00); 269 | img.pixels[p .. p + len] = color; 270 | j++; 271 | p += len - 1; 272 | } 273 | else 274 | { 275 | img.pixels[p] = pal[palid]; 276 | img.pixels[p].a = palid == 0 ? 0x00 : 0xFF; 277 | } 278 | } 279 | } 280 | else if (sprtype == 1) 281 | { 282 | for (auto p = 0; p < img.pixels.length; ++p) 283 | { 284 | // RGBA images are stored with a negative y-axis 285 | const x = p % img.width; 286 | const y = (p / img.width) + 1; 287 | const destPixel = img.pixels.length - (y * img.width) + x; 288 | 289 | version (BigEndian) 290 | { 291 | img.pixels[destPixel] = cast(Color) this._buffer.peekLE!uint(&offset); 292 | } 293 | else 294 | { 295 | import std.bitmanip : peek; 296 | 297 | img.pixels[destPixel] = cast(Color) this._buffer.peek!uint(&offset); 298 | } 299 | } 300 | } 301 | } 302 | 303 | void loadAllImageData(const Palette palette = Palette.init) 304 | { 305 | for (auto i = 0; i < this._images[0].length; ++i) 306 | { 307 | this.loadImageData(i, 0, palette); 308 | } 309 | for (auto i = 0; i < this._images[1].length; ++i) 310 | { 311 | this.loadImageData(i, 1, palette); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /source/uniqueid.d: -------------------------------------------------------------------------------- 1 | module uniqueid; 2 | 3 | import std.bitmanip : nativeToLittleEndian; 4 | import std.array : appender; 5 | import std.string : format; 6 | import config : Config, Gender, toInt; 7 | import draw : Canvas; 8 | 9 | string createUid(uint jobid, immutable(Config) config, immutable(Canvas) canvas) pure nothrow @safe 10 | { 11 | const data = configToByteArray(jobid, config, canvas); 12 | 13 | import std.digest.crc : crc32Of, crcHexString; 14 | 15 | return crcHexString(data.crc32Of); 16 | } 17 | 18 | private ubyte[] configToByteArray(uint jobid, immutable(Config) config, immutable(Canvas) canvas) pure nothrow @safe 19 | { 20 | immutable sz = int.sizeof; 21 | auto buffer = new ubyte[sz * 24]; 22 | 23 | auto i = 0; 24 | 25 | if (jobid < uint.max) 26 | { 27 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(jobid); 28 | } 29 | else 30 | { 31 | import std.digest.crc : crc32Of; 32 | import std.string : representation; 33 | import std.array : join; 34 | 35 | buffer[(sz * i) .. (sz * (++i))] = config.job.join(",").representation.crc32Of; 36 | } 37 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.action); 38 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.frame); 39 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.gender.toInt()); 40 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.head); 41 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.outfit); 42 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.garment); 43 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.weapon); 44 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.shield); 45 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.bodyPalette); 46 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.headPalette); 47 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.headdir.toInt()); 48 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.madogearType.toInt()); 49 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.enableShadow ? 1 : 0); 50 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.outputFormat.toInt()); 51 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(canvas.width); 52 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(canvas.height); 53 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(canvas.originx); 54 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(canvas.originy); 55 | 56 | import std.algorithm : min; 57 | 58 | const numheadgear = min(4, config.headgear.length); 59 | 60 | foreach (h; 0 .. numheadgear) 61 | { 62 | buffer[(sz * i) .. (sz * (++i))] = nativeToLittleEndian(config.headgear[h]); 63 | } 64 | 65 | return buffer[0 .. (sz * i)]; 66 | } 67 | -------------------------------------------------------------------------------- /source/validation.d: -------------------------------------------------------------------------------- 1 | module validation; 2 | 3 | bool isJobArgValid(const(string)[] jobids, int maxAmount = -1) pure @safe 4 | { 5 | // Do not render body 6 | if (jobids.length == 1 && jobids[0] == "none") 7 | { 8 | return true; 9 | } 10 | 11 | bool isValid = true; 12 | 13 | size_t jobcount = 0; 14 | 15 | foreach (jobidstr; jobids) 16 | { 17 | import std.algorithm.searching : countUntil; 18 | import std.string : representation; 19 | 20 | auto rangeIndex = countUntil(jobidstr.representation, '-'); 21 | 22 | import std.conv : to, ConvException; 23 | 24 | if (rangeIndex == 0) 25 | { 26 | isValid = false; 27 | break; 28 | } 29 | else if (rangeIndex < 0) 30 | { 31 | try 32 | { 33 | auto id = jobidstr.to!uint; 34 | if (id == uint.max) 35 | { 36 | isValid = false; 37 | break; 38 | } 39 | jobcount++; 40 | } 41 | catch (ConvException err) 42 | { 43 | isValid = false; 44 | break; 45 | } 46 | } 47 | else 48 | { 49 | if (rangeIndex + 1 >= jobidstr.length) 50 | { 51 | isValid = false; 52 | break; 53 | } 54 | 55 | try 56 | { 57 | auto start = jobidstr[0 .. rangeIndex].to!uint; 58 | auto end = jobidstr[rangeIndex + 1 .. $].to!uint; 59 | 60 | if (end < start) 61 | { 62 | isValid = false; 63 | break; 64 | } 65 | 66 | jobcount += end == start ? 1 : (end-start); 67 | } 68 | catch (ConvException err) 69 | { 70 | isValid = false; 71 | break; 72 | } 73 | } 74 | } 75 | 76 | if (maxAmount >= 0 && jobcount > maxAmount) 77 | { 78 | isValid = false; 79 | } 80 | 81 | return isValid; 82 | } 83 | 84 | import std.regex : ctRegex; 85 | 86 | immutable CanvasRegex = ctRegex!(`^([0-9]+)x([0-9]+)([\+\-][0-9]+)([\+\-][0-9]+)$`); 87 | 88 | bool isCanvasArgValid(const scope string canvas) pure @safe 89 | { 90 | if (canvas.length == 0 || canvas == string.init) 91 | { 92 | return true; 93 | } 94 | 95 | import std.regex : matchFirst; 96 | 97 | auto matchFound = matchFirst(canvas, CanvasRegex); 98 | 99 | if (matchFound.length == 5) 100 | { 101 | import std.conv : to, ConvException; 102 | try 103 | { 104 | matchFound[1].to!uint; 105 | matchFound[2].to!uint; 106 | matchFound[3].to!int; 107 | matchFound[4].to!int; 108 | } 109 | catch (ConvException err) 110 | { 111 | return false; 112 | } 113 | } 114 | else 115 | { 116 | return false; 117 | } 118 | 119 | return true; 120 | } 121 | 122 | -------------------------------------------------------------------------------- /zrenderer.docker.conf: -------------------------------------------------------------------------------- 1 | outdir=output 2 | resourcepath=resources 3 | enableUniqueFilenames=true 4 | returnExistingFiles=true 5 | 6 | [server] 7 | hosts=0.0.0.0 8 | port=11011 9 | tokenfile=secrets/accesstokens.conf 10 | 11 | -------------------------------------------------------------------------------- /zrenderer.example.conf: -------------------------------------------------------------------------------- 1 | ; Output directory where all rendered sprites will be saved to. 2 | ; Default value: output 3 | ;outdir=output 4 | 5 | ; Path to the resource directory. All resources are tried to be found within 6 | ; this directory. 7 | ; Default value: 8 | ;resourcepath= 9 | 10 | ; Job id(s) which should be rendered. Can contain multiple comma separated 11 | ; values as well as ranges (e.g. '1001-1999'). Providing a single value of 12 | ; 'none' will not render the body, only the head with headgers. 13 | ; Default value: 14 | ;job= 15 | 16 | ; Gender of the player character. Possible values are: 'male' (1) or 'female' 17 | ; (0). 18 | ; Default value: male 19 | ;gender=male 20 | 21 | ; Head id which should be used when drawing a player. 22 | ; Default value: 1 23 | ;head=1 24 | 25 | ; The alternative outfit for player characters. Not all characters have 26 | ; alternative outfits. In these cases the default character will be rendered 27 | ; instead. Value of 0 means no outfit. 28 | ; Default value: 0 29 | ;outfit=0 30 | 31 | ; Headgears which should be attached to the players head. Can contain up to 3 32 | ; comma separated values. 33 | ; Default value: 34 | ;headgear= 35 | 36 | ; Garment which should be attached to the players body. 37 | ; Default value: 0 38 | ;garment=0 39 | 40 | ; Weapon which should be attached to the players body. 41 | ; Default value: 0 42 | ;weapon=0 43 | 44 | ; Shield which should be attached to the players body. 45 | ; Default value: 0 46 | ;shield=0 47 | 48 | ; Action of the job which should be drawn. 49 | ; Default value: 0 50 | ;action=0 51 | 52 | ; Frame of the action which should be drawn. Set to -1 to draw all frames. 53 | ; Default value: -1 54 | ;frame=-1 55 | 56 | ; Palette for the body sprite. Set to -1 to use the standard palette. 57 | ; Default value: -1 58 | ;bodyPalette=-1 59 | 60 | ; Palette for the head sprite. Set to -1 to use the standard palette. 61 | ; Default value: -1 62 | ;headPalette=-1 63 | 64 | ; Direction in which the head should turn. This is only applied to player 65 | ; sprites and only to the stand and sit action. Possible values are: straight, 66 | ; left, right or all. If 'all' is set then this direction system is ignored and 67 | ; all frames are interpreted like any other one. 68 | ; Default value: all 69 | ;headdir=all 70 | 71 | ; The alternative madogear sprite for player characters. Only applicable to 72 | ; madogear jobs. Possible values are 'robot' (0) and 'suit' (2). 73 | ; Default value: robot 74 | ;madogearType=robot 75 | 76 | ; Draw shadow underneath the sprite. 77 | ; Default value: true 78 | ;enableShadow=true 79 | 80 | ; Generate single frames of an animation. 81 | ; Default value: false 82 | ;singleframes=false 83 | 84 | ; If enabled the output filenames will be the checksum of input parameters. This 85 | ; will ensure that each request creates a filename that is unique to the input 86 | ; parameters and no overlapping for the same job occurs. 87 | ; Default value: false 88 | ;enableUniqueFilenames=false 89 | 90 | ; Whether to return already existing sprites (true) or always re-render it 91 | ; (false). You should only use this option in conjuction with 92 | ; 'enableUniqueFilenames=true'. 93 | ; Default value: false 94 | ;returnExistingFiles=false 95 | 96 | ; Sets a canvas onto which the sprite should be rendered. The canvas requires 97 | ; two options: its size and an origin point inside the canvas where the sprite 98 | ; should be placed. The format is as following: x±±. An 99 | ; origin point of +0+0 is equal to the top left corner. Example: 100 | ; 200x250+100+125. This would create a canvas and place the sprite in the 101 | ; center. 102 | ; Default value: 103 | ;canvas= 104 | 105 | ; Defines the output format. Possible values are 'png' (0) or 'zip' (1). If zip 106 | ; is chosen the zip will contain png files. 107 | ; Default value: png 108 | ;outputFormat=png 109 | 110 | ; Log level. Defines the minimum level at which logs will be shown. Possible 111 | ; values are: all, trace, info, warning, error, critical, fatal or off. 112 | ; Default value: info 113 | ;loglevel=info 114 | 115 | [server] 116 | ; Hostnames of the server. Can contain multiple comma separated values. 117 | ; Default value: localhost 118 | ;hosts=localhost 119 | 120 | ; Port of the server. 121 | ; Default value: 11011 122 | ;port=11011 123 | 124 | ; Log file to write to. E.g. /var/log/zrenderer.log. Leaving it empty will log 125 | ; to stdout. 126 | ; Default value: 127 | ;logfile= 128 | 129 | ; Access tokens file. File in which access tokens will be stored in. If the file 130 | ; does not exist it will be generated. 131 | ; Default value: accesstokens.conf 132 | ;tokenfile=accesstokens.conf 133 | 134 | ; Setting this to true will add CORS headers to all responses as well as adding 135 | ; an additional OPTIONS route that returns the CORS headers. 136 | ; Default value: false 137 | ;enableCORS=false 138 | 139 | ; Comma separated list of origins that are allowed access through CORS. Set this 140 | ; to a single '*' to allow access from any origin. Example: https://example.com. 141 | ; Default value: 142 | ;allowCORSOrigin= 143 | 144 | ; Whether to use TLS/SSL to secure the connection. You will also need to set the 145 | ; certificate and private key when enabling this setting. We recommend not 146 | ; enabling this feature but instead use a reverse proxy that handles HTTPS for 147 | ; you. 148 | ; Default value: false 149 | ;enableSSL=false 150 | 151 | ; Path to the certificate chain file used by TLS/SSL. 152 | ; Default value: 153 | ;certificateChainFile= 154 | 155 | ; Path to the private key file used by TLS/SSL. 156 | ; Default value: 157 | ;privateKeyFile= 158 | 159 | --------------------------------------------------------------------------------