├── .gitattributes ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ ├── Document │ │ ├── Document.java │ │ ├── IDocumentListener.java │ │ └── LSDSavFile.java │ ├── com │ │ └── laszlosystems │ │ │ └── libresample4j │ │ │ ├── FilterKit.java │ │ │ ├── Resampler.java │ │ │ └── SampleBuffers.java │ ├── fontEditor │ │ ├── .gitignore │ │ ├── ChangeEventListener.java │ │ ├── FontEditor.java │ │ ├── FontEditorColorSelector.java │ │ ├── FontMap.java │ │ └── TileEditor.java │ ├── kitEditor │ │ ├── KitEditor.java │ │ ├── Sample.java │ │ ├── SamplePicker.java │ │ ├── SampleView.java │ │ ├── Sound.java │ │ ├── WaveFile.java │ │ └── sbc.java │ ├── lsdpatch │ │ ├── LSDPatcher.java │ │ ├── MainWindow.java │ │ ├── NewVersionChecker.java │ │ ├── RomUpgradeTool.java │ │ └── WwwUtil.java │ ├── paletteEditor │ │ ├── ColorPicker.java │ │ ├── ColorUtil.java │ │ ├── HuePanel.java │ │ ├── PaletteEditor.java │ │ ├── RGB555.java │ │ ├── SaturationBrightnessPanel.java │ │ ├── ScreenShotColors.java │ │ ├── Swatch.java │ │ ├── SwatchPair.java │ │ └── SwatchPanel.java │ ├── songManager │ │ └── SongManager.java │ ├── structures │ │ ├── LSDJFont.java │ │ └── ROMDataManipulator.java │ └── utils │ │ ├── CommandLineFunctions.java │ │ ├── EditorPreferences.java │ │ ├── FileDialogLauncher.java │ │ ├── FileDrop.java │ │ ├── FontIO.java │ │ ├── GlobalHolder.java │ │ ├── RomUtilities.java │ │ └── StretchIcon.java └── resources │ ├── META-INF │ └── MANIFEST.MF │ ├── instr.bmp │ ├── shift_down.png │ ├── shift_left.png │ ├── shift_right.png │ ├── shift_up.png │ └── song.bmp └── test ├── java ├── Document │ ├── DocumentTest.java │ └── LSDSavFileTest.java └── kitEditor │ └── SampleTest.java └── resources ├── empty.lsdprj ├── sine1s44khz.wav └── triangle_waves.lsdprj /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build with Maven 24 | run: mvn -B package --file pom.xml 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target 3 | 4 | # IntelliJ IDEA 5 | .idea 6 | .settings 7 | *.iml 8 | 9 | *.gb 10 | *.sav 11 | *.wav 12 | *.raw 13 | ./*.lsdprj 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.13.3] - 2024-09-20 10 | ### Fixed 11 | - Kit Editor: tiny window size on Linux. 12 | 13 | ## [1.13.1] - 2023-01-27 14 | ### Fixed 15 | - Palette Editor: Updated color correction to match Sameboy 0.15.x. 16 | 17 | ## [1.13.0] - 2022-04-11 18 | ### Fixed 19 | - Filename filtering in non-Windows file dialogs broke in 1.8.0. 20 | 21 | ### Added 22 | - Palette Editor: Color space combo box. 23 | - Palette Editor: "Reality" color space, which looks close to real Game Boy Color. 24 | 25 | ## [1.12.0] - 2021-10-01 26 | ### Added 27 | - Kit Editor: "Invert Polarity for GBA" preference. 28 | 29 | ### Changed 30 | - Kit Editor: Update dither per sample. 31 | - Kit Editor: Moved "half-speed" preference to menu. 32 | - ROM files may now use .gbc extension. 33 | 34 | ## [1.11.6] - 2021-08-18 35 | ### Fixed 36 | - Kit Editor: Dither preference. 37 | - Kit Editor: Allow filenames less than 3 characters in length. 38 | 39 | ### Changed 40 | - Kit Editor: Moved dither preference to main window. 41 | 42 | ### Added 43 | - Kit Editor: "Duplicate sample" and "Copy" in context menu 44 | - Kit Editor: Edit menu for "Trim all samples to equal length" and "Paste" 45 | 46 | ## [1.11.5] - 2021-06-16 47 | ### Changed 48 | - Kit Editor: Tweaked resampler lowpass-filter settings to preserve more treble. 49 | 50 | ### Added 51 | - Kit Editor: Preferences menu for low-pass filter and dither. 52 | 53 | ## [1.11.4] - 2021-06-01 54 | ### Fixed 55 | - Palette Editor: Color picker could not pick black (0,0,0) due to UI scaling bug. 56 | 57 | ### Added 58 | - Palette Editor: Show selected color RGB value in color picker. 59 | 60 | ## [1.11.3] - 2021-04-25 61 | ### Changed 62 | - Kit Editor: reverted 1.11.2 changes due to cymbal dithering problems. 63 | 64 | ## [1.11.2] - 2021-04-23 65 | ### Changed 66 | - Kit Editor: tweaked DC level to reduce DMG noise. works best with 67 | lsdj 9.2.2 and above. 68 | 69 | ## [1.11.1] - 2021-04-18 70 | ### Changed 71 | - Kit Editor: added internal versioning field to kits, that tells if they were 72 | created using lsdpatcher 1.11.1 or above. this allows adding old kits to 73 | lsdj 9.2.0+ without loss of quality. 74 | 75 | ## [1.11.0] - 2021-04-15 76 | ### Fixed 77 | - Kit Editor: sample preview playback had inverted polarity. 78 | - Song Manager: remember last used .lsdprj path. 79 | - Clearing kits would not free up space for adding .lsdprj songs. 80 | - Minor UI issues. 81 | 82 | ### Changed 83 | - Kit Editor: when creating kits, wave frames are now rotated right, so that 84 | the sample to be played back last is written first. 85 | this compensates for the Game Boy wave refresh bug that plays back samples 86 | in wrong order after changing wave. 87 | best used with Little Sound Dj 9.2.0 and above. 88 | 89 | ## [1.10.5] - 2021-03-05 90 | ### Fixed 91 | - Kit Editor: inversed sample polarity. broken since always. 92 | - Kit Editor: garbled sample names when renaming an uninitialized kit. 93 | - Kit Editor: slow switching to high numbered kits. 94 | - ROM upgrade tool would not find new versions ending with A-Z. 95 | - Disabled ROM upgrade if there are unsaved changes. 96 | 97 | ### Added 98 | - Kit Editor: F1/F2 shortcut for previous/next bank. 99 | - Kit Editor: Sample pad tooltip. 100 | 101 | ### Removed 102 | - Kit Editor: wave blending, which didn't quite work due to timing issues. 103 | 104 | ## [1.10.4] - 2021-02-22 105 | ### Fixed 106 | - Kit Editor: removed sample prelisten low-pass filter. 107 | 108 | ## [1.10.3] - 2021-02-12 109 | ### Fixed 110 | - Kit Editor: various volume and trimming errors. 111 | - Font Editor: avoid duplicate font names. 112 | - Font Editor: .png file extension got included in font name when loading a font PNG. 113 | - Error handling when palettes cannot be parsed. 114 | - .sav file not found for ROM images ending with ".gb.gb". 115 | - ROM upgrade did not preserve graphics characters. 116 | 117 | ### Changed 118 | - Subwindows are now modal. 119 | 120 | ### Added 121 | - Kit Editor: bank switching buttons. 122 | 123 | ## [1.10.2] - 2020-11-22 124 | ### Fixed 125 | - Kit Editor: when replacing samples, trim sample end to fit. 126 | 127 | ## [1.10.1] - 2020-11-10 128 | ### Fixed 129 | - New version check at startup. 130 | 131 | ## [1.10.0] - 2020-11-10 132 | ### Fixed 133 | - Kit Editor: sample duration right alignment. 134 | - Kit Editor: made text fields handle "enter" key. 135 | - Kit Editor: stop listening to keypresses when window is closed. 136 | - Kit Editor: make focus return to main window when bank is changed. 137 | - Various window resize issues. 138 | 139 | ### Added 140 | - Kit Editor: pitch spinner, for changing sample pitch by semitone. 141 | - Kit Editor: trim spinner, for reducing sample length. 142 | - New version check at startup. 143 | 144 | ### Changed 145 | - Kit Editor: now removes DC offset before resampling. 146 | - Kit Editor: changed "Reload samples" button to "Reload sample". 147 | - Kit Editor: when adding a too big sample, trim sample end to fit. 148 | - Kit Editor: disabled "Add sample" button when kit is full. 149 | 150 | ## [1.9.0] - 2020-10-31 151 | ### Fixed 152 | - Kit Editor: dramatically improved resampling using libresample4j. 153 | - Kit Editor: refresh sample view when the sample is reloaded. 154 | - Kit Editor: update "seconds free" after volume change. 155 | - Kit Editor: pad kit banks with "rst" instead of "nop" instructions, for crash detection. 156 | 157 | ### Added 158 | - Kit Editor: print sample duration in sample view. 159 | 160 | ## [1.8.1] - 2020-10-25 161 | ### Fixed 162 | - Kit Editor: loading kits with sample volumes stored in settings file. 163 | - Kit Editor: reduced wave blending noise for emulators that do not have the Game Boy wave refresh bug. 164 | - Palette Editor: force palette names to upper case. 165 | 166 | ## [1.8.0] - 2020-10-24 167 | ### Fixed 168 | - Palette Editor: avoid duplicate palette names when loading a palette. 169 | - Palette Editor: dragging color picker sliders is now more responsive. 170 | - Palette Editor: improved color picker visibility. 171 | - Kit Editor: update of "bytes used" field. 172 | - Font Editor: when loading font from .png, set font name from the file name. 173 | - Some file dialogs would not remember the last used directory. 174 | - Saving a ROM when no SAV has been loaded. 175 | - Switching .sav would not take effect until loading a ROM. 176 | 177 | ### Added 178 | - Kit Editor: MPC-like UI with pads. Play by clicking or keys 1234QWERASDFZXC. Right-click pad to rename, replace or delete sample. 179 | - Kit Editor: "Reload samples", "Save ROM", "Clear kit" buttons. 180 | - Kit Editor: automatic silence trimming. 181 | - Kit Editor: when saving kits, remember source sample files + volumes. 182 | - Font Editor: support for editing graphics characters. 183 | 184 | ### Changed 185 | - Kit Editor: "Add Sample" now automatically resamples, normalizes and dithers the sample. No need to prepare samples using sox anymore. 186 | - Kit Editor: switched to TPDF dither for improved sound. 187 | - Kit Editor: when adding samples, blend wave frames to reduce impact of the [Game Boy wave refresh bug](https://www.devrs.com/gb/files/gbsnd3.gif). 188 | - Kit Editor: volume control now adjusts sample volume instead of pre-listening volume. 189 | - Kit Editor: improved sound playback quality. 190 | - Kit Editor: click sample view to play. 191 | - Kit Editor: half-speed setting now also affects "Add sample". 192 | - Kit Editor: renamed "Export kit" to "Save kit". 193 | - Kit Editor: show unused space in seconds instead of bytes. 194 | - Palette Editor: improved mid-tone generation. 195 | - Various file dialog improvements. 196 | - Improved command line feedback. 197 | 198 | ### Removed 199 | - Font Editor: removed saving of .lsdfnt files, as well as loading/saving multiple fonts in one go. 200 | - Kit Editor: "Play sample on click" toggle. 201 | - Kit Editor: "Export all samples" button. 202 | 203 | ## [1.7.0] - 2020-10-06 204 | ### Fixed 205 | - Kit Editor: sample export broke in 1.6.0. 206 | - Song Editor: incorrect broken-song warnings. thx michael dufault! 207 | 208 | ### Added 209 | - Palette Editor: color picker! 210 | - Palette Editor: click in lsdj screens to select color. 211 | - Palette Editor: "swap color" and "clone color" buttons. 212 | - Palette Editor: "raw" button, which displays colors as-is. 213 | 214 | ### Changed 215 | - Palette Editor: switched color correction from Gambatte to Sameboy. 216 | - Palette Editor: updated screenshots to LSDj v8.9.0. 217 | - Palette Editor: create brighter mid-tones if the background is brighter than the foreground. 218 | - Each file extension now has its own last used load/save file path. 219 | 220 | ### Removed 221 | - Palette Editor: color spinners. 222 | 223 | ## [1.6.0] - 2020-10-02 224 | ### Fixed 225 | - Kit sample playback got stuck at times. 226 | - Old LSDj ROMs (like, v3) would not open. 227 | 228 | ### Added 229 | - Startup dialog to choose ROM, SAV and sub-tool. 230 | - Added song manager from LSDManager project. 231 | - Song manager warning on corrupted songs. 232 | - Song manager now saves LSDj Project files (.lsdprj) which contains both song and sample kits. 233 | - Upgrade ROM button, which downloads the latest ROM images from https://www.littlesounddj.com. The upgrade preserves custom kits, fonts and palettes. 234 | - Palette editor randomize button. 235 | 236 | ### Changed 237 | - Palette editor layout. 238 | 239 | ## [1.5.0] - 2020-09-13 240 | ### Changed 241 | - Merged LSDPatcher Redux v1.4.6. Full list of changes at [LSDPatcher Redux release page](https://github.com/Eiyeron/lsdpatch/releases). 242 | 243 | ## [1.4.2] - 2020-08-19 244 | ### Added 245 | - LSDj v8.8.3 support. 246 | 247 | ## [1.4.1] - 2020-07-04 248 | ### Added 249 | - LSDj v8.7.4 support. 250 | 251 | ## [1.4.0] - 2020-07-02 252 | ### Added 253 | - Palette editor "Desaturate preview" toggle. 254 | - Palette editor copy/paste. 255 | 256 | ## [1.3.0] - 2020-06-27 257 | ### Added 258 | - Allow variable number of palettes. Some LSDj versions have 6 palettes, others 7. 259 | 260 | ## [1.2.0] - 2020-03-05 261 | ### Changed 262 | - Java 8 now required. 263 | - Brighter background shade for DMG fonts. 264 | 265 | ## [1.1.6] - 2017-10-19 266 | ### Fixed 267 | - Recalculate ROM checksum on save. 268 | 269 | ## [1.1.5] - 2017-10-14 270 | ### Added 271 | - "Import kits from ROM" button. 272 | 273 | ## [1.1.4] - 2017-05-07 274 | ### Changed 275 | - Kit selector is now hexadecimal. 276 | - Brought back dot in kit list. 277 | 278 | ## [1.1.3] - 2017-01-26 279 | ### Changed 280 | - Made palette editor a bit bigger. 281 | 282 | ### Fixed 283 | - Shaded and inverted tiles would not always be generated by font editor. 284 | 285 | ## [1.1.2] - 2017-01-23 286 | ### Added 287 | - Load and save palettes. 288 | 289 | ### Fixed 290 | - When loading a ROM, palettes would be added twice. 291 | - Errors related to combo box in font editor. 292 | 293 | ## [1.1.1] - 2017-01-23 294 | ### Added 295 | - Font renaming. 296 | - Font editor grid. 297 | - Include font name in .lsdfnt 298 | 299 | ### Fixed 300 | - Out-of-bounds drawing in font editor. 301 | 302 | ## [1.1.0] - 2017-01-23 303 | ### Added 304 | - Font editor. 305 | 306 | ## [1.0.2] - 2017-01-20 307 | ### Changed 308 | - Made preview screens in palette editor bigger. 309 | 310 | ## [1.0.1] - 2017-01-20 311 | ### Fixed 312 | - Wrong behavior when loading invalid ROM images. 313 | 314 | ### Changed 315 | - Lowered required JRE version to 1.6. 316 | 317 | ## [1.0.0] - 2017-01-20 318 | ### Added 319 | - Palette editor, entered by menu option Palette->Edit Palette. 320 | 321 | ## [0.19] - 2011-08-20 322 | ### Fixed 323 | - Loading a long sample threw a confusing, empty error message. Thanks to Clay Morrow for reporting. 324 | 325 | 326 | [unreleased]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.3..HEAD 327 | [1.13.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.2..v1.13.3 328 | [1.13.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.1..v1.13.2 329 | [1.13.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.13.0..v1.13.1 330 | [1.13.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.12.0..v1.13.0 331 | [1.12.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.6..v1.12.0 332 | [1.11.6]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.5..v1.11.6 333 | [1.11.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.4..v1.11.5 334 | [1.11.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.3..v1.11.4 335 | [1.11.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.2..v1.11.3 336 | [1.11.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.1..v1.11.2 337 | [1.11.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.11.0..v1.11.1 338 | [1.11.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.5..v1.11.0 339 | [1.10.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.4..v1.10.5 340 | [1.10.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.3..v1.10.4 341 | [1.10.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.2..v1.10.3 342 | [1.10.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.1..v1.10.2 343 | [1.10.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.10.0..v1.10.1 344 | [1.10.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.9.0..v1.10.0 345 | [1.9.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.8.1..v1.9.0 346 | [1.8.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.8.0..v1.8.1 347 | [1.8.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.7.0..v1.8.0 348 | [1.7.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.6.0..v1.7.0 349 | [1.6.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.5.0..v1.6.0 350 | [1.5.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.2..v1.5.0 351 | [1.4.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.1..v1.4.2 352 | [1.4.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.4.0..v1.4.1 353 | [1.4.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.3.0..v1.4.0 354 | [1.3.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.2.0..v1.3.0 355 | [1.2.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.6..v1.2.0 356 | [1.1.6]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.5..v1.1.6 357 | [1.1.5]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.4..v1.1.5 358 | [1.1.4]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.3..v1.1.4 359 | [1.1.3]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.2..v1.1.3 360 | [1.1.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.1..v1.1.2 361 | [1.1.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.1.0..v1.1.1 362 | [1.1.0]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.2..v1.1.0 363 | [1.0.2]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.1..v1.0.2 364 | [1.0.1]: https://github.com/jkotlinski/lsdpatch/compare/v1.0.0..v1.0.1 365 | [1.0.0]: https://github.com/jkotlinski/lsdpatch/compare/v0.19...v1.0.0 366 | [0.19]: https://github.com/jkotlinski/lsdpatch/releases/tag/v0.19 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2001 by Johan Kotlinski, 2017 by Florian Dormont 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | =============================================================================== 22 | 23 | Filedrop.java, Copyright (C) 2007 by Robert Harder. No rights reserved. 24 | 25 | =============================================================================== 26 | 27 | StretchIcon.java, Copyright (C) 2016 by Darryl Burke. No rights reserved. 28 | 29 | =============================================================================== 30 | 31 | Color correction routine from Sameboy: 32 | 33 | MIT License 34 | 35 | Copyright (c) 2015-2020 Lior Halphon 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | 55 | =============================================================================== 56 | 57 | libresample4j 58 | Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. 59 | 60 | libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, 61 | which is in turn based on Julius Smith's Resample 1.7 library. 62 | http://www-ccrma.stanford.edu/~jos/resample/ 63 | 64 | License: LGPL -- see the file LICENSE.txt for more information 65 | 66 | -- 67 | 68 | This product includes software derived from the work of 69 | Julius Smith and Dominic Mazzoni 70 | (http://ccrma-www.stanford.edu/~jos/resample/Free_Resampling_Software.html) 71 | 72 | libresample 0.1.3 73 | Copyright 2003 Dominic Mazzoni . 74 | 75 | Resample 1.7 76 | Copyright 1994-2002 Julius O. Smith III , 77 | all rights reserved. 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LSDPatcher 2 | 3 | A tool for modifying songs, samples, fonts and palettes on [Little Sound Dj][lsdj] (LSDj) ROM images and save files. Requires 4 | [Java][java] 8+. If you have problems running the .jar on Windows, try [Jarfix][jarfix]. 5 | 6 | [Download][releases] | [Fonts][lsdfnts] | [Palettes][lsdpals] 7 | 8 | ## Building 9 | 10 | Build using [Maven](https://maven.apache.org/): `mvn package` 11 | 12 | ![Java CI with Maven](https://github.com/jkotlinski/lsdpatch/workflows/Java%20CI%20with%20Maven/badge.svg) 13 | 14 | [lsdj]: https://www.littlesounddj.com/ 15 | [sox]: http://sox.sourceforge.net/ 16 | [releases]: https://github.com/jkotlinski/lsdpatch/releases 17 | [wiki]: https://github.com/jkotlinski/lsdpatch/wiki/Documentation 18 | [jarfix]: http://johann.loefflmann.net/en/software/jarfix/index.html 19 | [java]: http://www.java.com/ 20 | [lsdfnts]: https://github.com/psgcabal/lsdfonts 21 | [lsdpals]: https://github.com/psgcabal/lsdpals 22 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.littlesounddj 8 | lsdpatch 9 | 1.13.3 10 | jar 11 | 12 | 13 | 14 | com.miglayout 15 | miglayout 16 | 3.7.4 17 | 18 | 19 | org.junit.jupiter 20 | junit-jupiter 21 | 5.7.0 22 | test 23 | 24 | 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-surefire-plugin 33 | 3.0.0-M5 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-compiler-plugin 38 | 3.8.1 39 | 40 | -Xlint:deprecation 41 | -XDignore.symbol.file 42 | 1.8 43 | 1.8 44 | UTF-8 45 | 46 | 47 | 48 | maven-assembly-plugin 49 | org.apache.maven.plugins 50 | 3.1.0 51 | 52 | 53 | make-executable-jar-with-dependencies 54 | package 55 | 56 | single 57 | 58 | 59 | false 60 | 61 | 62 | true 63 | true 64 | lsdpatch.LSDPatcher 65 | 66 | 67 | 68 | jar-with-dependencies 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/java/Document/Document.java: -------------------------------------------------------------------------------- 1 | package Document; 2 | 3 | import utils.EditorPreferences; 4 | import utils.RomUtilities; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.RandomAccessFile; 9 | import java.util.Arrays; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | public class Document { 14 | private boolean romDirty; 15 | private byte[] romImage; 16 | private File romFile; 17 | 18 | private boolean savDirty; 19 | private LSDSavFile savFile = new LSDSavFile(); 20 | 21 | private final List documentListeners = new LinkedList<>(); 22 | 23 | public void subscribe(IDocumentListener documentListener) { 24 | documentListeners.add(documentListener); 25 | } 26 | 27 | public File romFile() { 28 | return romFile; 29 | } 30 | 31 | private void publishDocumentDirty() { 32 | for (IDocumentListener documentListener : documentListeners) { 33 | documentListener.onDocumentDirty(isDirty()); 34 | } 35 | } 36 | 37 | private void setRomDirty(boolean dirty) { 38 | romDirty = dirty; 39 | publishDocumentDirty(); 40 | } 41 | 42 | private void setSavDirty(boolean dirty) { 43 | savDirty = dirty; 44 | publishDocumentDirty(); 45 | } 46 | 47 | public byte[] romImage() { 48 | return romImage == null ? null : romImage.clone(); 49 | } 50 | 51 | public void setRomImage(byte[] romImage) { 52 | if (Arrays.equals(romImage, this.romImage)) { 53 | return; 54 | } 55 | this.romImage = romImage; 56 | setRomDirty(true); 57 | } 58 | 59 | public void loadRomImage(String romPath) throws IOException { 60 | romFile = new File(romPath); 61 | romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 62 | setRomDirty(false); 63 | try { 64 | RandomAccessFile f = new RandomAccessFile(romFile, "r"); 65 | f.readFully(romImage); 66 | f.close(); 67 | EditorPreferences.setLastPath("gb", romPath); 68 | } catch (IOException ioe) { 69 | romImage = null; 70 | throw ioe; 71 | } 72 | } 73 | 74 | public void loadSavFile(String savPath) throws IOException { 75 | setSavDirty(false); 76 | try { 77 | savFile = new LSDSavFile(); 78 | savFile.loadFromSav(savPath); 79 | EditorPreferences.setLastPath("sav", savPath); 80 | } catch (IOException e) { 81 | savFile = null; 82 | throw e; 83 | } 84 | } 85 | 86 | public LSDSavFile savFile() { 87 | if (savFile == null) { 88 | return null; 89 | } 90 | try { 91 | return savFile.clone(); 92 | } catch (CloneNotSupportedException e) { 93 | return null; 94 | } 95 | } 96 | 97 | public void setSavFile(LSDSavFile savFile) { 98 | if (savFile == null) { 99 | this.savFile = null; 100 | setSavDirty(false); 101 | return; 102 | } 103 | if (this.savFile != null && savFile.equals(this.savFile)) { 104 | return; 105 | } 106 | this.savFile = savFile; 107 | setSavDirty(true); 108 | } 109 | 110 | public boolean isSavDirty() { 111 | return savDirty; 112 | } 113 | 114 | public boolean isRomDirty() { 115 | return romDirty; 116 | } 117 | 118 | public boolean isDirty() { 119 | return romDirty || savDirty; 120 | } 121 | 122 | public void setRomFile(File file) { 123 | romFile = file; 124 | } 125 | 126 | public void clearSavDirty() { 127 | setSavDirty(false); 128 | } 129 | 130 | public void clearRomDirty() { 131 | setRomDirty(false); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/Document/IDocumentListener.java: -------------------------------------------------------------------------------- 1 | package Document; 2 | 3 | public interface IDocumentListener { 4 | void onDocumentDirty(boolean dirty); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/laszlosystems/libresample4j/FilterKit.java: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * libresample4j 4 | * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. 5 | * 6 | * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, 7 | * which is in turn based on Julius Smith's Resample 1.7 library. 8 | * http://www-ccrma.stanford.edu/~jos/resample/ 9 | * 10 | * License: LGPL -- see the file LICENSE.txt for more information 11 | * 12 | *****************************************************************************/ 13 | package com.laszlosystems.libresample4j; 14 | 15 | /** 16 | * This file provides Kaiser-windowed low-pass filter support, 17 | * including a function to create the filter coefficients, and 18 | * two functions to apply the filter at a particular point. 19 | * 20 | *
 21 |  * reference: "Digital Filters, 2nd edition"
 22 |  *            R.W. Hamming, pp. 178-179
 23 |  *
 24 |  * Izero() computes the 0th order modified bessel function of the first kind.
 25 |  *    (Needed to compute Kaiser window).
 26 |  *
 27 |  * LpFilter() computes the coeffs of a Kaiser-windowed low pass filter with
 28 |  *    the following characteristics:
 29 |  *
 30 |  *       c[]  = array in which to store computed coeffs
 31 |  *       frq  = roll-off frequency of filter
 32 |  *       N    = Half the window length in number of coeffs
 33 |  *       Beta = parameter of Kaiser window
 34 |  *       Num  = number of coeffs before 1/frq
 35 |  *
 36 |  * Beta trades the rejection of the lowpass filter against the transition
 37 |  *    width from passband to stopband.  Larger Beta means a slower
 38 |  *    transition and greater stopband rejection.  See Rabiner and Gold
 39 |  *    (Theory and Application of DSP) under Kaiser windows for more about
 40 |  *    Beta.  The following table from Rabiner and Gold gives some feel
 41 |  *    for the effect of Beta:
 42 |  *
 43 |  * All ripples in dB, width of transition band = D*N where N = window length
 44 |  *
 45 |  *               BETA    D       PB RIP   SB RIP
 46 |  *               2.120   1.50  +-0.27      -30
 47 |  *               3.384   2.23    0.0864    -40
 48 |  *               4.538   2.93    0.0274    -50
 49 |  *               5.658   3.62    0.00868   -60
 50 |  *               6.764   4.32    0.00275   -70
 51 |  *               7.865   5.0     0.000868  -80
 52 |  *               8.960   5.7     0.000275  -90
 53 |  *               10.056  6.4     0.000087  -100
 54 |  * 
55 | */ 56 | public class FilterKit { 57 | 58 | // Max error acceptable in Izero 59 | private static final double IzeroEPSILON = 1E-21; 60 | 61 | private static double Izero(double x) { 62 | double sum, u, halfx, temp; 63 | int n; 64 | 65 | sum = u = n = 1; 66 | halfx = x / 2.0; 67 | do { 68 | temp = halfx / (double) n; 69 | n += 1; 70 | temp *= temp; 71 | u *= temp; 72 | sum += u; 73 | } while (u >= IzeroEPSILON * sum); 74 | return (sum); 75 | } 76 | 77 | public static void lrsLpFilter(double c[], int N, double frq, double Beta, int Num) { 78 | double IBeta, temp, temp1, inm1; 79 | int i; 80 | 81 | // Calculate ideal lowpass filter impulse response coefficients: 82 | c[0] = 2.0 * frq; 83 | for (i = 1; i < N; i++) { 84 | temp = Math.PI * (double) i / (double) Num; 85 | c[i] = Math.sin(2.0 * temp * frq) / temp; // Analog sinc function, 86 | // cutoff = frq 87 | } 88 | 89 | /* 90 | * Calculate and Apply Kaiser window to ideal lowpass filter. Note: last 91 | * window value is IBeta which is NOT zero. You're supposed to really 92 | * truncate the window here, not ramp it to zero. This helps reduce the 93 | * first sidelobe. 94 | */ 95 | IBeta = 1.0 / Izero(Beta); 96 | inm1 = 1.0 / ((double) (N - 1)); 97 | for (i = 1; i < N; i++) { 98 | temp = (double) i * inm1; 99 | temp1 = 1.0 - temp * temp; 100 | temp1 = (temp1 < 0 ? 0 : temp1); /* 101 | * make sure it's not negative 102 | * since we're taking the square 103 | * root - this happens on Pentium 104 | * 4's due to tiny roundoff errors 105 | */ 106 | c[i] *= Izero(Beta * Math.sqrt(temp1)) * IBeta; 107 | } 108 | } 109 | 110 | /** 111 | * 112 | * @param Imp impulse response 113 | * @param ImpD impulse response deltas 114 | * @param Nwing length of one wing of filter 115 | * @param Interp Interpolate coefs using deltas? 116 | * @param Xp_array Current sample array 117 | * @param Xp_index Current sample index 118 | * @param Ph Phase 119 | * @param Inc increment (1 for right wing or -1 for left) 120 | * @return 121 | */ 122 | public static float lrsFilterUp(float Imp[], float ImpD[], int Nwing, boolean Interp, float[] Xp_array, int Xp_index, double Ph, 123 | int Inc) { 124 | double a = 0; 125 | float v, t; 126 | 127 | Ph *= Resampler.Npc; // Npc is number of values per 1/delta in impulse 128 | // response 129 | 130 | v = 0.0f; // The output value 131 | 132 | float[] Hp_array = Imp; 133 | int Hp_index = (int) Ph; 134 | 135 | float[] End_array = Imp; 136 | int End_index = Nwing; 137 | 138 | float[] Hdp_array = ImpD; 139 | int Hdp_index = (int) Ph; 140 | 141 | if (Interp) { 142 | // Hdp = &ImpD[(int)Ph]; 143 | a = Ph - Math.floor(Ph); /* fractional part of Phase */ 144 | } 145 | 146 | if (Inc == 1) // If doing right wing... 147 | { // ...drop extra coeff, so when Ph is 148 | End_index--; // 0.5, we don't do too many mult's 149 | if (Ph == 0) // If the phase is zero... 150 | { // ...then we've already skipped the 151 | Hp_index += Resampler.Npc; // first sample, so we must also 152 | Hdp_index += Resampler.Npc; // skip ahead in Imp[] and ImpD[] 153 | } 154 | } 155 | 156 | if (Interp) 157 | while (Hp_index < End_index) { 158 | t = Hp_array[Hp_index]; /* Get filter coeff */ 159 | t += Hdp_array[Hdp_index] * a; /* t is now interp'd filter coeff */ 160 | Hdp_index += Resampler.Npc; /* Filter coeff differences step */ 161 | t *= Xp_array[Xp_index]; /* Mult coeff by input sample */ 162 | v += t; /* The filter output */ 163 | Hp_index += Resampler.Npc; /* Filter coeff step */ 164 | Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */ 165 | } 166 | else 167 | while (Hp_index < End_index) { 168 | t = Hp_array[Hp_index]; /* Get filter coeff */ 169 | t *= Xp_array[Xp_index]; /* Mult coeff by input sample */ 170 | v += t; /* The filter output */ 171 | Hp_index += Resampler.Npc; /* Filter coeff step */ 172 | Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */ 173 | } 174 | 175 | return v; 176 | } 177 | 178 | /** 179 | * 180 | * @param Imp impulse response 181 | * @param ImpD impulse response deltas 182 | * @param Nwing length of one wing of filter 183 | * @param Interp Interpolate coefs using deltas? 184 | * @param Xp_array Current sample array 185 | * @param Xp_index Current sample index 186 | * @param Ph Phase 187 | * @param Inc increment (1 for right wing or -1 for left) 188 | * @param dhb filter sampling period 189 | * @return 190 | */ 191 | public static float lrsFilterUD(float Imp[], float ImpD[], int Nwing, boolean Interp, float[] Xp_array, int Xp_index, double Ph, 192 | int Inc, double dhb) { 193 | float a; 194 | float v, t; 195 | double Ho; 196 | 197 | v = 0.0f; // The output value 198 | Ho = Ph * dhb; 199 | 200 | float[] End_array = Imp; 201 | int End_index = Nwing; 202 | 203 | if (Inc == 1) // If doing right wing... 204 | { // ...drop extra coeff, so when Ph is 205 | End_index--; // 0.5, we don't do too many mult's 206 | if (Ph == 0) // If the phase is zero... 207 | Ho += dhb; // ...then we've already skipped the 208 | } // first sample, so we must also 209 | // skip ahead in Imp[] and ImpD[] 210 | 211 | float[] Hp_array = Imp; 212 | int Hp_index; 213 | 214 | if (Interp) { 215 | float[] Hdp_array = ImpD; 216 | int Hdp_index; 217 | 218 | while ((Hp_index = (int) Ho) < End_index) { 219 | t = Hp_array[Hp_index]; // Get IR sample 220 | Hdp_index = (int) Ho; // get interp bits from diff table 221 | a = (float) (Ho - Math.floor(Ho)); // a is logically between 0 222 | // and 1 223 | t += Hdp_array[Hdp_index] * a; // t is now interp'd filter coeff 224 | t *= Xp_array[Xp_index]; // Mult coeff by input sample 225 | v += t; // The filter output 226 | Ho += dhb; // IR step 227 | Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS 228 | } 229 | } else { 230 | while ((Hp_index = (int) Ho) < End_index) { 231 | t = Hp_array[Hp_index]; // Get IR sample 232 | t *= Xp_array[Xp_index]; // Mult coeff by input sample 233 | v += t; // The filter output 234 | Ho += dhb; // IR step 235 | Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS 236 | } 237 | } 238 | 239 | return v; 240 | } 241 | 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/com/laszlosystems/libresample4j/SampleBuffers.java: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 3 | * libresample4j 4 | * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. 5 | * 6 | * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, 7 | * which is in turn based on Julius Smith's Resample 1.7 library. 8 | * http://www-ccrma.stanford.edu/~jos/resample/ 9 | * 10 | * License: LGPL -- see the file LICENSE.txt for more information 11 | * 12 | *****************************************************************************/ 13 | package com.laszlosystems.libresample4j; 14 | 15 | /** 16 | * Callback for producing and consuming samples. Enables on-the-fly conversion between sample types 17 | * (signed 16-bit integers to floats, for example) and/or writing directly to an output stream. 18 | */ 19 | public interface SampleBuffers { 20 | /** 21 | * @return number of input samples available 22 | */ 23 | 24 | int getInputBufferLength(); 25 | 26 | /** 27 | * @return number of samples the output buffer has room for 28 | */ 29 | int getOutputBufferLength(); 30 | 31 | /** 32 | * Copy length samples from the input buffer to the given array, starting at the given offset. 33 | * Samples should be in the range -1.0f to 1.0f. 34 | * 35 | * @param array array to hold samples from the input buffer 36 | * @param offset start writing samples here 37 | * @param length write this many samples 38 | */ 39 | void produceInput(float[] array, int offset, int length); 40 | 41 | /** 42 | * Copy length samples from the given array to the output buffer, starting at the given offset. 43 | * 44 | * @param array array to read from 45 | * @param offset start reading samples here 46 | * @param length read this many samples 47 | */ 48 | void consumeOutput(float[] array, int offset, int length); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/fontEditor/.gitignore: -------------------------------------------------------------------------------- 1 | /ChangeEventListener.class 2 | /FontEditor$1.class 3 | /FontEditor$2.class 4 | /FontEditor$3.class 5 | /FontEditor$4.class 6 | /FontEditor$5.class 7 | /FontEditor$6.class 8 | /FontEditor$7.class 9 | /FontEditor$8.class 10 | /FontEditor.class 11 | /FontEditorColorSelector.class 12 | /FontMap$TileSelectListener.class 13 | /FontMap.class 14 | /TileEditor$TileChangedListener.class 15 | /TileEditor.class 16 | /FontEditor$9.class 17 | /ChangeEventMouseSide.class 18 | /ChangeEventListener$ChangeEventMouseSide.class 19 | /FontEditorColorSelector$FontEditorColorListener.class 20 | -------------------------------------------------------------------------------- /src/main/java/fontEditor/ChangeEventListener.java: -------------------------------------------------------------------------------- 1 | package fontEditor; 2 | 3 | abstract class ChangeEventListener { 4 | public enum ChangeEventMouseSide { 5 | LEFT, 6 | RIGHT 7 | } 8 | 9 | public abstract void onChange(int color, ChangeEventMouseSide side); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/fontEditor/FontEditorColorSelector.java: -------------------------------------------------------------------------------- 1 | package fontEditor; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.event.MouseEvent; 6 | import java.awt.event.MouseListener; 7 | import java.util.ArrayList; 8 | 9 | import javax.swing.JPanel; 10 | import javax.swing.SwingUtilities; 11 | 12 | import fontEditor.ChangeEventListener.ChangeEventMouseSide; 13 | 14 | class FontEditorColorSelector { 15 | 16 | private final JPanel foregroundColorIndicator; 17 | private final JPanel backgroundColorIndicator; 18 | 19 | private final ArrayList listeners; 20 | 21 | private static class FontEditorColorListener implements MouseListener { 22 | final FontEditorColorSelector selector; 23 | final int color; 24 | 25 | FontEditorColorListener(FontEditorColorSelector selector, int color) { 26 | this.selector = selector; 27 | this.color = color; 28 | } 29 | 30 | @Override 31 | public void mouseClicked(MouseEvent e) { 32 | } 33 | 34 | @Override 35 | public void mouseEntered(MouseEvent e) { 36 | } 37 | 38 | @Override 39 | public void mouseExited(MouseEvent e) { 40 | } 41 | 42 | @Override 43 | public void mousePressed(MouseEvent e) { 44 | if (SwingUtilities.isRightMouseButton(e)) 45 | selector.sendEvent(color, ChangeEventMouseSide.RIGHT); 46 | if (SwingUtilities.isLeftMouseButton(e)) 47 | selector.sendEvent(color, ChangeEventMouseSide.LEFT); 48 | 49 | } 50 | 51 | @Override 52 | public void mouseReleased(MouseEvent e) { 53 | } 54 | } 55 | 56 | public FontEditorColorSelector(JPanel buttonPanel) { 57 | JPanel darkButton = new JPanel(); 58 | darkButton.setBackground(Color.BLACK); 59 | darkButton.setForeground(Color.BLACK); 60 | darkButton.addMouseListener(new FontEditorColorListener(this, 3)); 61 | 62 | JPanel mediumButton = new JPanel(); 63 | mediumButton.setBackground(Color.LIGHT_GRAY); 64 | mediumButton.addMouseListener(new FontEditorColorListener(this, 2)); 65 | 66 | JPanel lightButton = new JPanel(); 67 | lightButton.setBackground(Color.WHITE); 68 | lightButton.addMouseListener(new FontEditorColorListener(this, 1)); 69 | 70 | listeners = new ArrayList<>(); 71 | 72 | JPanel indicatorContainer = new JPanel(); 73 | indicatorContainer.setLayout(null); 74 | 75 | foregroundColorIndicator = new JPanel(); 76 | foregroundColorIndicator.setBackground(Color.WHITE); 77 | foregroundColorIndicator.setForeground(Color.WHITE); 78 | foregroundColorIndicator.setPreferredSize(new Dimension(24, 24)); 79 | foregroundColorIndicator.setBounds(8, 8, 24, 24); 80 | indicatorContainer.add(foregroundColorIndicator); 81 | 82 | backgroundColorIndicator = new JPanel(); 83 | backgroundColorIndicator.setBackground(Color.BLACK); 84 | backgroundColorIndicator.setForeground(Color.BLACK); 85 | backgroundColorIndicator.setPreferredSize(new Dimension(24, 24)); 86 | backgroundColorIndicator.setBounds(0, 0, 24, 24); 87 | indicatorContainer.add(backgroundColorIndicator); 88 | 89 | 90 | buttonPanel.add(indicatorContainer); 91 | buttonPanel.add(darkButton); 92 | buttonPanel.add(mediumButton); 93 | buttonPanel.add(lightButton); 94 | 95 | buttonPanel.setPreferredSize(new Dimension(200, 32)); 96 | 97 | } 98 | 99 | private void sendEvent(int color, ChangeEventMouseSide side) { 100 | Color buttonColor = Color.RED; 101 | switch (color) { 102 | case 1: 103 | buttonColor = Color.WHITE; 104 | break; 105 | case 2: 106 | buttonColor = Color.LIGHT_GRAY; 107 | break; 108 | case 3: 109 | buttonColor = Color.BLACK; 110 | break; 111 | } 112 | 113 | for (ChangeEventListener listener : listeners) { 114 | listener.onChange(color, side); 115 | } 116 | if (side == ChangeEventMouseSide.LEFT) 117 | foregroundColorIndicator.setBackground(buttonColor); 118 | else 119 | backgroundColorIndicator.setBackground(buttonColor); 120 | } 121 | 122 | void addChangeEventListener(ChangeEventListener changeEventListener) { 123 | listeners.add(changeEventListener); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/fontEditor/FontMap.java: -------------------------------------------------------------------------------- 1 | package fontEditor; 2 | 3 | import java.awt.*; 4 | 5 | import javax.swing.JPanel; 6 | 7 | import structures.LSDJFont; 8 | 9 | public class FontMap extends JPanel implements java.awt.event.MouseListener { 10 | private static final long serialVersionUID = -7745908775698863845L; 11 | private byte[] romImage = null; 12 | private int fontOffset = -1; 13 | private int gfxCharOffset = -1; 14 | private int tileZoom = 1; 15 | private int displayTileSize = 8; 16 | private boolean showGfxCharacters = false; 17 | 18 | public interface TileSelectListener { 19 | void tileSelected(int tile); 20 | } 21 | 22 | private TileSelectListener tileSelectedListener = null; 23 | 24 | FontMap() { 25 | addMouseListener(this); 26 | } 27 | 28 | void setShowGfxCharacters(boolean show) { 29 | showGfxCharacters = show; 30 | repaint(); 31 | } 32 | 33 | void setTileSelectListener(TileSelectListener l) { 34 | tileSelectedListener = l; 35 | } 36 | 37 | int getCurrentUnscaledMapHeight () { 38 | return showGfxCharacters ? LSDJFont.GFX_FONT_MAP_HEIGHT : LSDJFont.FONT_MAP_HEIGHT; 39 | } 40 | 41 | int getCurrentTileNumber () { 42 | return showGfxCharacters ? LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT : LSDJFont.TILE_COUNT; 43 | } 44 | 45 | public void paintComponent(Graphics g) { 46 | super.paintComponent(g); 47 | int currentHeight = getCurrentUnscaledMapHeight(); 48 | 49 | int widthScale = getWidth() / LSDJFont.FONT_MAP_WIDTH; 50 | int heightScale = getHeight() / currentHeight; 51 | tileZoom = Math.min(widthScale, heightScale); 52 | tileZoom = Math.max(tileZoom, 1); 53 | int offsetX = (getWidth() - LSDJFont.FONT_MAP_WIDTH * tileZoom) / 2; 54 | int offsetY = (getHeight() - currentHeight * tileZoom) / 2; 55 | setPreferredSize(new Dimension(LSDJFont.FONT_MAP_WIDTH * tileZoom, currentHeight * tileZoom)); 56 | 57 | for (int tile = 0; tile < tileCount(); ++tile) { 58 | paintTile(g, tile, offsetX, offsetY); 59 | } 60 | } 61 | 62 | private int tileCount() { 63 | return showGfxCharacters 64 | ? LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT 65 | : LSDJFont.TILE_COUNT; 66 | } 67 | 68 | private void switchColor(Graphics g, int c) { 69 | switch (c & 3) { 70 | case 0: 71 | g.setColor(Color.white); 72 | break; 73 | case 1: 74 | g.setColor(Color.lightGray); 75 | break; 76 | case 2: 77 | g.setColor(Color.darkGray); // Not used. 78 | break; 79 | case 3: 80 | g.setColor(Color.black); 81 | break; 82 | } 83 | } 84 | 85 | private int getColor(int tile, int x, int y) { 86 | int tileOffset; 87 | if (tile < LSDJFont.TILE_COUNT) { 88 | tileOffset = fontOffset; 89 | } else { 90 | tileOffset = gfxCharOffset; 91 | tile -= LSDJFont.TILE_COUNT; 92 | } 93 | tileOffset += tile * 16 + y * 2; 94 | int xMask = 7 - x; 95 | int value = (romImage[tileOffset] >> xMask) & 1; 96 | value |= ((romImage[tileOffset + 1] >> xMask) & 1) << 1; 97 | return value; 98 | } 99 | 100 | private void paintTile(Graphics g, int tile, int offsetX, int offsetY) { 101 | displayTileSize = 8 * tileZoom; 102 | int x = (tile % 8) * displayTileSize; 103 | int y = (tile / 8) * displayTileSize; 104 | 105 | for (int row = 0; row < 8; ++row) { 106 | for (int column = 0; column < 8; ++column) { 107 | switchColor(g, getColor(tile, column, row)); 108 | g.fillRect(offsetX + x + column * tileZoom, offsetY + y + row * tileZoom, tileZoom, tileZoom); 109 | } 110 | } 111 | if (tileZoom > 1) { 112 | g.setColor(new Color(0.f,0.f,0.4f,0.6f)); 113 | g.drawRect(offsetX + x, offsetY + y, tileZoom*8, tileZoom*8); 114 | } 115 | } 116 | 117 | void setRomImage(byte[] romImage) { 118 | this.romImage = romImage; 119 | } 120 | 121 | public byte[] romImage() { 122 | return romImage; 123 | } 124 | 125 | void setGfxCharOffset(int gfxCharOffset) { 126 | this.gfxCharOffset = gfxCharOffset; 127 | } 128 | 129 | void setFontOffset(int fontOffset) { 130 | this.fontOffset = fontOffset; 131 | repaint(); 132 | } 133 | 134 | public void mouseEntered(java.awt.event.MouseEvent e) { 135 | } 136 | 137 | public void mouseExited(java.awt.event.MouseEvent e) { 138 | } 139 | 140 | public void mouseReleased(java.awt.event.MouseEvent e) { 141 | } 142 | 143 | public void mousePressed(java.awt.event.MouseEvent e) { 144 | } 145 | 146 | public void mouseClicked(java.awt.event.MouseEvent e) { 147 | int offsetX = (getWidth() - LSDJFont.FONT_MAP_WIDTH * tileZoom) / 2; 148 | int offsetY = (getHeight() - getCurrentUnscaledMapHeight() * tileZoom) / 2; 149 | 150 | int realX = e.getX() - offsetX; 151 | int realY = e.getY() - offsetY; 152 | 153 | if (realX < 0 || realY < 0 || realX > LSDJFont.FONT_MAP_WIDTH * tileZoom || realY > getCurrentUnscaledMapHeight() * tileZoom) 154 | return; 155 | 156 | int tile = (realY / displayTileSize) * LSDJFont.FONT_NUM_TILES_X + 157 | realX / displayTileSize; 158 | if (tile < 0 || tile >= getCurrentTileNumber()) 159 | return; 160 | 161 | if (tileSelectedListener != null) { 162 | tileSelectedListener.tileSelected(tile); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/fontEditor/TileEditor.java: -------------------------------------------------------------------------------- 1 | package fontEditor; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | 6 | import javax.swing.JPanel; 7 | import javax.swing.SwingUtilities; 8 | 9 | import structures.LSDJFont; 10 | 11 | class TileEditor extends JPanel implements java.awt.event.MouseListener, java.awt.event.MouseMotionListener { 12 | 13 | private static final long serialVersionUID = 4048727729255703626L; 14 | 15 | public interface TileChangedListener { 16 | void tileChanged(); 17 | } 18 | 19 | private final LSDJFont font; 20 | private int selectedTile = 0; 21 | private int color = 3; 22 | private int rightColor = 3; 23 | 24 | private int[][] clipboard = null; 25 | 26 | private TileChangedListener tileChangedListener; 27 | 28 | TileEditor() { 29 | font = new LSDJFont(); 30 | addMouseListener(this); 31 | addMouseMotionListener(this); 32 | } 33 | 34 | void setRomImage(byte[] romImage) { 35 | font.setRomImage(romImage); 36 | } 37 | 38 | void setFontOffset(int offset) { 39 | font.setDataOffset(offset); 40 | repaint(); 41 | } 42 | 43 | void setGfxDataOffset(int offset) { 44 | font.setGfxDataOffset(offset); 45 | repaint(); 46 | } 47 | 48 | void setTile(int tile) { 49 | selectedTile = tile; 50 | repaint(); 51 | } 52 | 53 | int getTile() { 54 | return selectedTile; 55 | } 56 | 57 | void shiftUp(int tile) { 58 | font.rotateTileUp(tile); 59 | tileChanged(); 60 | } 61 | 62 | void shiftDown(int tile) { 63 | font.rotateTileDown(tile); 64 | tileChanged(); 65 | } 66 | 67 | void shiftRight(int tile) { 68 | font.rotateTileRight(tile); 69 | tileChanged(); 70 | } 71 | 72 | void shiftLeft(int tile) { 73 | font.rotateTileLeft(tile); 74 | tileChanged(); 75 | } 76 | 77 | private int getColor(int tile, int x, int y) { 78 | return font.getTilePixel(tile, x, y); 79 | } 80 | 81 | private void switchColor(Graphics g, int c) { 82 | switch (c & 3) { 83 | case 0: 84 | g.setColor(Color.white); 85 | break; 86 | case 1: 87 | g.setColor(Color.lightGray); 88 | break; 89 | case 2: 90 | g.setColor(Color.darkGray); // Not used. 91 | break; 92 | case 3: 93 | g.setColor(Color.black); 94 | break; 95 | } 96 | } 97 | 98 | private int getMinimumDimension() { 99 | return getWidth() < getHeight() ? getWidth() : getHeight(); 100 | } 101 | 102 | private void paintGrid(Graphics g) { 103 | g.setColor(java.awt.Color.gray); 104 | int minimumDimension = getMinimumDimension(); 105 | int offsetX = (getWidth() - minimumDimension) / 2; 106 | int offsetY = (getHeight() - minimumDimension) / 2; 107 | int dx = minimumDimension / 8; 108 | int minimumDimensionSquare = (minimumDimension / 8) * 8; 109 | for (int x = dx + offsetX; x < minimumDimensionSquare + offsetX; x += dx) { 110 | g.drawLine(x, offsetY, x, minimumDimensionSquare + offsetY); 111 | } 112 | 113 | int dy = minimumDimension / 8; 114 | for (int y = dy + offsetY; y < minimumDimensionSquare + offsetY; y += dy) { 115 | g.drawLine(offsetX, y, offsetX + minimumDimensionSquare, y); 116 | } 117 | } 118 | 119 | public void paintComponent(Graphics g) { 120 | super.paintComponent(g); 121 | int minimumDimension = getMinimumDimension(); 122 | int offsetX = (getWidth() - minimumDimension) / 2; 123 | int offsetY = (getHeight() - minimumDimension) / 2; 124 | for (int x = 0; x < 8; ++x) { 125 | for (int y = 0; y < 8; ++y) { 126 | int color = getColor(selectedTile, x, y); 127 | switchColor(g, color); 128 | int pixelWidth = minimumDimension / 8; 129 | int pixelHeight = minimumDimension / 8; 130 | g.fillRect(offsetX + x * pixelWidth, offsetY + y * pixelHeight, pixelWidth, pixelHeight); 131 | } 132 | } 133 | 134 | paintGrid(g); 135 | } 136 | 137 | private void doMousePaint(java.awt.event.MouseEvent e) { 138 | int minimumDimension = getMinimumDimension(); 139 | int offsetX = (getWidth() - minimumDimension) / 2; 140 | int offsetY = (getHeight() - minimumDimension) / 2; 141 | 142 | int x = ((e.getX() - offsetX) * 8) / minimumDimension; 143 | int y = ((e.getY() - offsetY) * 8) / minimumDimension; 144 | if (x < 0 || x >= 8 || y < 0 || y >= 8) 145 | return; 146 | if (SwingUtilities.isLeftMouseButton(e)) 147 | setColor(x, y, color); 148 | else if (SwingUtilities.isRightMouseButton(e)) 149 | setColor(x, y, rightColor); 150 | tileChanged(); 151 | } 152 | 153 | private void setColor(int x, int y, int color) { 154 | font.setTilePixel(selectedTile, x, y, color); 155 | } 156 | 157 | public void mouseEntered(java.awt.event.MouseEvent e) { 158 | } 159 | 160 | public void mouseExited(java.awt.event.MouseEvent e) { 161 | } 162 | 163 | public void mouseReleased(java.awt.event.MouseEvent e) { 164 | } 165 | 166 | public void mousePressed(java.awt.event.MouseEvent e) { 167 | } 168 | 169 | public void mouseClicked(java.awt.event.MouseEvent e) { 170 | doMousePaint(e); 171 | } 172 | 173 | public void mouseMoved(java.awt.event.MouseEvent e) { 174 | } 175 | 176 | public void mouseDragged(java.awt.event.MouseEvent e) { 177 | doMousePaint(e); 178 | } 179 | 180 | void setColor(int color) { 181 | assert color >= 1 && color <= 3; 182 | this.color = color; 183 | } 184 | 185 | void setRightColor(int color) { 186 | assert color >= 1 && color <= 3; 187 | this.rightColor = color; 188 | } 189 | 190 | void setTileChangedListener(TileChangedListener l) { 191 | tileChangedListener = l; 192 | } 193 | 194 | void copyTile() { 195 | if (clipboard == null) { 196 | clipboard = new int[8][8]; 197 | } 198 | for (int x = 0; x < 8; ++x) { 199 | for (int y = 0; y < 8; ++y) { 200 | clipboard[x][y] = getColor(selectedTile, x, y); 201 | } 202 | } 203 | } 204 | 205 | void generateShadedAndInvertedTiles() { 206 | font.generateShadedAndInvertedTiles(); 207 | } 208 | 209 | void pasteTile() { 210 | if (clipboard == null) { 211 | return; 212 | } 213 | for (int x = 0; x < 8; ++x) { 214 | for (int y = 0; y < 8; ++y) { 215 | int c = clipboard[x][y]; 216 | if (c < 3) { 217 | ++c; // Adjusts from Game Boy Color to editor color. 218 | } 219 | setColor(x, y, c); 220 | } 221 | } 222 | tileChanged(); 223 | } 224 | 225 | void tileChanged() { 226 | repaint(); 227 | generateShadedAndInvertedTiles(); 228 | tileChangedListener.tileChanged(); 229 | } 230 | 231 | void readImage(String name, BufferedImage image) { 232 | font.loadImageData(name, image); 233 | 234 | } 235 | 236 | BufferedImage createImage(boolean includeGfxCharacters) { 237 | return font.saveDataToImage(includeGfxCharacters); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/Sample.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | import java.io.*; 4 | import java.util.Random; 5 | import javax.sound.sampled.*; 6 | 7 | class Sample { 8 | private File file; 9 | private String name; 10 | private short[] originalSamples; 11 | private short[] processedSamples; 12 | private int untrimmedLengthInSamples = -1; 13 | private int readPos; 14 | private int volumeDb = 0; 15 | private int pitchSemitones = 0; 16 | private int trim = 0; 17 | private boolean dither = true; 18 | 19 | public Sample(short[] iBuf, String iName) { 20 | if (iBuf != null) { 21 | for (int j : iBuf) { 22 | assert (j >= Short.MIN_VALUE); 23 | assert (j <= Short.MAX_VALUE); 24 | } 25 | processedSamples = iBuf; 26 | } 27 | name = iName; 28 | } 29 | 30 | public Sample(Sample s) { 31 | file = s.file; 32 | name = s.name; 33 | originalSamples = s.originalSamples; 34 | processedSamples = s.processedSamples; 35 | untrimmedLengthInSamples = s.untrimmedLengthInSamples; 36 | readPos = s.readPos; 37 | volumeDb = s.volumeDb; 38 | pitchSemitones = s.pitchSemitones; 39 | trim = s.trim; 40 | dither = s.dither; 41 | } 42 | 43 | public String getName() { 44 | return name; 45 | } 46 | 47 | public void setName(String sampleName) { 48 | name = sampleName.toUpperCase().substring(0,3); 49 | } 50 | 51 | public int lengthInSamples() { 52 | return processedSamples.length; 53 | } 54 | 55 | public int untrimmedLengthInSamples() { 56 | return untrimmedLengthInSamples == -1 ? lengthInSamples() : untrimmedLengthInSamples; 57 | } 58 | 59 | public int untrimmedLengthInBytes() { 60 | int l = untrimmedLengthInSamples() / 2; 61 | l -= l % 0x10; 62 | return l; 63 | } 64 | 65 | public short[] workSampleData() { 66 | return (originalSamples != null ? originalSamples : processedSamples).clone(); 67 | } 68 | 69 | public int lengthInBytes() { 70 | int l = lengthInSamples() / 2; 71 | l -= l % 0x10; 72 | return l; 73 | } 74 | 75 | public void seekStart() { 76 | readPos = 0; 77 | } 78 | 79 | public short read() { 80 | return processedSamples[readPos++]; 81 | } 82 | 83 | public boolean canAdjustVolume() { 84 | return originalSamples != null; 85 | } 86 | 87 | 88 | 89 | // ------------------ 90 | 91 | static Sample createFromNibbles(byte[] nibbles, String name) { 92 | short[] buf = new short[nibbles.length * 2]; 93 | 94 | for (int i = 0; i < nibbles.length; ++i) { 95 | int n = nibbles[i]; 96 | buf[i * 2] = (short)(n & 0xf0); 97 | buf[i * 2 + 1] = (short)((n & 0xf) << 4); 98 | } 99 | for (int bufIt = 0; bufIt < buf.length; ++bufIt) { 100 | short s = (byte)(buf[bufIt] - 0x80); 101 | s *= 256; 102 | buf[bufIt] = s; 103 | } 104 | return new Sample(buf, name); 105 | } 106 | 107 | // ------------------ 108 | 109 | public static Sample createFromWav(File file, 110 | boolean dither, 111 | boolean halfSpeed, 112 | int volumeDb, 113 | int trim, 114 | int pitch) 115 | throws IOException, UnsupportedAudioFileException { 116 | Sample s = new Sample(null, file.getName().split("\\.")[0]); 117 | s.file = file; 118 | s.dither = dither; 119 | s.volumeDb = volumeDb; 120 | s.trim = trim; 121 | s.pitchSemitones = pitch; 122 | s.reload(halfSpeed); 123 | return s; 124 | } 125 | 126 | public static Sample dupeSample(Sample sample) { 127 | return new Sample(sample); 128 | } 129 | 130 | public void reload(boolean halfSpeed) throws IOException, UnsupportedAudioFileException { 131 | if (file == null) { 132 | return; 133 | } 134 | double outFactor = Math.pow(2.0, -pitchSemitones / 12.0); 135 | originalSamples = readSamples(file, halfSpeed, outFactor); 136 | processSamples(); 137 | } 138 | 139 | public void processSamples() { 140 | int[] intBuffer = toIntBuffer(originalSamples); 141 | normalize(intBuffer); 142 | intBuffer = trim(intBuffer); 143 | if (dither) { 144 | dither(intBuffer); 145 | } 146 | processedSamples = toShortBuffer(intBuffer); 147 | } 148 | 149 | private int[] trim(int[] intBuffer) { 150 | int headPos = headPos(intBuffer); 151 | int tailPos = tailPos(intBuffer); 152 | if (headPos > tailPos) { 153 | return new int[0]; 154 | } 155 | untrimmedLengthInSamples = tailPos + 1 - headPos; 156 | tailPos = Math.max(headPos, tailPos - trim * 32); 157 | int[] newBuffer = new int[tailPos + 1 - headPos]; 158 | System.arraycopy(intBuffer, headPos, newBuffer, 0, newBuffer.length); 159 | 160 | if (newBuffer.length >= 32) { 161 | return newBuffer; 162 | } 163 | 164 | // Extends to 32 samples. 165 | int[] zeroPadded = new int[32]; 166 | System.arraycopy(newBuffer, 0, zeroPadded, 0, newBuffer.length); 167 | return zeroPadded; 168 | } 169 | 170 | final int SILENCE_THRESHOLD = Short.MAX_VALUE / 16; 171 | 172 | private int headPos(int[] buf) { 173 | int i; 174 | for (i = 0; i < buf.length; ++i) { 175 | if (Math.abs(buf[i]) >= SILENCE_THRESHOLD) { 176 | break; 177 | } 178 | } 179 | return i; 180 | } 181 | 182 | private int tailPos(int[] buf) { 183 | int i; 184 | for (i = buf.length - 1; i >= 0; --i) { 185 | if (Math.abs(buf[i]) >= SILENCE_THRESHOLD) { 186 | break; 187 | } 188 | } 189 | return i; 190 | } 191 | 192 | private short[] toShortBuffer(int[] intBuffer) { 193 | short[] shortBuffer = new short[intBuffer.length]; 194 | for (int i = 0; i < intBuffer.length; ++i) { 195 | int s = intBuffer[i]; 196 | s = Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, s)); 197 | shortBuffer[i] = (short)s; 198 | } 199 | return shortBuffer; 200 | } 201 | 202 | private int[] toIntBuffer(short[] shortBuffer) { 203 | int[] intBuffer = new int[shortBuffer.length]; 204 | for (int i = 0; i < shortBuffer.length; ++i) { 205 | intBuffer[i] = shortBuffer[i]; 206 | } 207 | return intBuffer; 208 | } 209 | 210 | private static short[] readSamples(File file, boolean halfSpeed, double outRateFactor) throws UnsupportedAudioFileException, IOException { 211 | AudioInputStream ais = AudioSystem.getAudioInputStream(file); 212 | float inSampleRate = ais.getFormat().getSampleRate(); 213 | AudioFormat outFormat = new AudioFormat(inSampleRate, 16, 1, true, false); 214 | AudioInputStream convertedAis = AudioSystem.getAudioInputStream(outFormat, ais); 215 | byte[] b = new byte[convertedAis.available()]; 216 | int read = convertedAis.read(b); 217 | assert read == b.length; 218 | short[] samples = new short[b.length / 2]; 219 | for (int i = 0; i < samples.length; ++i) { 220 | samples[i] = (short) ((b[i * 2 + 1] * 256) + ((short)b[i * 2] & 0xff)); 221 | } 222 | convertedAis.close(); 223 | ais.close(); 224 | 225 | double outSampleRate = halfSpeed ? 5734 : 11468; 226 | outSampleRate *= outRateFactor; 227 | return Sound.resample(inSampleRate, outSampleRate, samples); 228 | } 229 | 230 | // Adds triangular probability density function dither noise. 231 | private void dither(int[] samples) { 232 | Random random = new Random(); 233 | float state = random.nextFloat(); 234 | for (int i = 0; i < samples.length; ++i) { 235 | int value = samples[i]; 236 | float r = state; 237 | state = random.nextFloat(); 238 | int noiseLevel = 256 * 16; 239 | value += (r - state) * noiseLevel; 240 | samples[i] = value; 241 | } 242 | } 243 | 244 | private void normalize(int[] samples) { 245 | double peak = Double.MIN_VALUE; 246 | for (int sample : samples) { 247 | double s = sample; 248 | s = s < 0 ? s / Short.MIN_VALUE : s / Short.MAX_VALUE; 249 | peak = Math.max(s, peak); 250 | } 251 | if (peak == 0) { 252 | return; 253 | } 254 | double volumeAdjust = Math.pow(10, volumeDb / 20.0); 255 | for (int i = 0; i < samples.length; ++i) { 256 | samples[i] = (int)((samples[i] * volumeAdjust) / peak); 257 | } 258 | } 259 | 260 | public int getVolumeDb() { 261 | return volumeDb; 262 | } 263 | 264 | public void setVolumeDb(int value) { 265 | volumeDb = value; 266 | } 267 | 268 | public File getFile() { 269 | return file; 270 | } 271 | 272 | public void setTrim(int value) { 273 | assert value >= 0; 274 | trim = value; 275 | } 276 | 277 | public int getTrim() { 278 | return trim; 279 | } 280 | 281 | public void setPitchSemitones(int value) { 282 | pitchSemitones = value; 283 | } 284 | 285 | public int getPitchSemitones() { 286 | return pitchSemitones; 287 | } 288 | 289 | public void setDither(boolean value) { 290 | dither = value; 291 | } 292 | 293 | public boolean getDither() { 294 | return dither; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/SamplePicker.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | import net.miginfocom.swing.MigLayout; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.*; 8 | import java.util.ArrayList; 9 | import java.util.TreeSet; 10 | 11 | import static java.awt.event.InputEvent.SHIFT_DOWN_MASK; 12 | 13 | public class SamplePicker extends JPanel { 14 | private Listener listener; 15 | 16 | interface Listener { 17 | void selectionChanged(); 18 | void playSample(); 19 | void deleteSample(); 20 | void dupeSample(); 21 | void replaceSample(); 22 | void renameSample(String s); 23 | void copySample(); 24 | } 25 | 26 | class Pad extends JToggleButton { 27 | int id; 28 | Pad(int id) { 29 | this.id = id; 30 | setPreferredSize(new Dimension(64, 64)); 31 | addMouseListener(new MouseAdapter() { 32 | @Override 33 | public void mousePressed(MouseEvent e) { 34 | super.mouseClicked(e); 35 | showPopup(e); 36 | } 37 | 38 | @Override 39 | public void mouseReleased(MouseEvent e) { 40 | super.mouseClicked(e); 41 | showPopup(e); 42 | } 43 | 44 | private void showPopup(MouseEvent e) { 45 | if (!e.isPopupTrigger() || getText().equals("---")) { 46 | return; 47 | } 48 | JPopupMenu menu = new JPopupMenu(); 49 | JMenuItem copy = new JMenuItem("Copy"); 50 | menu.add(copy); 51 | copy.addActionListener(e1 -> listener.copySample()); 52 | JMenuItem rename = new JMenuItem("Rename..."); 53 | menu.add(rename); 54 | rename.addActionListener(e1 -> { 55 | String name = JOptionPane.showInputDialog("Enter new sample name"); 56 | if (name != null) { 57 | listener.renameSample(name); 58 | } 59 | }); 60 | JMenuItem replace = new JMenuItem("Replace..."); 61 | menu.add(replace); 62 | replace.addActionListener(e1 -> listener.replaceSample()); 63 | JMenuItem duplicate = new JMenuItem("Duplicate"); 64 | menu.add(duplicate); 65 | duplicate.addActionListener(e1 -> listener.dupeSample()); 66 | JMenuItem delete = new JMenuItem("Delete"); 67 | menu.add(delete); 68 | delete.addActionListener(e1 -> listener.deleteSample()); 69 | menu.show(e.getComponent(), e.getX(), e.getY()); 70 | } 71 | }); 72 | } 73 | } 74 | 75 | private final ArrayList pads; 76 | private final TreeSet selectedIndices = new TreeSet<>(); 77 | 78 | SamplePicker() { 79 | setLayout(new MigLayout()); 80 | pads = new ArrayList<>(); 81 | for (int i = 0; i < 15; i++) { 82 | Pad pad = createPad(); 83 | pad.addMouseListener(new MouseAdapter() { 84 | @Override 85 | public void mousePressed(MouseEvent e) { 86 | super.mousePressed(e); 87 | if ((e.getModifiersEx() & SHIFT_DOWN_MASK) == 0) { 88 | selectedIndices.clear(); 89 | for (int i = 0; i < 15; ++i) { 90 | JToggleButton button = pads.get(i); 91 | button.setSelected(false); 92 | } 93 | } 94 | 95 | Pad sender = (Pad)e.getSource(); 96 | int min = Math.min(sender.id, selectedIndices.isEmpty() ? Integer.MAX_VALUE : selectedIndices.first()); 97 | int max = Math.max(sender.id, selectedIndices.isEmpty() ? Integer.MIN_VALUE : selectedIndices.last()); 98 | for (int i = 0; i < 15; ++i) { 99 | if (i >= min && i <= max) { 100 | selectedIndices.add(i); 101 | pads.get(i).setSelected(true); 102 | } 103 | } 104 | listener.selectionChanged(); 105 | if (e.getButton() == MouseEvent.BUTTON1) { 106 | listener.playSample(); 107 | } 108 | } 109 | }); 110 | } 111 | } 112 | 113 | @Override 114 | public void grabFocus() { 115 | pads.get(0).grabFocus(); 116 | } 117 | 118 | private Pad createPad() { 119 | Pad pad = new Pad(pads.size()); 120 | pad.setToolTipText("Play by keys: 1234 QWER ASDF ZXC"); 121 | pads.add(pad); 122 | add(pad, (pads.size() % 4) == 0 ? "wrap, sg button" : ""); 123 | return pad; 124 | } 125 | 126 | public void setListData(String[] listData) { 127 | assert listData.length == pads.size(); 128 | for (int i = 0; i < pads.size(); ++i) { 129 | pads.get(i).setText(listData[i]); 130 | } 131 | } 132 | 133 | public void setSelectedIndex(int selectedIndex) { 134 | selectedIndices.clear(); 135 | for (int i = 0; i < 15; ++i) { 136 | JToggleButton button = pads.get(i); 137 | button.setSelected(false); 138 | } 139 | if (selectedIndex == -1) { 140 | return; 141 | } 142 | selectedIndices.add(selectedIndex); 143 | pads.get(selectedIndex).setSelected(true); 144 | listener.selectionChanged(); 145 | } 146 | 147 | public int getSelectedIndex() { 148 | for (Pad pad : pads) { 149 | if (pad.isSelected()) { 150 | return pad.id; 151 | } 152 | } 153 | return selectedIndices.isEmpty() ? -1 : selectedIndices.first(); 154 | } 155 | 156 | public ArrayList getSelectedIndices() { 157 | return new ArrayList<>(selectedIndices); 158 | } 159 | 160 | public void addListSelectionListener(Listener listener) { 161 | this.listener = listener; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/SampleView.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | import java.awt.*; 4 | import java.awt.geom.GeneralPath; 5 | import java.util.Locale; 6 | 7 | public class SampleView extends Canvas { 8 | private byte[] buf; 9 | private float duration; 10 | 11 | public void setBufferContent(byte[] newBuffer, float duration) { 12 | buf = newBuffer; 13 | setBackground(Color.black); 14 | this.duration = duration; 15 | } 16 | 17 | @Override 18 | public void paint(Graphics gg) { 19 | Graphics2D g = (Graphics2D) gg; 20 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 21 | RenderingHints.VALUE_ANTIALIAS_ON); 22 | 23 | double w = g.getClipBounds().getWidth(); 24 | double h = g.getClipBounds().getHeight(); 25 | 26 | if (buf == null) { 27 | return; 28 | } 29 | 30 | GeneralPath gp = new GeneralPath(); 31 | gp.moveTo(0, h / 2); 32 | for (int it = 0; it < buf.length; ++it) { 33 | // Only draws every second sample. This is probably OK. 34 | double val = buf[it] & 0xf; 35 | val -= 7.5; 36 | val /= 7.5; 37 | gp.lineTo(it * w / (buf.length - 1), h * (1 - val) / 2); 38 | } 39 | g.setColor(Color.YELLOW); 40 | g.draw(gp); 41 | 42 | drawDuration(g, (int) w, (int) h); 43 | } 44 | 45 | private void drawDuration(Graphics2D g, int w, int h) { 46 | String durationText = String.format(Locale.US, "%.3fs", duration); 47 | int x = -g.getFontMetrics().stringWidth(durationText) - 1; 48 | int y = -2; 49 | g.setColor(Color.BLACK); 50 | g.drawString(durationText, w + x, h + y); 51 | --x; 52 | --y; 53 | g.setColor(Color.WHITE); 54 | g.drawString(durationText, w + x, h + y); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/Sound.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2001, Johan Kotlinski 2 | 3 | package kitEditor; 4 | 5 | import com.laszlosystems.libresample4j.Resampler; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import javax.sound.sampled.*; 11 | 12 | public class Sound { 13 | 14 | private static final ArrayList clipPool = new ArrayList<>(); 15 | private static final int PLAYBACK_RATE = 48000; 16 | 17 | private static short[] unpackNibbles(byte[] gbSample) { 18 | byte[] waveData = new byte[gbSample.length * 2]; 19 | int src = 0; 20 | int dst = 0; 21 | 22 | while (src < gbSample.length) { 23 | byte sample = gbSample[src++]; 24 | waveData[dst++] = (byte) (0xf0 & sample); 25 | waveData[dst++] = (byte) ((0x0f & sample) << 4); 26 | } 27 | 28 | short[] s = new short[waveData.length]; 29 | for (int i = 0; i < s.length; ++i) { 30 | int v = waveData[i] & 0xf0; 31 | v -= 0x78; 32 | v *= Short.MAX_VALUE; 33 | v /= 0x78; 34 | s[i] = (short)v; 35 | } 36 | return s; 37 | } 38 | 39 | private static Clip getClip() throws LineUnavailableException { 40 | for (Clip clip : clipPool) { 41 | if (!clip.isRunning()) { 42 | clip.close(); 43 | return clip; 44 | } 45 | } 46 | Clip newClip = AudioSystem.getClip(); 47 | clipPool.add(newClip); 48 | return newClip; 49 | } 50 | 51 | static void play(byte[] gbSample, boolean halfSpeed) throws LineUnavailableException, IOException { 52 | final int sampleRate = halfSpeed ? 5734 : 11468; 53 | byte[] b = toByteArray(resampleForPlayback(sampleRate, unpackNibbles(gbSample))); 54 | Clip clip = getClip(); 55 | clip.open(new AudioInputStream(new ByteArrayInputStream(b), 56 | new AudioFormat(PLAYBACK_RATE, 16, 1, true, false), 57 | b.length / 2)); 58 | clip.start(); 59 | } 60 | 61 | private static short[] resampleForPlayback(int srcRate, short[] src) { 62 | short[] dst = new short[Sound.PLAYBACK_RATE * src.length / srcRate]; 63 | // Nearest neighbor resampling is good for emulating Game Boy sound. 64 | for (int i = 0; i < dst.length; ++i) { 65 | dst[i] = src[i * srcRate / Sound.PLAYBACK_RATE]; 66 | } 67 | return dst; 68 | } 69 | 70 | private static byte[] toByteArray(short[] waveData) { 71 | byte[] b = new byte[waveData.length * 2]; 72 | for (int i = 0; i < waveData.length; ++i) { 73 | b[i * 2] = (byte)(waveData[i] & 0xff); 74 | b[i * 2 + 1] = (byte)(waveData[i] >> 8); 75 | } 76 | return b; 77 | } 78 | 79 | static void stopAll() { 80 | for (Clip clip : clipPool) { 81 | clip.stop(); 82 | } 83 | } 84 | 85 | public static short[] resample(double inSampleRate, double outSampleRate, short[] samples) { 86 | if (inSampleRate == outSampleRate) { 87 | return samples; 88 | } 89 | float[] inBuf = new float[samples.length]; 90 | float dcOffset = 0; 91 | for (int i = 0; i < inBuf.length; ++i) { 92 | inBuf[i] = (float) samples[i] / -Short.MIN_VALUE; 93 | dcOffset += inBuf[i] / inBuf.length; 94 | } 95 | 96 | // Removes DC offset. 97 | for (int i = 0; i < inBuf.length; ++i) { 98 | inBuf[i] -= dcOffset; 99 | } 100 | 101 | double factor = outSampleRate / inSampleRate; 102 | float[] outBuf = new float[(int)(inBuf.length * factor + 1)]; 103 | Resampler resampler = new Resampler(true, factor, factor); 104 | Resampler.Result result = resampler.process(factor, inBuf, 0, inBuf.length, true, outBuf, 0, outBuf.length); 105 | 106 | // avoid clipping 107 | float peak = 0; 108 | for (float v : outBuf) { 109 | peak = Math.max(peak, Math.abs(v)); 110 | } 111 | if (peak > 1) { 112 | for (int i = 0; i < outBuf.length; ++i) { 113 | outBuf[i] /= peak; 114 | } 115 | } 116 | 117 | short[] finalBuf = new short[result.outputSamplesGenerated]; 118 | for (int i = 0; i < finalBuf.length; ++i) { 119 | finalBuf[i] = (short)(outBuf[i] * Short.MAX_VALUE); 120 | } 121 | return finalBuf; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/WaveFile.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | 7 | public class WaveFile { 8 | public static void write(short[] pcm, File f) throws IOException { 9 | RandomAccessFile wavFile = new RandomAccessFile(f, "rw"); 10 | 11 | int payloadSize = pcm.length * 2; 12 | int fileSize = pcm.length * 2 + 0x2c; 13 | int waveSize = fileSize - 8; 14 | 15 | byte[] header = { 16 | 0x52, 0x49, 0x46, 0x46, // RIFF 17 | (byte) waveSize, 18 | (byte) (waveSize >> 8), 19 | (byte) (waveSize >> 16), 20 | (byte) (waveSize >> 24), 21 | 0x57, 0x41, 0x56, 0x45, // WAVE 22 | // --- fmt chunk 23 | 0x66, 0x6D, 0x74, 0x20, // fmt 24 | 16, 0, 0, 0, // fmt size 25 | 1, 0, // pcm 26 | 1, 0, // channel count 27 | (byte) 0xcc, 0x2c, 0, 0, // freq (11468 hz) 28 | (byte) 0xcc, 0x2c, 0, 0, // avg. bytes/sec 29 | 1, 0, // block align 30 | 16, 0, // bits per sample 31 | // --- data chunk 32 | 0x64, 0x61, 0x74, 0x61, // data 33 | (byte) payloadSize, 34 | (byte) (payloadSize >> 8), 35 | (byte) (payloadSize >> 16), 36 | (byte) (payloadSize >> 24) 37 | }; 38 | 39 | wavFile.write(header); 40 | 41 | byte[] byteBuffer = new byte[pcm.length * 2]; 42 | int dst = 0; 43 | for (short sample : pcm) { 44 | byteBuffer[dst++] = (byte) sample; 45 | byteBuffer[dst++] = (byte) (sample >> 8); 46 | } 47 | wavFile.write(byteBuffer); 48 | wavFile.close(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/kitEditor/sbc.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | // Sample bank creator. 4 | 5 | class sbc { 6 | 7 | public static void compile(byte[] dst, Sample[] samples, int[] byteLength, boolean gameBoyAdvancePolarity) { 8 | int offset = 0x60; //don't overwrite sample bank info! 9 | for (int sampleIt = 0; sampleIt < samples.length; sampleIt++) { 10 | Sample sample = samples[sampleIt]; 11 | if (sample == null) { 12 | break; 13 | } 14 | 15 | sample.seekStart(); 16 | int sampleLength = sample.lengthInSamples(); 17 | 18 | int addedBytes = 0; 19 | int[] outputBuffer = new int[32]; 20 | int outputCounter = 0; 21 | for (int i = 0; i < sampleLength; i++) { 22 | int s = sample.read(); 23 | s = (int)(Math.round((double)s / (256 * 16) + 7.5)); 24 | s = Math.min(0xf, Math.max(0, s)); 25 | if (!gameBoyAdvancePolarity) { 26 | // Use DMG polarity, where 0xf = -1.0 and 0 = 1.0. 27 | s = 0xf - s; 28 | } 29 | 30 | // Starting from LSDj 9.2.0, first sample is skipped to compensate for wave refresh bug. 31 | // This rotates the wave frame rightwards. 32 | outputBuffer[(outputCounter + 1) % 32] = s; 33 | 34 | if (outputCounter == 31) { 35 | for (int j = 0; j != 32; j += 2) { 36 | dst[offset++] = (byte) (outputBuffer[j] * 0x10 + outputBuffer[j + 1]); 37 | } 38 | outputCounter = -1; 39 | addedBytes += 0x10; 40 | } 41 | outputCounter++; 42 | } 43 | 44 | byteLength[sampleIt] = addedBytes; 45 | } 46 | while (offset < 0x4000) { 47 | dst[offset++] = -1; // rst opcode 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/lsdpatch/LSDPatcher.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2001, Johan Kotlinski 2 | 3 | package lsdpatch; 4 | 5 | import utils.CommandLineFunctions; 6 | import utils.GlobalHolder; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.font.TextAttribute; 11 | import java.util.HashMap; 12 | import java.util.prefs.Preferences; 13 | 14 | public class LSDPatcher { 15 | private static void initUi() { 16 | JFrame frame = new MainWindow(); 17 | // Validate frames that have preset sizes 18 | // Pack frames that have useful preferred size info, e.g. from their layout 19 | frame.pack(); 20 | frame.validate(); 21 | 22 | // Center the window 23 | Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 24 | Dimension frameSize = frame.getSize(); 25 | if (frameSize.height > screenSize.height) { 26 | frameSize.height = screenSize.height; 27 | } 28 | if (frameSize.width > screenSize.width) { 29 | frameSize.width = screenSize.width; 30 | } 31 | frame.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2); 32 | frame.setVisible(true); 33 | } 34 | 35 | private static void usage() { 36 | System.out.printf("LSDJPatcher v%s\n\n", NewVersionChecker.getCurrentVersion()); 37 | System.out.println("java -jar LSDJPatcher.jar"); 38 | System.out.println(" Opens the GUI.\n"); 39 | 40 | System.out.println("java -jar LSDJPatcher.jar fnt2png [--extended] "); 41 | System.out.println(" Exports the font file into a PNG\n"); 42 | 43 | System.out.println("java -jar LSDJPatcher.jar png2fnt "); 44 | System.out.println(" Converts the PNG into a font with given name.\n"); 45 | 46 | System.out.println("java -jar LSDJPatcher.jar romfnt2png [--extended] "); 47 | System.out.println(" Extracts the nth font from the given rom into a png named like the font.\n"); 48 | 49 | System.out.println("java -jar LSDJPatcher.jar png2romfnt "); 50 | System.out.println(" Imports the PNG into the rom with given name.\n"); 51 | 52 | System.out.println("java -jar LSDJPatcher.jar clone "); 53 | System.out.println(" Clones all customizations from a ROM file to another.\n"); 54 | 55 | } 56 | 57 | public static void main(String[] args) { 58 | if (args.length >= 1) { 59 | processArguments(args); 60 | return; 61 | } 62 | try { 63 | // Use the system's UI look when applicable 64 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 65 | System.setProperty("apple.laf.useScreenMenuBar", "true"); 66 | 67 | // Use font anti-aliasing when applicable 68 | System.setProperty("awt.useSystemAAFontSettings","on"); 69 | System.setProperty("swing.aatext", "true"); 70 | useJLabelFontForMenus(); 71 | 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } 75 | Preferences preferences = Preferences.userRoot().node(LSDPatcher.class.getName()); 76 | GlobalHolder.set(preferences, Preferences.class); 77 | 78 | initUi(); 79 | } 80 | 81 | private static void useJLabelFontForMenus() { 82 | // On some systems, the default font given to menus is a bit wonky with anti-aliasing. Using the one given 83 | // to JLabels will give a better result. 84 | Font systemFont = new JLabel().getFont(); 85 | HashMap attributes = new HashMap<>(systemFont.getAttributes()); 86 | attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_SEMIBOLD); 87 | attributes.put(TextAttribute.SIZE, systemFont.getSize()); 88 | Font selectedFont = Font.getFont(attributes); 89 | UIManager.put("Menu.font", selectedFont); 90 | UIManager.put("MenuBar.font", selectedFont); 91 | UIManager.put("MenuItem.font", selectedFont); 92 | } 93 | 94 | private static void processArguments(String[] args) { 95 | String command = args[0].toLowerCase(); 96 | 97 | boolean includeGfxCharacters = false; 98 | if(args.length > 2 && args[1].equalsIgnoreCase("--extended")) { 99 | includeGfxCharacters = true; 100 | } 101 | 102 | if (command.compareTo("fnt2png") == 0 && args.length == 3) { 103 | CommandLineFunctions.fontToPng(args[1], args[2]); 104 | } else if (command.compareTo("fnt2png") == 0 && args.length == 4 && includeGfxCharacters) { 105 | CommandLineFunctions.fontToPng(args[2], args[3]); 106 | } else if (command.compareTo("png2fnt") == 0 && args.length == 4) { 107 | CommandLineFunctions.pngToFont(args[1], args[2], args[3]); 108 | } else if (command.compareTo("romfnt2png") == 0 && args.length == 3) { 109 | // -1 to allow 1-3 range instead of 0-2 110 | CommandLineFunctions.extractFontToPng(args[1], Integer.parseInt(args[2]) - 1, false); 111 | } else if (command.compareTo("romfnt2png") == 0 && args.length == 4 && includeGfxCharacters) { 112 | // -1 to allow 1-3 range instead of 0-2 113 | CommandLineFunctions.extractFontToPng(args[2], Integer.parseInt(args[3]) - 1, true); 114 | } else if (command.compareTo("png2romfnt") == 0 && args.length == 5) { 115 | // -1 to allow 1-3 range instead of 0-2 116 | CommandLineFunctions.loadPngToRom(args[1], args[2], Integer.parseInt(args[3]) - 1, args[4]); 117 | } else if (command.compareTo("clone") == 0 && args.length == 3) { 118 | // -1 to allow 1-3 range instead of 0-2 119 | CommandLineFunctions.copyAllCustomizations(args[1], args[2]); 120 | } else { 121 | usage(); 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/lsdpatch/MainWindow.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020, Johan Kotlinski 2 | 3 | package lsdpatch; 4 | 5 | import Document.*; 6 | import fontEditor.FontEditor; 7 | import kitEditor.KitEditor; 8 | import net.miginfocom.swing.MigLayout; 9 | import paletteEditor.PaletteEditor; 10 | import songManager.SongManager; 11 | import utils.EditorPreferences; 12 | import utils.FileDialogLauncher; 13 | import utils.RomUtilities; 14 | 15 | import javax.swing.*; 16 | import java.awt.*; 17 | import java.awt.event.WindowAdapter; 18 | import java.awt.event.WindowEvent; 19 | import java.io.File; 20 | import java.io.FileOutputStream; 21 | import java.io.IOException; 22 | 23 | public class MainWindow extends JFrame implements IDocumentListener, KitEditor.Listener { 24 | JTextField romTextField = new JTextField(); 25 | JTextField savTextField = new JTextField(); 26 | 27 | JButton upgradeRomButton = new JButton("Upgrade ROM"); 28 | JButton songManagerButton = new JButton("Songs"); 29 | JButton editKitsButton = new JButton("Sample Kits"); 30 | JButton editFontsButton = new JButton("Fonts"); 31 | JButton editPalettesButton = new JButton("Palettes"); 32 | JButton saveButton = new JButton("Save..."); 33 | 34 | MainWindow() { 35 | document.subscribe(this); 36 | 37 | updateTitle(); 38 | JPanel panel = new JPanel(); 39 | getContentPane().add(panel); 40 | MigLayout rootLayout = new MigLayout("wrap 6"); 41 | panel.setLayout(rootLayout); 42 | 43 | addSelectors(panel); 44 | 45 | panel.add(new JSeparator(), "span 5"); 46 | 47 | upgradeRomButton.addActionListener(e -> openRomUpgradeTool()); 48 | panel.add(upgradeRomButton); 49 | 50 | songManagerButton.addActionListener(e -> openSongManager()); 51 | panel.add(songManagerButton); 52 | 53 | editKitsButton.addActionListener(e -> 54 | new KitEditor(this, document, this).setLocationRelativeTo(this)); 55 | panel.add(editKitsButton); 56 | 57 | editFontsButton.addActionListener(e -> openFontEditor()); 58 | panel.add(editFontsButton); 59 | 60 | editPalettesButton.addActionListener(e -> openPaletteEditor()); 61 | panel.add(editPalettesButton, "grow x"); 62 | 63 | setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); 64 | addWindowListener(new WindowAdapter() { 65 | @Override 66 | public void windowClosing(WindowEvent e) { 67 | if (!document.isDirty() || JOptionPane.showConfirmDialog(null, 68 | "Quit without saving changes?", 69 | "Unsaved changes", 70 | JOptionPane.OK_CANCEL_OPTION, 71 | JOptionPane.WARNING_MESSAGE) == JOptionPane.OK_OPTION) { 72 | setDefaultCloseOperation(EXIT_ON_CLOSE); 73 | } 74 | super.windowClosing(e); 75 | } 76 | }); 77 | 78 | setResizable(false); 79 | 80 | NewVersionChecker.checkGithub(this); 81 | } 82 | 83 | private void openRomUpgradeTool() { 84 | RomUpgradeTool romUpgradeTool = new RomUpgradeTool(this, document); 85 | romUpgradeTool.setLocationRelativeTo(this); 86 | romUpgradeTool.setVisible(true); 87 | } 88 | 89 | private void openSongManager() { 90 | SongManager savManager = new SongManager(this, document); 91 | savManager.setLocationRelativeTo(this); 92 | savManager.setVisible(true); 93 | } 94 | 95 | private void openPaletteEditor() { 96 | PaletteEditor editor = new PaletteEditor(this, document); 97 | editor.setLocationRelativeTo(this); 98 | editor.setVisible(true); 99 | } 100 | 101 | private void openFontEditor() { 102 | FontEditor fontEditor = new FontEditor(this, document); 103 | fontEditor.setLocationRelativeTo(this); 104 | fontEditor.setVisible(true); 105 | } 106 | 107 | final Document document = new Document(); 108 | 109 | private void addSelectors(JPanel panel) { 110 | romTextField.setMinimumSize(new Dimension(300, 0)); 111 | romTextField.setText(EditorPreferences.lastPath("gb")); 112 | romTextField.setEditable(false); 113 | panel.add(romTextField, "span 4, grow x"); 114 | 115 | JButton browseRomButton = new JButton("Browse..."); 116 | browseRomButton.addActionListener(e -> onBrowseRomButtonPress()); 117 | panel.add(browseRomButton); 118 | 119 | saveButton.setEnabled(false); 120 | saveButton.addActionListener(e -> onSave(true)); 121 | panel.add(saveButton, "span 1 4, grow y"); 122 | 123 | savTextField.setMinimumSize(new Dimension(300, 0)); 124 | savTextField.setEditable(false); 125 | savTextField.setText(EditorPreferences.lastPath("sav")); 126 | panel.add(savTextField, "span 4, grow x"); 127 | 128 | JButton browseSavButton = new JButton("Browse..."); 129 | browseSavButton.addActionListener(e -> onBrowseSavButtonPress()); 130 | panel.add(browseSavButton); 131 | 132 | try { 133 | document.loadRomImage(EditorPreferences.lastPath("gb")); 134 | } catch (IOException e) { 135 | resetRomTextField(); 136 | } 137 | try { 138 | document.loadSavFile(EditorPreferences.lastPath("sav")); 139 | } catch (IOException e) { 140 | resetSavTextField(); 141 | } 142 | updateButtonsFromTextFields(); 143 | } 144 | 145 | private void resetRomTextField() { 146 | romTextField.setText("Select LSDj ROM file -->"); 147 | } 148 | 149 | private void resetSavTextField() { 150 | savTextField.setText("Select LSDj .sav file -->"); 151 | } 152 | 153 | private void onBrowseRomButtonPress() { 154 | if (document.isDirty() && JOptionPane.showConfirmDialog(null, 155 | "Load new ROM without saving changes?", 156 | "Unsaved changes", 157 | JOptionPane.OK_CANCEL_OPTION, 158 | JOptionPane.WARNING_MESSAGE) == JOptionPane.CANCEL_OPTION) { 159 | return; 160 | } 161 | 162 | File romFile = FileDialogLauncher.load(this, 163 | "Select LSDj ROM Image", 164 | new String[]{ "gb", "gbc" }); 165 | if (romFile == null) { 166 | return; 167 | } 168 | 169 | String romPath = romFile.getAbsolutePath(); 170 | try { 171 | document.loadRomImage(romPath); 172 | romTextField.setText(romPath); 173 | } catch (IOException e) { 174 | resetRomTextField(); 175 | e.printStackTrace(); 176 | JOptionPane.showMessageDialog(this, 177 | e.getMessage() == null ? "Could not load " + romPath : e.getMessage(), 178 | "ROM load failed!", 179 | JOptionPane.ERROR_MESSAGE); 180 | } 181 | 182 | String savPath = romPath 183 | .replaceFirst(".gbc$", ".sav") 184 | .replaceFirst(".gb$", ".sav"); 185 | try { 186 | document.loadSavFile(savPath); 187 | savTextField.setText(savPath); 188 | EditorPreferences.setLastPath("sav", savPath); 189 | } catch (IOException e) { 190 | resetSavTextField(); 191 | e.printStackTrace(); 192 | } 193 | updateButtonsFromTextFields(); 194 | } 195 | 196 | private void onBrowseSavButtonPress() { 197 | if (document.isSavDirty() && JOptionPane.showConfirmDialog(null, 198 | "Load new .sav without saving changes?", 199 | "Unsaved changes", 200 | JOptionPane.OK_CANCEL_OPTION, 201 | JOptionPane.WARNING_MESSAGE) == JOptionPane.CANCEL_OPTION) { 202 | return; 203 | } 204 | 205 | File savFile = FileDialogLauncher.load(this, "Load Save File", "sav"); 206 | if (savFile == null) { 207 | return; 208 | } 209 | try { 210 | document.loadSavFile(savFile.getAbsolutePath()); 211 | savTextField.setText(savFile.getAbsolutePath()); 212 | } catch (IOException e) { 213 | resetSavTextField(); 214 | JOptionPane.showMessageDialog(this, 215 | e.getMessage(), 216 | ".sav load failed!", 217 | JOptionPane.ERROR_MESSAGE); 218 | e.printStackTrace(); 219 | } 220 | updateButtonsFromTextFields(); 221 | } 222 | 223 | void updateButtonsFromTextFields() { 224 | byte[] romImage = document.romImage(); 225 | boolean romOk = romImage != null; 226 | String savPath = savTextField.getText(); 227 | boolean savPathOk = savPath.endsWith(".sav") && new File(savPath).exists(); 228 | boolean foundPalettes = romOk && RomUtilities.validatePaletteData(romImage); 229 | 230 | romTextField.setBackground(romOk ? Color.white : Color.pink); 231 | savTextField.setBackground(savPathOk ? Color.white : Color.pink); 232 | 233 | editKitsButton.setEnabled(romOk); 234 | editFontsButton.setEnabled(romOk && foundPalettes); 235 | editPalettesButton.setEnabled(romOk && foundPalettes); 236 | upgradeRomButton.setEnabled(romOk && foundPalettes); 237 | songManagerButton.setEnabled(savPathOk && romOk); 238 | } 239 | 240 | public void onDocumentDirty(boolean dirty) { 241 | updateTitle(); 242 | upgradeRomButton.setEnabled(!dirty); 243 | saveButton.setEnabled(dirty); 244 | } 245 | 246 | private void updateTitle() { 247 | String title = "LSDPatcher v" + NewVersionChecker.getCurrentVersion(); 248 | if (document.romImage() != null) { 249 | title = title + " - " + document.romFile().getName(); 250 | if (document.isDirty()) { 251 | title = title + '*'; 252 | } 253 | } 254 | setTitle(title); 255 | } 256 | 257 | private void onSave(boolean saveSavFile) { 258 | File f = FileDialogLauncher.save(this, 259 | "Save ROM Image", 260 | new String[]{ "gb", "gbc" }); 261 | if (f == null) { 262 | return; 263 | } 264 | String romPath = f.getAbsolutePath(); 265 | 266 | try (FileOutputStream fileOutputStream = new FileOutputStream(romPath)) { 267 | byte[] romImage = document.romImage(); 268 | RomUtilities.fixChecksum(romImage); 269 | fileOutputStream.write(romImage); 270 | fileOutputStream.close(); 271 | if (document.savFile() != null && saveSavFile) { 272 | String savPath = romPath 273 | .replace(".gbc", ".sav") 274 | .replace(".gb", ".sav"); 275 | document.savFile().saveAs(savPath); 276 | savTextField.setText(savPath); 277 | document.loadSavFile(savPath); 278 | document.clearSavDirty(); 279 | EditorPreferences.setLastPath("sav", savPath); 280 | } 281 | romTextField.setText(romPath); 282 | document.setRomFile(new File(romPath)); 283 | document.clearRomDirty(); 284 | EditorPreferences.setLastPath("gb", romPath); 285 | saveButton.setEnabled(false); 286 | } catch (IOException e) { 287 | JOptionPane.showMessageDialog(this, 288 | e.getMessage(), 289 | "File save failed!", 290 | JOptionPane.ERROR_MESSAGE); 291 | } 292 | } 293 | 294 | @Override 295 | public void saveRom() { 296 | onSave(false); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/lsdpatch/NewVersionChecker.java: -------------------------------------------------------------------------------- 1 | package lsdpatch; 2 | 3 | import utils.GlobalHolder; 4 | 5 | import javax.swing.*; 6 | import java.io.IOException; 7 | import java.net.URL; 8 | 9 | public class NewVersionChecker { 10 | public static String getCurrentVersion() { 11 | String version = GlobalHolder.class.getPackage().getImplementationVersion(); 12 | if (version == null) { 13 | return "DEV"; 14 | } 15 | return version; 16 | } 17 | 18 | public static void checkGithub(JFrame parent) { 19 | String currentVersion = getCurrentVersion(); 20 | if (currentVersion.equals("DEV")) { 21 | return; 22 | } 23 | String response; 24 | try { 25 | String apiPath = "https://api.github.com/repos/jkotlinski/lsdpatch/releases/latest"; 26 | response = WwwUtil.fetchWwwPage(new URL(apiPath)); 27 | } catch (IOException e) { 28 | return; 29 | } 30 | if (response.contains("\"v" + currentVersion + '"')) { 31 | return; 32 | } 33 | if (JOptionPane.showConfirmDialog(parent, 34 | "A new LSDPatcher release is available. Do you want to see it?", 35 | "Version upgrade", 36 | JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { 37 | WwwUtil.openInBrowser("https://github.com/jkotlinski/lsdpatch/releases"); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/lsdpatch/RomUpgradeTool.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020, Johan Kotlinski 2 | 3 | package lsdpatch; 4 | 5 | import Document.Document; 6 | import net.miginfocom.swing.MigLayout; 7 | import structures.LSDJFont; 8 | import utils.RomUtilities; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.WindowAdapter; 12 | import java.awt.event.WindowEvent; 13 | import java.io.*; 14 | import java.net.URL; 15 | import java.util.regex.MatchResult; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | import java.util.zip.ZipInputStream; 19 | 20 | public class RomUpgradeTool extends JFrame { 21 | final String changeLogPath = "https://www.littlesounddj.com/lsd/latest/CHANGELOG.txt"; 22 | final String licensePath = "https://www.littlesounddj.com/lsd/latest/rom_images/LICENSE.txt"; 23 | final String developPath = "https://www.littlesounddj.com/lsd/latest/rom_images/develop/"; 24 | final String stablePath = "https://www.littlesounddj.com/lsd/latest/rom_images/stable/"; 25 | final String arduinoBoyPath = "https://www.littlesounddj.com/lsd/latest/rom_images/arduinoboy/"; 26 | 27 | private final File localRomFile; 28 | private byte[] localRomImage; 29 | private final byte[] remoteRomImage; 30 | private final Document document; 31 | 32 | RomUpgradeTool(JFrame parent, Document document) { 33 | parent.setEnabled(false); 34 | 35 | this.document = document; 36 | localRomFile = document.romFile(); 37 | localRomImage = document.romImage(); 38 | remoteRomImage = new byte[localRomImage.length]; 39 | 40 | JPanel panel = new JPanel(); 41 | getContentPane().add(panel); 42 | panel.setLayout(new MigLayout("wrap")); 43 | 44 | panel.add(new JLabel("Upgrade ROM to latest:")); 45 | JButton upgradeStableButton = new JButton("Stable version (recommended!)"); 46 | JButton upgradeDevelopButton = new JButton("Development version (experimental!)"); 47 | JButton upgradeArduinoBoyButton = new JButton("ArduinoBoy version"); 48 | JButton viewChangeLogButton = new JButton("View Changelog"); 49 | JButton viewLicenseButton = new JButton("View License Information"); 50 | panel.add(upgradeStableButton, "growx"); 51 | panel.add(upgradeDevelopButton, "growx"); 52 | panel.add(upgradeArduinoBoyButton, "growx"); 53 | panel.add(viewChangeLogButton, "growx, gaptop 10"); 54 | panel.add(viewLicenseButton, "growx"); 55 | pack(); 56 | 57 | upgradeStableButton.addActionListener(e -> upgrade(stablePath)); 58 | upgradeDevelopButton.addActionListener(e -> upgrade(developPath)); 59 | upgradeArduinoBoyButton.addActionListener(e -> upgrade(arduinoBoyPath)); 60 | viewChangeLogButton.addActionListener(e -> WwwUtil.openInBrowser(changeLogPath)); 61 | viewLicenseButton.addActionListener(e -> WwwUtil.openInBrowser(licensePath)); 62 | 63 | setResizable(false); 64 | 65 | addWindowListener(new WindowAdapter() { 66 | @Override 67 | public void windowClosing(WindowEvent e) { 68 | super.windowClosing(e); 69 | parent.setEnabled(true); 70 | } 71 | }); 72 | } 73 | 74 | private boolean versionCompare(String localVersion, String remoteVersion) { 75 | assert(remoteVersion.startsWith("lsdj")); 76 | remoteVersion = remoteVersion.substring(4, 9).replace('_', '.'); 77 | return remoteVersion.compareTo(localVersion) > 0; 78 | } 79 | 80 | private void upgrade(String basePath) { 81 | try { 82 | String localVersion = localVersion(); 83 | String remoteVersion = fetchLatestRemoteVersion(basePath); 84 | if (localVersion == null || remoteVersion == null) { 85 | JOptionPane.showMessageDialog(null, 86 | "Version information not found!", 87 | "Update failed!", 88 | JOptionPane.ERROR_MESSAGE); 89 | return; 90 | } 91 | if (!versionCompare(localVersion, remoteVersion)) { 92 | JOptionPane.showMessageDialog(this, 93 | localRomFile.getName() + " is already updated.", 94 | "No updates found!", 95 | JOptionPane.INFORMATION_MESSAGE); 96 | return; 97 | } 98 | int reply = JOptionPane.showConfirmDialog(this, 99 | "Current ROM version: " + localVersion() + '\n' + 100 | "Upgrade to " + remoteVersion + '?', 101 | "Upgrade?", 102 | JOptionPane.YES_NO_OPTION); 103 | if (reply != JOptionPane.YES_OPTION) { 104 | return; 105 | } 106 | ZipInputStream zipInputStream = new ZipInputStream(new URL(basePath + remoteVersion).openStream()); 107 | zipInputStream.getNextEntry(); 108 | int dstIndex = 0; 109 | while (dstIndex != remoteRomImage.length) { 110 | dstIndex += zipInputStream.read(remoteRomImage, dstIndex, remoteRomImage.length - dstIndex); 111 | } 112 | importAll(); 113 | } catch (IOException e) { 114 | JOptionPane.showMessageDialog(null, 115 | e.getMessage(), 116 | "Fetching new version failed!", 117 | JOptionPane.ERROR_MESSAGE); 118 | } 119 | } 120 | 121 | private String localVersion() { 122 | byte[] romImage = localRomImage; 123 | for (int i = 0; i < romImage.length; ++i) { 124 | if (romImage[i] == 'V' && romImage[i + 2] == '.' && romImage[i + 4] == '.') { 125 | String s = ""; 126 | s += (char)romImage[i + 1]; 127 | s += (char)romImage[i + 2]; 128 | s += (char)romImage[i + 3]; 129 | s += (char)romImage[i + 4]; 130 | s += (char)romImage[i + 5]; 131 | return s; 132 | } 133 | } 134 | return null; 135 | } 136 | 137 | private String fetchLatestRemoteVersion(String basePath) throws IOException { 138 | String page = WwwUtil.fetchWwwPage(new URL(basePath)); 139 | Pattern p = Pattern.compile("lsdj\\d_\\d_[0-9A-Z][-a-zA-Z]*\\.zip"); 140 | Matcher m = p.matcher(page); 141 | if (m.find()) { 142 | MatchResult matchResult = m.toMatchResult(); 143 | return matchResult.group(); 144 | } else { 145 | return null; 146 | } 147 | } 148 | 149 | private void importAll() { 150 | if (importKits() == 0) { 151 | JOptionPane.showMessageDialog(this, 152 | "Kit copy error.", 153 | "Kit import result.", JOptionPane.INFORMATION_MESSAGE); 154 | return; 155 | } 156 | if (!importFonts()) { 157 | JOptionPane.showMessageDialog(this, 158 | "Font copy error.", 159 | "Font import result.", JOptionPane.INFORMATION_MESSAGE); 160 | return; 161 | } 162 | if (!importPalettes()) { 163 | JOptionPane.showMessageDialog(this, 164 | "Palette copy error.", 165 | "Palette import result.", JOptionPane.INFORMATION_MESSAGE); 166 | return; 167 | } 168 | 169 | document.setRomImage(remoteRomImage); 170 | localRomImage = remoteRomImage; 171 | 172 | JOptionPane.showMessageDialog(this, 173 | "Upgraded to " + localVersion() + " successfully!", 174 | "ROM upgrade OK!", 175 | JOptionPane.INFORMATION_MESSAGE); 176 | 177 | dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); 178 | } 179 | 180 | private boolean importPalettes() { 181 | boolean isOk = false; 182 | RandomAccessFile otherOpenRom = null; 183 | try { 184 | byte[] otherRomImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 185 | otherOpenRom = new RandomAccessFile(localRomFile, "r"); 186 | byte[] romImage = remoteRomImage; 187 | 188 | otherOpenRom.readFully(otherRomImage); 189 | otherOpenRom.close(); 190 | 191 | if (!RomUtilities.validatePaletteData(remoteRomImage)) { 192 | throw new Exception("Could not read palette data from remote ROM image!"); 193 | } 194 | if (!RomUtilities.validatePaletteData(otherRomImage)) { 195 | throw new Exception("Could not read palette data from local ROM image!"); 196 | } 197 | 198 | int ownPaletteOffset = RomUtilities.findPaletteOffset(romImage); 199 | int ownPaletteNameOffset = RomUtilities.findPaletteNameOffset(romImage); 200 | 201 | int otherPaletteOffset = RomUtilities.findPaletteOffset(otherRomImage); 202 | int otherPaletteNameOffset = RomUtilities.findPaletteNameOffset(otherRomImage); 203 | 204 | if (RomUtilities.getNumberOfPalettes(otherRomImage) > RomUtilities.getNumberOfPalettes(romImage)) 205 | { 206 | throw new Exception("Current file doesn't have enough palette slots to get the palettes imported to."); 207 | } 208 | 209 | System.arraycopy(otherRomImage, otherPaletteOffset, romImage, ownPaletteOffset, RomUtilities.PALETTE_SIZE * RomUtilities.getNumberOfPalettes(otherRomImage)); 210 | System.arraycopy(otherRomImage, otherPaletteNameOffset, romImage, ownPaletteNameOffset, RomUtilities.PALETTE_NAME_SIZE * RomUtilities.getNumberOfPalettes(otherRomImage)); 211 | 212 | isOk = true; 213 | } catch (Exception e) { 214 | JOptionPane.showMessageDialog(this, e.getMessage(), "File error", 215 | JOptionPane.ERROR_MESSAGE); 216 | } finally { 217 | if (otherOpenRom != null) { 218 | try { 219 | otherOpenRom.close(); 220 | } catch (IOException e) { 221 | JOptionPane.showMessageDialog(this, e.getMessage(), "File error (wth)", 222 | JOptionPane.ERROR_MESSAGE); 223 | } 224 | } 225 | } 226 | return isOk; 227 | } 228 | 229 | private boolean importFonts() { 230 | boolean isOk = false; 231 | RandomAccessFile otherOpenRom = null; 232 | try { 233 | byte[] otherRomImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 234 | otherOpenRom = new RandomAccessFile(localRomFile, "r"); 235 | byte[] romImage = remoteRomImage; 236 | 237 | otherOpenRom.readFully(otherRomImage); 238 | otherOpenRom.close(); 239 | 240 | int ownFontOffset = RomUtilities.findFontOffset(romImage); 241 | int otherFontOffset = RomUtilities.findFontOffset(otherRomImage); 242 | 243 | System.arraycopy(otherRomImage, otherFontOffset, romImage, ownFontOffset, LSDJFont.FONT_SIZE * LSDJFont.FONT_COUNT); 244 | 245 | int ownGfxOffset = RomUtilities.findGfxFontOffset(romImage); 246 | int otherGfxOffset = RomUtilities.findGfxFontOffset(otherRomImage); 247 | System.arraycopy(otherRomImage, otherGfxOffset, romImage, ownGfxOffset, LSDJFont.GFX_SIZE); 248 | 249 | for (int i = 0; i < LSDJFont.FONT_COUNT; ++i) { 250 | RomUtilities.setFontName(romImage, i, RomUtilities.getFontName(otherRomImage, i)); 251 | } 252 | 253 | isOk = true; 254 | } catch (Exception e) { 255 | JOptionPane.showMessageDialog(this, e.getMessage(), "File error", 256 | JOptionPane.ERROR_MESSAGE); 257 | } finally { 258 | if (otherOpenRom != null) { 259 | try { 260 | otherOpenRom.close(); 261 | } catch (IOException e) { 262 | JOptionPane.showMessageDialog(this, e.getMessage(), "File error (wth)", 263 | JOptionPane.ERROR_MESSAGE); 264 | } 265 | } 266 | } 267 | return isOk; 268 | } 269 | 270 | private boolean isKitBank(int a_bank) { 271 | int l_offset = (a_bank) * RomUtilities.BANK_SIZE; 272 | byte l_char_1 = remoteRomImage[l_offset++]; 273 | byte l_char_2 = remoteRomImage[l_offset]; 274 | return (l_char_1 == 0x60 && l_char_2 == 0x40); 275 | } 276 | 277 | private boolean isEmptyKitBank(int a_bank) { 278 | int l_offset = (a_bank) * RomUtilities.BANK_SIZE; 279 | byte l_char_1 = remoteRomImage[l_offset++]; 280 | byte l_char_2 = remoteRomImage[l_offset]; 281 | return (l_char_1 == -1 && l_char_2 == -1); 282 | } 283 | 284 | private int importKits() { 285 | try { 286 | int outBank = 0; 287 | int copiedBankCount = 0; 288 | FileInputStream in = new FileInputStream(localRomFile.getAbsolutePath()); 289 | while (in.available() > 0) { 290 | byte[] inBuf = new byte[RomUtilities.BANK_SIZE]; 291 | int readBytes = in.read(inBuf); 292 | assert(readBytes == inBuf.length); 293 | if (inBuf[0] == 0x60 && inBuf[1] == 0x40) { 294 | //is kit bank 295 | outBank++; 296 | while (!isKitBank(outBank) && !isEmptyKitBank(outBank)) { 297 | outBank++; 298 | } 299 | int outPtr = outBank * RomUtilities.BANK_SIZE; 300 | for (int i = 0; i < RomUtilities.BANK_SIZE; i++) { 301 | remoteRomImage[outPtr++] = inBuf[i]; 302 | } 303 | copiedBankCount++; 304 | } 305 | } 306 | in.close(); 307 | return copiedBankCount; 308 | } catch (Exception e) { 309 | JOptionPane.showMessageDialog(this, e.getMessage(), "File error", 310 | JOptionPane.ERROR_MESSAGE); 311 | } 312 | return 0; 313 | } 314 | } -------------------------------------------------------------------------------- /src/main/java/lsdpatch/WwwUtil.java: -------------------------------------------------------------------------------- 1 | package lsdpatch; 2 | 3 | import java.awt.*; 4 | import java.io.BufferedReader; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | 12 | public class WwwUtil { 13 | static String fetchWwwPage(URL url) throws IOException { 14 | InputStream is; 15 | BufferedReader br; 16 | String line; 17 | StringBuilder lines = new StringBuilder(); 18 | 19 | is = url.openStream(); 20 | br = new BufferedReader(new InputStreamReader(is)); 21 | 22 | while ((line = br.readLine()) != null) { 23 | lines.append(line); 24 | } 25 | is.close(); 26 | return lines.toString(); 27 | } 28 | 29 | static void openInBrowser(String path) { 30 | try { 31 | Desktop.getDesktop().browse(new URI(path)); 32 | } catch (URISyntaxException | IOException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/ColorPicker.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import net.miginfocom.swing.MigLayout; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | public class ColorPicker extends JPanel implements HuePanel.Listener, SaturationBrightnessPanel.Listener { 9 | interface Listener { 10 | void colorChanged(int r, int g, int b); 11 | } 12 | 13 | private Listener listener; 14 | 15 | final HuePanel huePanel; 16 | final SaturationBrightnessPanel saturationBrightnessPanel; 17 | 18 | public ColorPicker() { 19 | huePanel = new HuePanel(); 20 | saturationBrightnessPanel = new SaturationBrightnessPanel(huePanel); 21 | 22 | huePanel.subscribe(this); 23 | saturationBrightnessPanel.subscribe(this); 24 | 25 | setLayout(new MigLayout()); 26 | 27 | add(saturationBrightnessPanel); 28 | add(huePanel, "gap 5"); 29 | } 30 | 31 | public void setColor(RGB555 rgb) { 32 | int r = rgb.r << 3; 33 | int g = rgb.g << 3; 34 | int b = rgb.b << 3; 35 | r *= 0xff; 36 | r /= 0xf8; 37 | g *= 0xff; 38 | g /= 0xf8; 39 | b *= 0xff; 40 | b /= 0xf8; 41 | 42 | float[] hsb = new float[3]; 43 | Color.RGBtoHSB(r, g, b, hsb); 44 | huePanel.setHue(hsb[0]); 45 | saturationBrightnessPanel.setSaturationBrightness(hsb[1], hsb[2]); 46 | saturationBrightnessPanel.printRGB555(rgb); 47 | } 48 | 49 | public void subscribe(Listener listener) { 50 | this.listener = listener; 51 | } 52 | 53 | private void broadcastColor() { 54 | float hue = huePanel.hue(); 55 | float saturation = saturationBrightnessPanel.saturation(); 56 | float brightness = saturationBrightnessPanel.brightness(); 57 | 58 | int rgb = Color.HSBtoRGB(hue, saturation, brightness); 59 | byte b = (byte)((rgb & 255) >> 3); 60 | rgb >>= 8; 61 | byte g = (byte)((rgb & 255) >> 3); 62 | rgb >>= 8; 63 | byte r = (byte)((rgb & 255) >> 3); 64 | if (listener != null) { 65 | listener.colorChanged(r, g, b); 66 | } 67 | } 68 | 69 | @Override 70 | public void hueChanged() { 71 | broadcastColor(); 72 | } 73 | 74 | @Override 75 | public void saturationBrightnessChanged() { 76 | broadcastColor(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/ColorUtil.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import static java.lang.Math.pow; 4 | import static java.lang.Math.round; 5 | 6 | public class ColorUtil { 7 | private static final int[] scaleChannelWithCurve = { 8 | 0, 6, 12, 20, 28, 36, 45, 56, 66, 76, 88, 100, 113, 125, 137, 149, 161, 172, 9 | 182, 192, 202, 210, 218, 225, 232, 238, 243, 247, 250, 252, 254, 255 10 | }; 11 | 12 | enum ColorSpace { 13 | Emulator, 14 | Reality, 15 | Raw 16 | } 17 | static public ColorSpace colorSpace = ColorSpace.Emulator; 18 | 19 | public static void setColorSpace(ColorSpace colorSpace_) { 20 | colorSpace = colorSpace_; 21 | } 22 | 23 | public static int to8bit(int color) { 24 | assert(color >= 0); 25 | assert(color < 32); 26 | color <<= 3; 27 | color *= 0xff; 28 | return color / 0xf8; 29 | } 30 | 31 | // From Sameboy. 32 | public static int colorCorrect(java.awt.Color c) { 33 | return colorCorrect(c.getRed(), c.getGreen(), c.getBlue()); 34 | } 35 | 36 | public static int colorCorrect(int r, int g, int b) { 37 | if (colorSpace == ColorSpace.Raw) { 38 | r = (((r >> 3) << 3) * 0xff) / 0xf8; 39 | g = (((g >> 3) << 3) * 0xff) / 0xf8; 40 | b = (((b >> 3) << 3) * 0xff) / 0xf8; 41 | return (r << 16) | (g << 8) | b; 42 | } 43 | 44 | r >>= 3; 45 | g >>= 3; 46 | b >>= 3; 47 | 48 | r = scaleChannelWithCurve[r]; 49 | g = scaleChannelWithCurve[g]; 50 | b = scaleChannelWithCurve[b]; 51 | 52 | double gamma = 2.2; 53 | int new_g = (int)round(pow((pow(g / 255.0, gamma) * 3 + pow(b / 255.0, gamma)) / 4, 1 / gamma) * 255); 54 | int new_r = r; 55 | int new_b = b; 56 | 57 | if (colorSpace == ColorSpace.Reality) { 58 | // r = new_r; 59 | g = new_g; 60 | // b = new_b; 61 | 62 | new_r = new_r * 15 / 16 + (g + b) / 32; 63 | new_g = new_g * 15 / 16 + (r + b) / 32; 64 | new_b = new_b * 15 / 16 + (r + g) / 32; 65 | 66 | new_r = new_r * (162 - 45) / 255 + 45; 67 | new_g = new_g * (167 - 41) / 255 + 41; 68 | new_b = new_b * (157 - 38) / 255 + 38; 69 | } 70 | 71 | return (new_r << 16) | (new_g << 8) | new_b; 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/paletteEditor/HuePanel.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.MouseEvent; 6 | import java.awt.event.MouseListener; 7 | import java.awt.event.MouseMotionListener; 8 | import java.awt.image.BufferedImage; 9 | import java.util.LinkedList; 10 | 11 | class HuePanel extends JPanel implements MouseListener, MouseMotionListener { 12 | int selectedPosition; 13 | final int width = 24; 14 | final int height = 244; 15 | private final LinkedList listeners = new LinkedList<>(); 16 | 17 | public interface Listener { 18 | void hueChanged(); 19 | } 20 | 21 | HuePanel() { 22 | setPreferredSize(new Dimension(width, height)); 23 | addMouseListener(this); 24 | addMouseMotionListener(this); 25 | } 26 | 27 | void setHue(float hue) { 28 | assert(hue >= 0); 29 | assert(hue <= 1); 30 | selectedPosition = (int) (hue * height); 31 | repaint(); 32 | } 33 | 34 | @Override 35 | public void paintComponent(Graphics g) { 36 | super.paintComponent(g); 37 | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 38 | for (int y = 0; y < height; ++y) { 39 | Color color = Color.getHSBColor((float) y / height, 1, 1); 40 | color = new Color(ColorUtil.colorCorrect(color)); 41 | for (int x = 0; x < width; ++x) { 42 | image.setRGB(x, y, color.getRGB()); 43 | } 44 | } 45 | g.drawImage(image, 0, 0, null); 46 | 47 | Graphics2D g2d = (Graphics2D) g; 48 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 49 | g2d.setColor(Color.BLACK); 50 | int r = 7; 51 | int w = 2; 52 | g2d.setStroke(new BasicStroke(w)); 53 | g2d.drawOval(width / 2 - r, selectedPosition - r, 2 * r, 2 * r); 54 | r -= w; 55 | g2d.setColor(Color.WHITE); 56 | g2d.drawOval(width / 2 - r, selectedPosition - r, 2 * r, 2 * r); 57 | } 58 | 59 | public float hue() { 60 | float hue = selectedPosition; 61 | hue /= height; 62 | assert(hue >= 0); 63 | assert(hue <= 1); 64 | return hue; 65 | } 66 | 67 | boolean mousePressed; 68 | 69 | @Override 70 | public void mouseClicked(MouseEvent e) { 71 | } 72 | 73 | @Override 74 | public void mousePressed(MouseEvent e) { 75 | mousePressed = true; 76 | mouseDragged(e); 77 | } 78 | 79 | @Override 80 | public void mouseReleased(MouseEvent e) { 81 | mousePressed = false; 82 | } 83 | 84 | @Override 85 | public void mouseEntered(MouseEvent e) { 86 | } 87 | 88 | @Override 89 | public void mouseExited(MouseEvent e) { 90 | } 91 | 92 | @Override 93 | public void mouseDragged(MouseEvent e) { 94 | selectedPosition = Math.max(0, Math.min(height, e.getY())); 95 | for (Listener listener : listeners) { 96 | listener.hueChanged(); 97 | } 98 | repaint(); 99 | } 100 | 101 | @Override 102 | public void mouseMoved(MouseEvent e) { 103 | } 104 | 105 | public void subscribe(Listener listener) { 106 | listeners.add(listener); 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/RGB555.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | public class RGB555 { 4 | int r; 5 | int g; 6 | int b; 7 | 8 | public RGB555() { 9 | r = -1; 10 | g = -1; 11 | b = -1; 12 | } 13 | 14 | public RGB555(int r, int g, int b) { 15 | setR(r); 16 | setG(g); 17 | setB(b); 18 | } 19 | 20 | public void setR(int r) { 21 | assert(r >= 0 && r < 32); 22 | this.r = r; 23 | } 24 | 25 | public void setG(int g) { 26 | assert(g >= 0 && g < 32); 27 | this.g = g; 28 | } 29 | 30 | public void setB(int b) { 31 | assert(b >= 0 && b < 32); 32 | this.b = b; 33 | } 34 | 35 | public int r() { 36 | return r; 37 | } 38 | 39 | public int g() { 40 | return g; 41 | } 42 | 43 | public int b() { 44 | return b; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/paletteEditor/SaturationBrightnessPanel.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.MouseEvent; 6 | import java.awt.event.MouseListener; 7 | import java.awt.event.MouseMotionListener; 8 | import java.awt.image.BufferedImage; 9 | 10 | public class SaturationBrightnessPanel extends JPanel implements HuePanel.Listener, MouseListener, MouseMotionListener { 11 | private RGB555 rgb555 = null; 12 | public void printRGB555(RGB555 rgb555) { 13 | this.rgb555 = rgb555; 14 | } 15 | 16 | interface Listener { 17 | void saturationBrightnessChanged(); 18 | } 19 | 20 | private Listener listener; 21 | 22 | final Point selection = new Point(); 23 | 24 | HuePanel huePanel; 25 | 26 | boolean mousePressed; 27 | 28 | public SaturationBrightnessPanel(HuePanel huePanel) { 29 | this.huePanel = huePanel; 30 | huePanel.subscribe(this); 31 | setPreferredSize(new Dimension(254, 244)); 32 | addMouseListener(this); 33 | addMouseMotionListener(this); 34 | } 35 | 36 | public void setSaturationBrightness(float saturation, float brightness) { 37 | assert(saturation >= 0); 38 | assert(saturation <= 1); 39 | assert(brightness >= 0); 40 | assert(brightness <= 1); 41 | selection.setLocation(saturation * getWidth(), (1 - brightness) * getHeight()); 42 | repaint(); 43 | } 44 | 45 | public void subscribe(Listener listener) { 46 | this.listener = listener; 47 | } 48 | 49 | @Override 50 | public void hueChanged() { 51 | repaint(); 52 | } 53 | 54 | public float saturation() { 55 | return (float) selection.getX() / getWidth(); 56 | } 57 | 58 | public float brightness() { 59 | return 1 - (float)selection.getY() / getHeight(); 60 | } 61 | 62 | @Override 63 | public void paintComponent(Graphics g) { 64 | super.paintComponent(g); 65 | BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); 66 | for (int y = 0; y < getHeight(); ++y) { 67 | for (int x = 0; x < getWidth(); ++x) { 68 | float s = (float) x / getWidth(); 69 | float b = 1 - (float) y / getHeight(); 70 | Color color = Color.getHSBColor(huePanel.hue(), s, b); 71 | color = new Color(ColorUtil.colorCorrect(color)); 72 | image.setRGB(x, y, color.getRGB()); 73 | } 74 | } 75 | g.drawImage(image, 0, 0, null); 76 | 77 | Graphics2D g2d = (Graphics2D) g; 78 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 79 | int radius = 7; 80 | int w = 2; 81 | g2d.setColor(Color.BLACK); 82 | g2d.setStroke(new BasicStroke(w)); 83 | g2d.drawOval((int) selection.getX() - radius, 84 | (int) selection.getY() - radius, 85 | 2 * radius, 2 * radius); 86 | g2d.setColor(Color.WHITE); 87 | radius -= w; 88 | g2d.drawOval((int) selection.getX() - radius, 89 | (int) selection.getY() - radius, 90 | 2 * radius, 2 * radius); 91 | 92 | String colorString = rgb555.r + "," + rgb555.g + "," + rgb555.b; 93 | FontMetrics fm = g2d.getFontMetrics(); 94 | g2d.drawString(colorString, 95 | getWidth() - fm.stringWidth(colorString), 96 | getHeight() - fm.getDescent()); 97 | } 98 | 99 | @Override 100 | public void mouseClicked(MouseEvent e) { 101 | } 102 | 103 | @Override 104 | public void mousePressed(MouseEvent e) { 105 | mousePressed = true; 106 | mouseDragged(e); 107 | } 108 | 109 | @Override 110 | public void mouseReleased(MouseEvent e) { 111 | mousePressed = false; 112 | } 113 | 114 | @Override 115 | public void mouseEntered(MouseEvent e) { 116 | } 117 | 118 | @Override 119 | public void mouseExited(MouseEvent e) { 120 | } 121 | 122 | @Override 123 | public void mouseDragged(MouseEvent e) { 124 | selection.x = Math.min(getWidth(), Math.max(0, e.getX())); 125 | selection.y = Math.min(getHeight(), Math.max(0, e.getY())); 126 | if (listener != null) { 127 | listener.saturationBrightnessChanged(); 128 | } 129 | repaint(); 130 | } 131 | 132 | @Override 133 | public void mouseMoved(MouseEvent e) { 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/ScreenShotColors.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | public class ScreenShotColors { 4 | static final public int NORMAL_BG = 0xff1a4577; 5 | static final public int NORMAL_MID = 0xffb06a76; 6 | static final public int NORMAL_FG = 0xfff58f77; 7 | static final public int SHADED_BG = 0xffebf1fd; 8 | static final public int SHADED_MID = 0xffcba9c5; 9 | static final public int SHADED_FG = 0xffa64556; 10 | static final public int ALT_BG = 0xff2d79bd; 11 | static final public int ALT_MID = 0xff76c0c3; 12 | static final public int ALT_FG = 0xffbdebd0; 13 | static final public int CUR_BG = 0xff88bdf5; 14 | static final public int CUR_MID = 0xff7578a8; 15 | static final public int CUR_FG = 0xff6e231a; 16 | static final public int SCROLL_BG = 0xffc4ecd0; 17 | static final public int SCROLL_MID = 0xff7fc1c4; 18 | static final public int SCROLL_FG = 0xff3a7abd; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/Swatch.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.util.LinkedList; 6 | import java.util.Random; 7 | 8 | public class Swatch extends JPanel { 9 | public RGB555 rgb() { 10 | return rgb555; 11 | } 12 | 13 | public interface Listener { 14 | void swatchChanged(); 15 | } 16 | final LinkedList listeners = new LinkedList<>(); 17 | private final RGB555 rgb555 = new RGB555(); 18 | 19 | public Swatch() { 20 | setPreferredSize(new Dimension(50, 37)); 21 | setBorder(BorderFactory.createLoweredBevelBorder()); 22 | } 23 | 24 | public int r() { 25 | return rgb555.r(); 26 | } 27 | 28 | public int g() { 29 | return rgb555.g(); 30 | } 31 | 32 | public int b() { 33 | return rgb555.b(); 34 | } 35 | 36 | public void setRGB(int r, int g, int b) { 37 | boolean changed = r != rgb555.r() || g != rgb555.g() || b != rgb555.b(); 38 | rgb555.setR(r); 39 | rgb555.setG(g); 40 | rgb555.setB(b); 41 | if (changed) { 42 | for (Listener listener : listeners) { 43 | listener.swatchChanged(); 44 | } 45 | } 46 | setBackground(new Color(ColorUtil.colorCorrect(new Color(r << 3, g << 3, b << 3)))); 47 | } 48 | 49 | public void randomize(Random rand) { 50 | setRGB(rand.nextInt(32), rand.nextInt(32), rand.nextInt(32)); 51 | } 52 | 53 | public void addListener(Listener listener) { 54 | listeners.add(listener); 55 | } 56 | 57 | public void deselect() { 58 | setBorder(BorderFactory.createLoweredBevelBorder()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/paletteEditor/SwatchPair.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.MouseAdapter; 6 | import java.awt.event.MouseEvent; 7 | import java.util.*; 8 | 9 | class SwatchPair implements Swatch.Listener { 10 | public interface Listener { 11 | void swatchSelected(Swatch swatch); 12 | void swatchChanged(); 13 | } 14 | private final LinkedList listeners = new LinkedList<>(); 15 | public void addListener(Listener listener) { 16 | listeners.add(listener); 17 | } 18 | @Override 19 | public void swatchChanged() { 20 | for (Listener listener : listeners) { 21 | listener.swatchChanged(); 22 | } 23 | } 24 | 25 | public final Swatch bgSwatch = new Swatch(); 26 | public final Swatch fgSwatch = new Swatch(); 27 | 28 | public SwatchPair() { 29 | createSwatches(); 30 | 31 | bgSwatch.addListener(this); 32 | fgSwatch.addListener(this); 33 | } 34 | 35 | public void registerToPanel(JPanel panel, String entryName) { 36 | panel.add(bgSwatch, "grow"); 37 | bgSwatch.setToolTipText(entryName + " background"); 38 | panel.add(fgSwatch, "grow, wrap"); 39 | fgSwatch.setToolTipText(entryName + " text"); 40 | } 41 | 42 | public void selectBackground() { 43 | select(bgSwatch); 44 | } 45 | 46 | public void selectForeground() { 47 | select(fgSwatch); 48 | } 49 | 50 | private void select(Swatch swatch) { 51 | for (Listener listener : listeners) { 52 | listener.swatchSelected(swatch); 53 | } 54 | } 55 | 56 | private void createSwatches() { 57 | bgSwatch.addMouseListener(new MouseAdapter() { 58 | @Override 59 | public void mousePressed(MouseEvent e) { 60 | super.mousePressed(e); 61 | select(bgSwatch); 62 | }}); 63 | 64 | fgSwatch.addMouseListener(new MouseAdapter() { 65 | @Override 66 | public void mousePressed(MouseEvent e) { 67 | super.mousePressed(e); 68 | select(fgSwatch); 69 | }}); 70 | } 71 | 72 | public void setColors(Color foregroundColor, Color backgroundColor) { 73 | bgSwatch.setRGB(backgroundColor.getRed() >> 3, 74 | backgroundColor.getGreen() >> 3, 75 | backgroundColor.getBlue() >> 3); 76 | fgSwatch.setRGB(foregroundColor.getRed() >> 3, 77 | foregroundColor.getGreen() >> 3, 78 | foregroundColor.getBlue() >> 3); 79 | } 80 | 81 | public void randomize(Random rand) { 82 | bgSwatch.randomize(rand); 83 | fgSwatch.randomize(rand); 84 | } 85 | 86 | private RGB555 findMidTone(RGB555 bg, RGB555 fg) { 87 | ColorUtil.ColorSpace prevColorSpace = ColorUtil.colorSpace; 88 | ColorUtil.colorSpace = ColorUtil.ColorSpace.Emulator; 89 | Color target = midToneTarget(bg, fg); 90 | RGB555 bestRgb = findBestRgb(new RGB555(15, 15, 15), target); 91 | ColorUtil.colorSpace = prevColorSpace; 92 | return bestRgb; 93 | } 94 | 95 | private Color midToneTarget(RGB555 bg, RGB555 fg) { 96 | int r1 = ColorUtil.to8bit(bg.r()); 97 | int g1 = ColorUtil.to8bit(bg.g()); 98 | int b1 = ColorUtil.to8bit(bg.b()); 99 | int r2 = ColorUtil.to8bit(fg.r()); 100 | int g2 = ColorUtil.to8bit(fg.g()); 101 | int b2 = ColorUtil.to8bit(fg.b()); 102 | Color bgColor = new Color(ColorUtil.colorCorrect( new Color(r1, g1, b1))); 103 | Color fgColor = new Color(ColorUtil.colorCorrect( new Color(r2, g2, b2))); 104 | int k = 55; 105 | int midR = (bgColor.getRed() * k + fgColor.getRed() * (100 - k)) / 100; 106 | int midG = (bgColor.getGreen() * k + fgColor.getGreen() * (100 - k)) / 100; 107 | int midB = (bgColor.getBlue() * k + fgColor.getBlue() * (100 - k)) / 100; 108 | return new Color(midR, midG, midB); 109 | } 110 | 111 | private RGB555 findBestRgb(RGB555 start, Color target) { 112 | TreeMap map = new TreeMap<>(); 113 | double startDiff = diff(target, start.r(), start.g(), start.b()); 114 | map.put(startDiff, start); 115 | add(map, target, start, 1, 0, 0); 116 | add(map, target, start, -1, 0, 0); 117 | add(map, target, start, 0, 1, 0); 118 | add(map, target, start, 0, -1, 0); 119 | add(map, target, start, 0, 0, 1); 120 | add(map, target, start, 0, 0, -1); 121 | if (map.firstKey() == startDiff) { 122 | return start; 123 | } 124 | return findBestRgb(map.firstEntry().getValue(), target); 125 | } 126 | 127 | private void add(TreeMap map, Color target, RGB555 start, int rd, int gd, int bd) { 128 | int r = start.r() + rd; 129 | int g = start.g() + gd; 130 | int b = start.b() + bd; 131 | if (r < 0 || r > 31 || g < 0 || g > 31 || b < 0 || b > 31) { 132 | return; 133 | } 134 | RGB555 rgb555 = new RGB555(start.r() + rd, start.g() + gd, start.b() + bd); 135 | map.put(diff(target, r, g, b), rgb555); 136 | } 137 | 138 | private static double diff(Color target, int r, int g, int b) { 139 | int rgb24 = ColorUtil.colorCorrect( 140 | ColorUtil.to8bit(r), 141 | ColorUtil.to8bit(g), 142 | ColorUtil.to8bit(b)); 143 | int rr = rgb24 >> 16; 144 | int gg = (rgb24 >> 8) & 0xff; 145 | int bb = rgb24 & 0xff; 146 | 147 | // red-mean 148 | double rm = (rr + target.getRed()) / 2.0; 149 | double rd = rr - target.getRed(); 150 | double gd = gg - target.getGreen(); 151 | double bd = bb - target.getBlue(); 152 | 153 | return Math.sqrt((2.0 + rm / 256.0) * rd * rd + 154 | 4.0 * gd * gd + 155 | ((2.0 + (255.0 - rm) / 256.0) * bd * bd)); 156 | } 157 | 158 | public void writeToRom(byte[] romImage, int offset) { 159 | int r1 = bgSwatch.r(); 160 | int g1 = bgSwatch.g(); 161 | int b1 = bgSwatch.b(); 162 | // gggrrrrr 0bbbbbgg 163 | romImage[offset] = (byte) (r1 | (g1 << 5)); 164 | romImage[offset + 1] = (byte) ((g1 >> 3) | (b1 << 2)); 165 | 166 | int r2 = fgSwatch.r(); 167 | int g2 = fgSwatch.g(); 168 | int b2 = fgSwatch.b(); 169 | romImage[offset + 6] = (byte) (r2 | (g2 << 5)); 170 | romImage[offset + 7] = (byte) ((g2 >> 3) | (b2 << 2)); 171 | 172 | // Mid-tone. 173 | RGB555 rgbMid = findMidTone(new RGB555(r1, g1, b1), new RGB555(r2, g2, b2)); 174 | 175 | romImage[offset + 2] = (byte) (rgbMid.r() | (rgbMid.g() << 5)); 176 | romImage[offset + 3] = (byte) ((rgbMid.g() >> 3) | (rgbMid.b() << 2)); 177 | romImage[offset + 4] = romImage[offset + 2]; 178 | romImage[offset + 5] = romImage[offset + 3]; 179 | } 180 | 181 | public void deselect() { 182 | bgSwatch.deselect(); 183 | fgSwatch.deselect(); 184 | } 185 | } -------------------------------------------------------------------------------- /src/main/java/paletteEditor/SwatchPanel.java: -------------------------------------------------------------------------------- 1 | package paletteEditor; 2 | 3 | import net.miginfocom.swing.MigLayout; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.util.LinkedList; 8 | import java.util.Random; 9 | 10 | public class SwatchPanel extends JPanel implements SwatchPair.Listener { 11 | SwatchPair.Listener listener; 12 | 13 | private final LinkedList swatchPairs = new LinkedList<>(); 14 | private final Random random = new Random(); 15 | 16 | public final SwatchPair normalSwatchPair = new SwatchPair(); 17 | public final SwatchPair shadedSwatchPair = new SwatchPair(); 18 | public final SwatchPair alternateSwatchPair = new SwatchPair(); 19 | public final SwatchPair cursorSwatchPair = new SwatchPair(); 20 | public final SwatchPair scrollBarSwatchPair = new SwatchPair(); 21 | 22 | private Swatch selectedSwatch; 23 | 24 | public SwatchPanel() { 25 | setLayout(new MigLayout()); 26 | 27 | JButton randomizeButton = new JButton("Randomize all"); 28 | randomizeButton.addActionListener((e) -> randomize()); 29 | add(randomizeButton, "grow, span, wrap"); 30 | 31 | JButton cloneButton = new JButton("Clone color"); 32 | cloneButton.setPreferredSize(new Dimension(0, 0)); 33 | cloneButton.addActionListener(e -> cloneStart()); 34 | add(cloneButton, "grow, span, wrap"); 35 | 36 | JButton swapButton = new JButton("Swap color"); 37 | swapButton.setPreferredSize(new Dimension(0, 0)); 38 | swapButton.addActionListener(e -> swapStart()); 39 | add(swapButton, "grow, span, wrap"); 40 | 41 | add(normalSwatchPair, "Normal"); 42 | add(shadedSwatchPair, "Shaded"); 43 | add(alternateSwatchPair, "Alternate"); 44 | add(cursorSwatchPair, "Cursor"); 45 | add(scrollBarSwatchPair, "Scroll Bar"); 46 | } 47 | 48 | enum CommandState { 49 | OFF, 50 | SWAP, 51 | CLONE 52 | } 53 | CommandState commandState; 54 | private void swapStart() { 55 | if (selectedSwatch == null) { 56 | return; 57 | } 58 | commandState = CommandState.SWAP; 59 | updateCursor(); 60 | } 61 | private void cloneStart() { 62 | if (selectedSwatch == null) { 63 | return; 64 | } 65 | commandState = CommandState.CLONE; 66 | updateCursor(); 67 | } 68 | 69 | private void updateCursor() { 70 | setCursor(new Cursor(commandState == CommandState.OFF 71 | ? Cursor.DEFAULT_CURSOR 72 | : Cursor.HAND_CURSOR)); 73 | } 74 | 75 | public void addListener(SwatchPair.Listener listener) { 76 | this.listener = listener; 77 | } 78 | 79 | public void add(SwatchPair swatchPair, String swatchPairName) { 80 | swatchPair.registerToPanel(this, swatchPairName); 81 | swatchPairs.add(swatchPair); 82 | swatchPair.addListener(this); 83 | } 84 | 85 | public void randomize() { 86 | for (SwatchPair swatchPair : swatchPairs) { 87 | swatchPair.randomize(random); 88 | } 89 | } 90 | 91 | private void handleClone(Swatch swatch) { 92 | if (commandState != CommandState.CLONE) { 93 | return; 94 | } 95 | swatch.setRGB(selectedSwatch.r(), selectedSwatch.g(), selectedSwatch.b()); 96 | commandState = CommandState.OFF; 97 | updateCursor(); 98 | } 99 | 100 | private void handleSwap(Swatch swatch) { 101 | if (commandState != CommandState.SWAP) { 102 | return; 103 | } 104 | int r = swatch.r(); 105 | int g = swatch.g(); 106 | int b = swatch.b(); 107 | swatch.setRGB(selectedSwatch.r(), selectedSwatch.g(), selectedSwatch.b()); 108 | selectedSwatch.setRGB(r, g, b); 109 | commandState = CommandState.OFF; 110 | updateCursor(); 111 | } 112 | 113 | @Override 114 | public void swatchSelected(Swatch swatch) { 115 | handleSwap(swatch); 116 | handleClone(swatch); 117 | selectedSwatch = swatch; 118 | for (SwatchPair swatchPair : swatchPairs) { 119 | swatchPair.deselect(); 120 | } 121 | int w = 2; 122 | swatch.setBorder(BorderFactory.createCompoundBorder( 123 | BorderFactory.createMatteBorder(w, w, w, w, Color.BLACK), 124 | BorderFactory.createMatteBorder(w, w, w, w, Color.WHITE))); 125 | if (listener != null) { 126 | listener.swatchSelected(swatch); 127 | } 128 | } 129 | 130 | @Override 131 | public void swatchChanged() { 132 | if (listener != null) { 133 | listener.swatchChanged(); 134 | } 135 | } 136 | 137 | public void writeToRom(byte[] romImage, int selectedPaletteOffset) { 138 | normalSwatchPair.writeToRom(romImage, selectedPaletteOffset); 139 | shadedSwatchPair.writeToRom(romImage, selectedPaletteOffset + 8); 140 | alternateSwatchPair.writeToRom(romImage, selectedPaletteOffset + 16); 141 | cursorSwatchPair.writeToRom(romImage, selectedPaletteOffset + 24); 142 | scrollBarSwatchPair.writeToRom(romImage, selectedPaletteOffset + 32); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/songManager/SongManager.java: -------------------------------------------------------------------------------- 1 | package songManager; 2 | 3 | import Document.Document; 4 | import Document.LSDSavFile; 5 | import net.miginfocom.swing.MigLayout; 6 | import utils.EditorPreferences; 7 | import utils.FileDialogLauncher; 8 | 9 | import java.awt.*; 10 | import javax.swing.JButton; 11 | import javax.swing.JList; 12 | import java.awt.event.WindowAdapter; 13 | import java.awt.event.WindowEvent; 14 | import java.io.*; 15 | 16 | import javax.swing.event.ListSelectionListener; 17 | import javax.swing.event.ListSelectionEvent; 18 | import javax.swing.*; 19 | 20 | public class SongManager extends JFrame implements ListSelectionListener { 21 | JButton addLsdSngButton = new JButton(); 22 | JButton clearSlotButton = new JButton(); 23 | JButton exportLsdSngButton = new JButton(); 24 | JProgressBar jRamUsageIndicator = new JProgressBar(); 25 | JList songList = new JList<>( new String[] { " " } ); 26 | JScrollPane songs = new JScrollPane(songList); 27 | 28 | byte[] romImage; 29 | 30 | LSDSavFile savFile; 31 | 32 | public SongManager(JFrame parent, Document document) { 33 | parent.setEnabled(false); 34 | 35 | romImage = document.romImage(); 36 | savFile = document.savFile(); 37 | 38 | addLsdSngButton.setText("Add songs..."); 39 | addLsdSngButton.addActionListener(e -> addLsdSngButton_actionPerformed()); 40 | clearSlotButton.setEnabled(false); 41 | clearSlotButton.setText("Remove songs"); 42 | clearSlotButton.addActionListener(e -> clearSlotButton_actionPerformed()); 43 | exportLsdSngButton.setEnabled(false); 44 | exportLsdSngButton.setToolTipText("Export song to .lsdprj"); 45 | exportLsdSngButton.setText("Export songs..."); 46 | exportLsdSngButton.addActionListener(e -> exportLsdSngButton_actionPerformed()); 47 | songList.addListSelectionListener(this); 48 | 49 | jRamUsageIndicator.setString(""); 50 | jRamUsageIndicator.setStringPainted(true); 51 | 52 | this.setResizable(false); 53 | this.setTitle("Song Manager"); 54 | 55 | songs.setMinimumSize(new Dimension(0, 180)); 56 | 57 | java.awt.Container panel = this.getContentPane(); 58 | MigLayout layout = new MigLayout("wrap", "[]8[]"); 59 | panel.setLayout(layout); 60 | panel.add(songs, "cell 0 0 1 6, growx, growy"); 61 | panel.add(jRamUsageIndicator, "cell 0 6 1 1, growx"); 62 | panel.add(addLsdSngButton, "cell 1 0 1 1, growx"); 63 | panel.add(exportLsdSngButton, "cell 1 1 1 1, growx"); 64 | panel.add(clearSlotButton, "cell 1 2 1 1, growx, gaptop 10, aligny top"); 65 | 66 | pack(); 67 | setVisible(true); 68 | 69 | addWindowListener(new WindowAdapter() { 70 | @Override 71 | public void windowClosing(WindowEvent e) { 72 | super.windowClosing(e); 73 | document.setSavFile(savFile); 74 | document.setRomImage(romImage); 75 | parent.setEnabled(true); 76 | } 77 | }); 78 | 79 | savFile.populateSongList(songList); 80 | updateRamUsageIndicator(); 81 | } 82 | 83 | public void clearSlotButton_actionPerformed() { 84 | if (songList.isSelectionEmpty()) { 85 | JOptionPane.showMessageDialog(this, "Please select a song!", 86 | "No song selected!", JOptionPane.ERROR_MESSAGE); 87 | return; 88 | } 89 | int[] songs = songList.getSelectedIndices(); 90 | 91 | for (int song : songs) 92 | savFile.clearSong(song); 93 | savFile.populateSongList(songList); 94 | updateRamUsageIndicator(); 95 | } 96 | 97 | private void updateRamUsageIndicator() { 98 | jRamUsageIndicator.setMaximum(savFile.totalBlockCount()); 99 | jRamUsageIndicator.setValue(savFile.usedBlockCount()); 100 | jRamUsageIndicator.setString("File mem. used: " 101 | + savFile.usedBlockCount() + "/" + savFile.totalBlockCount()); 102 | } 103 | 104 | public void exportLsdSngButton_actionPerformed() { 105 | if (songList.isSelectionEmpty()) { 106 | JOptionPane.showMessageDialog(this, "Please select a song!", 107 | "No song selected!", JOptionPane.ERROR_MESSAGE); 108 | return; 109 | } 110 | 111 | int[] songs = songList.getSelectedIndices(); 112 | 113 | if (songs.length == 1) { 114 | File f = FileDialogLauncher.save(this, "Export Song", "lsdprj"); 115 | if (f == null) { 116 | return; 117 | } 118 | savFile.exportSongToFile(songs[0], f.getAbsolutePath(), romImage); 119 | } else if (songs.length > 1) { 120 | JFileChooser fileChooser = new JFileChooser(EditorPreferences.lastDirectory("lsdprj")); 121 | fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 122 | fileChooser.setDialogTitle( 123 | "Batch export selected songs to .lsdprj files"); 124 | int ret_val = fileChooser.showDialog(null, "Choose Directory"); 125 | 126 | if (JFileChooser.APPROVE_OPTION == ret_val) { 127 | String directory = fileChooser.getSelectedFile().getAbsolutePath(); 128 | 129 | for (int song : songs) { 130 | String filename = savFile.getFileName(song).toLowerCase() 131 | + "-" + savFile.version(song) + ".lsdprj"; 132 | String path = directory + File.separator + filename; 133 | String[] options = { "Yes", "No", "Cancel" }; 134 | File f = new File(path); 135 | if (f.exists()) { 136 | int overWrite = JOptionPane.showOptionDialog( 137 | this, "File \"" 138 | + filename 139 | + "\" already exists.\n" 140 | + "Overwrite existing file?", "Warning", 141 | JOptionPane.YES_NO_CANCEL_OPTION, 142 | JOptionPane.WARNING_MESSAGE, null, options, 143 | options[1]); 144 | 145 | if (overWrite == JOptionPane.YES_OPTION) { 146 | boolean deleted; 147 | try { 148 | deleted = f.delete(); 149 | } catch (Exception fileInUse) { 150 | deleted = false; 151 | } 152 | if (!deleted) { 153 | JOptionPane.showMessageDialog(this, 154 | "Could not delete file."); 155 | continue; 156 | } 157 | } else if (overWrite == JOptionPane.NO_OPTION) { 158 | continue; 159 | } else if (overWrite == JOptionPane.CANCEL_OPTION) 160 | return; 161 | } 162 | if (savFile.getBlocksUsed(song) > 0) { 163 | savFile.exportSongToFile(song, path, romImage); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | public void addLsdSngButton_actionPerformed() { 171 | FileDialog fileDialog = new FileDialog(this, 172 | "Load Songs", 173 | FileDialog.LOAD); 174 | fileDialog.setDirectory(EditorPreferences.lastDirectory("lsdprj")); 175 | fileDialog.setFile("*.lsdsng;*.lsdprj"); 176 | fileDialog.setMultipleMode(true); 177 | fileDialog.setVisible(true); 178 | 179 | File[] files = fileDialog.getFiles(); 180 | if (files.length == 0) { 181 | return; 182 | } 183 | 184 | try { 185 | for (File f : files) { 186 | if (f.getName().toLowerCase().endsWith(".lsdsng") || 187 | f.getName().toLowerCase().endsWith(".lsdprj")) { 188 | savFile.addSongFromFile(f.getAbsoluteFile().toString(), romImage); 189 | EditorPreferences.setLastPath("lsdprj", f.getAbsolutePath()); 190 | } else { 191 | JOptionPane.showMessageDialog(this, 192 | "Unknown file extension: " + f.getName(), 193 | "Song add failed", 194 | JOptionPane.ERROR_MESSAGE); 195 | } 196 | } 197 | } catch (Exception e) { 198 | JOptionPane.showMessageDialog(this, 199 | e.getMessage(), 200 | "Song add failed", 201 | JOptionPane.ERROR_MESSAGE); 202 | } 203 | savFile.populateSongList(songList); 204 | updateRamUsageIndicator(); 205 | } 206 | 207 | @Override 208 | public void valueChanged(ListSelectionEvent e) { 209 | boolean enable = !songList.isSelectionEmpty(); 210 | clearSlotButton.setEnabled(enable); 211 | 212 | int[] songs = songList.getSelectedIndices(); 213 | if (songs.length == 1) { 214 | enable = savFile.isValid(songs[0]); 215 | } 216 | exportLsdSngButton.setEnabled(enable); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/structures/LSDJFont.java: -------------------------------------------------------------------------------- 1 | package structures; 2 | 3 | import java.awt.Color; 4 | import java.awt.image.BufferedImage; 5 | 6 | /** 7 | * Helper class to access and manipulate font data. 8 | * This class acts as a span over data owned elsewhere and acts in it as it was its own. 9 | * @author Eiyeron 10 | */ 11 | public class LSDJFont extends ROMDataManipulator { 12 | public static final int TILE_COUNT = 71; 13 | public static final int GFX_TILE_COUNT = 46; 14 | public static final int FONT_NUM_TILES_X = 8; 15 | public static final int FONT_NUM_TILES_Y = (int)Math.ceil(TILE_COUNT/ (float)FONT_NUM_TILES_X); 16 | public static final int GFX_FONT_NUM_TILES_Y = (int)Math.ceil((TILE_COUNT + GFX_TILE_COUNT)/ (float)FONT_NUM_TILES_X); 17 | public static final int FONT_MAP_WIDTH = FONT_NUM_TILES_X * 8; 18 | public static final int FONT_MAP_HEIGHT = FONT_NUM_TILES_Y * 8; 19 | public static final int GFX_FONT_MAP_HEIGHT = (GFX_FONT_NUM_TILES_Y) * 8; 20 | public static final int FONT_HEADER_SIZE = 130; 21 | public static final int FONT_COUNT = 3; 22 | public static final int FONT_SIZE = 0xe96; 23 | public static final int FONT_NAME_LENGTH = 4; 24 | public static final int FONT_TILE_SIZE = 16; 25 | public static final int GFX_SIZE = FONT_TILE_SIZE * GFX_TILE_COUNT; 26 | 27 | private int gfxDataOffset = -1; 28 | 29 | public void setGfxDataOffset(int gfxDataOffset) { 30 | this.gfxDataOffset = gfxDataOffset; 31 | } 32 | 33 | private int getTileDataLocation(int index) { 34 | if (index >= TILE_COUNT) { 35 | index -= TILE_COUNT; 36 | return getGfxTileDataLocation(index); 37 | } 38 | if (index < 0 || index >= TILE_COUNT) 39 | { 40 | // TODO exception? 41 | return -1; 42 | } 43 | return getDataOffset() + index * FONT_TILE_SIZE; 44 | } 45 | 46 | private int getGfxTileDataLocation(int index) { 47 | if (index < 0 || index >= GFX_TILE_COUNT) 48 | { 49 | // TODO exception? 50 | return -1; 51 | } 52 | return gfxDataOffset + index * FONT_TILE_SIZE; 53 | } 54 | 55 | public int getPixel(int x, int y) { 56 | if (x < 0 || x >= FONT_MAP_WIDTH || y < 0 || y >= GFX_FONT_MAP_HEIGHT) 57 | return -1; 58 | 59 | int tileToRead = (y / 8) * 8 + x / 8; 60 | int tileOffset = getTileDataLocation(tileToRead) + (y % 8) * 2; 61 | int xMask = 7 - (x % 8); 62 | int value = (romImage[tileOffset] >> xMask) & 1; 63 | value |= ((romImage[tileOffset + 1] >> xMask) & 1) << 1; 64 | return value; 65 | } 66 | // - Tile data manipulation - 67 | // Note : those functions only affect the normal variant tileset. 68 | // In the future it might be good to either provide alternative functions 69 | // or to extend them to allow editing the other variants too. 70 | 71 | public int getTilePixel(int tile, int localX, int localY) { 72 | return getPixel((tile % FONT_NUM_TILES_X) * 8 + (localX % 8), (tile / FONT_NUM_TILES_X) * 8 + (localY % 8)); 73 | } 74 | 75 | private void setPixel(int x, int y, int color) { 76 | assert color >= 1 && color <= 3; 77 | if (x < 0 || x >= FONT_MAP_WIDTH || y < 0 || y >= GFX_FONT_MAP_HEIGHT) 78 | return; 79 | int localX = x % 8; 80 | int localY = y % 8; 81 | int tileToEdit = (y / 8) * 8 + x / 8; 82 | 83 | int tileOffset = getTileDataLocation(tileToEdit) + localY * 2; 84 | int xMask = 0x80 >> localX; 85 | romImage[tileOffset] &= 0xff ^ xMask; 86 | romImage[tileOffset + 1] &= 0xff ^ xMask; 87 | switch (color) { 88 | case 3: 89 | romImage[tileOffset + 1] |= xMask; 90 | case 2: 91 | romImage[tileOffset] |= xMask; 92 | } 93 | } 94 | 95 | public void setTilePixel(int tile, int localX, int localY, int color) { 96 | setPixel((tile % FONT_NUM_TILES_X) * 8 + (localX % 8), 97 | (tile / FONT_NUM_TILES_X) * 8 + (localY % 8), color); 98 | } 99 | 100 | public void rotateTileUp(int tile) { 101 | int tileOffset = getTileDataLocation(tile); 102 | byte line0origin1 = romImage[tileOffset]; 103 | byte line0origin2 = romImage[tileOffset + 1]; 104 | for (int i = 0; i < 8; i++) { 105 | int lineTargetOffset = tileOffset + i * 2; 106 | int lineOriginOffset = tileOffset + (i + 1 % 8) * 2; 107 | romImage[lineTargetOffset] = romImage[lineOriginOffset]; 108 | romImage[lineTargetOffset + 1] = romImage[lineOriginOffset + 1]; 109 | } 110 | romImage[tileOffset + 7 * 2] = line0origin1; 111 | romImage[tileOffset + 7 * 2 + 1] = line0origin2; 112 | } 113 | 114 | public void rotateTileDown(int tile) { 115 | int tileOffset = getTileDataLocation(tile); 116 | byte line7origin1 = romImage[tileOffset + 7 * 2]; 117 | byte line7origin2 = romImage[tileOffset + 7 * 2 + 1]; 118 | for (int i = 7; i > 0; i--) { 119 | int lineTargetOffset = tileOffset + i * 2; 120 | int lineOriginOffset = tileOffset + (i - 1) * 2; 121 | romImage[lineTargetOffset] = romImage[lineOriginOffset]; 122 | romImage[lineTargetOffset + 1] = romImage[lineOriginOffset + 1]; 123 | } 124 | romImage[tileOffset] = line7origin1; 125 | romImage[tileOffset + 1] = line7origin2; 126 | } 127 | 128 | public void rotateTileRight(int tile) { 129 | int tileOffset = getTileDataLocation(tile); 130 | for (int i = 0; i < FONT_TILE_SIZE; i++) { 131 | byte currentByte = romImage[tileOffset + i]; 132 | byte shiftedByte = (byte) (((currentByte & 1) << 7) | ((currentByte >> 1) & 0x7F)); 133 | romImage[tileOffset + i] = shiftedByte; 134 | } 135 | } 136 | 137 | public void rotateTileLeft(int tile) { 138 | int tileOffset = getTileDataLocation(tile); 139 | for (int i = 0; i < FONT_TILE_SIZE; i++) { 140 | byte currentByte = romImage[tileOffset + i]; 141 | byte shiftedByte = (byte) (((currentByte & 0x80) >> 7) | (currentByte << 1)); 142 | romImage[tileOffset + i] = shiftedByte; 143 | } 144 | } 145 | 146 | 147 | /** 148 | * Generates the inverted and shaded font variants from the normal tileset. 149 | */ 150 | public void generateShadedAndInvertedTiles() { 151 | for (int i = 2; i < TILE_COUNT; i++) { 152 | generateShadedTileVariant(i); 153 | generateInvertedTileVariant(i); 154 | } 155 | } 156 | 157 | public void generateInvertedTileVariant(int index) { 158 | if (index < 2 || index > TILE_COUNT) { 159 | // TODO exception? 160 | return; 161 | } 162 | int sourceLocation = getTileDataLocation(index); // The two first tiles are not mirrored. 163 | int invertedLocation = sourceLocation + 0x4d2; 164 | for (int i = 0; i < FONT_TILE_SIZE; i += 2) { 165 | romImage[invertedLocation + i] = (byte) ~romImage[sourceLocation + i + 1]; 166 | romImage[invertedLocation + i + 1] = (byte) ~romImage[sourceLocation + i]; 167 | } 168 | } 169 | 170 | 171 | public void generateShadedTileVariant(int index) { 172 | if (index < 2 || index > TILE_COUNT) { 173 | // TODO exception? 174 | return; 175 | } 176 | int sourceLocation = getTileDataLocation(index); // The two first tiles are not mirrored. 177 | int shadedLocation = sourceLocation + 0x4d2 * 2; 178 | for (int i = 0; i < FONT_TILE_SIZE; i += 2) { 179 | int sourceByte = romImage[sourceLocation + i]; 180 | if (i % 4 == 2) { 181 | romImage[shadedLocation + i] = (byte)(sourceByte | 0xaa); 182 | } else { 183 | romImage[shadedLocation + i] = (byte)(sourceByte | 0x55); 184 | } 185 | romImage[shadedLocation + i + 1] = romImage[sourceLocation + i + 1]; 186 | } 187 | } 188 | 189 | private int grayIndexToColor(int index) { 190 | switch (index) { 191 | case 0 : 192 | return 0xFFFFFF; 193 | case 1 : 194 | return 0x969696; 195 | case 2 : 196 | return 0x808080; 197 | case 3 : 198 | return 0x000000; 199 | default: 200 | return 0xDeadBeef; 201 | } 202 | } 203 | 204 | public String loadImageData(String name, BufferedImage image) { 205 | int numTiles = image.getHeight()/8 * image.getWidth()/8; 206 | // Limiting to either loading text tiles or load all tiles. No partial graphical tiles loading. 207 | int maxTileIndex = numTiles < LSDJFont.GFX_TILE_COUNT + LSDJFont.TILE_COUNT ? LSDJFont.TILE_COUNT :LSDJFont.TILE_COUNT + LSDJFont.GFX_TILE_COUNT; 208 | for (int y = 0; y < image.getHeight(); y++) { 209 | for (int x = 0; x < image.getWidth(); x++) { 210 | int currentTileIndex = (y / 8) * 8 + x / 8; 211 | if (currentTileIndex >= maxTileIndex) break; 212 | int rgb = image.getRGB(x, y); 213 | float[] color = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null); 214 | int lum = (int) (color[2] * 255); 215 | 216 | int col = 0; 217 | if (lum >= 192) 218 | col = 1; 219 | else if (lum >= 64) 220 | col = 2; 221 | else if (lum >= 0) 222 | col = 3; 223 | setTilePixel(currentTileIndex, x%8, y%8, col); 224 | } 225 | } 226 | StringBuilder sub; 227 | if (name.length() < 4) { 228 | sub = new StringBuilder(name); 229 | for (int i = 0; i < 4 - sub.length(); i++) 230 | sub.append(" "); 231 | } else 232 | sub = new StringBuilder(name.substring(0, 4)); 233 | return sub.toString(); 234 | } 235 | 236 | public BufferedImage saveDataToImage(Boolean includeGfxCharacters) { 237 | BufferedImage image = new BufferedImage(LSDJFont.FONT_MAP_WIDTH, includeGfxCharacters ? LSDJFont.GFX_FONT_MAP_HEIGHT : LSDJFont.FONT_MAP_HEIGHT, 238 | BufferedImage.TYPE_INT_RGB); 239 | 240 | int tileCount = includeGfxCharacters ? TILE_COUNT + GFX_TILE_COUNT : TILE_COUNT; 241 | for (int tile = 0; tile < tileCount; ++tile) { 242 | int baseX = (tile % FONT_NUM_TILES_X)*8; 243 | int baseY = (tile / FONT_NUM_TILES_X)*8; 244 | for (int y = 0; y < 8; ++y) { 245 | for (int x = 0; x < 8; ++x) { 246 | int colorIndex = getTilePixel(tile, x, y); 247 | image.setRGB(baseX + x, baseY + y, grayIndexToColor(colorIndex)); 248 | } 249 | } 250 | } 251 | return image; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/structures/ROMDataManipulator.java: -------------------------------------------------------------------------------- 1 | package structures; 2 | 3 | /** 4 | * Base class to centralize the concept of having an access to a ROM's binary data for edition. 5 | */ 6 | public abstract class ROMDataManipulator { 7 | 8 | protected int dataOffset = 0; 9 | protected byte[] romImage = null; 10 | 11 | public void setRomImage(byte[] romImage) { 12 | this.romImage = romImage; 13 | } 14 | 15 | public int getDataOffset() { 16 | return dataOffset; 17 | } 18 | 19 | public void setDataOffset(int dataOffset) { 20 | this.dataOffset = dataOffset; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/utils/CommandLineFunctions.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.util.Arrays; 8 | import java.util.Vector; 9 | 10 | import javax.imageio.ImageIO; 11 | 12 | import structures.LSDJFont; 13 | 14 | public class CommandLineFunctions { 15 | public static void pngToFont(String name, String pngFile, String fntFile) { 16 | try { 17 | byte[] buffer = new byte[LSDJFont.FONT_NUM_TILES_X * LSDJFont.FONT_NUM_TILES_Y * 16]; 18 | BufferedImage image = ImageIO.read(new File(pngFile)); 19 | if (image.getWidth() != LSDJFont.FONT_MAP_WIDTH && image.getHeight() != LSDJFont.FONT_MAP_HEIGHT) { 20 | System.err.println("Wrong size!"); 21 | return; 22 | } 23 | 24 | LSDJFont font = new LSDJFont(); 25 | font.setRomImage(buffer); 26 | font.setDataOffset(0); 27 | font.generateShadedAndInvertedTiles(); 28 | String sub = font.loadImageData(name, image); 29 | 30 | FontIO.saveFnt(new File(fntFile), sub, buffer); 31 | 32 | System.out.println("OK!"); 33 | } catch (IOException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | public static void fontToPng(String fntFile, String pngFile) { 39 | try { 40 | byte[] buffer = new byte[LSDJFont.FONT_NUM_TILES_X * LSDJFont.GFX_FONT_NUM_TILES_Y * 16]; 41 | FontIO.loadFnt(new File(fntFile), buffer); 42 | LSDJFont font = new LSDJFont(); 43 | font.setRomImage(buffer); 44 | font.setDataOffset(0); 45 | BufferedImage image = font.saveDataToImage(false); 46 | ImageIO.write(image, "PNG", new File(pngFile)); 47 | System.out.println("OK!"); 48 | } catch (IOException e) { 49 | e.printStackTrace(); 50 | } 51 | } 52 | 53 | public static void extractFontToPng(String romFileName, int numFont, boolean includeGfxCharacters) { 54 | if (numFont < 0 || numFont > 2) { 55 | // Already -1-ed. 56 | System.err.println("the font index must be comprised between 1 and 3."); 57 | return; 58 | } 59 | try { 60 | byte[] romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 61 | RandomAccessFile romFile = new RandomAccessFile(new File(romFileName), "r"); 62 | romFile.readFully(romImage); 63 | romFile.close(); 64 | LSDJFont font = new LSDJFont(); 65 | 66 | font.setRomImage(romImage); 67 | int selectedFontOffset = RomUtilities.findFontOffset(romImage) + ((numFont + 1) % 3) * LSDJFont.FONT_SIZE 68 | + LSDJFont.FONT_HEADER_SIZE; 69 | font.setDataOffset(selectedFontOffset); 70 | font.setGfxDataOffset(RomUtilities.findGfxFontOffset(romImage)); 71 | BufferedImage image = font.saveDataToImage(includeGfxCharacters); 72 | ImageIO.write(image, "PNG", new File(RomUtilities.getFontName(romImage, numFont) + ".png")); 73 | 74 | System.out.println("OK!"); 75 | } catch (IOException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | 80 | public static void loadPngToRom(String romFileName, String imageFileName, int numFont, String fontName) { 81 | if (numFont < 0 || numFont > 2) { 82 | // Already -1-ed. 83 | System.err.println("the font index must be comprised between 1 and 3."); 84 | return; 85 | } 86 | try { 87 | byte[] romImage = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 88 | RandomAccessFile romFile = new RandomAccessFile(new File(romFileName), "rw"); 89 | romFile.readFully(romImage); 90 | LSDJFont font = new LSDJFont(); 91 | 92 | font.setRomImage(romImage); 93 | int selectedFontOffset = RomUtilities.findFontOffset(romImage) + ((numFont + 1) % 3) * LSDJFont.FONT_SIZE 94 | + LSDJFont.FONT_HEADER_SIZE; 95 | font.setDataOffset(selectedFontOffset); 96 | font.generateShadedAndInvertedTiles(); 97 | 98 | String correctedName = font.loadImageData(fontName, ImageIO.read(new File(imageFileName))); 99 | RomUtilities.setFontName(romImage, numFont, correctedName); 100 | romFile.seek(0); 101 | romFile.write(romImage); 102 | romFile.close(); 103 | 104 | System.out.println("OK!"); 105 | } catch (IOException e) { 106 | e.printStackTrace(); 107 | } 108 | } 109 | 110 | // TODO Merge with KitEditor's own version 111 | private static boolean isRomBankAKit(int bankIndex, byte[] romImage) { 112 | int l_offset = bankIndex * RomUtilities.BANK_SIZE; 113 | byte l_char_1 = romImage[l_offset++]; 114 | byte l_char_2 = romImage[l_offset]; 115 | return (l_char_1 == 0x60 && l_char_2 == 0x40); 116 | } 117 | 118 | // TODO Merge with KitEditor's own version 119 | private static boolean isRomBankEmpty(int bankIndex, byte[] romImage) { 120 | int l_offset = bankIndex * RomUtilities.BANK_SIZE; 121 | byte l_char_1 = romImage[l_offset++]; 122 | byte l_char_2 = romImage[l_offset]; 123 | return (l_char_1 == -1 && l_char_2 == -1); 124 | } 125 | // TODO replace KitEditor's own version with that 126 | private static void clearKitBank(int bankIndex, byte[] romImage) { 127 | int baseOffset = bankIndex * RomUtilities.BANK_SIZE; 128 | int endOfBank = (bankIndex + 1) * RomUtilities.BANK_SIZE; 129 | 130 | Arrays.fill(romImage, baseOffset, endOfBank, (byte)0xFF); 131 | } 132 | 133 | public static void copyAllCustomizations(String originFileName, String destinationFileName) 134 | { 135 | try { 136 | byte[] originRomFile = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 137 | byte[] destinationRomFile = new byte[RomUtilities.BANK_SIZE * RomUtilities.BANK_COUNT]; 138 | RandomAccessFile originFile = new RandomAccessFile(new File(originFileName), "r"); 139 | originFile.readFully(originRomFile); 140 | 141 | RandomAccessFile destinationFile = new RandomAccessFile(new File(destinationFileName), "rw"); 142 | destinationFile.readFully(destinationRomFile); 143 | 144 | if (RomUtilities.getNumberOfPalettes(originRomFile) > RomUtilities.getNumberOfPalettes(destinationRomFile)) { 145 | System.err.println("Warning: Palettes skipped due to lack of space!"); 146 | } 147 | 148 | { 149 | int inBaseGfxOffset = RomUtilities.findGfxFontOffset(originRomFile); 150 | int outBaseGfxOffset = RomUtilities.findGfxFontOffset(destinationRomFile); 151 | System.arraycopy(originRomFile, inBaseGfxOffset, destinationRomFile, outBaseGfxOffset, 152 | (LSDJFont.GFX_TILE_COUNT * LSDJFont.FONT_TILE_SIZE)); 153 | } 154 | 155 | { 156 | int inBaseFontOffset = RomUtilities.findFontOffset(originRomFile); 157 | int outBaseFontOffset = RomUtilities.findFontOffset(destinationRomFile); 158 | 159 | System.arraycopy(originRomFile, inBaseFontOffset, destinationRomFile, outBaseFontOffset, 160 | (LSDJFont.FONT_SIZE + LSDJFont.FONT_HEADER_SIZE) * LSDJFont.FONT_COUNT); 161 | 162 | int inBaseFontNameOffset = RomUtilities.findFontNameOffset(originRomFile); 163 | int outBaseFontNameOffset = RomUtilities.findFontNameOffset(destinationRomFile); 164 | System.arraycopy(originRomFile, inBaseFontNameOffset, destinationRomFile, outBaseFontNameOffset, 165 | LSDJFont.FONT_NAME_LENGTH * LSDJFont.FONT_COUNT); 166 | } 167 | 168 | { 169 | int paletteCount = Math.min(RomUtilities.getNumberOfPalettes(originRomFile), 170 | RomUtilities.getNumberOfPalettes(destinationRomFile)); 171 | int inPaletteOffset = RomUtilities.findPaletteOffset(originRomFile); 172 | int outPaletteOffset = RomUtilities.findPaletteOffset(destinationRomFile); 173 | System.arraycopy(originRomFile, inPaletteOffset, destinationRomFile, outPaletteOffset, 174 | RomUtilities.PALETTE_SIZE * paletteCount); 175 | 176 | int inPaletteNameOffset = RomUtilities.findPaletteNameOffset(originRomFile); 177 | int outPaletteNameOffset = RomUtilities.findPaletteNameOffset(destinationRomFile); 178 | System.arraycopy(originRomFile, inPaletteNameOffset, destinationRomFile, outPaletteNameOffset, 179 | RomUtilities.PALETTE_NAME_SIZE * paletteCount); 180 | } 181 | 182 | Vector inKitsToCopy = new Vector<>(); 183 | for (int index = 0; index < RomUtilities.BANK_COUNT; ++index) { 184 | if (isRomBankAKit(index, originRomFile)) { 185 | inKitsToCopy.add(index); 186 | } 187 | } 188 | Vector outAvailableKitSlots = new Vector<>(); 189 | for (int index = 0; index < RomUtilities.BANK_COUNT; ++index) { 190 | if (isRomBankAKit(index, destinationRomFile) || isRomBankEmpty(index, destinationRomFile)) { 191 | outAvailableKitSlots.add(index); 192 | } 193 | } 194 | 195 | if (outAvailableKitSlots.size() < inKitsToCopy.size()) { 196 | System.err.printf("The destination file doesn't have enough kit slots (%d < %d). Aborting.", 197 | outAvailableKitSlots.size(), inKitsToCopy.size()); 198 | return; 199 | } 200 | 201 | int numToClone = inKitsToCopy.size(); 202 | for (int index = 0; index < numToClone; ++index) { 203 | int inIndexOfKitToCopy = inKitsToCopy.get(index); 204 | int outIndexOfKitToOverwrite = outAvailableKitSlots.get(index); 205 | System.arraycopy( 206 | originRomFile, inIndexOfKitToCopy * RomUtilities.BANK_SIZE, 207 | destinationRomFile, outIndexOfKitToOverwrite * RomUtilities.BANK_SIZE, 208 | RomUtilities.BANK_SIZE 209 | ); 210 | } 211 | // Cleaning the destination file 212 | for (int index = numToClone; index < outAvailableKitSlots.size(); ++index) { 213 | clearKitBank(outAvailableKitSlots.get(index), destinationRomFile); 214 | } 215 | 216 | RomUtilities.fixChecksum(destinationRomFile); 217 | destinationFile.seek(0); 218 | destinationFile.write(destinationRomFile); 219 | destinationFile.close(); 220 | 221 | System.out.println("OK!"); 222 | } catch (IOException e) { 223 | e.printStackTrace(); 224 | } 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/utils/EditorPreferences.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.File; 4 | import java.util.prefs.BackingStoreException; 5 | import java.util.prefs.Preferences; 6 | 7 | public class EditorPreferences { 8 | private static String userDir() { 9 | return System.getProperty("user.dir"); 10 | } 11 | 12 | public static String getKey(String name, String defaultValue) { 13 | return GlobalHolder.get(Preferences.class).get(name, defaultValue); 14 | } 15 | 16 | public static void putKey(String name, String value) { 17 | GlobalHolder.get(Preferences.class).put(name, value); 18 | } 19 | 20 | public static String lastPath(String extension) { 21 | return getKey("lastPath" + extension, userDir()); 22 | } 23 | 24 | public static String lastDirectory(String extension) { 25 | return new File(lastPath(extension)).getParent(); 26 | } 27 | 28 | public static void setLastPath(String extension, String value) { 29 | putKey("lastPath" + extension, value); 30 | } 31 | 32 | public static void clearAll() { 33 | try { 34 | GlobalHolder.get(Preferences.class).clear(); 35 | } catch (BackingStoreException e) { 36 | e.printStackTrace(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/utils/FileDialogLauncher.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.io.File; 6 | 7 | class CustomFileFilter implements java.io.FilenameFilter { 8 | CustomFileFilter(String[] fileExtensions) { 9 | this.fileExtensions = fileExtensions; 10 | } 11 | 12 | @Override 13 | public boolean accept(File dir, String name) { 14 | for (String fileExtension : fileExtensions) { 15 | if (name.endsWith(fileExtension)) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | } 21 | 22 | String[] fileExtensions; 23 | } 24 | 25 | public class FileDialogLauncher { 26 | public static File load(JFrame parent, String title, String fileExtension) { 27 | return open(parent, title, new String[] { fileExtension }, FileDialog.LOAD); 28 | } 29 | 30 | public static File load(JFrame parent, String title, String[] fileExtensions) { 31 | return open(parent, title, fileExtensions, FileDialog.LOAD); 32 | } 33 | 34 | public static File save(JFrame parent, String title, String fileExtension) { 35 | return open(parent, title, new String[] { fileExtension }, FileDialog.SAVE); 36 | } 37 | 38 | public static File save(JFrame parent, String title, String[] fileExtensions) { 39 | return open(parent, title, fileExtensions, FileDialog.SAVE); 40 | } 41 | 42 | private static void setFilenameFilter(FileDialog fileDialog, String[] fileExtensions) { 43 | if (System.getProperty("os.name").contains("Windows")) { 44 | // Works on Windows only. 45 | StringBuilder fileMatch = new StringBuilder("*." + fileExtensions[0]); 46 | for (int i = 1; i < fileExtensions.length; ++i) { 47 | fileMatch.append(";*.").append(fileExtensions[i]); 48 | } 49 | fileDialog.setFile(fileMatch.toString()); 50 | } else { 51 | // Does not work on Windows. 52 | fileDialog.setFilenameFilter(new CustomFileFilter(fileExtensions)); 53 | } 54 | } 55 | 56 | private static File open(JFrame parent, String title, String[] fileExtensions, int mode) { 57 | FileDialog fileDialog = new FileDialog(parent, title, mode); 58 | fileDialog.setDirectory(EditorPreferences.lastDirectory(fileExtensions[0])); 59 | setFilenameFilter(fileDialog, fileExtensions); 60 | fileDialog.setVisible(true); 61 | 62 | String directory = fileDialog.getDirectory(); 63 | String fileName = fileDialog.getFile(); 64 | if (fileName == null) { 65 | return null; 66 | } 67 | 68 | boolean filenameMatchesExtension = false; 69 | String selectedExtension = null; 70 | for (String fileExtension : fileExtensions) { 71 | if (fileName.toLowerCase().endsWith("." + fileExtension)) { 72 | filenameMatchesExtension = true; 73 | selectedExtension = fileExtension; 74 | break; 75 | } 76 | } 77 | if (!filenameMatchesExtension) { 78 | selectedExtension = fileExtensions[0]; 79 | fileName += "." + fileExtensions[0]; 80 | if (mode == FileDialog.SAVE && 81 | new File(directory + fileName).exists() && 82 | JOptionPane.showConfirmDialog(parent, 83 | fileName + " already exists.\n" + 84 | "Do you want to replace it?", 85 | "Confirm Save As", 86 | JOptionPane.YES_NO_OPTION, 87 | JOptionPane.WARNING_MESSAGE) == JOptionPane.NO_OPTION) { 88 | return null; 89 | } 90 | } 91 | String path = directory + fileName; 92 | assert selectedExtension != null; 93 | EditorPreferences.setLastPath(selectedExtension, path); 94 | return new File(path); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/utils/FontIO.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | 7 | import structures.LSDJFont; 8 | 9 | public class FontIO { 10 | 11 | static void loadFnt(File file, byte[] array) throws IOException { 12 | loadFnt(file, array, 0); 13 | } 14 | 15 | public static String loadFnt(File file, byte[] array, int arrayOffset) throws IOException { 16 | StringBuilder name = new StringBuilder(); 17 | int bytesPerTile = 16; 18 | int fontSize = LSDJFont.TILE_COUNT * bytesPerTile; 19 | java.io.RandomAccessFile f = new java.io.RandomAccessFile(file, "r"); 20 | 21 | for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; ++i) { 22 | name.append((char) f.read()); 23 | } 24 | 25 | for (int i = 0; i < fontSize; ++i) { 26 | array[i + arrayOffset] = (byte) f.read(); 27 | } 28 | 29 | f.close(); 30 | return name.toString(); 31 | } 32 | 33 | static void saveFnt(File file, String fontName, byte[] array) throws IOException { 34 | saveFnt(file, fontName, array, 0); 35 | } 36 | 37 | public static void saveFnt(File file, String fontName, byte[] array, int arrayOffset) throws IOException { 38 | FileOutputStream f = new java.io.FileOutputStream(file); 39 | f.write(fontName.charAt(0)); 40 | f.write(fontName.charAt(1)); 41 | f.write(fontName.charAt(2)); 42 | f.write(fontName.charAt(3)); 43 | int bytesPerTile = 16; 44 | int fontSize = LSDJFont.TILE_COUNT * bytesPerTile; 45 | for (int i = 0; i < fontSize; ++i) { 46 | f.write(array[i + arrayOffset]); 47 | } 48 | f.close(); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/utils/GlobalHolder.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Singletons is an useful template but it does have one big issue: it doesnn't 8 | * comply with the Single Responsibility Principle as it both holds an instance 9 | * and manages its lifetime. 10 | *

11 | * This class only offers a way to store and access a global instance as long as 12 | * the using code manages the lifetime of said global. 13 | * Allows for using a kind-of namespace and class override, 14 | * providing the overriding class is a child of the given object. 15 | * 16 | * @author Florian Dormont 17 | */ 18 | public class GlobalHolder { 19 | static private Map globals = null; 20 | 21 | private static void lazyInstantiation() { 22 | if (globals == null) 23 | globals = new HashMap<>(); 24 | } 25 | 26 | public static void set(Object object) { 27 | lazyInstantiation(); 28 | globals.put(object.getClass().getCanonicalName(), object); 29 | } 30 | 31 | public static void set(C object, Class cls) { 32 | lazyInstantiation(); 33 | globals.put(cls.getCanonicalName(), object); 34 | } 35 | 36 | public static void set(C object, Class cls, String namespace) { 37 | lazyInstantiation(); 38 | globals.put(namespace + "::" + cls.getCanonicalName(), object); 39 | } 40 | 41 | public static void set(Object object, String namespace) { 42 | lazyInstantiation(); 43 | globals.put(namespace + "::" + object.getClass().getCanonicalName(), object); 44 | } 45 | 46 | @SuppressWarnings("unchecked") 47 | public static C get(Class cls) { 48 | lazyInstantiation(); 49 | return (C) globals.get(cls.getCanonicalName()); 50 | } 51 | 52 | @SuppressWarnings("unchecked") 53 | public static C get(Class cls, String namespace) { 54 | lazyInstantiation(); 55 | return (C) globals.get(namespace + "::" + cls.getCanonicalName()); 56 | } 57 | 58 | public static C release(Class cls) { 59 | lazyInstantiation(); 60 | C c = get(cls); 61 | globals.put(c.getClass().getCanonicalName(), null); 62 | return c; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/utils/RomUtilities.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import structures.LSDJFont; 4 | 5 | public class RomUtilities { 6 | public static final int BANK_COUNT = 64; 7 | public static final int BANK_SIZE = 0x4000; 8 | 9 | public static final int COLOR_SET_SIZE = 4 * 2; // one color set contains 4 colors 10 | public static final int NUM_COLOR_SETS = 5; // one palette contains 5 color sets 11 | public static final int PALETTE_SIZE = COLOR_SET_SIZE * NUM_COLOR_SETS; 12 | public static final int PALETTE_NAME_SIZE = 5; 13 | 14 | private static int findGrayscalePaletteNames(byte[] romImage) 15 | { 16 | for (int i = 0x4000 * 27;i < 0x4000 * 28; ++i) { 17 | if (romImage[i] != 0 && 18 | romImage[i + 1] != 0 && 19 | romImage[i + 2] != 0 && 20 | romImage[i + 3] != 0 && 21 | romImage[i + 4] == 0 && 22 | romImage[i + 5] != 0 && 23 | romImage[i + 6] != 0 && 24 | romImage[i + 7] != 0 && 25 | romImage[i + 8] != 0 && 26 | romImage[i + 9] == 0 && 27 | romImage[i + 10] != 0 && 28 | romImage[i + 11] != 0 && 29 | romImage[i + 12] != 0 && 30 | romImage[i + 13] != 0 && 31 | romImage[i + 14] == 0) 32 | { 33 | return i + 15; 34 | } 35 | } 36 | return -1; 37 | } 38 | 39 | private static int findScreenBackgroundData(byte[] romImage) 40 | { 41 | int numPalettes = getNumberOfPalettes(romImage); 42 | if (numPalettes == -1) 43 | { 44 | return -1; 45 | } 46 | for (int i = 0x4000; i < 0x8000; ++i) 47 | { 48 | if (romImage[i] == 0 && 49 | romImage[i + 1] == 0 && 50 | romImage[i + 2] == 0 && 51 | romImage[i + 3] == 0 && 52 | romImage[i + 4] == 0 && 53 | romImage[i + 5] == 0 && 54 | romImage[i + 6] == 0 && 55 | romImage[i + 7] == 0 && 56 | romImage[i + 8] == 0 && 57 | romImage[i + 9] == 0 && 58 | romImage[i + 10] == 0 && 59 | romImage[i + 11] == 0 && 60 | romImage[i + 12] == 0 && 61 | romImage[i + 13] == 0 && 62 | romImage[i + 14] == 0 && 63 | romImage[i + 15] == 0 && 64 | romImage[i + 16] == 0 && 65 | romImage[i + 17] == 72 && 66 | romImage[i + 18] == 72 && 67 | romImage[i + 19] == 72) 68 | { 69 | return i; 70 | } 71 | } 72 | return -1; 73 | } 74 | 75 | public static int getNumberOfPalettes(byte[] romImage) 76 | { 77 | int baseOffset = findGrayscalePaletteNames(romImage); 78 | if (baseOffset == -1) 79 | { 80 | return -1; 81 | } 82 | 83 | int numPalettes = 0; 84 | for (int j = baseOffset + 4; romImage[j] == 0; j +=5) 85 | { 86 | ++numPalettes; 87 | } 88 | return numPalettes/2; 89 | } 90 | 91 | public static int findPaletteOffset(byte[] romImage) { 92 | // Finds the palette location by searching for the screen 93 | // backgrounds, which are defined directly after the palettes 94 | // in bank 1. 95 | int baseOffset = findScreenBackgroundData(romImage); 96 | if (baseOffset == -1) 97 | { 98 | return -1; 99 | } 100 | return baseOffset - getNumberOfPalettes(romImage) * PALETTE_SIZE; 101 | } 102 | 103 | public static int findPaletteNameOffset(byte[] romImage) { 104 | // Palette names are in bank 27. 105 | int baseOffset = findGrayscalePaletteNames(romImage); 106 | if (baseOffset == -1) 107 | { 108 | return -1; 109 | } 110 | 111 | return baseOffset + 5 * getNumberOfPalettes(romImage); 112 | } 113 | 114 | // Returns address of first graphics character. 115 | public static int findGfxFontOffset(byte[] romImage) { 116 | for (int i = 30 * 0x4000; i < 31 * 0x4000; ++i) { 117 | if (romImage[i] == 1 && romImage[i + 1] == 46 && romImage[i + 2] == 0 && romImage[i + 3] == 1) { 118 | return i + 2 + 8 * 16; 119 | } 120 | } 121 | return -1; 122 | } 123 | 124 | public static int findFontOffset(byte[] romImage) { 125 | int gfxOffset = findGfxFontOffset(romImage); 126 | int gfxCharacterCount = 46; 127 | int gfxCharacterSize = 16; 128 | return gfxOffset == -1 ? -1 : gfxOffset + gfxCharacterCount * gfxCharacterSize; 129 | } 130 | 131 | public static int findFontNameOffset(byte[] romImage) { 132 | // Palette names are in bank 27. 133 | int baseOffset = findGrayscalePaletteNames(romImage); 134 | if (baseOffset == -1) 135 | { 136 | return -1; 137 | } 138 | return baseOffset - 15; 139 | } 140 | 141 | public static String getFontName(byte[] romImage, int font) { 142 | int fontNameSize = 5; 143 | int nameOffset = findFontNameOffset(romImage); 144 | StringBuilder s = new StringBuilder(); 145 | for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; i++) { 146 | s.append((char) romImage[nameOffset + font * fontNameSize + i]); 147 | } 148 | return s.toString(); 149 | } 150 | 151 | public static void setFontName(byte[] romImage, int fontIndex, String fontName) { 152 | StringBuilder fontNameBuilder = new StringBuilder(fontName); 153 | while (fontNameBuilder.length() < 4) { 154 | fontNameBuilder.append(" "); 155 | } 156 | fontName = fontNameBuilder.toString(); 157 | int fontNameSize = 5; 158 | int nameOffset = findFontNameOffset(romImage); 159 | for (int i = 0; i < LSDJFont.FONT_NAME_LENGTH; i++) { 160 | romImage[nameOffset + fontIndex * fontNameSize + i] = (byte) fontName.charAt(i); 161 | } 162 | } 163 | 164 | public static void fixChecksum(byte[] romImage) { 165 | int checksum014D = 0; 166 | for (int i = 0x134; i < 0x14D; ++i) { 167 | checksum014D = checksum014D - romImage[i] - 1; 168 | } 169 | romImage[0x14D] = (byte) (checksum014D & 0xFF); 170 | 171 | int checksum014E = 0; 172 | for (int i = 0; i < romImage.length; ++i) { 173 | if (i == 0x14E || i == 0x14F) { 174 | continue; 175 | } 176 | checksum014E += romImage[i] & 0xFF; 177 | } 178 | 179 | romImage[0x14E] = (byte) ((checksum014E & 0xFF00) >> 8); 180 | romImage[0x14F] = (byte) (checksum014E & 0x00FF); 181 | } 182 | 183 | public static boolean validatePaletteData(byte[] romImage) { 184 | return getNumberOfPalettes(romImage) > 0 && 185 | findPaletteNameOffset(romImage) > 0 && 186 | findPaletteOffset(romImage) > 0; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/utils/StretchIcon.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.awt.Component; 4 | import java.awt.Container; 5 | import java.awt.Graphics; 6 | import java.awt.Graphics2D; 7 | import java.awt.Image; 8 | import java.awt.Insets; 9 | import java.awt.RenderingHints; 10 | import java.awt.image.BufferedImage; 11 | import java.awt.image.ImageObserver; 12 | import java.net.URL; 13 | 14 | import javax.swing.ImageIcon; 15 | 16 | /** 17 | * An Icon that scales its image to fill the component area, excluding any border or insets, optionally maintaining the image's 18 | * aspect ratio by padding and centering the scaled image horizontally or vertically. 19 | *

20 | * The class is a drop-in replacement for ImageIcon, except that the no-argument constructor is not supported. 21 | *

22 | * As the size of the Icon is determined by the size of the component in which it is displayed, utils.StretchIcon must only be used 23 | * in conjunction with a component and layout that does not depend on the size of the component's Icon. 24 | * 25 | * @version 1.1 01/15/2016 26 | * @author Darryl 27 | */ 28 | public class StretchIcon extends ImageIcon 29 | { 30 | /** 31 | * 32 | */ 33 | private static final long serialVersionUID = 1L; 34 | /** 35 | * Determines whether the aspect ratio of the image is maintained. Set to false to allow th image to distort to fill the 36 | * component. 37 | */ 38 | protected boolean proportionate = true; 39 | 40 | /** 41 | * Creates a utils.StretchIcon from an array of bytes. 42 | * 43 | * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG 44 | * 45 | * @see ImageIcon#ImageIcon(byte[]) 46 | */ 47 | public StretchIcon(byte[] imageData) 48 | { 49 | super(imageData); 50 | } 51 | 52 | /** 53 | * Creates a utils.StretchIcon from an array of bytes with the specified behavior. 54 | * 55 | * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG 56 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 57 | * fill the component. 58 | * 59 | * @see ImageIcon#ImageIcon(byte[]) 60 | */ 61 | public StretchIcon(byte[] imageData, boolean proportionate) 62 | { 63 | super(imageData); 64 | this.proportionate = proportionate; 65 | } 66 | 67 | /** 68 | * Creates a utils.StretchIcon from an array of bytes. 69 | * 70 | * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG 71 | * @param description a brief textual description of the image 72 | * 73 | * @see ImageIcon#ImageIcon(byte[], java.lang.String) 74 | */ 75 | public StretchIcon(byte[] imageData, String description) 76 | { 77 | super(imageData, description); 78 | } 79 | 80 | /** 81 | * Creates a utils.StretchIcon from an array of bytes with the specified behavior. 82 | * 83 | * @see ImageIcon#ImageIcon(byte[]) 84 | * @param imageData an array of pixels in an image format supported by the AWT Toolkit, such as GIF, JPEG, or (as of 1.3) PNG 85 | * @param description a brief textual description of the image 86 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 87 | * fill the component. 88 | * 89 | * @see ImageIcon#ImageIcon(byte[], java.lang.String) 90 | */ 91 | public StretchIcon(byte[] imageData, String description, boolean proportionate) 92 | { 93 | super(imageData, description); 94 | this.proportionate = proportionate; 95 | } 96 | 97 | /** 98 | * Creates a utils.StretchIcon from the image. 99 | * 100 | * @param image the image 101 | * 102 | * @see ImageIcon#ImageIcon(java.awt.Image) 103 | */ 104 | public StretchIcon(Image image) 105 | { 106 | super(image); 107 | } 108 | 109 | /** 110 | * Creates a utils.StretchIcon from the image with the specified behavior. 111 | * 112 | * @param image the image 113 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 114 | * fill the component. 115 | * 116 | * @see ImageIcon#ImageIcon(java.awt.Image) 117 | */ 118 | public StretchIcon(Image image, boolean proportionate) 119 | { 120 | super(image); 121 | this.proportionate = proportionate; 122 | } 123 | 124 | /** 125 | * Creates a utils.StretchIcon from the image. 126 | * 127 | * @param image the image 128 | * @param description a brief textual description of the image 129 | * 130 | * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String) 131 | */ 132 | public StretchIcon(Image image, String description) 133 | { 134 | super(image, description); 135 | } 136 | 137 | /** 138 | * Creates a utils.StretchIcon from the image with the specified behavior. 139 | * 140 | * @param image the image 141 | * @param description a brief textual description of the image 142 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 143 | * fill the component. 144 | * 145 | * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String) 146 | */ 147 | public StretchIcon(Image image, String description, boolean proportionate) 148 | { 149 | super(image, description); 150 | this.proportionate = proportionate; 151 | } 152 | 153 | /** 154 | * Creates a utils.StretchIcon from the specified file. 155 | * 156 | * @param filename a String specifying a filename or path 157 | * 158 | * @see ImageIcon#ImageIcon(java.lang.String) 159 | */ 160 | public StretchIcon(String filename) 161 | { 162 | super(filename); 163 | } 164 | 165 | /** 166 | * Creates a utils.StretchIcon from the specified file with the specified behavior. 167 | * 168 | * @param filename a String specifying a filename or path 169 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 170 | * fill the component. 171 | * 172 | * @see ImageIcon#ImageIcon(java.lang.String) 173 | */ 174 | public StretchIcon(String filename, boolean proportionate) 175 | { 176 | super(filename); 177 | this.proportionate = proportionate; 178 | } 179 | 180 | /** 181 | * Creates a utils.StretchIcon from the specified file. 182 | * 183 | * @param filename a String specifying a filename or path 184 | * @param description a brief textual description of the image 185 | * 186 | * @see ImageIcon#ImageIcon(java.lang.String, java.lang.String) 187 | */ 188 | public StretchIcon(String filename, String description) 189 | { 190 | super(filename, description); 191 | } 192 | 193 | /** 194 | * Creates a utils.StretchIcon from the specified file with the specified behavior. 195 | * 196 | * @param filename a String specifying a filename or path 197 | * @param description a brief textual description of the image 198 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 199 | * fill the component. 200 | * 201 | * @see ImageIcon#ImageIcon(java.awt.Image, java.lang.String) 202 | */ 203 | public StretchIcon(String filename, String description, boolean proportionate) 204 | { 205 | super(filename, description); 206 | this.proportionate = proportionate; 207 | } 208 | 209 | /** 210 | * Creates a utils.StretchIcon from the specified URL. 211 | * 212 | * @param location the URL for the image 213 | * 214 | * @see ImageIcon#ImageIcon(java.net.URL) 215 | */ 216 | public StretchIcon(URL location) 217 | { 218 | super(location); 219 | } 220 | 221 | /** 222 | * Creates a utils.StretchIcon from the specified URL with the specified behavior. 223 | * 224 | * @param location the URL for the image 225 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 226 | * fill the component. 227 | * 228 | * @see ImageIcon#ImageIcon(java.net.URL) 229 | */ 230 | public StretchIcon(URL location, boolean proportionate) 231 | { 232 | super(location); 233 | this.proportionate = proportionate; 234 | } 235 | 236 | /** 237 | * Creates a utils.StretchIcon from the specified URL. 238 | * 239 | * @param location the URL for the image 240 | * @param description a brief textual description of the image 241 | * 242 | * @see ImageIcon#ImageIcon(java.net.URL, java.lang.String) 243 | */ 244 | public StretchIcon(URL location, String description) 245 | { 246 | super(location, description); 247 | } 248 | 249 | /** 250 | * Creates a utils.StretchIcon from the specified URL with the specified behavior. 251 | * 252 | * @param location the URL for the image 253 | * @param description a brief textual description of the image 254 | * @param proportionate true to retain the image's aspect ratio, false to allow distortion of the image to 255 | * fill the component. 256 | * 257 | * @see ImageIcon#ImageIcon(java.net.URL, java.lang.String) 258 | */ 259 | public StretchIcon(URL location, String description, boolean proportionate) 260 | { 261 | super(location, description); 262 | this.proportionate = proportionate; 263 | } 264 | 265 | /** 266 | * Paints the icon. The image is reduced or magnified to fit the component to which it is painted. 267 | *

268 | * If the proportion has not been specified, or has been specified as true, the aspect ratio of the image will be preserved 269 | * by padding and centering the image horizontally or vertically. Otherwise the image may be distorted to fill the component it is 270 | * painted to. 271 | *

272 | * If this icon has no image observer,this method uses the c component as the observer. 273 | * 274 | * @param c the component to which the Icon is painted. This is used as the observer if this icon has no image observer 275 | * @param g the graphics context 276 | * @param x not used. 277 | * @param y not used. 278 | * 279 | * @see ImageIcon#paintIcon(java.awt.Component, java.awt.Graphics, int, int) 280 | */ 281 | @Override 282 | public synchronized void paintIcon(Component c, Graphics g, int x, int y) 283 | { 284 | Image image = getImage(); 285 | if (image == null) 286 | { 287 | return; 288 | } 289 | Insets insets = ((Container) c).getInsets(); 290 | x = insets.left; 291 | y = insets.top; 292 | 293 | int w = c.getWidth() - x - insets.right; 294 | int h = c.getHeight() - y - insets.bottom; 295 | 296 | if (proportionate) 297 | { 298 | int iw = image.getWidth(c); 299 | int ih = image.getHeight(c); 300 | 301 | if ((iw * h) < (ih * w)) 302 | { 303 | iw = (h * iw) / ih; 304 | x += (w - iw) / 2; 305 | w = iw; 306 | } 307 | else 308 | { 309 | ih = (w * ih) / iw; 310 | y += (h - ih) / 2; 311 | h = ih; 312 | } 313 | } 314 | ImageObserver io = getImageObserver(); 315 | g.drawImage(image, x, y, w, h, io == null ? c : io); 316 | } 317 | 318 | /** 319 | * Overridden to return 0. The size of this Icon is determined by the size of the component. 320 | * 321 | * @return 0 322 | */ 323 | @Override 324 | public int getIconWidth() 325 | { 326 | return 0; 327 | } 328 | 329 | /** 330 | * Overridden to return 0. The size of this Icon is determined by the size of the component. 331 | * 332 | * @return 0 333 | */ 334 | @Override 335 | public int getIconHeight() 336 | { 337 | return 0; 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: lsdpatch.LSDPatcher 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/instr.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/instr.bmp -------------------------------------------------------------------------------- /src/main/resources/shift_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/shift_down.png -------------------------------------------------------------------------------- /src/main/resources/shift_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/shift_left.png -------------------------------------------------------------------------------- /src/main/resources/shift_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/shift_right.png -------------------------------------------------------------------------------- /src/main/resources/shift_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/shift_up.png -------------------------------------------------------------------------------- /src/main/resources/song.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/main/resources/song.bmp -------------------------------------------------------------------------------- /src/test/java/Document/DocumentTest.java: -------------------------------------------------------------------------------- 1 | package Document; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.File; 7 | import java.io.FileNotFoundException; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | 11 | class DocumentTest { 12 | 13 | @Test 14 | void savFile() { 15 | Document document = new Document(); 16 | Assertions.assertNotNull(document.savFile()); 17 | } 18 | 19 | @Test 20 | void setSavFile() throws IOException { 21 | Document document = new Document(); 22 | LSDSavFile savFile = document.savFile(); 23 | Assertions.assertNotNull(savFile); 24 | Assertions.assertFalse(document.isSavDirty()); 25 | 26 | LSDSavFile newSavFile = new LSDSavFile(); 27 | document.setSavFile(newSavFile); 28 | Assertions.assertFalse(document.isSavDirty()); 29 | 30 | File tempFile = File.createTempFile("lsdpatcher", ".sav"); 31 | tempFile.deleteOnExit(); 32 | FileOutputStream fos = new FileOutputStream(tempFile); 33 | for (int i = 0; i < 0x8000 * 4; ++i) { 34 | fos.write(1); 35 | } 36 | fos.close(); 37 | newSavFile.loadFromSav(tempFile.getAbsolutePath()); 38 | document.setSavFile(newSavFile); 39 | Assertions.assertTrue(newSavFile.equals(document.savFile())); 40 | Assertions.assertTrue(document.isSavDirty()); 41 | 42 | try { 43 | document.loadSavFile("invalid_path"); 44 | Assertions.fail("loadSavFile did not throw"); 45 | } catch (FileNotFoundException ignored) { 46 | } 47 | 48 | document.setSavFile(null); 49 | Assertions.assertNull(document.savFile()); 50 | Assertions.assertFalse(document.isSavDirty()); 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/java/Document/LSDSavFileTest.java: -------------------------------------------------------------------------------- 1 | package Document; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.File; 9 | import java.util.Arrays; 10 | import java.util.Objects; 11 | 12 | class LSDSavFileTest { 13 | private LSDSavFile savFile; 14 | 15 | @BeforeEach 16 | void createLsdSavFile() { 17 | savFile = new LSDSavFile(); 18 | Arrays.fill(savFile.workRam, (byte)-1); // Resets block allocation table. 19 | savFile.workRam[0] = 0; // Satisfies 64 kb SRAM check. 20 | } 21 | 22 | @Test 23 | @DisplayName("Add songs until out of blocks, validate all") 24 | void isValid_addSongsUntilOutOfBlocks() { 25 | ClassLoader classLoader = getClass().getClassLoader(); 26 | File file = new File(Objects.requireNonNull(classLoader.getResource("triangle_waves.lsdprj")).getFile()); 27 | int addedSongs = 0; 28 | try { 29 | while (true) { 30 | savFile.addSongFromFile(file.getAbsolutePath(), null); 31 | ++addedSongs; 32 | } 33 | } catch (Exception e) { 34 | Assertions.assertEquals(e.getMessage(), "Out of blocks!"); 35 | } 36 | Assertions.assertEquals(addedSongs, 19); 37 | for (int song = 0; song < addedSongs; ++song) { 38 | Assertions.assertTrue(savFile.isValid(song)); 39 | } 40 | } 41 | 42 | @Test 43 | void testClone() throws CloneNotSupportedException { 44 | LSDSavFile savFile = new LSDSavFile(); 45 | LSDSavFile clone = savFile.clone(); 46 | Assertions.assertNotNull(clone); 47 | Assertions.assertNotSame(savFile, clone); 48 | } 49 | 50 | @Test 51 | void saveAs() throws Exception { 52 | LSDSavFile savFile = new LSDSavFile(); 53 | File file = File.createTempFile("lsdpatcher", ".sav"); 54 | file.deleteOnExit(); 55 | savFile.saveAs(file.getAbsolutePath()); 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/java/kitEditor/SampleTest.java: -------------------------------------------------------------------------------- 1 | package kitEditor; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import javax.sound.sampled.UnsupportedAudioFileException; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.net.URL; 10 | 11 | class SampleTest { 12 | 13 | @Test 14 | void createFromWav() throws IOException, UnsupportedAudioFileException { 15 | ClassLoader classLoader = getClass().getClassLoader(); 16 | URL url = classLoader.getResource("sine1s44khz.wav"); 17 | assert url != null; 18 | File file = new File(url.getFile()); 19 | Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0); 20 | Assertions.assertNotNull(sample); 21 | Assertions.assertEquals("sine1s44khz", sample.getName()); 22 | Assertions.assertEquals(11467, sample.lengthInSamples()); 23 | Assertions.assertEquals(5728, sample.lengthInBytes()); 24 | 25 | int sum = 0; 26 | int min = Integer.MAX_VALUE; 27 | int max = Integer.MIN_VALUE; 28 | for (int i = 0; i < sample.lengthInSamples(); ++i) { 29 | int s = sample.read(); 30 | sum += s; 31 | min = Math.min(s, min); 32 | max = Math.max(s, max); 33 | } 34 | int avg = sum / sample.lengthInSamples(); 35 | Assertions.assertEquals(0, avg); 36 | Assertions.assertEquals(-Short.MAX_VALUE, min); 37 | Assertions.assertEquals(Short.MAX_VALUE, max); 38 | } 39 | 40 | @Test 41 | void decreaseVolume() throws IOException, UnsupportedAudioFileException { 42 | ClassLoader classLoader = getClass().getClassLoader(); 43 | URL url = classLoader.getResource("sine1s44khz.wav"); 44 | assert url != null; 45 | File file = new File(url.getFile()); 46 | 47 | Sample sample = Sample.createFromWav(file, false, false, -20, 0, 0); 48 | 49 | int sum = 0; 50 | int min = Integer.MAX_VALUE; 51 | int max = Integer.MIN_VALUE; 52 | for (int i = 0; i < sample.lengthInSamples(); ++i) { 53 | int s = sample.read(); 54 | sum += s; 55 | min = Math.min(s, min); 56 | max = Math.max(s, max); 57 | } 58 | int avg = sum / sample.lengthInSamples(); 59 | Assertions.assertEquals(0, avg); 60 | Assertions.assertEquals(Short.MIN_VALUE / 10, min); 61 | Assertions.assertEquals(Short.MAX_VALUE / 10, max); 62 | } 63 | 64 | @Test 65 | void trim() throws IOException, UnsupportedAudioFileException { 66 | ClassLoader classLoader = getClass().getClassLoader(); 67 | URL url = classLoader.getResource("sine1s44khz.wav"); 68 | assert url != null; 69 | File file = new File(url.getFile()); 70 | 71 | Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0); 72 | Assertions.assertEquals(11467, sample.untrimmedLengthInSamples()); 73 | Assertions.assertEquals(11467, sample.lengthInSamples()); 74 | 75 | sample = Sample.createFromWav(file, false, false, 0, 1, 0); 76 | Assertions.assertEquals(11467, sample.untrimmedLengthInSamples()); 77 | Assertions.assertEquals(11435, sample.lengthInSamples()); 78 | 79 | sample = Sample.createFromWav(file, false, false, 0, 10000, 0); 80 | Assertions.assertEquals(11467, sample.untrimmedLengthInSamples()); 81 | Assertions.assertEquals(32, sample.lengthInSamples()); 82 | } 83 | 84 | @Test 85 | void pitch() throws IOException, UnsupportedAudioFileException { 86 | ClassLoader classLoader = getClass().getClassLoader(); 87 | URL url = classLoader.getResource("sine1s44khz.wav"); 88 | assert url != null; 89 | File file = new File(url.getFile()); 90 | 91 | Sample sample = Sample.createFromWav(file, false, false, 0, 0, 0); 92 | Assertions.assertEquals(11467, sample.lengthInSamples()); 93 | 94 | // octave down 95 | sample = Sample.createFromWav(file, false, false, 0, 0, -12); 96 | Assertions.assertEquals(11467 * 2 + 1, sample.lengthInSamples()); 97 | 98 | // octave up 99 | sample = Sample.createFromWav(file, false, false, 0, 0, 12); 100 | Assertions.assertEquals(11467 / 2, sample.lengthInSamples()); 101 | } 102 | } -------------------------------------------------------------------------------- /src/test/resources/empty.lsdprj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/test/resources/empty.lsdprj -------------------------------------------------------------------------------- /src/test/resources/sine1s44khz.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/test/resources/sine1s44khz.wav -------------------------------------------------------------------------------- /src/test/resources/triangle_waves.lsdprj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkotlinski/lsdpatch/eb13df1d6414ac66a27dd2e199fddc6c887b680d/src/test/resources/triangle_waves.lsdprj --------------------------------------------------------------------------------