├── .gitignore ├── COPYING.txt ├── README.md ├── after-tools ├── bin │ └── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── after │ └── tools │ ├── CheckMain.java │ ├── Main.java │ ├── map │ ├── MapSurgery.java │ └── MapSurgeryMain.java │ └── net │ ├── NetWorkbenchMain.java │ └── web │ ├── WebAdapterBaseWebClientRunnable.java │ └── WebAdapterMain.java ├── after ├── bin │ └── .gitignore ├── eden.dmb ├── eden.ext.dmb ├── pom.xml └── src │ ├── main │ └── java │ │ ├── aedium │ │ ├── DummyAnnotationToShutupIDEVerification.java │ │ ├── ElementMarkers.java │ │ ├── NonNull.java │ │ ├── Nullable.java │ │ └── package-info.java │ │ └── after │ │ ├── DMBMapping.java │ │ ├── Eden.java │ │ ├── algorithms │ │ ├── CYCSUB.java │ │ ├── NQCRC.java │ │ ├── RUNSUB.java │ │ ├── RUNSUBFailedException.java │ │ ├── XORJump9.java │ │ └── package-info.java │ │ ├── annotations │ │ ├── CacheFileID.java │ │ ├── ClassID.java │ │ ├── DMBList.java │ │ ├── HasNoMeaning.java │ │ ├── InstanceID.java │ │ ├── ListID.java │ │ ├── ListOfProcID.java │ │ ├── ListOfVarID.java │ │ ├── MobTypeID.java │ │ ├── ProcID.java │ │ ├── StringID.java │ │ ├── VarID.java │ │ └── package-info.java │ │ ├── dm │ │ └── package-info.java │ │ ├── io │ │ ├── CacheID.java │ │ ├── DMB.java │ │ ├── DMBCacheFileTable.java │ │ ├── DMBClassTable.java │ │ ├── DMBEntryBasedSubblock.java │ │ ├── DMBGrid.java │ │ ├── DMBHeader.java │ │ ├── DMBInstanceTable.java │ │ ├── DMBListTable.java │ │ ├── DMBMapAdditionalData.java │ │ ├── DMBMobTypeTable.java │ │ ├── DMBObjectEntryBasedSubblock.java │ │ ├── DMBProcTable.java │ │ ├── DMBStandardObjectIDs.java │ │ ├── DMBStringTable.java │ │ ├── DMBSubblock7.java │ │ ├── DMBValue.java │ │ ├── DMBVarTable.java │ │ ├── RADEntry.java │ │ ├── RSCEntryContent.java │ │ └── framework │ │ │ ├── DMBContext.java │ │ │ ├── DMBReadContext.java │ │ │ ├── DMBWriteContext.java │ │ │ ├── ReadContext.java │ │ │ └── WriteContext.java │ │ └── net │ │ └── PacketFrame.java │ └── test │ └── java │ └── after │ └── tests │ ├── AlgorithmTest.java │ ├── EdenTest.java │ └── EquivalenceTest.java ├── algorithms ├── CYCSUB.md ├── NQCRC.md ├── RUNSUB.md └── XORJUMP9.md ├── formats ├── DMB.Bytecode.md ├── DMB.TypeValue.md ├── DMB.md ├── DMI.md ├── RAD.md ├── RSC.md └── SAV.md ├── pom.xml ├── protocol ├── Protocol.md └── WebClient.md ├── research-projects ├── Makefile ├── a-very-long-output.dme ├── a.dms ├── b.dms ├── chessboard.dm ├── chessboard.dmm ├── mapsurgery-game.dme ├── mapsurgery-map.dme ├── mapsurgery-map.dmm ├── modified-types.dme ├── proc-inherit-quirks-1.dme ├── proc-inherit-quirks-2.dme ├── proc-inherit-quirks-3.dme ├── rand-inherit.dme ├── savefile.dme ├── turf-atom-equivalence.dme ├── world-channel.dme └── world-oddprops.dme └── scripts ├── brute_force_xor_jump_9.lua ├── cycsub_r.lua ├── general_xor.lua ├── nqcrc.lua ├── rad_analyzer.lua ├── runsub_r.lua ├── runsub_t.lua └── savefile_analyzer.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.jar 2 | *.class 3 | *.zip 4 | *.iml 5 | .idea 6 | .classpath 7 | .project 8 | .settings 9 | .gradle 10 | 11 | after/build 12 | after/target 13 | after-tools/build 14 | after-tools/target 15 | after/external/test-subjects 16 | 17 | # these contain BYOND stdlib stuff 18 | research-projects/*.dmb 19 | research-projects/*.lk 20 | research-projects/*.int 21 | research-projects/*.rsc 22 | # clutters the repo 23 | research-projects/*.sav 24 | # not sure what to do with these, and it's not like they're hard to get 25 | protocol/*.dump.txt 26 | 27 | scripts/tmp.* 28 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial BYOND Data Documentation 2 | 3 | This is incomplete documentation regarding BYOND data. 4 | 5 | There's a document which is supposed to contain protocol information but research hasn't gone far enough for it to be really worth anything. 6 | 7 | ## License 8 | 9 | byond-data-docs - documentation on BYOND formats 10 | Written starting in 2020 by 20kdc (see README.md for additional credits) 11 | To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. 12 | You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . 13 | 14 | Note that said copy of the CC0 is in 'COPYING.txt'. 15 | 16 | ## Credits 17 | 18 | 20kdc: This documentation, general structure research 19 | 20 | extools (SpaceManiac, MCHSL, 1fbff5f83b23d39d38b1dfcb4cac8d9b et al.): DM bytecode information 21 | 22 | Notes on this regard: This doesn't contain extools code. 23 | That said, if extools wants me to add their license here, ask via an issue or something. 24 | 25 | Tomeno: important parts of string decryption and protocol decryption 26 | 27 | BobOfDoom, NGGJamie, Crispy: Documented the BYOND Topic protocol at https://github.com/NGGJamie/byond-topics 28 | This was useful to understand BYOND packet framing, not covering encryption 29 | 30 | Willox: Documented some of the previously unknown properties at https://github.com/willox/dmasm/blob/dmb/src/dmb/loader.rs 31 | 32 | Stephen001: Partial documentation of some format version (right now unknown but >= 307 < 494) at https://github.com/Stephen001/Lego 33 | 34 | The Red Book (Super Saiyan X, LordAndrew, Kaiochao, DarkCampainger et al.): Informed me of the existence of modified types. 35 | 36 | ZeWaka: Informed me of the existence of Lummox JR's information on the DMI format (at http://www.byond.com/forum/post/189170#comment1077677 ) 37 | -------------------------------------------------------------------------------- /after-tools/bin/.gitignore: -------------------------------------------------------------------------------- 1 | /main/ 2 | /test/ 3 | -------------------------------------------------------------------------------- /after-tools/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | t20kdc.bda 8 | after-tools 9 | 0.666-SNAPSHOT 10 | 11 | after-tools 12 | Tools for manipulating BYOND data files. DO NOT SYSTEM-PACKAGE - ALWAYS INCLUDE IN APPLICATION JAR. 13 | 14 | 15 | t20kdc.bda 16 | bda-parent-project 17 | 0.666-SNAPSHOT 18 | .. 19 | 20 | 21 | 22 | 23 | 24 | org.apache.maven.plugins 25 | maven-assembly-plugin 26 | 27 | 28 | 29 | after.tools.Main 30 | 31 | 32 | 33 | 34 | 35 | dagoodz 36 | package 37 | 38 | single 39 | 40 | 41 | 42 | jar-with-dependencies 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | t20kdc.bda 54 | after 55 | 0.666-SNAPSHOT 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/CheckMain.java: -------------------------------------------------------------------------------- 1 | package after.tools; 2 | 3 | import after.io.DMB; 4 | import after.tools.map.MapSurgeryMain; 5 | 6 | /** 7 | * Testing utility to ensure stuff can be read. 8 | */ 9 | public class CheckMain { 10 | public static void main(String[] args) throws Exception { 11 | if (args.length != 1) { 12 | System.out.println("check help"); 13 | return; 14 | } 15 | // -- Read -- 16 | DMB dmbG = MapSurgeryMain.get(args[0]); 17 | System.out.println("ok"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/Main.java: -------------------------------------------------------------------------------- 1 | package after.tools; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStreamReader; 5 | import java.util.Random; 6 | 7 | import after.tools.map.MapSurgeryMain; 8 | import after.tools.net.NetWorkbenchMain; 9 | import after.tools.net.web.WebAdapterMain; 10 | 11 | /** 12 | * Entrypoint for easier access to sub-tools. 13 | */ 14 | public class Main { 15 | private static Tool[] tools = new Tool[] { 16 | new Tool("mapsurgery", " GAME-DMB MAP-DMB OUTPUT", "Transplants a map from one DMB into another.", MapSurgeryMain.class), 17 | new Tool("proxy", "", "Launches a BYOND protocol proxy on localhost:5100 which proxies to localhost:5101. Decrypts messages.", NetWorkbenchMain.class), 18 | new Tool("broken-webadapter", " PORT", "DOESN'T WORK PROPERLY, attempts to get the webclient to do something", WebAdapterMain.class), 19 | new Tool("check", " DMB", "Checks that reading a DMB file works.", CheckMain.class), 20 | new Tool("exit", "", "Exits.", QuitMain.class) 21 | }; 22 | 23 | public static void main(String[] args) throws Exception { 24 | if (args.length == 0) { 25 | String[] slogan = new String[] { 26 | "Tools for reverse-engineering to rebuild after the end of the BYOND.", 27 | "Community Edition.", 28 | "Java client when? (when you code it)", 29 | "Decompiler when? (when you code it)", 30 | "Have you tried 'proxy'?", 31 | "Have you tried 'mapsurgery'?", 32 | "Home Edition.\n" + 33 | "+---------------------------------------------+\n" + 34 | "| Activate AFTER |\n" + 35 | "+---------------------------------------------+\n" + 36 | "| AFTER: Home Edition has not been activated. |\n" + 37 | "| Click here to activate AFTER: Home Edition. |\n" + 38 | "+---------------------------------------------+", 39 | "Activating WinRAR supports a dangerously monopolistic company known for dodgy license agreements.\n" + 40 | "...that's right, I'm talking about RARLAB. (Seriously, have you looked at the unrar license? No, not the GPL'd early versions.)" 41 | }; 42 | System.out.println("AFTER: " + slogan[new Random().nextInt(slogan.length)]); 43 | BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 44 | while (true) { 45 | System.out.append("> "); 46 | System.out.flush(); 47 | try { 48 | String[] words = br.readLine().split(" "); 49 | if (words.length != 0) 50 | main(words); 51 | } catch (Exception e2) { 52 | e2.printStackTrace(); 53 | } 54 | } 55 | } 56 | for (Tool t : tools) { 57 | if (args[0].equals(t.name)) { 58 | String[] remainingArgs = new String[args.length - 1]; 59 | System.arraycopy(args, 1, remainingArgs, 0, remainingArgs.length); 60 | t.main.getMethod("main", String[].class).invoke(null, new Object[] {remainingArgs}); 61 | return; 62 | } 63 | } 64 | help(); 65 | } 66 | 67 | private static void help() { 68 | System.out.println("java -jar after-tools.jar SUBCOMMAND ARGS..."); 69 | for (Tool t : tools) { 70 | System.out.println(t.name + t.args); 71 | System.out.println(" " + t.description); 72 | } 73 | } 74 | 75 | private static class Tool { 76 | public final String name, args, description; 77 | public final Class main; 78 | public Tool(String n, String a, String d, Class m) { 79 | name = n; 80 | args = a; 81 | description = d; 82 | main = m; 83 | } 84 | } 85 | 86 | private static class QuitMain { 87 | public static void main(String[] args) { 88 | System.exit(0); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/map/MapSurgery.java: -------------------------------------------------------------------------------- 1 | package after.tools.map; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | 7 | import after.DMBMapping; 8 | import after.io.DMB; 9 | import after.io.DMBEntryBasedSubblock; 10 | import after.io.DMBInstanceTable; 11 | import after.io.DMBMapAdditionalData; 12 | import after.io.DMBValue; 13 | 14 | public class MapSurgery { 15 | /** 16 | * Imports a map from another DMB file, deleting the existing grid. 17 | * @param dmbG the place to inject the map 18 | * @param dmbM the map to inject 19 | */ 20 | public static void run(DMB dmbG, DMB dmbM) { 21 | // reset grid (though don't fill in arrays yet) 22 | int total = dmbM.grid.turf.length; 23 | dmbG.grid.turf = new int[total]; 24 | dmbG.grid.area = new int[total]; 25 | dmbG.grid.additionalTurfs = new int[total]; 26 | dmbG.grid.xSize = dmbM.grid.xSize; 27 | dmbG.grid.ySize = dmbM.grid.ySize; 28 | dmbG.grid.zSize = dmbM.grid.zSize; 29 | // prepare fallback instance 30 | int fallbackTurfInstance = getOrMakeTurfInstance(dmbG); 31 | DMBMapping mapping = new DMBMapping(dmbM, dmbG); 32 | HashMap, Integer> lists = new HashMap, Integer>(); 33 | // fill in grids 34 | for (int i = 0; i < total; i++) { 35 | // set sane defaults 36 | dmbG.grid.turf[i] = fallbackTurfInstance; 37 | dmbG.grid.area[i] = DMBEntryBasedSubblock.OBJ_NULL; 38 | dmbG.grid.additionalTurfs[i] = DMBEntryBasedSubblock.OBJ_NULL; 39 | // apply stuff 40 | Integer cTurf = mapping.mapValue(DMBValue.INSTANCE_TYPEPATH, dmbM.grid.turf[i]); 41 | if (cTurf != null) { 42 | dmbG.grid.turf[i] = cTurf; 43 | } else { 44 | System.out.println("unable to map turf @ " + i); 45 | } 46 | Integer cArea = mapping.mapValue(DMBValue.INSTANCE_TYPEPATH, dmbM.grid.area[i]); 47 | if (cArea != null) { 48 | dmbG.grid.area[i] = cArea; 49 | } else { 50 | System.out.println("unable to map area @ " + i); 51 | } 52 | if (dmbM.grid.additionalTurfs[i] != DMBEntryBasedSubblock.OBJ_NULL) { 53 | int[] addTurfsO = dmbM.lists.entries.get(dmbM.grid.additionalTurfs[i]); 54 | ArrayList addTurfsN = new ArrayList<>(addTurfsO.length); 55 | for (int x = 0; x < addTurfsO.length; x++) { 56 | Integer v = mapping.mapValue(DMBValue.TURF_TYPEPATH, addTurfsO[x]); 57 | if (v != null) 58 | addTurfsN.add(v); 59 | } 60 | Integer existingList = lists.get(addTurfsN); 61 | if (existingList == null) { 62 | int[] addTurfsNA = new int[addTurfsN.size()]; 63 | for (int x = 0; x < addTurfsNA.length; x++) 64 | addTurfsNA[x] = addTurfsN.get(x); 65 | int res = dmbM.lists.add(addTurfsNA); 66 | lists.put(addTurfsN, res); 67 | existingList = res; 68 | } 69 | dmbG.grid.additionalTurfs[i] = existingList; 70 | } 71 | } 72 | dmbG.mapAdditionalData.entries.clear(); 73 | for (DMBMapAdditionalData.Entry ent : dmbM.mapAdditionalData.entries) { 74 | DMBMapAdditionalData.Entry oh = new DMBMapAdditionalData.Entry(); 75 | oh.offset = ent.offset; 76 | Integer mapped = mapping.mapValue(DMBValue.INSTANCE_TYPEPATH, oh.instance); 77 | if (mapped != null) 78 | oh.instance = mapped; 79 | dmbG.mapAdditionalData.entries.add(ent); 80 | } 81 | dmbG.fixStrings(); 82 | } 83 | 84 | private static int getOrMakeTurfInstance(DMB dmbG) { 85 | int target = dmbG.standardObjectIDs.turf; 86 | for (int i = 0, iC = dmbG.instances.entries.size(); i < iC; i++) { 87 | DMBInstanceTable.Entry ent = dmbG.instances.entries.get(i); 88 | if ((ent.value.type == DMBValue.TURF_TYPEPATH) && (ent.value.value == target) && (ent.initializer == DMBEntryBasedSubblock.OBJ_NULL)) 89 | return i; 90 | } 91 | DMBInstanceTable.Entry ent = new DMBInstanceTable.Entry(); 92 | ent.value = new DMBValue(DMBValue.TURF_TYPEPATH, target); 93 | ent.initializer = DMBEntryBasedSubblock.OBJ_NULL; 94 | return dmbG.instances.add(ent); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/map/MapSurgeryMain.java: -------------------------------------------------------------------------------- 1 | package after.tools.map; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | 8 | import after.io.DMB; 9 | import after.io.framework.DMBReadContext; 10 | import after.io.framework.DMBWriteContext; 11 | 12 | public class MapSurgeryMain { 13 | public static void main(String[] args) throws Exception { 14 | if (args.length != 3) { 15 | System.out.println("check help"); 16 | return; 17 | } 18 | // -- Read -- 19 | DMB dmbG = get(args[0]); 20 | DMB dmbM = get(args[1]); 21 | // -- Map transplantation into dmbG -- 22 | MapSurgery.run(dmbG, dmbM); 23 | // -- Write -- 24 | FileOutputStream fos = new FileOutputStream(args[2]); 25 | dmbG.write(new DMBWriteContext(false) { 26 | @Override 27 | protected void i8Internal(int value) { 28 | try { 29 | fos.write(value); 30 | } catch (IOException e) { 31 | throw new RuntimeException(e); 32 | } 33 | } 34 | }); 35 | fos.close(); 36 | } 37 | 38 | public static DMB get(String args) throws Exception { 39 | byte[] gameDMBData = Files.readAllBytes(new File(args).toPath()); 40 | DMB d = new DMB(); 41 | d.read(new DMBReadContext(gameDMBData, true)); 42 | return d; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/net/NetWorkbenchMain.java: -------------------------------------------------------------------------------- 1 | package after.tools.net; 2 | 3 | import java.io.DataInputStream; 4 | import java.io.DataOutputStream; 5 | import java.net.InetSocketAddress; 6 | import java.net.ServerSocket; 7 | import java.net.Socket; 8 | import java.nio.ByteOrder; 9 | import java.nio.channels.SelectionKey; 10 | import java.nio.channels.Selector; 11 | import java.nio.channels.ServerSocketChannel; 12 | import java.nio.channels.SocketChannel; 13 | import java.util.Set; 14 | 15 | import after.algorithms.RUNSUB; 16 | import after.net.PacketFrame; 17 | 18 | /** 19 | * Testing utility for studying the protocol. 20 | */ 21 | public class NetWorkbenchMain { 22 | public static void main(String[] args) throws Exception { 23 | System.out.println("Listening on port 5100. Expects target server on localhost port 5101."); 24 | ServerSocketChannel ss = ServerSocketChannel.open(); 25 | ss.bind(new InetSocketAddress(5100)); 26 | 27 | SocketChannel sockC = ss.accept(); 28 | SocketChannel sockS = SocketChannel.open(new InetSocketAddress("localhost", 5101)); 29 | 30 | Selector main = Selector.open(); 31 | 32 | DataInputStream disC = new DataInputStream(sockC.socket().getInputStream()); 33 | DataInputStream disS = new DataInputStream(sockS.socket().getInputStream()); 34 | 35 | PacketFrame pfC = new PacketFrame(); 36 | PacketFrame pfS = new PacketFrame(); 37 | 38 | DataOutputStream dosC = new DataOutputStream(sockC.socket().getOutputStream()); 39 | DataOutputStream dosS = new DataOutputStream(sockS.socket().getOutputStream()); 40 | 41 | // various states 42 | int NEST_NORMAL = 0; 43 | int NEST_CTOS_HANDSHAKE = 1; 44 | int NEST_STOC_HANDSHAKE = 2; 45 | int state = NEST_CTOS_HANDSHAKE; 46 | 47 | int encryptionKey = 0; 48 | 49 | while (true) { 50 | // blocking off... 51 | sockC.configureBlocking(false); 52 | sockS.configureBlocking(false); 53 | 54 | SelectionKey keyC = sockC.register(main, SelectionKey.OP_READ); 55 | SelectionKey keyS = sockS.register(main, SelectionKey.OP_READ); 56 | 57 | main.select(); 58 | Set active = main.selectedKeys(); 59 | 60 | boolean hitC = active.contains(keyC); 61 | boolean hitS = active.contains(keyS); 62 | 63 | keyC.cancel(); 64 | keyS.cancel(); 65 | 66 | main.selectNow(); 67 | 68 | sockC.configureBlocking(true); 69 | sockS.configureBlocking(true); 70 | 71 | // ...back to blocking for simpler logic (this is just a workbench) 72 | 73 | if (hitC) { 74 | pfC.read(disC); 75 | pfC.write(dosS); 76 | logAndDecrypt("C", pfC, encryptionKey); 77 | if (state == NEST_CTOS_HANDSHAKE && pfC.type == 1) { 78 | // initial handshake 79 | pfC.hasSeq = true; 80 | state = NEST_STOC_HANDSHAKE; 81 | encryptionKey = pfC.data.getInt(8); 82 | encryptionKey += pfC.data.getInt(0) + (pfC.data.getInt(4) * 0x10000); 83 | } else if (state != NEST_NORMAL) { 84 | System.out.println("*** ctos " + pfC.type + " is odd"); 85 | } 86 | } 87 | if (hitS) { 88 | pfS.read(disS); 89 | pfS.write(dosC); 90 | logAndDecrypt("S", pfS, encryptionKey); 91 | if (state == NEST_STOC_HANDSHAKE && pfS.type == 1) { 92 | // skip past padding 93 | int ptr = 11; 94 | while (true) { 95 | int v = pfS.data.getInt(ptr); 96 | ptr += 4; 97 | v += 0x71bd632f; 98 | v &= 0x04008000; 99 | if (v == 0) 100 | break; 101 | } 102 | encryptionKey += pfS.data.getInt(ptr); 103 | // right, done 104 | state = NEST_NORMAL; 105 | } else if (state != NEST_NORMAL) { 106 | System.out.println("*** stoc " + pfS.type + " is odd"); 107 | } 108 | } 109 | } 110 | } 111 | 112 | private static void logAndDecrypt(String where, PacketFrame pf, int key) { 113 | System.out.println(where + " " + Integer.toHexString(pf.sequence & 0xFFFF) + " " + Integer.toHexString(pf.type & 0xFFFF)); 114 | int len = pf.data.position(); 115 | for (int pass = 0; pass < 2; pass++) { 116 | for (int i = 0; i < len; i++) { 117 | String hx = Integer.toHexString(pf.dataArray[i] & 0xFF); 118 | if (hx.length() == 1) 119 | hx = "0" + hx; 120 | System.out.print(" " + hx); 121 | } 122 | System.out.println(); 123 | if (pass == 0 && key != 0) { 124 | System.out.println("decrypted"); 125 | RUNSUB.decrypt(pf.dataArray, 0, len, key); 126 | len--; 127 | } else { 128 | // final pass: ASCII 129 | for (int i = 0; i < len; i++) { 130 | char c = (char) (pf.dataArray[i] & 0xFF); 131 | if (c >= 32 && c <= 0x7E) { 132 | System.out.print(" " + c + " "); 133 | } else { 134 | System.out.print(" . "); 135 | } 136 | } 137 | System.out.println(); 138 | break; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/net/web/WebAdapterBaseWebClientRunnable.java: -------------------------------------------------------------------------------- 1 | package after.tools.net.web; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.DataInputStream; 5 | import java.io.DataOutputStream; 6 | import java.net.Socket; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.security.MessageDigest; 11 | import java.util.Base64; 12 | import java.util.LinkedList; 13 | 14 | /** 15 | * Internals. 16 | */ 17 | abstract class WebAdapterBaseWebClientRunnable implements Runnable { 18 | public final Socket clientSocket; 19 | public DataInputStream clientInput; 20 | public DataOutputStream clientOutput; 21 | public WebAdapterBaseWebClientRunnable(Socket s) { 22 | clientSocket = s; 23 | } 24 | 25 | @Override 26 | public void run() { 27 | try { 28 | clientInput = new DataInputStream(clientSocket.getInputStream()); 29 | clientOutput = new DataOutputStream(clientSocket.getOutputStream()); 30 | 31 | // Initialization, Part 1 (request parsing) 32 | LinkedList headerLines = new LinkedList<>(); 33 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 34 | while (true) { 35 | byte b = clientInput.readByte(); 36 | if (b == '\r') 37 | continue; 38 | if (b == '\n') { 39 | String line = new String(baos.toByteArray(), StandardCharsets.UTF_8); 40 | baos.reset(); 41 | if (line.equals("")) 42 | break; 43 | headerLines.add(line); 44 | } else { 45 | baos.write(b); 46 | } 47 | } 48 | 49 | // Initialization, Part 2 (do we want to give the browser a file?) 50 | String path = headerLines.getFirst().split(" ")[1]; 51 | 52 | boolean isWebSocket = false; 53 | String webSocketKey = null; 54 | 55 | for (int i = 0; i < headerLines.size(); i++) { 56 | String[] kv = headerLines.get(i).split(":"); 57 | if (kv[0].trim().equalsIgnoreCase("Upgrade")) 58 | isWebSocket = true; 59 | else if (kv[0].trim().equalsIgnoreCase("Sec-WebSocket-Key")) { 60 | // just to be sure 61 | baos.reset(); 62 | baos.write(kv[1].trim().getBytes(StandardCharsets.UTF_8)); 63 | baos.write(("258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes(StandardCharsets.UTF_8)); 64 | // "seemingly" overcomplicated? SEEMINGLY? 65 | // No. NO. THIS IS UTTER INSANITY. 66 | // Dear goodness what is WRONG with whoever designed this 67 | webSocketKey = Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA1").digest(baos.toByteArray())); 68 | } 69 | } 70 | 71 | System.out.println(path + " " + isWebSocket + " " + webSocketKey); 72 | 73 | if (isWebSocket) { 74 | clientOutput.writeBytes("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"); 75 | if (webSocketKey != null) 76 | clientOutput.writeBytes("Sec-WebSocket-Accept: " + webSocketKey + "\r\n"); 77 | clientOutput.writeBytes("Sec-WebSocket-Version: 13\r\n\r\n"); 78 | handleWebSocket(); 79 | } else { 80 | String status = "200 OK"; 81 | byte[] contentResult; 82 | if (path.contains("\\")) { 83 | status = "404 Not Found"; 84 | contentResult = "NO".getBytes(); 85 | } else { 86 | String[] bits = path.split("/"); 87 | if (bits.length == 0 || bits[bits.length - 1].equals("play")) { 88 | StringBuilder ongoing = new StringBuilder(); 89 | // this is actually all that needs to be done 90 | ongoing.append(""); 91 | ongoing.append(""); 92 | ongoing.append("
"); 93 | contentResult = ongoing.toString().getBytes(); 94 | } else { 95 | try { 96 | contentResult = Files.readAllBytes(Paths.get(bits[bits.length - 1])); 97 | } catch (Exception e2) { 98 | status = "404 Not Found"; 99 | contentResult = e2.getMessage().getBytes(); 100 | } 101 | } 102 | } 103 | clientOutput.writeBytes("HTTP/1.1 " + status + "\r\nConnection: close\r\n"); 104 | clientOutput.writeBytes("Content-Length: " + contentResult.length + "\r\n\r\n"); 105 | clientOutput.write(contentResult); 106 | clientOutput.flush(); 107 | } 108 | 109 | } catch (Exception e) { 110 | e.printStackTrace(); 111 | } finally { 112 | try { 113 | clientSocket.close(); 114 | } catch (Exception e2) { 115 | // DROPPING A RESOURCE MUST NEVER BE ALLOWED TO FAIL. 116 | } 117 | } 118 | } 119 | 120 | public abstract void handleWebSocket() throws Exception; 121 | } 122 | -------------------------------------------------------------------------------- /after-tools/src/main/java/after/tools/net/web/WebAdapterMain.java: -------------------------------------------------------------------------------- 1 | package after.tools.net.web; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.DataInputStream; 5 | import java.io.DataOutputStream; 6 | import java.io.File; 7 | import java.net.ServerSocket; 8 | import java.net.Socket; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.security.MessageDigest; 13 | import java.util.Base64; 14 | import java.util.LinkedList; 15 | 16 | import after.net.PacketFrame; 17 | 18 | /** 19 | * Ok, so, APPARENTLY, the WebClient protocol has been made different enough in ways that make writing my own adapter... 20 | * not worth the effort, so the NEW purpose of this is as a workbench for webclient stuff 21 | * might make it an adapter again in future 22 | */ 23 | public class WebAdapterMain { 24 | 25 | public static void main(String[] args) throws Exception { 26 | LinkedList spoonfeedWinset = new LinkedList<>(); 27 | for (String s : new File(".").list()) 28 | if (s.endsWith(".dms") && !s.equalsIgnoreCase("defaultSkin.dms")) 29 | spoonfeedWinset.add(Files.readAllBytes(Paths.get(s))); 30 | spoonfeedWinset.add(Files.readAllBytes(Paths.get("defaultSkin.dms"))); 31 | 32 | int sourcePort = Integer.parseInt(args[0]); 33 | 34 | ServerSocket ss = new ServerSocket(sourcePort); 35 | 36 | while (true) { 37 | Socket clientSocket = ss.accept(); 38 | new Thread(new WebAdapterBaseWebClientRunnable(clientSocket) { 39 | 40 | @Override 41 | public void handleWebSocket() throws Exception { 42 | PacketFrame webFrame = new PacketFrame(); 43 | 44 | // Say hello to the client 45 | webFrame.begin(0x0001); 46 | webFrame.data.putInt(0x200); 47 | webFrame.data.putInt(0x200); 48 | webFrame.writeWebSocket(clientOutput); 49 | 50 | // Winset 51 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 52 | baos.write(0); 53 | baos.write(0); 54 | for (byte[] sp : spoonfeedWinset) { 55 | int len = sp.length; 56 | baos.write(len); 57 | baos.write(len >> 8); 58 | baos.write(sp); 59 | } 60 | PacketFrame.writeWebSocketInternal(clientOutput, (short) 0x00E5, baos.toByteArray(), 0, baos.size()); 61 | 62 | // Get the client to boot 63 | webFrame.begin(0x000E); 64 | webFrame.data.putShort((short) 0); 65 | webFrame.data.putInt(0); 66 | webFrame.writeWebSocket(clientOutput); 67 | 68 | // put up a test verb 69 | webFrame.begin(0x00d2); 70 | webFrame.data.put((byte) 0); 71 | webFrame.data.putShort((short) 1); 72 | webFrame.data.putShort((short) 0); 73 | webFrame.data.put((byte) 2); 74 | for (int i = 0; i < 3; i++) { 75 | webFrame.data.putShort((short) 5); 76 | byte[] bt = "HELLO".getBytes(); 77 | for (byte b : bt) 78 | webFrame.data.put(b); 79 | } 80 | webFrame.data.putShort((short) 0); 81 | webFrame.data.put((byte) 0); 82 | webFrame.writeWebSocket(clientOutput); 83 | 84 | // stop 85 | while (true) { 86 | Thread.sleep(5000); 87 | webFrame.begin(0x0027); 88 | byte[] bt = "QUACK!
\n".getBytes(); 89 | for (byte b : bt) 90 | webFrame.data.put(b); 91 | webFrame.data.put((byte) 0); 92 | webFrame.writeWebSocket(clientOutput); 93 | } 94 | } 95 | }).start(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /after/bin/.gitignore: -------------------------------------------------------------------------------- 1 | /main/ 2 | /test/ 3 | -------------------------------------------------------------------------------- /after/eden.dmb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/20kdc/byond-data-docs/bf6023dfa3860312a299f2d335e251d2dece5abd/after/eden.dmb -------------------------------------------------------------------------------- /after/eden.ext.dmb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/20kdc/byond-data-docs/bf6023dfa3860312a299f2d335e251d2dece5abd/after/eden.ext.dmb -------------------------------------------------------------------------------- /after/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | t20kdc.bda 8 | after 9 | 0.666-SNAPSHOT 10 | 11 | after 12 | Library for reading and writing BYOND DMB files. DO NOT SYSTEM-PACKAGE - ALWAYS INCLUDE IN APPLICATION JAR. 13 | 14 | 15 | t20kdc.bda 16 | bda-parent-project 17 | 0.666-SNAPSHOT 18 | .. 19 | 20 | 21 | 22 | 23 | junit 24 | junit 25 | 4.13.1 26 | test 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /after/src/main/java/aedium/DummyAnnotationToShutupIDEVerification.java: -------------------------------------------------------------------------------- 1 | package aedium; 2 | 3 | /** 4 | * Set "NonNullByDefault" to this. 5 | */ 6 | public @interface DummyAnnotationToShutupIDEVerification { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /after/src/main/java/aedium/ElementMarkers.java: -------------------------------------------------------------------------------- 1 | package aedium; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Annotation; 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | 11 | @Documented 12 | @Retention(RUNTIME) 13 | @Target(FIELD) 14 | public @interface ElementMarkers { 15 | Class[] value(); 16 | } 17 | -------------------------------------------------------------------------------- /after/src/main/java/aedium/NonNull.java: -------------------------------------------------------------------------------- 1 | package aedium; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.FIELD; 5 | import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 6 | import static java.lang.annotation.ElementType.PARAMETER; 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | import java.lang.annotation.Documented; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.Target; 13 | 14 | /** 15 | * RUNTIME retention. Checked by ALFRED. 16 | * Also serves as the local equivalent to the Eclipse thingy. 17 | * DO NOT JUST BLINDLY REPLACE THIS WITH ECLIPSE JDT ANNOTATIONS. 18 | */ 19 | @Documented 20 | @Retention(RUNTIME) 21 | @Target({ TYPE, FIELD, PARAMETER, LOCAL_VARIABLE, ANNOTATION_TYPE }) 22 | public @interface NonNull { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /after/src/main/java/aedium/Nullable.java: -------------------------------------------------------------------------------- 1 | package aedium; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.FIELD; 5 | import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 6 | import static java.lang.annotation.ElementType.PARAMETER; 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | import java.lang.annotation.Documented; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.Target; 13 | 14 | /** 15 | * RUNTIME retention. Checked by ALFRED. 16 | * DO NOT JUST BLINDLY REPLACE THIS WITH ECLIPSE JDT ANNOTATIONS. 17 | */ 18 | @Documented 19 | @Retention(RUNTIME) 20 | @Target({ TYPE, FIELD, PARAMETER, LOCAL_VARIABLE, ANNOTATION_TYPE }) 21 | public @interface Nullable { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /after/src/main/java/aedium/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Quick & dirty reimplementation of the Eclipse nullability annotations. 3 | * However, they are set to *runtime* retention for use in AFTER. 4 | */ 5 | package aedium; -------------------------------------------------------------------------------- /after/src/main/java/after/DMBMapping.java: -------------------------------------------------------------------------------- 1 | package after; 2 | 3 | import java.util.HashMap; 4 | 5 | import after.annotations.ClassID; 6 | import after.annotations.HasNoMeaning; 7 | import after.annotations.InstanceID; 8 | import after.annotations.MobTypeID; 9 | import after.io.DMB; 10 | import after.io.DMBClassTable; 11 | import after.io.DMBEntryBasedSubblock; 12 | import after.io.DMBInstanceTable; 13 | import after.io.DMBMobTypeTable; 14 | import after.io.DMBProcTable; 15 | import after.io.DMBStringTable; 16 | import after.io.DMBValue; 17 | 18 | /** 19 | * Creates a mapping between two DMB files (a source and a target). 20 | * Note that instances and strings may be automatically created if necessary. 21 | */ 22 | public class DMBMapping { 23 | public final DMB dmbSrc, dmbTgt; 24 | 25 | // mapping tables 26 | // setup in constructor, never changed after 27 | private HashMap classMap = new HashMap<>(); 28 | private HashMap mobTypeMap = new HashMap<>(); 29 | // dynamic 30 | private HashMap stringMap = new HashMap<>(); 31 | private HashMap instanceMap = new HashMap<>(); 32 | 33 | public final int emptyListTgt; 34 | 35 | public DMBMapping(DMB s, DMB t) { 36 | dmbSrc = s; 37 | dmbTgt = t; 38 | int emptyListWork = -1; 39 | for (int i = 0, iC = dmbTgt.lists.entries.size(); i < iC; i++) { 40 | if (dmbTgt.lists.entries.get(i).length == 0) { 41 | emptyListWork = i; 42 | break; 43 | } 44 | } 45 | if (emptyListWork == -1) 46 | emptyListWork = dmbTgt.lists.add(new int[0]); 47 | emptyListTgt = emptyListWork; 48 | classMap.put(DMBEntryBasedSubblock.OBJ_NULL, DMBEntryBasedSubblock.OBJ_NULL); 49 | for (int i = 0, iC = dmbSrc.classes.entries.size(); i < iC; i++) { 50 | DMBClassTable.Entry iV = dmbSrc.classes.entries.get(i); 51 | if (iV.name == DMBEntryBasedSubblock.OBJ_NULL) 52 | continue; 53 | byte[] iVN = dmbSrc.strings.entries.get(iV.name); 54 | for (int j = 0, jC = dmbTgt.classes.entries.size(); j < jC; j++) { 55 | DMBClassTable.Entry jV = dmbTgt.classes.entries.get(j); 56 | byte[] jVN = dmbTgt.strings.entries.get(jV.name); 57 | if (DMBStringTable.equal(iVN, jVN)) { 58 | classMap.put(i, j); 59 | break; 60 | } 61 | } 62 | } 63 | mobTypeMap.put(DMBEntryBasedSubblock.OBJ_NULL, DMBEntryBasedSubblock.OBJ_NULL); 64 | for (int i = 0, iC = dmbSrc.mobTypes.entries.size(); i < iC; i++) { 65 | DMBMobTypeTable.Entry iV = dmbSrc.mobTypes.entries.get(i); 66 | Integer mappedClass = classMap.get(iV.clazz); 67 | if (mappedClass == null) 68 | continue; 69 | int mappedClassI = mappedClass; 70 | for (int j = 0, jC = dmbTgt.mobTypes.entries.size(); j < jC; j++) { 71 | DMBMobTypeTable.Entry jV = dmbTgt.mobTypes.entries.get(j); 72 | if (mappedClassI == jV.clazz) { 73 | mobTypeMap.put(i, j); 74 | break; 75 | } 76 | } 77 | } 78 | stringMap.put(DMBEntryBasedSubblock.OBJ_NULL, DMBEntryBasedSubblock.OBJ_NULL); 79 | instanceMap.put(DMBEntryBasedSubblock.OBJ_NULL, DMBEntryBasedSubblock.OBJ_NULL); 80 | } 81 | 82 | /** 83 | * Maps a string ID into the target. 84 | * Or creates one if necessary. 85 | */ 86 | private int mapString(int i) { 87 | Integer mapped = stringMap.get(i); 88 | if (mapped != null) 89 | return mapped; 90 | byte[] iV = dmbSrc.strings.entries.get(i); 91 | for (int j = 0, jC = dmbTgt.strings.entries.size(); j < jC; j++) { 92 | byte[] jV = dmbTgt.strings.entries.get(j); 93 | if (DMBStringTable.equal(iV, jV)) { 94 | stringMap.put(i, j); 95 | break; 96 | } 97 | } 98 | int r = dmbTgt.strings.add(dmbSrc.strings.entries.get(i)); 99 | stringMap.put(i, r); 100 | return r; 101 | } 102 | 103 | /** 104 | * Maps an instance ID to the target. 105 | * Or creates one if necessary. 106 | * Can return Java null if the instance requires a class/mobType that doesn't exist. 107 | */ 108 | private Integer mapInstance(int i) { 109 | Integer mapped = instanceMap.get(i); 110 | if (mapped != null) 111 | return mapped; 112 | // If it has no initializer, then we do things the easy way. 113 | // If it has an initializer, then things get hard and we might as well just build a new instance. 114 | DMBInstanceTable.Entry iV = dmbSrc.instances.entries.get(i); 115 | // No matter what, we have to map this for equality checks / etc. 116 | DMBValue iVValueMapped = mapValue(iV.value); 117 | if (iVValueMapped == null) 118 | return null; 119 | // There's a possibility this is dreadfully simple. Check for that. 120 | if (iV.initializer == DMBEntryBasedSubblock.OBJ_NULL) { 121 | for (int j = 0, jC = dmbTgt.instances.entries.size(); j < jC; j++) { 122 | DMBInstanceTable.Entry jV = dmbTgt.instances.entries.get(j); 123 | if (jV.initializer == DMBEntryBasedSubblock.OBJ_NULL) { 124 | if (iVValueMapped.equals(jV.value)) { 125 | instanceMap.put(i, j); 126 | return j; 127 | } 128 | } 129 | } 130 | } 131 | // Not so simple, then. 132 | Integer proc = mapInitializerProc(iV.initializer); 133 | if (proc == null) 134 | return null; 135 | DMBInstanceTable.Entry newInst = new DMBInstanceTable.Entry(); 136 | newInst.value = iVValueMapped; 137 | newInst.initializer = proc; 138 | int newInstId = dmbTgt.instances.add(newInst); 139 | instanceMap.put(i, newInstId); 140 | return newInstId; 141 | } 142 | 143 | /** 144 | * Maps an initializer proc. Note that this always creates a new proc. 145 | * @param i source proc ID 146 | * @return target proc ID on success, null on failure 147 | */ 148 | private Integer mapInitializerProc(int i) { 149 | DMBProcTable.Entry iV = dmbSrc.procs.entries.get(i); 150 | int[] originalCode = dmbSrc.lists.entries.get(iV.code); 151 | int[] newCode = new int[originalCode.length]; 152 | int oPC = 0; 153 | int nPC = 0; 154 | while (oPC < originalCode.length) { 155 | int op = originalCode[oPC++]; 156 | newCode[nPC++] = op; 157 | if (op == 0) { 158 | // END 159 | break; 160 | } else if (op == 0x34) { 161 | // SETVAR 162 | int tmp; 163 | tmp = newCode[nPC++] = originalCode[oPC++]; 164 | if (tmp != 65500) { 165 | System.out.println("incorrect initializer sv[1] " + tmp); 166 | return null; 167 | } 168 | tmp = newCode[nPC++] = originalCode[oPC++]; 169 | if (tmp != 65486) { 170 | System.out.println("incorrect initializer sv[2] " + tmp); 171 | return null; 172 | } 173 | int field = originalCode[oPC++]; 174 | //System.out.println("initializer dbg: " + dmbSrc.strings.getString(field) + " = ..."); 175 | newCode[nPC++] = mapString(field); 176 | } else if (op == 0x50) { 177 | // PUSHI 178 | newCode[nPC++] = originalCode[oPC++]; 179 | } else if (op == 0x60) { 180 | // PUSHVAL 181 | int type = newCode[nPC++] = originalCode[oPC++]; 182 | if (type == DMBValue.NUMBER) { 183 | newCode[nPC++] = originalCode[oPC++]; 184 | newCode[nPC++] = originalCode[oPC++]; 185 | } else { 186 | int val = originalCode[oPC++]; 187 | Integer res = mapValue(type, val); 188 | if (res == null) { 189 | System.out.println("initializer pushval map failure"); 190 | return null; 191 | } 192 | newCode[nPC++] = res; 193 | } 194 | } else { 195 | System.out.println("bad initializer, op " + op); 196 | return null; 197 | } 198 | } 199 | if (nPC != newCode.length) 200 | System.out.println("warning: initializer code size different (bad mapping?)"); 201 | DMBProcTable.Entry newProc = new DMBProcTable.Entry(); 202 | newProc.args = emptyListTgt; 203 | newProc.code = dmbTgt.lists.add(newCode); 204 | newProc.locals = emptyListTgt; 205 | newProc.flags = iV.flags; 206 | return dmbTgt.procs.add(newProc); 207 | } 208 | 209 | /** 210 | * Maps a source value to a target value. 211 | * Note that this can return java null (unmappable) 212 | */ 213 | public Integer mapValue(int type, int value) { 214 | switch (type) { 215 | case DMBValue.NULL: 216 | case DMBValue.NUMBER: 217 | return value; 218 | case DMBValue.STRING: 219 | return mapString(value); 220 | case DMBValue.AREA_TYPEPATH: 221 | case DMBValue.DATUM_TYPEPATH: 222 | case DMBValue.IMAGE_TYPEPATH: 223 | case DMBValue.OBJ_TYPEPATH: 224 | case DMBValue.TURF_TYPEPATH: 225 | return classMap.get(value); 226 | case DMBValue.INSTANCE_TYPEPATH: 227 | return mapInstance(value); 228 | case DMBValue.MOB_TYPEPATH: 229 | return mobTypeMap.get(value); 230 | default: 231 | System.err.println("DMBMapping cannot map type " + type); 232 | return null; 233 | } 234 | } 235 | 236 | /** 237 | * Maps a source value to a target value. 238 | * Note that this can return java null (unmappable) 239 | */ 240 | public DMBValue mapValue(DMBValue dv) { 241 | Integer res = mapValue(dv.type, dv.value); 242 | if (res == null) 243 | return null; 244 | return new DMBValue(dv.type, res); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /after/src/main/java/after/Eden.java: -------------------------------------------------------------------------------- 1 | package after; 2 | 3 | import after.io.DMB; 4 | import after.io.DMBClassTable; 5 | import after.io.DMBInstanceTable; 6 | import after.io.DMBMobTypeTable; 7 | import after.io.DMBValue; 8 | 9 | import static after.io.DMBObjectEntryBasedSubblock.OBJ_NULL; 10 | 11 | import after.annotations.MobTypeID; 12 | 13 | /** 14 | * Current state of the BYOND-less DMB project ('Eden'). 15 | */ 16 | public class Eden { 17 | private static int addClass(DMB dmb, String name, int parent) { 18 | DMBClassTable.Entry datum = new DMBClassTable.Entry(); 19 | datum.name = dmb.strings.of(name); 20 | datum.parent = parent; 21 | return dmb.classes.add(datum); 22 | } 23 | 24 | private static DMBClassTable.Entry cls(DMB dmb, int id) { 25 | return dmb.classes.entries.get(id); 26 | } 27 | 28 | public static DMB newEden() { 29 | DMB dmb = new DMB(); 30 | // I suspect this is relied upon, don't want to jinx it 31 | dmb.strings.entries.add(new byte[0]); 32 | 33 | int mob = addClass(dmb, "", OBJ_NULL); 34 | dmb.standardObjectIDs.client = mob; 35 | 36 | DMBMobTypeTable.Entry mobType = new DMBMobTypeTable.Entry(); 37 | mobType.clazz = mob; 38 | dmb.mobTypes.add(mobType); 39 | dmb.standardObjectIDs.mob = 0; 40 | 41 | dmb.fixStrings(); 42 | return dmb; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/CYCSUB.java: -------------------------------------------------------------------------------- 1 | package after.algorithms; 2 | 3 | /** 4 | * Implementation of CYCSUB. 5 | * This implementation operates on whole buffers at a time in-place. 6 | */ 7 | public class CYCSUB { 8 | /** 9 | * Decrypts data using CYCSUB. 10 | */ 11 | public static void decrypt(byte[] data, int offset, int length, byte[] key, int keyOffset, int keyLength) { 12 | byte checksum = 0; 13 | int keyIndex = 0; 14 | for (int i = length - 1; i >= 0; i--) { 15 | byte roundKey = (byte) (checksum + key[keyOffset + keyIndex]); 16 | // decryption: apply checksum after round key (to get decrypted value) 17 | data[i + offset] -= roundKey; 18 | checksum += data[i + offset]; 19 | keyIndex = (keyIndex + 1) % keyLength; 20 | } 21 | } 22 | 23 | /** 24 | * Encrypts data using CYCSUB. 25 | */ 26 | public static void encrypt(byte[] data, int offset, int length, byte[] key, int keyOffset, int keyLength) { 27 | byte checksum = 0; 28 | int keyIndex = 0; 29 | for (int i = length - 1; i >= 0; i--) { 30 | byte roundKey = (byte) (checksum + key[keyOffset + keyIndex]); 31 | // encryption: apply checksum before round key (to get decrypted value) 32 | checksum += data[i + offset]; 33 | data[i + offset] += roundKey; 34 | keyIndex = (keyIndex + 1) % keyLength; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/NQCRC.java: -------------------------------------------------------------------------------- 1 | package after.algorithms; 2 | 3 | /** 4 | * Implementation of NQCRC. 5 | */ 6 | public class NQCRC { 7 | private static final int[] nqcrcTable = new int[256]; 8 | static { 9 | for (int i = 0; i < nqcrcTable.length; i++) { 10 | int value = i << 0x18; 11 | for (int j = 0; j < 8; j++) 12 | value = (value << 1) ^ (value < 0 ? 0xAF : 0); 13 | nqcrcTable[i] = value; 14 | } 15 | } 16 | 17 | /** 18 | * Advances NQCRC by a single byte. 19 | */ 20 | public static int hash(int hash, byte byt) { 21 | return (hash << 8) ^ nqcrcTable[(hash >>> 24) ^ (byt & 0xFF)]; 22 | } 23 | 24 | /** 25 | * Advances NQCRC by an array of bytes. 26 | */ 27 | public static int hash(int hash, byte[] data) { 28 | for (byte b : data) 29 | hash = hash(hash, b); 30 | return hash; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/RUNSUB.java: -------------------------------------------------------------------------------- 1 | package after.algorithms; 2 | 3 | /** 4 | * Implementation of RUNSUB. 5 | * This implementation operates on whole buffers at a time in-place. 6 | */ 7 | public class RUNSUB { 8 | /** 9 | * Decrypts data using RUNSUB. 10 | * The last byte is checked against the checksum but otherwise ignored. 11 | * If the decryption fails, RUNSUBFailedException is thrown, but the data is put in the buffer. 12 | * 13 | * @throws RUNSUBFailedException On checksum failure 14 | */ 15 | public static void decrypt(byte[] data, int offset, int length, int key) { 16 | byte checksum = 0; 17 | for (int i = 0; i < length - 1; i++) { 18 | byte roundKey = (byte) (checksum + (key >> (checksum % 32))); 19 | // decryption: apply checksum after round key (to get decrypted value) 20 | data[i + offset] -= roundKey; 21 | checksum += data[i + offset]; 22 | } 23 | if (checksum != data[(length - 1) + offset]) 24 | throw new RUNSUBFailedException(data[(length - 1) + offset], checksum); 25 | } 26 | 27 | /** 28 | * Encrypts data using RUNSUB. 29 | * The last byte is overwritten with the checksum. 30 | */ 31 | public static void encrypt(byte[] data, int offset, int length, int key) { 32 | byte checksum = 0; 33 | for (int i = 0; i < length - 1; i++) { 34 | byte roundKey = (byte) (checksum + (key >> (checksum % 32))); 35 | // encryption: apply checksum before round key (to get decrypted value) 36 | checksum += data[i + offset]; 37 | data[i + offset] += roundKey; 38 | } 39 | data[(length - 1) + offset] = checksum; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/RUNSUBFailedException.java: -------------------------------------------------------------------------------- 1 | package after.algorithms; 2 | 3 | /** 4 | * Indicates that a checksum failure happened during RUNSUB decryption. 5 | * 6 | * This is a RuntimeException on the same basis that NumberFormatException is a RuntimeException, 7 | * i.e. that it would be obstructive and annoying to make this a normal Exception. 8 | */ 9 | public class RUNSUBFailedException extends RuntimeException { 10 | /** 11 | * The expected checksum: The one written in the file. 12 | */ 13 | public final byte expected; 14 | 15 | /** 16 | * The resulting checksum: The actual result. 17 | */ 18 | public final byte result; 19 | 20 | public RUNSUBFailedException(byte e, byte g) { 21 | expected = e; 22 | result = g; 23 | } 24 | 25 | @Override 26 | public String getMessage() { 27 | return "RUNSUB decryption failed - stated checksum " + expected + ", got " + result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/XORJump9.java: -------------------------------------------------------------------------------- 1 | package after.algorithms; 2 | 3 | public class XORJump9 { 4 | /** 5 | * Runs XORJUMP9 on some data. 6 | * @return The modified key. 7 | */ 8 | public static byte xorJump9(byte[] data, byte key) { 9 | for (int i = 0; i < data.length; i++) { 10 | data[i] ^= key; 11 | key += 9; 12 | } 13 | return key; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/algorithms/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodings and algorithms used in various formats. 3 | */ 4 | package after.algorithms; 5 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/CacheFileID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface CacheFileID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/ClassID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ClassID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/DMBList.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface DMBList { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/HasNoMeaning.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Marker type for use by DMBValue 12 | */ 13 | @Documented 14 | @Retention(RUNTIME) 15 | @Target(FIELD) 16 | public @interface HasNoMeaning { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/InstanceID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface InstanceID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/ListID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ListID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/ListOfProcID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ListOfProcID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/ListOfVarID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ListOfVarID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/MobTypeID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface MobTypeID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/ProcID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ProcID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/StringID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface StringID { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/VarID.java: -------------------------------------------------------------------------------- 1 | package after.annotations; 2 | 3 | import static java.lang.annotation.ElementType.FIELD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Almost certainly won't be used as an actual annotation. 12 | * Exists as a marker. 13 | */ 14 | @Documented 15 | @Retention(RUNTIME) 16 | @Target(FIELD) 17 | public @interface VarID { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /after/src/main/java/after/annotations/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Marker annotations used by everything except this. 3 | */ 4 | package after.annotations; -------------------------------------------------------------------------------- /after/src/main/java/after/dm/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for working with DM. In text form. 3 | */ 4 | package after.dm; 5 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/CacheID.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | /** 4 | * Represents a Cache ID as used in both the DMB and RSC file formats. 5 | */ 6 | public final class CacheID { 7 | public static final CacheID ZERO = new CacheID(0, (byte) 0); 8 | 9 | public final int uniqueID; 10 | public final byte typeOrSomething; 11 | 12 | public CacheID(int uid, byte type) { 13 | uniqueID = uid; 14 | typeOrSomething = type; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "(" + typeOrSomething + ", " + uniqueID + ")"; 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return uniqueID; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object obj) { 29 | if (!(obj instanceof CacheID)) 30 | return false; 31 | CacheID cid = ((CacheID) obj); 32 | return (cid.typeOrSomething == typeOrSomething) && (cid.uniqueID == uniqueID); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMB.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | import java.nio.ByteBuffer; 5 | import java.nio.ByteOrder; 6 | 7 | import aedium.NonNull; 8 | import after.io.framework.DMBReadContext; 9 | import after.io.framework.DMBWriteContext; 10 | 11 | /** 12 | * This is the root, the DMB file as a whole. 13 | */ 14 | public class DMB { 15 | 16 | @NonNull 17 | public DMBHeader header = new DMBHeader(); 18 | @NonNull 19 | public DMBGrid grid = new DMBGrid(); 20 | 21 | public int totalSizeOfAllStrings; 22 | 23 | @NonNull 24 | public DMBClassTable classes = new DMBClassTable(); 25 | @NonNull 26 | public DMBMobTypeTable mobTypes = new DMBMobTypeTable(); 27 | @NonNull 28 | public DMBStringTable strings = new DMBStringTable(); 29 | @NonNull 30 | public DMBListTable lists = new DMBListTable(); 31 | @NonNull 32 | public DMBProcTable procs = new DMBProcTable(); 33 | @NonNull 34 | public DMBVarTable vars = new DMBVarTable(); 35 | @NonNull 36 | public DMBSubblock7 sb7 = new DMBSubblock7(); 37 | @NonNull 38 | public DMBInstanceTable instances = new DMBInstanceTable(); 39 | @NonNull 40 | public DMBMapAdditionalData mapAdditionalData = new DMBMapAdditionalData(); 41 | @NonNull 42 | public DMBStandardObjectIDs standardObjectIDs = new DMBStandardObjectIDs(); 43 | @NonNull 44 | public DMBCacheFileTable cacheFiles = new DMBCacheFileTable(); 45 | 46 | public void read(DMBReadContext rc) { 47 | // Sub-Block -1 (Header) 48 | rc.section("header"); 49 | header.read(rc); 50 | 51 | // Sub-Block 0 (The Grid) 52 | rc.section("grid"); 53 | grid.read(rc); 54 | 55 | // Interlude 56 | rc.section("totalSizeOfAllStrings"); 57 | totalSizeOfAllStrings = rc.i32(); 58 | if (rc.debug) 59 | System.out.println(" = " + totalSizeOfAllStrings); 60 | 61 | // Classes Table 62 | rc.section("classes"); 63 | classes.read(rc); 64 | if (rc.debug) 65 | System.out.println(" = " + classes.entries.size()); 66 | 67 | // Mob Type Table 68 | rc.section("mobTypes"); 69 | mobTypes.read(rc); 70 | if (rc.debug) 71 | System.out.println(" = " + mobTypes.entries.size()); 72 | 73 | // String Table 74 | rc.section("strings"); 75 | strings.read(rc); 76 | 77 | // List Table 78 | rc.section("lists"); 79 | lists.read(rc); 80 | 81 | // Proc Table 82 | rc.section("procs"); 83 | procs.read(rc); 84 | 85 | // Var Table 86 | rc.section("vars"); 87 | vars.read(rc); 88 | 89 | // Sub-Block 7 90 | rc.section("sb7"); 91 | sb7.read(rc); 92 | 93 | // Instance Table 94 | rc.section("instances"); 95 | instances.read(rc); 96 | 97 | // Map Additional Data 98 | rc.section("mapAdditionalData"); 99 | mapAdditionalData.read(rc); 100 | 101 | // Map Additional Data 102 | rc.section("standardObjectIDs"); 103 | standardObjectIDs.read(rc); 104 | 105 | rc.section("cacheFiles"); 106 | cacheFiles.read(rc); 107 | 108 | // -- other unfinished -- 109 | rc.section("unfinished"); 110 | int unfinished = rc.remaining(); 111 | if (unfinished != 0) 112 | if (rc.debug) 113 | System.err.println("warning: ignored 0x" + Integer.toHexString(unfinished)); 114 | } 115 | 116 | public void write(DMBWriteContext wc) { 117 | wc.section("header"); 118 | header.write(wc); 119 | 120 | wc.section("grid"); 121 | grid.write(wc); 122 | 123 | wc.section("totalSizeOfAllStrings"); 124 | wc.i32(totalSizeOfAllStrings); 125 | 126 | wc.section("classes"); 127 | classes.write(wc); 128 | 129 | wc.section("mobTypes"); 130 | mobTypes.write(wc); 131 | 132 | wc.section("strings"); 133 | strings.write(wc); 134 | 135 | wc.section("lists"); 136 | lists.write(wc); 137 | 138 | wc.section("procs"); 139 | procs.write(wc); 140 | 141 | wc.section("vars"); 142 | vars.write(wc); 143 | 144 | wc.section("sb7"); 145 | sb7.write(wc); 146 | 147 | wc.section("instances"); 148 | instances.write(wc); 149 | 150 | wc.section("mapAdditionalData"); 151 | mapAdditionalData.write(wc); 152 | 153 | wc.section("standardObjectIDs"); 154 | standardObjectIDs.write(wc); 155 | 156 | wc.section("cacheFiles"); 157 | cacheFiles.write(wc); 158 | } 159 | 160 | /** 161 | * Fixes the string table stuff after it's been twiddled with 162 | */ 163 | public void fixStrings() { 164 | strings.hash = strings.calculateHash(); 165 | totalSizeOfAllStrings = strings.calculateTotalSize(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBCacheFileTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import after.io.framework.DMBReadContext; 4 | import after.io.framework.DMBWriteContext; 5 | 6 | /** 7 | * References to resources. 8 | */ 9 | public class DMBCacheFileTable extends DMBEntryBasedSubblock { 10 | @Override 11 | protected CacheID createDummyValue() { 12 | return CacheID.ZERO; 13 | } 14 | 15 | @Override 16 | public CacheID readEntry(DMBReadContext rc) { 17 | int uniqueID = rc.i32(); 18 | byte typeOrSomething = rc.i8(); 19 | return new CacheID(uniqueID, typeOrSomething); 20 | } 21 | 22 | @Override 23 | public void writeEntry(DMBWriteContext wc, CacheID entry) { 24 | wc.i32(entry.uniqueID); 25 | wc.i8(entry.typeOrSomething); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBClassTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import after.annotations.CacheFileID; 6 | import after.annotations.ClassID; 7 | import after.annotations.ListID; 8 | import after.annotations.ListOfProcID; 9 | import after.annotations.ProcID; 10 | import after.annotations.StringID; 11 | import after.io.framework.DMBReadContext; 12 | import after.io.framework.DMBWriteContext; 13 | 14 | public class DMBClassTable extends DMBObjectEntryBasedSubblock { 15 | 16 | @Override 17 | public Entry make() { 18 | return new Entry(); 19 | } 20 | 21 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 22 | public static final int CF_MOB = 2; 23 | public static final int CF_ATOM = 4; 24 | public static final int CF_AREA = 8; 25 | 26 | @StringID 27 | public int name; 28 | @ClassID // Nullable 29 | public int parent = OBJ_NULL; 30 | @StringID // Nullable 31 | public int objName = OBJ_NULL; 32 | @StringID // Nullable 33 | public int description = OBJ_NULL; 34 | @CacheFileID // Nullable 35 | public int icon = OBJ_NULL; 36 | @StringID // Nullable 37 | public int iconState = OBJ_NULL; 38 | 39 | // need to figure this out too 40 | public byte direction = 2; 41 | 42 | // >= 307 43 | 44 | public boolean dmSpecialTypeLong; 45 | public int dmSpecialType = 1; 46 | 47 | // -- 48 | 49 | @StringID // maybe nullable 50 | public int text = OBJ_NULL; 51 | 52 | // >= 494 53 | 54 | @StringID // Nullable 55 | public int maptext; 56 | public short maptextWidth; 57 | public short maptextHeight; 58 | 59 | // >= 508 60 | 61 | public short maptextX; 62 | public short maptextY; 63 | 64 | // -- 65 | 66 | @StringID // Nullable 67 | public int suffix = OBJ_NULL; 68 | // see CF_* constants 69 | public int flags; 70 | @ListOfProcID // Nullable (not the elements) 71 | public int verbTable = OBJ_NULL; 72 | @ListOfProcID // Nullable (not the elements) 73 | public int procTable = OBJ_NULL; 74 | @ProcID // Nullable 75 | public int initializer = OBJ_NULL; 76 | @ListID // Marked Nullable because honestly it probably is, but unsure 77 | public int initializedVarsTable = OBJ_NULL; 78 | @ListID // Nullable, (complex key/value pairs thing, see docs) 79 | public int varTable = OBJ_NULL; 80 | 81 | // >= 267 82 | 83 | public float layer; 84 | 85 | // >= 500 86 | 87 | public byte hasTransform; 88 | public float[] transform = new float[6]; 89 | 90 | // >= 509 91 | 92 | public byte hasColourMatrix; 93 | public float[] colourMatrix = new float[20]; 94 | 95 | // >= 306 96 | 97 | @ListID // Nullable, complex key/value pairs thing 98 | public int overridingVarList = OBJ_NULL; 99 | 100 | @Override 101 | public void read(DMBReadContext rc) { 102 | name = rc.id(); 103 | parent = rc.id(); 104 | objName = rc.id(); 105 | description = rc.id(); 106 | icon = rc.id(); 107 | iconState = rc.id(); 108 | direction = rc.io.get(); 109 | 110 | if (rc.vGEN >= 307) { 111 | dmSpecialType = rc.io.get(); 112 | dmSpecialTypeLong = dmSpecialType == 0x0F; 113 | if (dmSpecialTypeLong) 114 | dmSpecialType = rc.io.getInt(); 115 | } else { 116 | dmSpecialType = 1; 117 | } 118 | 119 | text = rc.id(); 120 | 121 | if (rc.vRHS >= 494) { 122 | maptext = rc.id(); 123 | maptextWidth = rc.io.getShort(); 124 | maptextHeight = rc.io.getShort(); 125 | } 126 | 127 | if (rc.vRHS >= 508) { 128 | maptextX = rc.io.getShort(); 129 | maptextY = rc.io.getShort(); 130 | } 131 | 132 | suffix = rc.id(); 133 | if (rc.vGEN >= 306) { 134 | flags = rc.io.getInt(); 135 | } else { 136 | flags = rc.io.get() & 0xFF; 137 | } 138 | verbTable = rc.id(); 139 | procTable = rc.id(); 140 | initializer = rc.id(); 141 | initializedVarsTable = rc.id(); 142 | varTable = rc.id(); 143 | 144 | if (rc.vGEN >= 267) 145 | layer = rc.io.getFloat(); 146 | 147 | if (rc.vRHS >= 500) { 148 | hasTransform = rc.io.get(); 149 | if (hasTransform != 0) 150 | rc.floats(transform); 151 | } 152 | 153 | if (rc.vRHS >= 509) { 154 | hasColourMatrix = rc.io.get(); 155 | if (hasColourMatrix != 0) 156 | rc.floats(colourMatrix); 157 | } 158 | 159 | if (rc.vGEN >= 306) 160 | overridingVarList = rc.id(); 161 | } 162 | 163 | @Override 164 | public void write(DMBWriteContext wc) { 165 | wc.id(name); 166 | wc.id(parent); 167 | wc.id(objName); 168 | wc.id(description); 169 | wc.id(icon); 170 | wc.id(iconState); 171 | wc.i8(direction); 172 | 173 | if (wc.vGEN >= 307) { 174 | if (dmSpecialTypeLong) { 175 | wc.i8(0x0F); 176 | wc.i32(dmSpecialType); 177 | } else { 178 | wc.i8(dmSpecialType); 179 | } 180 | } 181 | 182 | wc.id(text); 183 | 184 | if (wc.vRHS >= 494) { 185 | wc.id(maptext); 186 | wc.i16(maptextWidth); 187 | wc.i16(maptextHeight); 188 | } 189 | 190 | if (wc.vRHS >= 508) { 191 | wc.i16(maptextX); 192 | wc.i16(maptextY); 193 | } 194 | 195 | wc.id(suffix); 196 | if (wc.vGEN >= 306) { 197 | wc.i32(flags); 198 | } else { 199 | wc.i8(flags); 200 | } 201 | wc.id(verbTable); 202 | wc.id(procTable); 203 | wc.id(initializer); 204 | wc.id(initializedVarsTable); 205 | wc.id(varTable); 206 | if (wc.vGEN >= 267) 207 | wc.i32(Float.floatToIntBits(layer)); 208 | 209 | if (wc.vRHS >= 500) { 210 | wc.i8(hasTransform); 211 | if (hasTransform != 0) 212 | wc.floats(transform); 213 | } 214 | 215 | if (wc.vRHS >= 509) { 216 | wc.i8(hasColourMatrix); 217 | if (hasColourMatrix != 0) 218 | wc.floats(colourMatrix); 219 | } 220 | 221 | if (wc.vGEN >= 306) 222 | wc.id(overridingVarList); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBEntryBasedSubblock.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.util.LinkedList; 4 | 5 | import after.io.framework.DMBReadContext; 6 | import after.io.framework.DMBWriteContext; 7 | 8 | /** 9 | * This implements the basic 'list of X' logic. This is NOT exclusive with having other properties. 10 | */ 11 | public abstract class DMBEntryBasedSubblock { 12 | public static int OBJ_NULL = 0xFFFF; 13 | 14 | /** 15 | * The list of entries. This is a linked list for easier manipulation. 16 | */ 17 | public LinkedList entries = new LinkedList<>(); 18 | 19 | public DMBEntryBasedSubblock() { 20 | super(); 21 | } 22 | 23 | protected abstract X createDummyValue(); 24 | 25 | /** 26 | * Indicates if a given index is reserved. 27 | */ 28 | public boolean indexReserved(int index) { 29 | return index == OBJ_NULL; 30 | } 31 | 32 | protected int readCount(DMBReadContext rc) { 33 | return rc.id(); 34 | } 35 | 36 | public void read(DMBReadContext rc) { 37 | int count = readCount(rc); 38 | for (int i = 0; i < count; i++) 39 | entries.add(readEntry(rc)); 40 | } 41 | 42 | protected void writeCount(DMBWriteContext wc) { 43 | wc.id(entries.size()); 44 | } 45 | 46 | public void write(DMBWriteContext wc) { 47 | writeCount(wc); 48 | for (X entry : entries) 49 | writeEntry(wc, entry); 50 | } 51 | 52 | public int add(X value) { 53 | int res = entries.size(); 54 | while (indexReserved(res)) { 55 | entries.add(createDummyValue()); 56 | res++; 57 | } 58 | entries.add(value); 59 | return res; 60 | } 61 | 62 | public abstract X readEntry(DMBReadContext rc); 63 | public abstract void writeEntry(DMBWriteContext wc, X entry); 64 | } -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBGrid.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import aedium.ElementMarkers; 6 | import aedium.NonNull; 7 | import after.annotations.InstanceID; 8 | import after.io.framework.DMBReadContext; 9 | import after.io.framework.DMBWriteContext; 10 | 11 | /** 12 | * This represents the '3D' map. 13 | */ 14 | public class DMBGrid { 15 | public int xSize = 0, ySize = 0, zSize = 0; 16 | // Array of nullable (0xFFFF) InstanceIDs 17 | @ElementMarkers({InstanceID.class}) 18 | @NonNull 19 | public int[] turf = new int[0]; 20 | // Array of nullable (0xFFFF) InstanceIDs 21 | @ElementMarkers({InstanceID.class}) 22 | @NonNull 23 | public int[] area = new int[0]; 24 | // Array of nullable (0xFFFF) ListIDs of ObjectIDs 25 | @NonNull 26 | public int[] additionalTurfs = new int[0]; 27 | 28 | public void read(DMBReadContext rc) { 29 | xSize = rc.io.getShort() & 0xFFFF; 30 | ySize = rc.io.getShort() & 0xFFFF; 31 | zSize = rc.io.getShort() & 0xFFFF; 32 | int total = xSize * ySize * zSize; 33 | turf = new int[total]; 34 | area = new int[total]; 35 | additionalTurfs = new int[total]; 36 | 37 | int i = 0; 38 | while (i < total) { 39 | turf[i] = rc.id(); 40 | area[i] = rc.id(); 41 | additionalTurfs[i] = rc.id(); 42 | int dup = rc.io.get() & 0xFF; 43 | for (int j = 0; j < dup; j++) { 44 | turf[i + j] = turf[i]; 45 | area[i + j] = area[i]; 46 | additionalTurfs[i + j] = additionalTurfs[i]; 47 | } 48 | i += dup; 49 | } 50 | } 51 | 52 | public void write(DMBWriteContext wc) { 53 | wc.i16(xSize); 54 | wc.i16(ySize); 55 | wc.i16(zSize); 56 | int total = xSize * ySize * zSize; 57 | int rleStart = -1; 58 | for (int i = 0; i < total; i++) { 59 | boolean reset = false; 60 | // reset conditions { 61 | // ensure that an RLE length can never exceed 255 62 | if (i - rleStart == 255) 63 | reset = true; 64 | // detect if an RLE change occurred 65 | if (rleStart != -1) { 66 | if (turf[i] != turf[rleStart]) 67 | reset = true; 68 | else if (area[i] != area[rleStart]) 69 | reset = true; 70 | else if (additionalTurfs[i] != additionalTurfs[rleStart]) 71 | reset = true; 72 | } 73 | // actually apply resets 74 | if (reset) { 75 | wc.i8(i - rleStart); 76 | rleStart = -1; 77 | } 78 | // if not extending (this doesn't necessarily mean a reset happened, for first tile), write & start 79 | if (rleStart == -1) { 80 | wc.id(turf[i]); 81 | wc.id(area[i]); 82 | wc.id(additionalTurfs[i]); 83 | rleStart = i; 84 | } 85 | } 86 | if (rleStart != -1) 87 | wc.i8(total - rleStart); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBHeader.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import aedium.NonNull; 7 | import after.io.framework.DMBContext; 8 | import after.io.framework.DMBReadContext; 9 | import after.io.framework.DMBWriteContext; 10 | 11 | public class DMBHeader { 12 | // These are converted in Latin-1 style for lack of any better ideas 13 | public String shebang = null; 14 | 15 | private static int DEFAULT_VERSION = 230; 16 | 17 | public int vGEN = DEFAULT_VERSION, vLHS = DEFAULT_VERSION, vRHS = DEFAULT_VERSION; 18 | public int flags = 0; 19 | public int exFlags; 20 | 21 | public void read(DMBReadContext rc) { 22 | StringBuilder lineText = new StringBuilder(); 23 | boolean compatLine = false; 24 | shebang = ""; 25 | while (true) { 26 | byte b = rc.io.get(); 27 | if (b == 10) { 28 | String text = lineText.toString(); 29 | if (!compatLine) { 30 | if (text.startsWith("#")) { 31 | shebang += text + "\n"; 32 | // move the base pointer forward to the next line 33 | // if there's more comment lines, this keeps happening 34 | rc.basePointer = rc.io.position(); 35 | } else { 36 | vLHS = vRHS = vGEN = Integer.parseInt(text.split(" v")[1]); 37 | compatLine = true; 38 | } 39 | } else { 40 | String[] versionBits = text.split(" v")[1].split(" "); 41 | if (versionBits.length < 2) { 42 | int v = Integer.parseInt(versionBits[0]); 43 | vLHS = vRHS = v; 44 | } else { 45 | vLHS = Integer.parseInt(versionBits[0]); 46 | vRHS = Integer.parseInt(versionBits[1]); 47 | } 48 | // Done 49 | break; 50 | } 51 | lineText = new StringBuilder(); 52 | } else { 53 | lineText.append((char) (b & 0xFF)); 54 | } 55 | } 56 | 57 | flags = rc.io.getInt(); 58 | exFlags = 0; 59 | if ((flags & 0x80000000) != 0) 60 | exFlags = rc.io.getInt(); 61 | 62 | updateContext(rc); 63 | 64 | if (rc.debug) { 65 | System.out.println(" version " + vGEN + " " + vLHS + " " + vRHS); 66 | System.out.println(" large object IDs = " + rc.largeObjectIDs); 67 | System.out.println(" base pointer = " + rc.basePointer); 68 | } 69 | } 70 | 71 | public void write(DMBWriteContext wc) { 72 | if (shebang != null) 73 | wc.bytes(shebang.getBytes(StandardCharsets.ISO_8859_1)); 74 | wc.basePointer = wc.position; 75 | wc.bytes(("world bin v" + vGEN).getBytes(StandardCharsets.ISO_8859_1)); 76 | wc.i8(10); 77 | wc.bytes(("min compatibility v" + vLHS + " " + vRHS).getBytes(StandardCharsets.ISO_8859_1)); 78 | wc.i8(10); 79 | 80 | wc.i32(flags); 81 | if ((flags & 0x80000000) != 0) 82 | wc.i32(exFlags); 83 | 84 | updateContext(wc); 85 | } 86 | 87 | private void updateContext(DMBContext c) { 88 | c.vGEN = vGEN; 89 | c.vLHS = vLHS; 90 | c.vRHS = vRHS; 91 | c.largeObjectIDs = (flags & 0x40000000) != 0; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBInstanceTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import after.annotations.ClassID; 6 | import after.annotations.ProcID; 7 | import after.io.framework.DMBReadContext; 8 | import after.io.framework.DMBWriteContext; 9 | 10 | public class DMBInstanceTable extends DMBObjectEntryBasedSubblock { 11 | @Override 12 | public Entry make() { 13 | return new Entry(); 14 | } 15 | 16 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 17 | public DMBValue value = DMBValue.NULL_VALUE; 18 | @ProcID // Nullable 19 | public int initializer = OBJ_NULL; 20 | 21 | @Override 22 | public void read(DMBReadContext rc) { 23 | byte bt = rc.io.get(); 24 | int v = rc.io.getInt(); 25 | value = new DMBValue(bt, v); 26 | initializer = rc.id(); 27 | } 28 | 29 | @Override 30 | public void write(DMBWriteContext wc) { 31 | wc.i8(value.type); 32 | wc.i32(value.value); 33 | wc.id(initializer); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBListTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.util.LinkedList; 4 | 5 | import after.io.framework.DMBReadContext; 6 | import after.io.framework.DMBWriteContext; 7 | 8 | /** 9 | * Lists of arrays of ObjectIDs. 10 | */ 11 | public class DMBListTable extends DMBEntryBasedSubblock { 12 | @Override 13 | protected int[] createDummyValue() { 14 | return new int[0]; 15 | } 16 | 17 | @Override 18 | public int[] readEntry(DMBReadContext rc) { 19 | int[] ie = new int[rc.io.getShort() & 0xFFFF]; 20 | rc.ids(ie); 21 | return ie; 22 | } 23 | 24 | @Override 25 | public void writeEntry(DMBWriteContext wc, int[] entry) { 26 | wc.i16(entry.length); 27 | wc.ids(entry); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBMapAdditionalData.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import after.io.framework.DMBReadContext; 4 | import after.io.framework.DMBWriteContext; 5 | 6 | public class DMBMapAdditionalData extends DMBObjectEntryBasedSubblock { 7 | @Override 8 | public Entry make() { 9 | return new Entry(); 10 | } 11 | 12 | @Override 13 | protected int readCount(DMBReadContext rc) { 14 | return rc.io.getInt(); 15 | } 16 | 17 | @Override 18 | protected void writeCount(DMBWriteContext wc) { 19 | wc.i32(entries.size()); 20 | } 21 | 22 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 23 | // Offset from last entry 24 | public short offset; 25 | // Nullable InstanceID 26 | public int instance = DMBObjectEntryBasedSubblock.OBJ_NULL; 27 | 28 | @Override 29 | public void read(DMBReadContext rc) { 30 | offset = rc.io.getShort(); 31 | instance = rc.id(); 32 | } 33 | 34 | @Override 35 | public void write(DMBWriteContext wc) { 36 | wc.i16(offset); 37 | wc.id(instance); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBMobTypeTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import after.annotations.ClassID; 6 | import after.annotations.StringID; 7 | import after.io.framework.DMBReadContext; 8 | import after.io.framework.DMBWriteContext; 9 | 10 | public class DMBMobTypeTable extends DMBObjectEntryBasedSubblock { 11 | @Override 12 | public Entry make() { 13 | return new Entry(); 14 | } 15 | 16 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 17 | @ClassID 18 | public int clazz; 19 | @StringID // Nullable 20 | public int key; 21 | public byte sightFlags; 22 | public int sightFlagsEx; 23 | public byte seeInDark; 24 | public byte seeInvisible; 25 | 26 | @Override 27 | public void read(DMBReadContext rc) { 28 | clazz = rc.id(); 29 | key = rc.id(); 30 | sightFlags = rc.io.get(); 31 | if (sightFlags < 0) { 32 | sightFlagsEx = rc.io.getInt(); 33 | seeInDark = rc.io.get(); 34 | seeInvisible = rc.io.get(); 35 | } 36 | } 37 | 38 | @Override 39 | public void write(DMBWriteContext wc) { 40 | wc.id(clazz); 41 | wc.id(key); 42 | wc.i8(sightFlags); 43 | if (sightFlags < 0) { 44 | wc.i32(sightFlagsEx); 45 | wc.i8(seeInDark); 46 | wc.i8(seeInvisible); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBObjectEntryBasedSubblock.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.util.LinkedList; 4 | 5 | import after.io.framework.DMBReadContext; 6 | import after.io.framework.DMBWriteContext; 7 | 8 | public abstract class DMBObjectEntryBasedSubblock extends DMBEntryBasedSubblock { 9 | @Override 10 | protected X createDummyValue() { 11 | return make(); 12 | } 13 | 14 | @Override 15 | public X readEntry(DMBReadContext rc) { 16 | X entry = make(); 17 | entry.read(rc); 18 | return entry; 19 | } 20 | 21 | @Override 22 | public void writeEntry(DMBWriteContext rc, X entry) { 23 | entry.write(rc); 24 | } 25 | 26 | public abstract X make(); 27 | 28 | public static interface Entry { 29 | void read(DMBReadContext rc); 30 | void write(DMBWriteContext wc); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBProcTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import after.annotations.ListID; 6 | import after.annotations.ListOfVarID; 7 | import after.annotations.StringID; 8 | import after.io.framework.DMBReadContext; 9 | import after.io.framework.DMBWriteContext; 10 | 11 | public class DMBProcTable extends DMBObjectEntryBasedSubblock { 12 | @Override 13 | public Entry make() { 14 | return new Entry(); 15 | } 16 | 17 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 18 | // GEN >= 224 | LO 19 | @StringID // Nullable 20 | public int path = OBJ_NULL; 21 | // -- 22 | @StringID // Nullable 23 | public int name = OBJ_NULL; 24 | @StringID // Nullable 25 | public int desc = OBJ_NULL; 26 | @StringID // Nullable 27 | public int verbCategory = OBJ_NULL; 28 | public byte verbSrcParam = -1; 29 | public byte verbSrcKind; 30 | public byte flags; 31 | public int flagsExx; 32 | public byte flagsExy; 33 | // ListID (list containing DM bytecode) 34 | @ListID 35 | public int code = OBJ_NULL; // fill these with NULL so that the 0 doesn't get used by mistake 36 | @ListOfVarID 37 | public int locals = OBJ_NULL; 38 | @ListID 39 | public int args = OBJ_NULL; 40 | 41 | @Override 42 | public void read(DMBReadContext rc) { 43 | if (rc.vGEN >= 224 || rc.largeObjectIDs) 44 | path = rc.id(); 45 | name = rc.id(); 46 | desc = rc.id(); 47 | verbCategory = rc.id(); 48 | verbSrcParam = rc.io.get(); 49 | verbSrcKind = rc.io.get(); 50 | flags = rc.io.get(); 51 | if (flags < 0) { 52 | flagsExx = rc.io.getInt(); 53 | flagsExy = rc.io.get(); 54 | } 55 | code = rc.id(); 56 | locals = rc.id(); 57 | args = rc.id(); 58 | } 59 | 60 | @Override 61 | public void write(DMBWriteContext wc) { 62 | if (wc.vGEN >= 224 || wc.largeObjectIDs) 63 | wc.id(path); 64 | wc.id(name); 65 | wc.id(desc); 66 | wc.id(verbCategory); 67 | wc.i8(verbSrcParam); 68 | wc.i8(verbSrcKind); 69 | wc.i8(flags); 70 | if (flags < 0) { 71 | wc.i32(flagsExx); 72 | wc.i8(flagsExy); 73 | } 74 | wc.id(code); 75 | wc.id(locals); 76 | wc.id(args); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBStandardObjectIDs.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import after.annotations.CacheFileID; 4 | import after.annotations.ClassID; 5 | import after.annotations.ListOfProcID; 6 | import after.annotations.MobTypeID; 7 | import after.annotations.ProcID; 8 | import after.annotations.StringID; 9 | import after.io.framework.DMBReadContext; 10 | import after.io.framework.DMBWriteContext; 11 | import static after.io.DMBObjectEntryBasedSubblock.OBJ_NULL; 12 | 13 | import aedium.ElementMarkers; 14 | 15 | public class DMBStandardObjectIDs { 16 | @MobTypeID // Nullable 17 | public int mob = OBJ_NULL; 18 | // Nullable ClassID 19 | @ClassID // Nullable 20 | public int turf = OBJ_NULL; 21 | @ClassID // Nullable 22 | public int area = OBJ_NULL; 23 | @ListOfProcID 24 | public int procs = OBJ_NULL; 25 | @ProcID // Nullable 26 | public int globalVariableInitializer = OBJ_NULL; 27 | @StringID // Nullable 28 | public int domain = OBJ_NULL; 29 | @StringID 30 | public int name = OBJ_NULL; 31 | // GEN < 368 (yes, under) 32 | public int idOld1 = OBJ_NULL; 33 | // -- 34 | public int tickTimeMillis = 100; 35 | @ClassID 36 | public int client = OBJ_NULL; 37 | // GEN >= 308 38 | @ClassID 39 | public int image = OBJ_NULL; 40 | // -- 41 | public byte clientLazyEye; 42 | public byte clientDir = 1; 43 | // GEN >= 415 44 | public short clientControlFreak; 45 | // -- 46 | public byte unkE; 47 | // GEN >= 230 48 | @StringID // Nullable 49 | public int clientScript = OBJ_NULL; 50 | // -- 51 | // GEN >= 507 52 | @ElementMarkers(CacheFileID.class) 53 | public int[] clientScriptFiles = new int[0]; 54 | // GEN < 507 (yes, under) 55 | public int idX = OBJ_NULL; 56 | // GEN >= 232 57 | public short unkF = 2827; 58 | // GEN >= 235 && GEN < 368 59 | public short unkPX; 60 | // GEN >= 236 && GEN < 368 61 | public short unkPY; 62 | // GEN >= 341 63 | @StringID // Nullable 64 | public int hubPasswordHashed = OBJ_NULL; 65 | // GEN >= 266 66 | @StringID // Nullable 67 | public int serverName = OBJ_NULL; 68 | public int hubNumber; 69 | public int gameVersion; 70 | // GEN >= 272 71 | public short cacheLifespan = 30; 72 | @StringID // Nullable 73 | public int clientCommandText; 74 | @StringID // Nullable 75 | public int clientCommandPrompt; 76 | // GEN >= 276 77 | @StringID // Nullable 78 | public int hub = OBJ_NULL; 79 | // GEN >= 305 80 | @StringID // Nullable (?) but really should be "default" 81 | public int channel = OBJ_NULL; 82 | // GEN >= 360 83 | @CacheFileID // Nullable 84 | public int skin = OBJ_NULL; 85 | 86 | // LHS >= 455 87 | public short iconSizeX = 32; 88 | public short iconSizeY = 32; 89 | public short mapFormat = (short) 32768; 90 | 91 | public void read(DMBReadContext rc) { 92 | mob = rc.id(); 93 | turf = rc.id(); 94 | area = rc.id(); 95 | procs = rc.id(); 96 | globalVariableInitializer = rc.id(); 97 | domain = rc.id(); 98 | name = rc.id(); 99 | if (rc.vGEN < 368) 100 | idOld1 = rc.id(); 101 | tickTimeMillis = rc.io.getInt(); 102 | client = rc.id(); 103 | if (rc.vGEN >= 308) 104 | image = rc.id(); 105 | clientLazyEye = rc.io.get(); 106 | clientDir = rc.io.get(); 107 | if (rc.vGEN >= 415) 108 | clientControlFreak = rc.io.getShort(); 109 | unkE = rc.io.get(); 110 | if (rc.vGEN >= 230) 111 | clientScript = rc.id(); 112 | if (rc.vGEN >= 507) { 113 | clientScriptFiles = new int[rc.io.getShort() & 0xFFFF]; 114 | rc.ids(clientScriptFiles); 115 | } 116 | if (rc.vGEN < 507) { 117 | idX = rc.id(); 118 | } 119 | if (rc.vGEN >= 232) 120 | unkF = rc.io.getShort(); 121 | if (rc.vGEN >= 235 && rc.vGEN < 368) 122 | unkPX = rc.io.getShort(); 123 | if (rc.vGEN >= 236 && rc.vGEN < 368) 124 | unkPY = rc.io.getShort(); 125 | if (rc.vGEN >= 341) 126 | hubPasswordHashed = rc.id(); 127 | if (rc.vGEN >= 266) { 128 | serverName = rc.id(); 129 | hubNumber = rc.io.getInt(); 130 | gameVersion = rc.io.getInt(); 131 | } 132 | if (rc.vGEN >= 272) { 133 | cacheLifespan = rc.io.getShort(); 134 | clientCommandText = rc.id(); 135 | clientCommandPrompt = rc.id(); 136 | } 137 | if (rc.vGEN >= 276) 138 | hub = rc.id(); 139 | if (rc.vGEN >= 305) 140 | channel = rc.id(); 141 | if (rc.vGEN >= 360) 142 | skin = rc.id(); 143 | 144 | if (rc.vLHS >= 455) { 145 | iconSizeX = rc.io.getShort(); 146 | iconSizeY = rc.io.getShort(); 147 | mapFormat = rc.io.getShort(); 148 | } else { 149 | iconSizeX = 32; 150 | iconSizeY = 32; 151 | mapFormat = (short) 32768; 152 | } 153 | } 154 | 155 | public void write(DMBWriteContext wc) { 156 | wc.id(mob); 157 | wc.id(turf); 158 | wc.id(area); 159 | wc.id(procs); 160 | wc.id(globalVariableInitializer); 161 | wc.id(domain); 162 | wc.id(name); 163 | if (wc.vGEN < 368) 164 | wc.id(idOld1); 165 | wc.i32(tickTimeMillis); 166 | wc.id(client); 167 | if (wc.vGEN >= 308) 168 | wc.id(image); 169 | wc.i8(clientLazyEye); 170 | wc.i8(clientDir); 171 | if (wc.vGEN >= 415) 172 | wc.i16(clientControlFreak); 173 | wc.i8(unkE); 174 | if (wc.vGEN >= 230) 175 | wc.id(clientScript); 176 | if (wc.vGEN >= 507) { 177 | wc.i16(clientScriptFiles.length); 178 | wc.ids(clientScriptFiles); 179 | } 180 | if (wc.vGEN < 507) 181 | wc.id(idX); 182 | if (wc.vGEN >= 232) 183 | wc.i16(unkF); 184 | if (wc.vGEN >= 235 && wc.vGEN < 368) 185 | wc.i16(unkPX); 186 | if (wc.vGEN >= 236 && wc.vGEN < 368) 187 | wc.i16(unkPY); 188 | if (wc.vGEN >= 341) 189 | wc.id(hubPasswordHashed); 190 | if (wc.vGEN >= 266) { 191 | wc.id(serverName); 192 | wc.i32(hubNumber); 193 | wc.i32(gameVersion); 194 | } 195 | if (wc.vGEN >= 266) { 196 | wc.i16(cacheLifespan); 197 | wc.id(clientCommandText); 198 | wc.id(clientCommandPrompt); 199 | } 200 | if (wc.vGEN >= 276) 201 | wc.id(hub); 202 | if (wc.vGEN >= 305) 203 | wc.id(channel); 204 | if (wc.vGEN >= 360) 205 | wc.id(skin); 206 | if (wc.vLHS >= 455) { 207 | wc.i16(iconSizeX); 208 | wc.i16(iconSizeY); 209 | wc.i16(mapFormat); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBStringTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.LinkedList; 5 | 6 | import after.algorithms.NQCRC; 7 | import after.algorithms.XORJump9; 8 | import after.io.framework.DMBReadContext; 9 | import after.io.framework.DMBWriteContext; 10 | 11 | /** 12 | * The DM string table. 13 | */ 14 | public class DMBStringTable extends DMBEntryBasedSubblock { 15 | /** 16 | * The NQCRC hash of the string table. Use calculateHash to get the correct value for this. 17 | */ 18 | public int hash = -1; 19 | 20 | /** 21 | * Gets (doesn't apply) the correct hash value. 22 | */ 23 | public int calculateHash() { 24 | int value = -1; 25 | for (byte[] entry : entries) { 26 | value = NQCRC.hash(value, entry); 27 | value = NQCRC.hash(value, (byte) 0); 28 | } 29 | return value; 30 | } 31 | 32 | /** 33 | * Gets (doesn't apply) the total size of all strings (you'll need this) 34 | */ 35 | public int calculateTotalSize() { 36 | int value = 0; 37 | for (byte[] entry : entries) 38 | value += entry.length + 1; 39 | return value; 40 | } 41 | 42 | @Override 43 | public boolean indexReserved(int index) { 44 | // Try extremely hard to avoid hitting the subvar strings. 45 | if ((index >= 0xFFC0) && (index <= 0xFFFF)) 46 | return true; 47 | return super.indexReserved(index); 48 | } 49 | 50 | @Override 51 | public void read(DMBReadContext rc) { 52 | super.read(rc); 53 | if (rc.vGEN >= 468) 54 | hash = rc.io.getInt(); 55 | } 56 | 57 | @Override 58 | protected byte[] createDummyValue() { 59 | return new byte[0]; 60 | } 61 | 62 | @Override 63 | public byte[] readEntry(DMBReadContext rc) { 64 | int totalLength = 0; 65 | while (true) { 66 | short val = (short) (rc.io.position() - rc.basePointer); 67 | val ^= rc.io.getShort(); 68 | totalLength += val & 0xFFFF; 69 | if (val != (short) -1) 70 | break; 71 | } 72 | byte[] data = new byte[totalLength]; 73 | byte key = (byte) (rc.io.position() - rc.basePointer); 74 | rc.io.get(data); 75 | XORJump9.xorJump9(data, key); 76 | return data; 77 | } 78 | 79 | @Override 80 | public void write(DMBWriteContext wc) { 81 | super.write(wc); 82 | if (wc.vGEN >= 468) 83 | wc.i32(hash); 84 | } 85 | 86 | @Override 87 | public void writeEntry(DMBWriteContext wc, byte[] str) { 88 | int remainingLength = str.length; 89 | while (remainingLength >= 0xFFFF) { 90 | wc.i16((wc.position - wc.basePointer) ^ 0xFFFF); 91 | remainingLength -= 0xFFFF; 92 | } 93 | wc.i16((wc.position - wc.basePointer) ^ remainingLength); 94 | 95 | byte[] data = new byte[str.length]; 96 | System.arraycopy(str, 0, data, 0, str.length); 97 | byte key = (byte) (wc.position - wc.basePointer); 98 | XORJump9.xorJump9(data, key); 99 | wc.bytes(data); 100 | } 101 | 102 | // -- Utilities -- 103 | 104 | /** 105 | * Deliberately ignores Unicode for lossless conversion. 106 | */ 107 | public static String fromWTF(byte[] dat) { 108 | return new String(dat, StandardCharsets.ISO_8859_1); 109 | } 110 | 111 | public static boolean equal(byte[] a, byte[] b) { 112 | if (a.length != b.length) 113 | return false; 114 | for (int i = 0; i < a.length; i++) 115 | if (a[i] != b[i]) 116 | return false; 117 | return true; 118 | } 119 | 120 | /** 121 | * Gets a string by the "internal" value. 122 | * Uses ISO\_8859_1 to guarantee lossless conversion. 123 | */ 124 | public String getString(int i) { 125 | return fromWTF(entries.get(i)); 126 | } 127 | 128 | /** 129 | * Sets a string by the "internal" value. 130 | * Potentially not lossless if you were to put Unicode into it. 131 | * Otherwise, uses ISO\_8859_1 to guarantee lossless conversion. 132 | */ 133 | public void setString(int i, String val) { 134 | entries.set(i, val.getBytes(StandardCharsets.ISO_8859_1)); 135 | } 136 | 137 | /** 138 | * Gets a string ID by content or makes one. 139 | * Expects the content to be a new byte array that doesn't get modified. 140 | */ 141 | public int of(byte[] val) { 142 | int idx = 0; 143 | for (byte[] ent : entries) { 144 | if (val.length == ent.length) { 145 | int i; 146 | for (i = 0; i < val.length; i++) 147 | if (val[i] != ent[i]) 148 | break; 149 | if (i == val.length) 150 | return idx; 151 | } 152 | idx++; 153 | } 154 | entries.add(val); 155 | return idx; 156 | } 157 | 158 | /** 159 | * Gets a string ID by content or makes one. 160 | */ 161 | public int of(String val) { 162 | return of(val.getBytes(StandardCharsets.ISO_8859_1)); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBSubblock7.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | import java.util.LinkedList; 5 | 6 | import aedium.ElementMarkers; 7 | import aedium.NonNull; 8 | import after.annotations.ClassID; 9 | import after.annotations.ProcID; 10 | import after.io.framework.DMBReadContext; 11 | import after.io.framework.DMBWriteContext; 12 | 13 | public class DMBSubblock7 { 14 | // list of ProcIDs 15 | @ElementMarkers({ProcID.class}) 16 | @NonNull 17 | public LinkedList entries = new LinkedList<>(); 18 | 19 | public void print(PrintStream ps, DMB base) { 20 | ps.println("# sb7"); 21 | int index = 0; 22 | for (Integer ent : entries) { 23 | ps.println(index + ": " + ent); 24 | index++; 25 | } 26 | } 27 | 28 | public void read(DMBReadContext rc) { 29 | int count = rc.id(); 30 | for (int i = 0; i < count; i++) 31 | entries.add(rc.id()); 32 | } 33 | 34 | public void write(DMBWriteContext wc) { 35 | wc.id(entries.size()); 36 | for (int i : entries) 37 | wc.id(i); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBValue.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | /** 4 | * Yup, it's a microclass. Deal with it. 5 | */ 6 | public final class DMBValue { 7 | // Constants named with help from dmdism, but not all names match. 8 | /** no value */ 9 | public static final int NULL = 0; 10 | /** StringID */ 11 | public static final int STRING = 6; 12 | /** MobTypeID */ 13 | public static final int MOB_TYPEPATH = 8; 14 | /** ClassID */ 15 | public static final int OBJ_TYPEPATH = 9; 16 | /** ClassID */ 17 | public static final int TURF_TYPEPATH = 10; 18 | /** ClassID */ 19 | public static final int AREA_TYPEPATH = 11; 20 | /** CacheFileID */ 21 | public static final int RESOURCE = 12; 22 | /** ClassID */ 23 | public static final int DATUM_TYPEPATH = 32; 24 | /** (no value?) */ 25 | public static final int SAVEFILE_TYPEPATH = 36; 26 | /** (no value?) */ 27 | public static final int FILE_TYPEPATH = 39; 28 | /** (no value?) */ 29 | public static final int LIST_TYPEPATH = 40; 30 | /** float */ 31 | public static final int NUMBER = 42; 32 | /** InstanceID */ 33 | public static final int INSTANCE_TYPEPATH = 41; 34 | /** ClassID */ 35 | public static final int IMAGE_TYPEPATH = 63; 36 | 37 | public static final DMBValue NULL_VALUE = new DMBValue(0, 0); 38 | 39 | public final int type; 40 | public final int value; 41 | 42 | public DMBValue(int t, int v) { 43 | type = t; 44 | value = v; 45 | } 46 | 47 | /** 48 | * If the DMBValue is a NUMBER, this gets the resulting value. 49 | */ 50 | public float getFloatValue() { 51 | return Float.intBitsToFloat(value); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return (type * 0x100) + value; 57 | } 58 | 59 | @Override 60 | public boolean equals(Object obj) { 61 | if (!(obj instanceof DMBValue)) 62 | return false; 63 | DMBValue dv = (DMBValue) obj; 64 | if (dv.type != type) 65 | return false; 66 | if (dv.value != value) 67 | return false; 68 | return true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/DMBVarTable.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.PrintStream; 4 | 5 | import after.annotations.StringID; 6 | import after.io.framework.DMBReadContext; 7 | import after.io.framework.DMBWriteContext; 8 | 9 | public class DMBVarTable extends DMBObjectEntryBasedSubblock { 10 | // GEN & LHS >= 512, ListID 11 | public int unk; 12 | 13 | @Override 14 | public Entry make() { 15 | return new Entry(); 16 | } 17 | 18 | @Override 19 | public void read(DMBReadContext rc) { 20 | super.read(rc); 21 | if (rc.vGEN >= 512 && rc.vLHS >= 512) 22 | unk = rc.io.getInt(); 23 | } 24 | 25 | @Override 26 | public void write(DMBWriteContext wc) { 27 | super.write(wc); 28 | if (wc.vGEN >= 512 && wc.vLHS >= 512) 29 | wc.i32(unk); 30 | } 31 | 32 | public static class Entry implements DMBObjectEntryBasedSubblock.Entry { 33 | public DMBValue value = DMBValue.NULL_VALUE; 34 | @StringID 35 | public int name; 36 | 37 | @Override 38 | public void read(DMBReadContext rc) { 39 | int t = rc.io.get(); 40 | int v = rc.io.getInt(); 41 | value = new DMBValue(t, v); 42 | name = rc.id(); 43 | } 44 | 45 | @Override 46 | public void write(DMBWriteContext wc) { 47 | wc.i8(value.type); 48 | wc.i32(value.value); 49 | wc.id(name); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/RADEntry.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.function.Supplier; 6 | 7 | import aedium.NonNull; 8 | import after.io.framework.ReadContext; 9 | import after.io.framework.WriteContext; 10 | 11 | /** 12 | * Random-access datafile entry. 13 | */ 14 | public class RADEntry { 15 | public Content content; 16 | 17 | @Override 18 | public String toString() { 19 | return content.toString(); 20 | } 21 | 22 | public void read(ReadContext rc, Supplier validContent) { 23 | byte[] data = new byte[rc.i32()]; 24 | byte type = rc.i8(); 25 | // System.out.println(data.length + ";" + type); 26 | rc.bytes(data); 27 | if (type == 1) { 28 | content = validContent.get(); 29 | } else { 30 | content = new InvalidContent(); 31 | } 32 | content.read(new ReadContext() { 33 | int ptr = 0; 34 | @Override 35 | public byte i8() { 36 | return data[ptr++]; 37 | } 38 | 39 | @Override 40 | public void bytes(byte[] group) { 41 | System.arraycopy(data, ptr, group, 0, group.length); 42 | ptr += group.length; 43 | } 44 | }, data.length); 45 | } 46 | 47 | public void write(WriteContext wc) { 48 | ByteArrayOutputStream store = new ByteArrayOutputStream(); 49 | content.write(new WriteContext() { 50 | @Override 51 | public void i8(int value) { 52 | store.write(value); 53 | } 54 | 55 | @Override 56 | public void bytes(byte[] data) { 57 | store.write(data, 0, data.length); 58 | } 59 | }); 60 | byte[] data = store.toByteArray(); 61 | wc.i32(data.length); 62 | wc.i8((content instanceof InvalidContent) ? 0 : 1); 63 | wc.bytes(data); 64 | } 65 | 66 | public interface Content { 67 | void read(ReadContext rc, int size); 68 | void write(WriteContext wc); 69 | } 70 | 71 | /** 72 | * Represents invalid data that may be recoverable or may be completely corrupt. 73 | */ 74 | public class InvalidContent implements Content { 75 | @NonNull 76 | public byte[] data = new byte[0]; 77 | 78 | @Override 79 | public String toString() { 80 | return "deleted (" + data.length + " bytes)"; 81 | } 82 | 83 | @Override 84 | public void read(ReadContext rc, int size) { 85 | data = new byte[size]; 86 | rc.bytes(data); 87 | } 88 | 89 | @Override 90 | public void write(WriteContext wc) { 91 | wc.bytes(data); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/RSCEntryContent.java: -------------------------------------------------------------------------------- 1 | package after.io; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.function.Supplier; 6 | 7 | import aedium.NonNull; 8 | import after.io.framework.ReadContext; 9 | import after.io.framework.WriteContext; 10 | 11 | /** 12 | * There shouldn't be anything else, but there COULD be. 13 | */ 14 | public class RSCEntryContent implements RADEntry.Content { 15 | public static final Supplier SUPPLIER = () -> { 16 | return new RSCEntryContent(); 17 | }; 18 | 19 | @NonNull 20 | public CacheID id = CacheID.ZERO; 21 | 22 | public int time = 0; 23 | public int originalTime = 0; 24 | 25 | @NonNull 26 | public byte[] name = new byte[0]; 27 | @NonNull 28 | public byte[] data = new byte[0]; 29 | 30 | @Override 31 | public String toString() { 32 | return id + " " + new String(name, StandardCharsets.UTF_8); 33 | } 34 | 35 | @Override 36 | public void read(ReadContext rc, int size) { 37 | byte type = rc.i8(); 38 | int uid = rc.i32(); 39 | id = new CacheID(uid, type); 40 | time = rc.i32(); 41 | originalTime = rc.i32(); 42 | data = new byte[rc.i32()]; 43 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 44 | while (true) { 45 | byte b = rc.i8(); 46 | if (b == 0) 47 | break; 48 | baos.write(b); 49 | } 50 | name = baos.toByteArray(); 51 | // System.out.println("RSC file: " + baos.toString() + "#" + baos.size() + "[" + data.length + "]"); 52 | rc.bytes(data); 53 | } 54 | 55 | @Override 56 | public void write(WriteContext wc) { 57 | wc.i8(id.typeOrSomething); 58 | wc.i32(id.uniqueID); 59 | wc.i32(time); 60 | wc.i32(originalTime); 61 | wc.i32(data.length); 62 | wc.bytes(name); 63 | wc.i8(0); 64 | wc.bytes(data); 65 | } 66 | } -------------------------------------------------------------------------------- /after/src/main/java/after/io/framework/DMBContext.java: -------------------------------------------------------------------------------- 1 | package after.io.framework; 2 | 3 | public abstract class DMBContext { 4 | public boolean debug; 5 | 6 | // if you don't want to know, take the blue pill. you'll forget all about this. 7 | // but if you take the red pill, you'll see how deep this versioning problem really goes... 8 | // *link to DMB.md* 9 | public int vGEN, vLHS, vRHS; 10 | 11 | public int basePointer; 12 | public boolean largeObjectIDs; 13 | 14 | public DMBContext(boolean dbg) { 15 | debug = dbg; 16 | } 17 | 18 | abstract protected int position(); 19 | 20 | public void section(String text) { 21 | if (debug) 22 | System.out.println(text + " @ 0x" + Integer.toHexString(position())); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/framework/DMBReadContext.java: -------------------------------------------------------------------------------- 1 | package after.io.framework; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | 6 | /** 7 | * Read context. 8 | */ 9 | public class DMBReadContext extends DMBContext implements ReadContext { 10 | public ByteBuffer io; 11 | 12 | public DMBReadContext(byte[] data, boolean dbg) { 13 | super(dbg); 14 | 15 | io = ByteBuffer.wrap(data); 16 | io.order(ByteOrder.LITTLE_ENDIAN); 17 | } 18 | 19 | @Override 20 | public int position() { 21 | return io.position(); 22 | } 23 | 24 | public int remaining() { 25 | return io.capacity() - position(); 26 | } 27 | 28 | @Override 29 | public byte i8() { 30 | return io.get(); 31 | } 32 | 33 | public int id() { 34 | if (largeObjectIDs) 35 | return i32(); 36 | return i16() & 0xFFFF; 37 | } 38 | 39 | public void ids(int[] group) { 40 | for (int i = 0; i < group.length; i++) 41 | group[i] = id(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/framework/DMBWriteContext.java: -------------------------------------------------------------------------------- 1 | package after.io.framework; 2 | 3 | /** 4 | * Write context. 5 | */ 6 | public abstract class DMBWriteContext extends DMBContext implements WriteContext { 7 | public int position = 0; 8 | 9 | public DMBWriteContext(boolean dbg) { 10 | super(dbg); 11 | } 12 | 13 | @Override 14 | protected int position() { 15 | return position; 16 | } 17 | 18 | protected abstract void i8Internal(int value); 19 | 20 | @Override 21 | public void i8(int i) { 22 | i8Internal(i); 23 | position++; 24 | } 25 | 26 | public void id(int i) { 27 | if (largeObjectIDs) { 28 | i32(i); 29 | } else { 30 | i16(i); 31 | } 32 | } 33 | public void ids(int[] it) { 34 | for (int i : it) 35 | id(i); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/framework/ReadContext.java: -------------------------------------------------------------------------------- 1 | package after.io.framework; 2 | 3 | public interface ReadContext { 4 | byte i8(); 5 | 6 | default short i16() { 7 | int a = i8() & 0xFF; 8 | return (short) (((i8() & 0xFF) << 8) | a); 9 | } 10 | 11 | default int i32() { 12 | int a = i16() & 0xFFFF; 13 | return (((i16() & 0xFFFF) << 16) | a); 14 | } 15 | 16 | default float f32() { 17 | return Float.intBitsToFloat(i32()); 18 | } 19 | 20 | default void ints(int[] group) { 21 | for (int i = 0; i < group.length; i++) 22 | group[i] = i32(); 23 | } 24 | 25 | default void floats(float[] group) { 26 | for (int i = 0; i < group.length; i++) 27 | group[i] = f32(); 28 | } 29 | 30 | default void bytes(byte[] group) { 31 | for (int i = 0; i < group.length; i++) 32 | group[i] = i8(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /after/src/main/java/after/io/framework/WriteContext.java: -------------------------------------------------------------------------------- 1 | package after.io.framework; 2 | 3 | public interface WriteContext { 4 | void i8(int value); 5 | 6 | default void i16(int value) { 7 | i8(value); 8 | i8(value >> 8); 9 | } 10 | 11 | default void i32(int value) { 12 | i16(value); 13 | i16(value >> 16); 14 | } 15 | 16 | default void f32(float value) { 17 | i32(Float.floatToRawIntBits(value)); 18 | } 19 | 20 | default void ints(int[] it) { 21 | for (int i : it) 22 | i32(i); 23 | } 24 | 25 | default void floats(float[] it) { 26 | for (float i : it) 27 | f32(i); 28 | } 29 | 30 | default void bytes(byte[] data) { 31 | for (byte b : data) 32 | i8(b); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /after/src/main/java/after/net/PacketFrame.java: -------------------------------------------------------------------------------- 1 | package after.net; 2 | 3 | import java.io.DataInputStream; 4 | import java.io.DataOutputStream; 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.nio.ByteBuffer; 8 | import java.nio.ByteOrder; 9 | 10 | /** 11 | * This is the framing of a packet. 12 | * This doesn't handle encryption, on purpose. 13 | * It's also not thread-safe, even for 'thread-safe-looking' operations. 14 | */ 15 | public class PacketFrame { 16 | /** 17 | * If this packet frame has a sequence number. 18 | * (non-handshake client packets only) 19 | */ 20 | public boolean hasSeq; 21 | 22 | /** 23 | * Sequence number. 24 | */ 25 | public short sequence; 26 | 27 | /** 28 | * The type of packet. 29 | */ 30 | public short type; 31 | 32 | /** 33 | * The data. The position in the buffer indicates the length of the data. 34 | */ 35 | public final ByteBuffer data = ByteBuffer.allocate(0xFFFF); 36 | 37 | /** 38 | * The backing array of the data buffer. 39 | */ 40 | public final byte[] dataArray = data.array(); 41 | 42 | /** 43 | * Array in which a written packet is constructed. 44 | * (There's reasons for this, really...) 45 | */ 46 | private final byte[] writeDataArray = new byte[0x10005]; 47 | 48 | public PacketFrame() { 49 | data.order(ByteOrder.LITTLE_ENDIAN); 50 | } 51 | 52 | public void read(DataInputStream dis) throws IOException { 53 | if (hasSeq) 54 | sequence = dis.readShort(); 55 | type = dis.readShort(); 56 | int len = dis.readUnsignedShort(); 57 | dis.readFully(dataArray, 0, len); 58 | data.position(len); 59 | } 60 | 61 | public void write(OutputStream dos) throws IOException { 62 | int ptr = 0; 63 | // This is another one of those "read-sensitive" applications. 64 | // In practice this means shenanigans. 65 | if (hasSeq) { 66 | writeDataArray[ptr++] = (byte) (sequence >> 8); 67 | writeDataArray[ptr++] = (byte) sequence; 68 | } 69 | writeDataArray[ptr++] = (byte) (type >> 8); 70 | writeDataArray[ptr++] = (byte) type; 71 | int len = data.position(); 72 | writeDataArray[ptr++] = (byte) (len >> 8); 73 | writeDataArray[ptr++] = (byte) len; 74 | System.arraycopy(dataArray, 0, writeDataArray, ptr, len); 75 | ptr += len; 76 | // And now commit. 77 | dos.write(writeDataArray, 0, ptr); 78 | } 79 | 80 | public void writeWebSocket(DataOutputStream dos) throws IOException { 81 | writeWebSocketInternal(dos, type, dataArray, 0, data.position()); 82 | } 83 | 84 | public static void writeWebSocketInternal(DataOutputStream dos, short type, byte[] data, int offset, int length) throws IOException { 85 | // FIN, Binary 86 | dos.write(0x82); 87 | int pktLen = length + 2; 88 | if (pktLen < 126) { 89 | dos.write(pktLen); 90 | } else if (pktLen < 65536) { 91 | dos.write(126); 92 | dos.writeShort(pktLen); 93 | } else { 94 | dos.write(0x7F); 95 | dos.writeLong(length + 2); 96 | } 97 | dos.writeShort(type); 98 | dos.write(data, offset, length); 99 | } 100 | 101 | public void begin(int t) { 102 | data.position(0); 103 | type = (short) t; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /after/src/test/java/after/tests/AlgorithmTest.java: -------------------------------------------------------------------------------- 1 | package after.tests; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import org.junit.Test; 6 | 7 | import after.algorithms.CYCSUB; 8 | import after.algorithms.NQCRC; 9 | import after.algorithms.RUNSUB; 10 | import after.algorithms.XORJump9; 11 | 12 | /** 13 | * Acts as a set of test vectors for various algorithms. 14 | */ 15 | public class AlgorithmTest { 16 | @Test 17 | public void testNQCRC() { 18 | assert NQCRC.hash(0, "The quick brown fox jumps over the lazy dog.\n".getBytes(StandardCharsets.UTF_8)) == 0x93fe309a; 19 | assert NQCRC.hash(0, "3\n".getBytes(StandardCharsets.UTF_8)) == 0xda08bd33; 20 | } 21 | 22 | @Test 23 | public void testXORJUMP9() { 24 | byte[] bloop = new byte[] {(byte) 0x00, (byte) 0x80, (byte) 0x40}; 25 | assert XORJump9.xorJump9(bloop, (byte) 0x40) == 0x5B; 26 | assert bloop[0] == 0x40; 27 | assert bloop[1] == (byte) 0xC9; 28 | assert bloop[2] == 0x12; 29 | } 30 | 31 | @Test 32 | public void testRUNSUB() { 33 | byte[] data = new byte[] {(byte) 0xf8, 0x38, 0x78, (byte) 0xb8, (byte) 0xc2, 0x3b, 0x0a}; 34 | byte[] dataCopy = new byte[data.length]; 35 | System.arraycopy(data, 0, dataCopy, 0, data.length); 36 | 37 | int key = 0xa374c5b8; 38 | 39 | // Decrypt 40 | RUNSUB.decrypt(data, 0, data.length, key); 41 | assert data[0] == '@'; 42 | assert data[1] == '@'; 43 | assert data[2] == '@'; 44 | assert data[3] == '@'; 45 | assert data[4] == '\n'; 46 | assert data[5] == 0; 47 | 48 | // Encrypt again and compare to the original 49 | RUNSUB.encrypt(data, 0, data.length, key); 50 | for (int i = 0; i < data.length; i++) 51 | assert data[i] == dataCopy[i]; 52 | } 53 | 54 | @Test 55 | public void testCYCSUB() { 56 | byte[] data = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05}; 57 | byte[] key = new byte[] {0x10, 0x20, 0x1F}; 58 | byte[] dataCopy = new byte[data.length]; 59 | System.arraycopy(data, 0, dataCopy, 0, data.length); 60 | 61 | // Encrypt 62 | CYCSUB.encrypt(data, 0, data.length, key, 0, key.length); 63 | 64 | // Decrypt again and compare to the original 65 | CYCSUB.decrypt(data, 0, data.length, key, 0, key.length); 66 | for (int i = 0; i < data.length; i++) 67 | assert data[i] == dataCopy[i]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /after/src/test/java/after/tests/EdenTest.java: -------------------------------------------------------------------------------- 1 | package after.tests; 2 | 3 | import java.io.FileOutputStream; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | import org.junit.Test; 7 | 8 | import after.Eden; 9 | import after.io.DMB; 10 | import after.io.DMBProcTable; 11 | import after.io.framework.DMBReadContext; 12 | import after.io.framework.DMBWriteContext; 13 | 14 | /** 15 | * This is an ongoing project to create an "eden file", a completely non-BYOND-created DMB file. 16 | * It will be considered successful when BYOND is capable of loading and running the DMB. 17 | * I've gotten to that point, so there's also a little side project to add a verb to that. 18 | */ 19 | public class EdenTest { 20 | @Test 21 | public void runEdenTest() throws Exception { 22 | DMB dmb = Eden.newEden(); 23 | // --- 24 | put(dmb, "eden.dmb"); 25 | // --- 26 | 27 | // verb table 28 | int lVT = dmb.lists.add(new int[] {0}); 29 | // verb code 30 | // GETVAR src 31 | // PUSHVAL ">:D" 32 | // OUTPUT 33 | // END 34 | int lVC = dmb.lists.add(new int[] {0x33, 0xFFCE, 0x60, 0x06, dmb.strings.of(">:D"), 0x03, 0}); 35 | 36 | DMBProcTable.Entry testProc = new DMBProcTable.Entry(); 37 | testProc.name = dmb.strings.of(">:D"); 38 | testProc.verbCategory = dmb.strings.of(">:D"); 39 | testProc.code = lVC; 40 | dmb.procs.add(testProc); 41 | 42 | dmb.classes.entries.get(dmb.standardObjectIDs.client).verbTable = lVT; 43 | 44 | dmb.fixStrings(); 45 | // --- 46 | put(dmb, "eden.ext.dmb"); 47 | } 48 | 49 | private static void put(DMB dmb, String where) throws Exception { 50 | FileOutputStream putEden = new FileOutputStream(where); 51 | dmb.write(new DMBWriteContext(true) { 52 | @Override 53 | protected void i8Internal(int value) { 54 | try { 55 | putEden.write(value); 56 | } catch (Exception e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | }); 61 | putEden.close(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /after/src/test/java/after/tests/EquivalenceTest.java: -------------------------------------------------------------------------------- 1 | package after.tests; 2 | 3 | import java.io.File; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | import java.util.Collection; 8 | import java.util.LinkedList; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.junit.runners.Parameterized; 14 | 15 | import after.io.DMB; 16 | import after.io.framework.DMBReadContext; 17 | import after.io.framework.DMBWriteContext; 18 | 19 | /** 20 | * Performs a general battery of sanity tests given source test subjects. 21 | */ 22 | @RunWith(Parameterized.class) 23 | public class EquivalenceTest { 24 | @Parameterized.Parameters(name = "{0}") 25 | public static Collection data() { 26 | final LinkedList tests = new LinkedList(); 27 | File[] files = new File("external/test-subjects").listFiles(); 28 | if (files != null) 29 | for (File f : files) 30 | tests.add(new Object[] {f.toPath()}); 31 | return tests; 32 | } 33 | 34 | public final Path path; 35 | 36 | public EquivalenceTest(Path p) { 37 | path = p; 38 | } 39 | 40 | @Test 41 | public void runEquivalenceTest() throws Exception { 42 | byte[] data = Files.readAllBytes(path); 43 | DMB d = new DMB(); 44 | d.read(new DMBReadContext(data, true)); 45 | AtomicInteger index = new AtomicInteger(); 46 | d.write(new DMBWriteContext(true) { 47 | @Override 48 | protected void i8Internal(int value) { 49 | byte res = (byte) value; 50 | int where = index.getAndIncrement(); 51 | if (res != data[where]) 52 | throw new RuntimeException("Mismatch at 0x" + Integer.toHexString(where)); 53 | } 54 | }); 55 | int finalWhere = index.get(); 56 | if (finalWhere != data.length) 57 | throw new RuntimeException("Too short, 0x" + Integer.toHexString(finalWhere)); 58 | 59 | if (d.strings.calculateTotalSize() != d.totalSizeOfAllStrings) 60 | throw new RuntimeException("invalid string total size"); 61 | 62 | if (d.strings.calculateHash() != d.strings.hash) 63 | throw new RuntimeException("invalid string hash"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /algorithms/CYCSUB.md: -------------------------------------------------------------------------------- 1 | # CYCSUB (WARNING: DOCUMENTS AND IMPLEMENTATIONS HAVE NOT BEEN PROPERLY TESTED) 2 | 3 | The CYCSUB encryption algorithm ("ServerEncrypt"/"ServerDecrypt") operates on buffers of data at a time. 4 | 5 | It has a key of some amount of bytes. If the key is zero-length, the encryption algorithm is completely deactivated (is effectively NOP). 6 | 7 | Note that this includes the deactivation of the running checksum component as seen below. 8 | 9 | It has two state values: 10 | 11 | 1. A Uint8 running checksum of unencrypted data. (`checksum`) This is a checksum in the literal 'add' sense, and wraps. 12 | 2. An index within the key (`keyIndex`) - this starts at 0. 13 | 14 | It iterates *in reverse* through bytes in the buffer. To be clear, the instructions presented are given assuming that the buffer index (and no other indices) are going in reverse, from end to beginning. 15 | 16 | For each byte in the buffer, encryption adds `checksum + key[keyIndex]` to the data byte. 17 | 18 | The key index is then incremented, wrapping to the beginning of the key if it would go past the end. 19 | 20 | The checksum is updated after each byte. Again, it's a checksum of the unencrypted data. 21 | 22 | Since encryption is addition, decryption is just subtraction. 23 | 24 | ## Notes on actual usage 25 | 26 | ### Unknown 27 | This is a two-layered, encryption first being performed with something called the "domain name" (stored in the DMB file as a string - unknown index, but then scrambled), and then being performed with the key `3c 7c 33 5e 0a 71 0d 5b`. 28 | 29 | -------------------------------------------------------------------------------- /algorithms/NQCRC.md: -------------------------------------------------------------------------------- 1 | # NQCRC 2 | 3 | NQCRC is an MSB-first CRC-32 with 0xAF as the polynomial. 4 | 5 | The way NQCRC is written implies that the initial value of the hash and what happens to "finalize it" is situation-dependent. 6 | 7 | The following documentation exists because just saying all this is not how you write proper format documentation. 8 | 9 | NQCRC, as with most CRC-32 implementations, uses a table of 256 32-bit values, one for each possible byte. 10 | 11 | The per-byte NQCRC algorithm is `uint32_t hash = (hash << 8) ^ nqcrcTable[(hash >>> 24) ^ (byt & 0xFF)];`. 12 | 13 | This functionally matches the description of an msbit-first CRC-32 in: https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks#Multi-bit_computation 14 | 15 | Following with the theme, the table computation is the same. 16 | 17 | ```java 18 | private static final int[] nqcrcTable = new int[256]; 19 | static { 20 | for (int i = 0; i < nqcrcTable.length; i++) { 21 | int value = i << 0x18; 22 | for (int j = 0; j < 8; j++) 23 | value = (value << 1) ^ (value < 0 ? 0xAF : 0); 24 | nqcrcTable[i] = value; 25 | } 26 | } 27 | ``` 28 | 29 | The result is the NQCRC value for that index. 30 | 31 | -------------------------------------------------------------------------------- /algorithms/RUNSUB.md: -------------------------------------------------------------------------------- 1 | # RUNSUB 2 | 3 | The RUNSUB encryption algorithm operates on buffers of data at a time. 4 | 5 | It has two state values: 6 | 7 | 1. A Uint8 running checksum of unencrypted data. This is a checksum in the literal 'add' sense, and wraps. 8 | 2. A Uint32 key. 9 | 10 | For each byte, encryption adds `checksum + (key >> (checksum % 32))` to the data byte (the shift zero-extends). 11 | 12 | The checksum is updated after each byte. Again, it's a checksum of the unencrypted data. 13 | 14 | It writes the checksum byte unencrypted at the end of the message. 15 | 16 | Since encryption is addition, decryption is just subtraction. 17 | 18 | -------------------------------------------------------------------------------- /algorithms/XORJUMP9.md: -------------------------------------------------------------------------------- 1 | # XOR Jump 9 2 | 3 | XOR Jump 9 is an obfuscation algorithm. 4 | 5 | It isn't really worth calling it encryption. 6 | 7 | BYOND is not the first or the last piece of software to feature this sort of thing. I had a rant here about this pattern, but removed it before publishing. Just know that it's stupid and it annoys me. 8 | 9 | XOR Jump 9 is a symmetric algorithm. 10 | 11 | It takes some input plaintext or ciphertext and an input key byte. 12 | 13 | It is only different a constant XOR because it adds 9 to the key byte after each byte has been processed. 14 | 15 | You might notice that this doesn't sound much better than a constant XOR. 16 | 17 | ```lua 18 | local function xorjump9(data, key) 19 | local result = "" 20 | for i = 1, #data do 21 | result = result .. string.char(data:byte(i) ~ key) 22 | key = (key + 9) % 256 23 | end 24 | return result 25 | end 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /formats/DMB.Bytecode.md: -------------------------------------------------------------------------------- 1 | # DMB Bytecode Format 2 | 3 | Sorry, I don't actually have any documentation on this yet. 4 | 5 | However, MCHSL & SpaceManiac's project, extools, is known to contain a disassembler for this. 6 | 7 | ## PUSHVAL 8 | 9 | PUSHVAL's argument is the usual [TypeValue](./DMB.TypeValue.md) pair. 10 | 11 | However, due to the 16-bit nature of lists (normally), there's a slight modification to the encoding. 12 | 13 | For floats (type 42), and SPECIFICALLY floats, they have 2 value entries rather than 1. 14 | These are laid out large word first (big-endian), even though the individual list byte pairs remain little-endian. 15 | So the actual on-disk byte order, with 0 being MSB, is 1032. 16 | 17 | The way you will actually receive it after your (assumed) list reader abstraction, it'll just be the high word followed by the low word. 18 | 19 | I don't know what the form is when large object IDs are enabled, but assume it's unchanged. 20 | 21 | ## Possible Extools Errata 22 | 23 | 1. CALLNR (0x2A) is really more like another form of GETVAR. 24 | 2. 0xC8 opcode; appears to have 1 arg 25 | 3. 0x18 opcode; appears to be similar to "<<" ; uses 6 stack slots, *might* return something 26 | 4. Access modifier 0xFFCD (65485) is actually "usr" 27 | 5. `SRC_PROC_SPEC` access modifier has the important bits commented out, breaking everything 28 | 6. SUBVAR right-hand-side parsing is completely wrong - the SUBVAR is just two accesses concatenated. 29 | 7. 0xDB opcode; appears to be `text2ascii`, no args 30 | 8. 0xDC opcode; appears to be `ascii2text`, no args 31 | 9. 0x29 type id; appears to be `type path for modified type`. Thanks to the Red Book for cluing me in on the existence of these. My name for this type is `INSTANCE_TYPEPATH`. Takes an instance table ID 32 | 10. 0x3F type id; `IMAGE_TYPEPATH`, takes a class ID like the others 33 | 34 | The following code goes through a few different calls and accesses: 35 | 36 | ``` 37 | /datum 38 | var/datum/a 39 | proc/universally_trouble() 40 | usr.a.a.a.a.a.a.a.a.universally_trouble() 41 | universally_trouble() 42 | a.universally_trouble() 43 | universally_carrot() 44 | 45 | proc/universally_carrot() 46 | 47 | ``` 48 | 49 | ## Instance Initializer Procs 50 | 51 | Instance initializer procs are theoretically not special. 52 | 53 | However, they have relatively special syntax, being part of DMM files. 54 | 55 | As such, they seem to only contain the following opcodes (as named by extools dmdism): 56 | 57 | PUSHI (80, (unsigned int)) 58 | 59 | PUSHVAL (96, (type), (val), (only present if type = 42: val ex.)) 60 | 61 | SETVAR src.? (52, 65500, 65486, (StringID)) 62 | 63 | END (0) 64 | 65 | These are always in the form PUSHVAL or PUSHI followed by SETVAR, with the list terminated with END. 66 | 67 | (I haven't yet seen any others, but that doesn't necessarily mean they don't exist.) 68 | 69 | -------------------------------------------------------------------------------- /formats/DMB.TypeValue.md: -------------------------------------------------------------------------------- 1 | # DMB Type/Value Pairs 2 | 3 | Type is a BYOND type number (this type numbering is shared with the protocol). Of these, at least the following are valid: 4 | 5 | (this is more-or-less collated from extools/dmdism) 6 | 7 | - 0 (null, value ignored) 8 | - 6 (string, as StringID) 9 | - 8 (/mob class, as MobTypeID) 10 | - 9 (/atom/movable class, as ClassID) 11 | - 10 (/atom class, as ClassID) 12 | - 11 (/area class, as ClassID) 13 | - 32 (/datum class, as ClassID) 14 | - 36 (/savefile class, value ignored) 15 | - 40 (/list class, value ignored) 16 | - 42 (float, value should be transmuted from int to float) 17 | - 59 (/client class, as ClassID) 18 | - 63 (/image class, as ClassID) 19 | 20 | Regarding classes, it's important to note that the 'type of class' decides the underlying native type that is created. 21 | 22 | It's NOT based on inheritance - inheritance is used to decide the BYOND type number, not the other way around. 23 | 24 | It's NOT based on flags - these seem to be for the object runtime's sake (i.e. which native procs, such as Move, exist) 25 | 26 | This can allow things like the world's "client" class always being instantiated as a /client derivative even if it isn't one, 27 | as the instantiation of the world's "client" class is done knowing the target will always be a BYOND client. 28 | 29 | The DM compiler's analysis of this appears to completely stop at the first "native type" it sees. 30 | 31 | -------------------------------------------------------------------------------- /formats/DMB.md: -------------------------------------------------------------------------------- 1 | # INCOMPLETE BUT STRUCTURALLY COMPLETE UP TO V512 2 | 3 | Addendum to this disclaimer - I haven't yet found indications of anything going particularly *wrong* when using this information on V514. 4 | 5 | But that may simply be due to a lack of feature use (i.e. RHS version differences). 6 | 7 | ## Taxonomy 8 | 9 | An "ObjectID" is a generic object ID which there isn't details for yet. 10 | 11 | These all use a format dependent on a flag in the file header. 12 | 13 | A "ClassID" is a reference into the Class Table. 14 | 15 | A "MobTypeID" is a reference into the Mob Types Table. 16 | 17 | A "StringID" is a reference into the String Table. 18 | 19 | A "ListID" is a reference into the List Table. 20 | 21 | A "ProcID" is a reference into the Proc Table. 22 | 23 | A "InstanceID" is a reference into the Instance Table. 24 | 25 | A "CacheFileID" is a reference into the Cache Files Table. 26 | 27 | A lot of these IDs allow 0xFFFF to mean "none". (Even if LARGE OBJECT IDs is set, when it would logically be 0xFFFFFFFF - if making a DMB, have fun with that) 28 | 29 | These will be referred to as "nullable" IDs. 30 | 31 | There are sometimes references to IDs which aren't actually in ObjectID form. 32 | 33 | These are in the form "ClassID-as-Uint32" (i.e. a ClassID in the form of a Uint32) 34 | 35 | ## The Three Wise Version Numbers 36 | 37 | There are 3 version numbers for a DMB file. 38 | 39 | There is the GEN Version, the LHS Version, and the RHS Version. 40 | 41 | All of these are important to loading a DMB file for reasons I do not understand. 42 | 43 | Why it is not a single version number I do not understand. 44 | 45 | ## Header 46 | 47 | The DMB file format, for the grand total of 3 plain ASCII lines it has, uses Unix newlines. 48 | 49 | The DMB file format starts with an optional "shebang line" ("#!") for Unix executable interpreter business. 50 | 51 | When a specific set of executor options are *not* being forced, it's (likely?) possible to write arbitrary comment lines. 52 | For perfect read logic, read lines at the start of the file until you find one without "#", then backtrack a character. 53 | Record all of this and consider it a blob of data that you have absolutely no reason to care about whatsoever (because that's what it is). 54 | 55 | It follows (or starts) with a line of the form `world bin v512\n` (at time of writing). 56 | 57 | A value which we'll call the "base pointer/key" (which is presumably used so that file-position-based encryption doesn't royally fail for shebang interpreter changes) is set to the file position of the start of this line (the 'w'). 58 | 59 | This is a 32-bit value that gets downcast as necessary. 60 | 61 | This version is the GEN Version. 62 | 63 | If it's below 222, information is not available on these versions at this time. 64 | There's also some problem in this document or the current implementation of it regarding versions below 230. 65 | 66 | Known versions always follow with one of two line types: 67 | 68 | 1. A compatibility line of the form `min compatibility v512 468\n` (at time of writing). 69 | 70 | 2. A compatibility line of the form `min compatibility v222\n` (not checked). 71 | 72 | In the first case, the LHS version and RHS version are available. 73 | 74 | In the second case, the number is both the LHS version and the RHS version. 75 | 76 | Integer types in BYOND data are little-endian, to align with the x86 processor. 77 | 78 | A UInt32 follows. This is a set of game flags. 79 | 80 | Bit 31 (highest): Another UInt32 follows. (Unsure on meaning.) 81 | Bit 30: LARGE OBJECT IDs 82 | 83 | There's also a bunch of unknown stuff. (Seems to default to 832, but this isn't necessary) 84 | 85 | It's important to note that ObjectID is Uint16 normally and Uint32 if LARGE OBJECT IDs is set. 86 | 87 | For analysis reasons, it's important to note that LARGE OBJECT IDs did not exist until at *least* after v368. 88 | 89 | ## Sub-Block 0 (The Grid) 90 | 91 | Next, three UInt16s follow: Width, Height, Z-Levels. 92 | 93 | The arrangement of the grid is as you would expect: 94 | 95 | 1. The outer loop is the per-Z-Level loop. 96 | 2. The next inner loop is the per-row loop. 97 | 3. The next next inner loop is per-cell loop. 98 | 99 | However, unlike what you would expect, Y coordinates in BYOND go upwards and coordinates start at 1. 100 | 101 | The part where coordinates start at 1 is not preserved in the file, but Y coordinates going upwards is. 102 | 103 | This data is run-length-compressed, and made up of the following 'groups': 104 | 105 | 1. Nullable InstanceID turfID 106 | 2. Nullable InstanceID areaID 107 | 3. Nullable ListID additionalTurfIDs (a list of InstanceIDs) 108 | 4. Uint8 (Amount of copies. This is NOT incremented, 0x00 is presumably invalid.) 109 | 110 | As far as I can tell, the DMM order is: additionalIDs, turfID, areaID. 111 | 112 | ## Total Size Of All Strings In The File 113 | 114 | 1. A Uint32 containing the total size of all strings in the file, including null terminators. 115 | 116 | If you get weird "protomem"-related errors, it's this field. 117 | 118 | ## Sub-Block 1 (Classes) 119 | 120 | This is the set of classes. 121 | 122 | This starts with an ObjectID, which is the amount of entries. 123 | 124 | Each class has: 125 | 126 | 1. StringID name (this is the actual name, the value of 'type') 127 | 2. Nullable ClassID parentType (this is the value of 'parent_type') 128 | 3. Nullable StringID objName (this is the value of 'name'. IFL) 129 | 4. Nullable StringID description (this is the value of 'description'. IFL) 130 | 5. Nullable CacheFileID icon (the 'icon' property) 131 | 6. Nullable StringID iconState (the 'icon_state' property) 132 | 7. Uint8 direction (the 'direction' property - 2 is a good default) 133 | 134 | For GEN Versions >= 307, additionally { 135 | 136 | 1. Uint8 dmInterface (See: The Annotated Standard Class Hierarchy. IFL). 137 | 2. If it's 0x0F (*not* 0xFF!), then a Uint32 follows to replace it. 138 | 139 | If the format version is insufficient, this defaults to 1. 140 | To preserve the original file, it's important to preserve the 'long-ness' status separately. 141 | Also note that the default-to-1 behavior is generally a good idea even outside 142 | format version shenanigans. 143 | 144 | } 145 | 146 | 1. StringID text (the 'text' property, might be nullable) 147 | 148 | For RHS Versions >= 494, additionally { 149 | 150 | 1. Nullable StringID maptext (thanks to Willox for finding this) 151 | 2. Uint16 maptextWidth (thanks to Willox for finding this) 152 | 3. Uint16 maptextHeight (thanks to Willox for finding this) 153 | 154 | } 155 | 156 | For RHS Versions >= 508, additionally { 157 | 158 | 1. Uint16 maptextX (thanks to Willox for finding this) 159 | 2. Uint16 maptextY (thanks to Willox for finding this) 160 | 161 | } 162 | 163 | 1. Nullable StringID suffix (the 'suffix' property, IFL) 164 | 2. (For GEN Versions >= 306, a Uint32, otherwise a Uint8) flags (check The Annotated Standard Class Hierarchy. IFL) 165 | 3. Nullable ListID verbList (ID of a list of ProcIDs: verbs) 166 | 4. Nullable ListID procList (ID of a list of ProcIDs: procs) 167 | 5. Nullable ProcID initializer (initializes an awful lot of stuff. IFL) 168 | 6. Nullable ListID initializedVarsList (same format as the Overriding Vars List, so see that subsection. IFL) 169 | 6. Nullable ListID definingVarList (see relevant subsection) 170 | 171 | For GEN Versions >= 267, additionally { 172 | 173 | 1. Float32 layer 174 | 175 | } 176 | 177 | For RHS Versions >= 500, additionally { 178 | 179 | 1. Uint8 hasTransform, but really a boolean 180 | 2. If hasColourMatrix, Array of 6 Float32s (Affine transform (TODO: Details?) - thanks to Willox for finding this) 181 | 182 | } 183 | 184 | For RHS Versions >= 509, additionally { 185 | 186 | 1. Uint8 hasColourMatrix, really a boolean 187 | 2. If hasColourMatrix, Array of 20 Float32s (Colour matrix (TODO: Details?) - thanks to Willox for finding this) 188 | 189 | } 190 | 191 | For GEN Versions >= 306, additionally { 192 | 193 | 1. Nullable ListID overridingVarList (see relevant subsection) 194 | 195 | } 196 | 197 | ### Defining Var List 198 | 199 | The Defining Var List is a set of key/value pairs implicitly terminated. 200 | 201 | The Defining Var List uses a VarID key (the actual var). 202 | 203 | The values are flags. 204 | 205 | 0x0001 means "global". 206 | 207 | 0x0002 means "const". This implies "global" and is always seen with "global". 208 | 209 | 0x0004 means "tmp". 210 | 211 | ### Overriding Var List 212 | 213 | The Overriding Var List is a set of key/value pairs implicitly terminated. 214 | 215 | The Overriding Var List uses a StringID key (the name of the var to be overridden). 216 | 217 | This is followed with a type value (see The Var Table for further information on the general concepts). 218 | 219 | Details on what happens for large-object-IDs ARE NOT CERTAIN. 220 | 221 | But specifically for 16-bit object IDs and float values, the value is written in two pieces, 'semi-big-endian'. 222 | 223 | So `225, 42, 16896, 0` means `[string 225] = float 32.0`. 224 | 225 | ## Sub-Block 2 (The Mob Type Table) 226 | 227 | Like Sub-Block 1, this starts with an ObjectID entry count. 228 | 229 | Each entry attaches mob metadata to a `/mob`-derived class. 230 | 231 | For each entry: 232 | 233 | 1. ClassID clazz 234 | 2. Nullable ObjectID key (as in `key = "PlayerName"`) 235 | 3. Uint8 sightFlags (IFL) 236 | According to Lego, if this doesn't have 0x80, see\_in\_dark is 2, see\_invisible is based on flag 0x02, and sight\_flags is really 0. 237 | For reproducibility purposes it's useful to keep these as separate entities. 238 | 239 | If sightFlags is >= 0x80: 240 | 241 | 1. Uint32 sightFlagsEx (IFL) 242 | 2. Uint8 seeInDark (IFL) 243 | 3. Uint8 seeInvisible (IFL) 244 | 245 | ## Sub-Block 3 (The String Table) 246 | 247 | The string table contains all DMStrings. 248 | 249 | As is the pattern so far, there is some amount of entries represented as an ObjectID. 250 | 251 | An ongoing 32-bit hash [NQCRC](../algorithms/NQCRC.md) is maintained throughout the string table, initialized to -1. 252 | 253 | For each entry { 254 | 255 | The entries use encrypted lengths, and encrypted data - this is where the "base pointer/key" is involved. 256 | 257 | Lengths are Uint16 XOR'd with the file position of the value minus the "base pointer/key". 258 | 259 | The entry length is prefixed over and over with (encrypted) 0xFFFF to reach the required total. 260 | 261 | The 0xFFFF prefix adds 0xFFFF to the length total, and then reads another length (which can itself be 0xFFFF). 262 | 263 | Otherwise the final length value is added to the total and length reading stops. 264 | 265 | With official software, if the string length happens to be exactly equal to 0xFFFF, it doesn't write 0xFFFF 0x0000, it just writes 0xFFFF. 266 | 267 | ``` 268 | #define F "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 269 | #define G F+F 270 | #define H G+G 271 | #define I H+H 272 | #define J I+I 273 | #define K J+J 274 | #define L K+K 275 | #define M L+L 276 | #define N M+M 277 | #define O N+N 278 | #define P O+O 279 | 280 | /mob 281 | text = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"+F+G+H+I+J+K+L+M+N+O+P 282 | ``` 283 | 284 | Finally, the string is written, encrypted with [XORJUMP](../algorithms/XORJUMP9.md). 285 | 286 | The key is again the file position (at string start) minus the "base pointer/key". 287 | 288 | Strings, including empty ones, are hashed - including the terminating null byte, which is *not* actually present. 289 | 290 | } 291 | 292 | Importantly, after the entries, there is a footer. 293 | 294 | For GEN Versions >= 468 { 295 | 296 | 1. Uint32 hash (This contains the result of the ongoing hash for verification) 297 | 298 | } 299 | 300 | ### String Contents And Their Escaping 301 | 302 | DMB has a number of very complex string escaping rules. 303 | 304 | 305 | ## Sub-Block 4 (The List Table) 306 | 307 | Like Sub-Block 1, this starts with an ObjectID entry count. 308 | 309 | This doesn't really have a set content format; it's a generic list container for other bits to use for dynamically sized lists. 310 | 311 | It's guaranteed to contain only lists of ObjectIDs, but that's as far as guarantees go. 312 | 313 | For each entry: 314 | 315 | 1. Uint16 subCount 316 | 2. Array of subCount Nullable ObjectIDs subEntries 317 | 318 | ## Sub-Block 5 (The Proc Table) 319 | 320 | Like Sub-Block 1, this starts with an ObjectID entry count. 321 | 322 | For each entry: 323 | 324 | For GEN Versions >= 224 OR if large object IDs are on { 325 | 326 | 1. Nullable StringID path (Full path) 327 | NOTE: No, this isn't used for inheritance. See below. 328 | 329 | } 330 | 331 | 1. Nullable StringID name (This is the display name for verbs, but *it is also used for inheritance*.) 332 | 2. Nullable StringID desc (The description of the verb - thanks to Willox for finding this) 333 | 3. Nullable StringID verbCategory (NOTE: Verbs with a null category don't show up in the menu.) 334 | 4. Uint8 verbSrcParam (usually 0xFF - thanks to Willox for finding this) 335 | 5. Uint8 verbSrcKind (usually 0 - thanks to Willox for finding this, see below) 336 | 6. Uint8 flags (See below) 337 | 7. If flags has the high bit set, Uint32 unk, Uint8 unk. 338 | 8. ListID code (See [Bytecode](./DMB.Bytecode.md) for further information) 339 | 9. ListID of VarID locals (note that this isn't nullable) 340 | 10. ListID args (note that this isn't nullable) (it's also complicated - VarIDs are involved, but also other stuff) 341 | 342 | ``` 343 | verbSrcKind meanings (thanks to Willox for these!): 344 | 0x01: in view 345 | 0x02: in oview 346 | 0x03: usr.loc 347 | 0x05: in range 348 | 0x08: in usr 349 | 0x20: = usr 350 | 351 | flags: 352 | 353 | Thanks to Willox for these: 354 | 0x01: hidden 355 | 0x02: src_equal 356 | 0x04: wait_for 357 | 0x20: no_category 358 | 0x40: yes_category 359 | 360 | 0x80: Has extended flags 361 | ``` 362 | 363 | ## Sub-Block 6 (The Var Table) 364 | 365 | Like Sub-Block 1, this starts with an ObjectID entry count. 366 | 367 | For each entry { 368 | 369 | 1. Uint8 type 370 | 2. Uint32 value 371 | 3. StringID name 372 | 373 | For further information on the type/value part, see [TypeValue](./DMB.TypeValue.md). 374 | 375 | } 376 | 377 | If the GEN Version *and* the LHS Version are both >= 512, this has a footer { 378 | 379 | 1. Nullable ListID-as-UInt32 unk 380 | 381 | } 382 | 383 | ## Sub-Block 7 384 | 385 | Like Sub-Block 1, this starts with an ObjectID entry count. 386 | 387 | Each entry is a ProcID. 388 | 389 | ## Sub-Block 8 (The Instance Table) 390 | 391 | Like Sub-Block 1, this starts with an ObjectID entry count. 392 | 393 | For each entry: 394 | 395 | 1. Uint8 type 396 | 2. Uint32 value 397 | 3. Nullable ProcID initializer (used to set per-instance properties) 398 | 399 | For further information on the type/value part, see [TypeValue](./DMB.TypeValue.md). 400 | But to be particular, this represents a 401 | 402 | A turf has the values (10, (some class ID), 0xFFFF). 403 | 404 | It is important to note that as far as I am aware, most properties of an initializer proc don't matter. 405 | 406 | Check [Bytecode](./DMB.Bytecode.md) for more information. 407 | 408 | ## Sub-Block 9 (Map Additional Data) 409 | 410 | This attaches instances to the map. 411 | 412 | Uint32, total objects. 413 | 414 | For each object: 415 | 416 | 1. Uint16 offset 417 | 418 | That is the offset in tiles (based on the same universal map index logic as for the map itself) from the last object. 419 | If there is no last object, the start of the map. 420 | 421 | 2. Nullable InstanceID instance 422 | 423 | It is reasonable to assume that null instances are used to pad out long empty regions of space. 424 | (i.e. they don't mean anything) 425 | 426 | ## Sub-Block 10 (Standard Object IDs) 427 | 428 | 1. Nullable MobTypeID mob 429 | 2. Nullable ClassID turf 430 | 3. Nullable ClassID area 431 | 4. Nullable ListID of ProcID procs (this contains procs attached to /world) 432 | 5. Nullable ProcID globalVariableInitializer (this contains global variable initializers) 433 | 6. Nullable StringID domain (this is `world.domain`, a mysterious, seemingly undocumented property - thanks to Willox for finding this) 434 | 7. StringID name 435 | 436 | If GEN Version < 368 (YES, UNDER) { 437 | 438 | 1. ObjectID unk 439 | 440 | } 441 | 442 | 1. Uint32 tickTimeMillis (this is `world.tick_lag`, multiplied by 100 - essentially, the time per tick measured in integer milliseconds.) 443 | 2. ClassID client 444 | 445 | If GEN Version >= 308 { 446 | 447 | 1. ClassID image 448 | 449 | } 450 | 451 | 1. Uint8 clientLazyEye (this is `client.lazy_eye`, used for camera control - thanks to Willox for finding this) 452 | 2. Uint8 clientDir (this is `client.dir` - defaults to 1 (NORTH). - thanks to Willox for finding this) 453 | 454 | If GEN Version >= 415 { 455 | 456 | 1. Uint16 clientControlFreak (this is `client.control_freak`, used to restrict skins and macros - thanks to Willox for finding this) 457 | 458 | } 459 | 460 | 1. Uint8 unk (again) 461 | 462 | If GEN Version >= 230 { 463 | 464 | 1. Nullable StringID clientScript (this is `client.script` *specifically* as a string - thanks to Willox for finding this) 465 | 466 | } 467 | 468 | If GEN Version >= 507 { 469 | 470 | 1. Uint16 clientScriptCacheFileCount 471 | 2. Array of clientScriptCacheFileCount CacheFileIDs clientScriptCacheFiles (this is every `client.script` file - thanks to Willox for finding this) 472 | 473 | } 474 | 475 | If GEN Version < 507 (yes, under) { 476 | 477 | 1. Nullable ObjectID unkSpecial 478 | 479 | } 480 | 481 | If GEN Version >= 232 { 482 | 483 | 1. Uint16 unk (Seems to default to 2827, but isn't necessary) 484 | 485 | } 486 | 487 | If GEN Version >= 235 && GEN Version < 368 { 488 | 489 | 1. Uint16 unk 490 | 491 | } 492 | 493 | If GEN Version >= 236 && GEN Version < 368 { 494 | 495 | 1. Uint16 unk 496 | 497 | } 498 | 499 | If GEN Version >= 341 { 500 | 501 | 1. Nullable StringID hubPasswordHashed (this is the 'hashed' hub password) 502 | 503 | } 504 | 505 | If GEN Version >= 266 { 506 | 507 | 1. Nullable StringID serverName 508 | 2. Uint32 hubNumber (This is where `world.hub` goes when a number is provided - thanks to Willox for finding this) 509 | 3. Uint32 gameVersion (`world.version`, not to be confused with the BYOND version - thanks to Willox for finding this) 510 | 511 | } 512 | 513 | If GEN Version >= 272 { 514 | 515 | 1. Uint16 cacheLifespan (Best set to 30. `world.cache_lifespan`, in days - thanks to Willox for finding this) 516 | 2. Nullable StringID clientCommandText (`client.command_text` - thanks to Willox for finding this) 517 | 3. Nullable StringID clientCommandPrompt (`client.command_prompt` (undocumented?) - thanks to Willox for finding this) 518 | 519 | } 520 | 521 | If GEN Version >= 276 { 522 | 523 | 1. Nullable StringID hub (this is the `user.game` hub name, i.e. `world.hub`) 524 | 525 | } 526 | 527 | If GEN Version >= 305 { 528 | 529 | 1. Nullable StringID channel (`world.channel` ; might not ACTUALLY be nullable ; undocumented but DreamMaker understands it ; seems to be the string "default"?) 530 | 531 | } 532 | 533 | If GEN Version >= 360 { 534 | 535 | 1. Nullable CacheFileID skin (DMI and friends - thanks to Willox for finding this) 536 | 537 | } 538 | 539 | If LHS Version >= 455 { 540 | 541 | 1. Uint16 iconSizeX (default 32) 542 | 2. Uint16 iconSizeY (default 32) 543 | 3. Uint16 mapFormat (default 32768 - thanks to Willox for finding this) 544 | 545 | } 546 | 547 | ## Sub-Block 11 (The Cache Files Table) 548 | 549 | Like Sub-Block 1, this starts with an ObjectID entry count. 550 | 551 | For each entry: 552 | 553 | 1. Uint32 uniqueID 554 | 2. Uint8 typeOrSomething 555 | 556 | These correspond to entries in the relevant RSC files. 557 | 558 | (The order is swapped. This is known and correct.) 559 | 560 | ## The Requirements For A DMB File To Be Loadable (Eden) 561 | 562 | ### Minimum 563 | 564 | Honestly, I'm not even sure what the minimum is. 565 | Check the included source at this point, and go down from there. 566 | 567 | ### Important Things Of Note 568 | 569 | BYOND *may* expect strings to have their DM-compiler-generated "word IDs". 570 | 571 | That is, strings being compared by-ID to constants without the runtime verifying that they are in fact the correct constants. 572 | 573 | ### The Annotated Standard Class Hierarchy 574 | 575 | It's important to note that some classes are actually BYOND built-in types. 576 | 577 | Additionally, some class code is part of BYOND and is always generated for every file. 578 | 579 | ``` 580 | class flags: 581 | CF_MOB = 0x0002 582 | CF_ATOM = 0x0004 Seems to control access to atom procs in some way. 583 | CF_AREA = 0x0008 584 | 585 | 'DMIF' is dmInterface, seen as _dm_interface in stddef.dm (see https://github.com/tgstation/FastDMM/blob/master/src/main/resources/stddef.dm ) 586 | (This is a bitfield, but actually attempting to match the flags as described there doesn't always work - atom/movable are always off.) 587 | 588 | 'TP' is the type when it shows up in the Instance Table. 589 | 590 | DMIF FLAG TP PARENT 591 | /client = 0x0001 0x0000 0 592 | /datum = 0x0001 0x0000 0 593 | /sound = 0x0221 0x0000 /datum 594 | /icon = 0x0301 0x0000 /datum 595 | /matrix = 0x0401 0x0000 /datum 596 | /regex = 0x2001 0x0000 /datum 597 | /dm_filter = 0x0001 0x0000 /datum 598 | /image = 0x0041 0x0004 0x3F /datum 599 | /mutable_appearance = 0x4041 0x0004 0x3F /image 600 | /database = 0x1001 0x0000 /datum 601 | /database/query = 0x1001 0x0000 /database 602 | /atom = 0x0001 0x0004 0x0A /datum 603 | /turf = 0x0001 0x0004 0x0A /atom 604 | /area = 0x0001 0x000C 0x0B /atom 605 | /atom/movable = 0x0001 0x0004 0x09 /atom 606 | /obj = 0x0001 0x0004 0x09 /atom/movable 607 | /mob = 0x0001 0x0006 0x08 /atom/movable 608 | ``` 609 | 610 | There's some additional notes worth keeping in mind: 611 | 612 | 1. You may notice /world isn't here. That's because /world isn't a class. 613 | 2. Turfs and atoms are one and the same. 614 | 3. Objs and movable atoms are also one and the same. 615 | 616 | -------------------------------------------------------------------------------- /formats/DMI.md: -------------------------------------------------------------------------------- 1 | # DMI files (*.dmi) (INFORMATION INCOMPLETE) 2 | 3 | Note: This documentation is likely less complete. I'm generally focusing on stuff that hasn't already been documented. 4 | This DMI information is included as an attempt at completeness and should hopefully be enough to get something working. 5 | In particular, movement states aren't covered AT ALL. 6 | 7 | DMI files are either PNG files with some metadata, or files using a custom format. 8 | It seems that older files use the custom format. 9 | 10 | ## DMI-in-PNG 11 | 12 | DMI-in-PNG files are PNG files. They contain nothing in them that is non-standard. 13 | As PNG files, I've seen some of them have partial transparency, which may explain the format change. 14 | 15 | However, they contain a zTXt chunk: http://libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.zTXt 16 | 17 | This has the keyword "Description"; ExifTool is able to detect this, see: https://exiftool.org/TagNames/PNG.html#TextualData 18 | 19 | So it's not absolutely necessary to have a specialized editor in order to perform editing of DMI files. 20 | 21 | If you're in a particular hurry and willing to use external commands, `exiftool -j images.dmi` will output the DMI in the `Description` field of the first (`[0]`) object in the array (which is the root of the output). 22 | 23 | ### DMI Textual Metadata Format 24 | 25 | So presumably you've extracted the description string and you want a definitive idea of format. 26 | 27 | It's newline-delimited (basically a text file). 28 | It's likely lexed with the exact same DM lexer as everything else, though not sure on printing. 29 | 30 | It starts with a comment: `# BEGIN DMI`. 31 | 32 | It then has a block of the form: 33 | 34 | ``` 35 | version = 4.0 36 | width = 32 37 | height = 32 38 | ``` 39 | 40 | The tabs likely mean something as with the rest of DM, not tested and best not to assume. 41 | 42 | Each state is of the form: 43 | 44 | ``` 45 | state = "somestate" 46 | dirs = 1 47 | frames = 1 48 | ``` 49 | 50 | The images are allocated in a "left to right, then top to bottom" standard grid pattern. 51 | 52 | This list of images is split by states, and then those states are split by frames, and then those frames are split by directions. 53 | 54 | For a 4-direction state, directions are in the order south/north/east/west. 55 | 56 | Finally, it ends with `# END DMI`. 57 | 58 | ## Old DMI Binary Format 59 | 60 | Lummox Jr describes the old DMI binary format here: http://www.byond.com/forum/post/189170#comment1077677 61 | 62 | The information will not be copied here unless something happens to the original. 63 | 64 | -------------------------------------------------------------------------------- /formats/RAD.md: -------------------------------------------------------------------------------- 1 | # Random Access Data format 2 | 3 | This is the format behind the RSC and savefile formats. 4 | 5 | The idea appears to be for the kind of arbitrary-access writing needed for maintaining `byond.rsc` or modifying savegames. 6 | 7 | It's actually a concatenated list of entries. 8 | 9 | Values are little-endian as with DMB. 10 | 11 | The outer entry structure is: 12 | 13 | 1. Uint32 entryLength 14 | 2. Uint8 valid (0x01 for valid entries, 0x00 for invalid entries. Note that invalid entries will have nonsensical content that can crash your reader.) 15 | 3. Array of entryLength Uint8s entryContent 16 | 17 | -------------------------------------------------------------------------------- /formats/RSC.md: -------------------------------------------------------------------------------- 1 | # RSC (*.rsc) Format 2 | 3 | Status on documentation: Complete reading-wise but checksum details not worked out yet for writing. `byond.rsc` considered irrelevant *unless the encryption crosses over into protocol research*. 4 | 5 | ## Details 6 | 7 | For the outer structure, see [RAD](./RAD.md). 8 | 9 | Values remain little-endian as with DMB. 10 | 11 | The inner structure identifies entry components and is the actual data. 12 | 13 | 1. Uint8 typeOrSomething (corresponds to Cache File typeOrSomething in DMB) 14 | 2. Uint32 uniqueID (corresponds to Cache File uniqueID in DMB) - *this is probably also a checksum / hash / ??? of the data and/or entry, but details are unknown* 15 | 3. Uint32 timestamp (seconds since the Unix epoch, UTC) 16 | 4. Uint32 originalTimestamp (this is the modification time *of the imported file* (presumably to determine which files to update). In `byond.rsc`, this is 0.) 17 | 5. Uint32 dataLength 18 | 6. Zero-terminated string filename 19 | 7. Array of dataLength Uint8s data 20 | 21 | The data in these entries is *usually* unencrypted unless you're poking `byond.rsc`. 22 | 23 | DreamDaemon does not understand encrypted entries. 24 | 25 | Regarding the mapping to the DMB file: The order is swapped. This is known and correct. 26 | 27 | ## Entry Types 28 | 29 | ``` 30 | 0x01: MIDI file. 31 | 0x02: OGG or WAV file. 32 | 0x03: DMI PNG file. 33 | 0x06: Plain PNG file. 34 | 0x0B: Plain JPG file. 35 | 36 | The flag 0x80 can be added to any of these to indicate encryption (details unknown). 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /formats/SAV.md: -------------------------------------------------------------------------------- 1 | # MOSTLY COMPLETE 2 | 3 | For the outer structure, see [RAD](./RAD.md). 4 | 5 | Values remain little-endian as with DMB. 6 | 7 | The inner structure identifies entry components and is the actual data. 8 | 9 | ## Entry Content 10 | 11 | The outer structure allows skipping through entries easily. 12 | 13 | The inner structure identifies entry components and is the actual data. 14 | 15 | There seem to be multiple different sub-types of entry content. 16 | 17 | In particular, the first entry contains some sort of header information. 18 | 19 | The second entry is for the default key, even if the savefile doesn't have one. 20 | 21 | ``` 22 | entry 1: 00 02 00 00 27 01 00 00 00 00 00 00 00 00 00 00 23 | |?generator?|?mincompat?| ?Unknown? | ?Unknown? | 24 | 25 | entry 2: 00 00 00 00 ff ff ff ff 00 00 00 00 00 26 | | index | parent |kL|dataLength | 27 | ``` 28 | 29 | Otherwise, data entries follow this general format: 30 | 31 | 1. Uint32 index (starts at 0) 32 | 2. Uint32 parent (-1, unless the entry has a parent, in which case it is the parent entry's index) 33 | 3. Uint8 keyLength 34 | 4. Array of keyLength Uint8s - [XORJUMP9](../algorithms/XORJUMP9.md) encrypted, key 0x53 35 | 5. Uint32 dataLength 36 | 6. Array of dataLength Uint8s - [XORJUMP9](../algorithms/XORJUMP9.md) encrypted, key 0x3A 37 | 38 | ## Entry Value Formats 39 | 40 | Savefile entry values are lists - the amount of values is implied by the total size of the blob, as split into these values. 41 | 42 | Value contents will be shown in decrypted form. 43 | 44 | They use a type/value system, but it's a different one to the one used elsewhere. 45 | 46 | An important note is that it's very possible for empty entries to show up. 47 | 48 | They're used for things such as parenting. 49 | 50 | ``` 51 | float: 52 | 04 ** ** ** ** 53 | 04 00 00 00 40 = 2.0 54 | 55 | string: 56 | 01 lL lH (bytes...) 57 | 01 04 00 41 41 41 41 = "AAAA" 58 | 59 | class reference: 60 | 03 lL lH (bytes...) 61 | 03 0B 00 2F 6D 6F 62 2F 63 61 6E 61 72 79 = /mob/canary 62 | 63 | object reference: 64 | 0B lL lH (bytes...) 65 | 0B 04 00 2F 2F 2E 30 = object("//.0") (this refers to a key of ".0" - see objects) 66 | 67 | ``` 68 | 69 | ### Objects 70 | 71 | Objects don't really have their own type. They get a blank entry with sub-entries containing the details. 72 | 73 | The `type` sub-entry is a class reference. 74 | Other sub-entries are equivalent to their corresponding fields. 75 | Only changed entries are saved. 76 | 77 | ### Relation to the textual format 78 | 79 | The relation between the binary format and the text format is incredibly literal. 80 | 81 | Given the following in the textual format: 82 | ``` 83 | . = object("//.0") 84 | .0 85 | type = /mob/canary 86 | fluffiness_text = "This canary is extremely fluffy." 87 | ``` 88 | 89 | The savefile that it was generated from contained: 90 | ``` 91 | header entry 92 | unk: 01 93 | raw: 00 02 00 00 27 01 00 00 00 00 00 00 00 00 00 00 94 | 95 | data entry 96 | unk: 01 97 | raw: 00 00 00 00 ff ff ff ff 00 07 00 00 00 31 47 4c 7a 71 49 40 98 | key: 99 | dat: 0b 04 00 2f 2f 2e 30 100 | dac: . . . / / . 0 101 | 102 | data entry 103 | unk: 01 104 | raw: 01 00 00 00 00 00 00 00 02 7d 6c 00 00 00 00 105 | key: .0 106 | dat: 107 | dac: 108 | 109 | data entry 110 | unk: 01 111 | raw: 02 00 00 00 01 00 00 00 04 27 25 15 0b 0e 00 00 00 39 48 4c 7a 33 08 12 56 e1 ea fa fc d4 d6 112 | key: type 113 | dat: 03 0b 00 2f 6d 6f 62 2f 63 61 6e 61 72 79 114 | dac: . . . / m o b / c a n a r y 115 | 116 | data entry 117 | unk: 01 118 | raw: 03 00 00 00 01 00 00 00 0f 35 30 10 08 11 e9 e7 f7 e8 d7 f2 c2 da b0 a5 23 00 00 00 3b 63 4c 01 36 0e 03 59 e1 ea fa fc d4 d6 98 a8 b9 f3 b9 9d 9a 85 65 64 77 77 5d 0d 50 53 3d 37 3c 1a 42 119 | key: fluffiness_text 120 | dat: 01 20 00 54 68 69 73 20 63 61 6e 61 72 79 20 69 73 20 65 78 74 72 65 6d 65 6c 79 20 66 6c 75 66 66 79 2e 121 | dac: . . T h i s c a n a r y i s e x t r e m e l y f l u f f y . 122 | ``` 123 | 124 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | t20kdc.bda 8 | bda-parent-project 9 | pom 10 | 0.666-SNAPSHOT 11 | 12 | 13 | UTF-8 14 | 1.8 15 | 1.8 16 | 17 | 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-toolchains-plugin 23 | 1.1 24 | toolchain 25 | 26 | 27 | 28 | 1.8 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | after 38 | after-tools 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /protocol/Protocol.md: -------------------------------------------------------------------------------- 1 | # INCOMPLETE 2 | 3 | ## Endianness And Formats 4 | 5 | The BYOND protocol uses big-endian for types and lengths. Outside of this, uncertain but probably LE everywhere. 6 | 7 | Because of this, endianness has to be explicitly stated in docs when anything is being transmitted. 8 | 9 | Strings are null-terminated, not length-prefixed. 10 | 11 | ## Framing 12 | 13 | The BYOND protocol is split into frames. 14 | 15 | Each frame has the following form: 16 | 17 | For CtoS frames that aren't the handshake { 18 | 19 | 1. Uint16BE sequence (Only present on CtoS frames that aren't the handshake.) 20 | 21 | } 22 | 23 | 1. Uint16BE type 24 | 2. Uint16BE length 25 | 3. Array of length Uint8s data 26 | 27 | Each frame *must* be sent as a single write call. (It has to do with the PSH flag.) 28 | 29 | The sequence number is initialized in the client's handshake, and is advanced with the following method: 30 | 31 | 1. Multiply the current sequence number by 0x43d4 - the result must be 32-bit. 32 | 2. Add this result to itself divided by 0xFFF1 and multiplied by 15, again all 32-bit. 33 | 3. This result, cast to Uint16, is the new sequence number except for one last rule: 34 | 3. If the sequence number is 0 (keeping in mind it's Uint16): Set it to 1. 35 | 36 | Additionally, there's a Uint32 piece of state not encoded here: The encryption key. If it is not 0, encryption is active. 37 | 38 | The encryption algorithm has been named [RUNSUB](../algorithms/RUNSUB.md) for now, and applies to the data. 39 | 40 | It's woven into the handshake procedure so that neither a modified client nor a modified server can force a zero encryption key, only both. 41 | 42 | ## Packets (CtoS) 43 | 44 | ### 0x0000: Quit 45 | 46 | This is sent to indicate quitting. 47 | 48 | It seems to be a single null byte, which could indicate that it's actually a string (uncertain). 49 | 50 | ### 0x0001: Handshake 51 | 52 | This is sent at the start of any connection. 53 | 54 | It always seems to have 18 bytes of content in modern versions. 55 | 56 | 1. Uint32LE byondVersion 57 | 2. Uint32LE minVersion 58 | 3. Uint32LE encryptionKeyModified 59 | 4. Uint16LE firstSequenceNumber (this is the first sequence number the client will use) 60 | 5. Uint32LE byondMinorVersion (note: this isn't present in v354, but I don't know which version it was added to) 61 | 62 | The exact details of the encryption key modification are `encryptionKeyModified = encryptionKey - ((minVersion * 0x10000) + byondVersion)`. 63 | 64 | The server sends back it's own handshake - 0x0001 - in response. 65 | 66 | ### 0x0002: Invoke Verb 67 | 68 | Further details unknown, just know it involves some sorta client-local verb index system (see StoC 0x0011) 69 | 70 | My tests look like: 71 | 72 | `02 00 00 00 02 00 00 00 00 00` (invokes verb index 0) 73 | `02 00 01 00 02 00 00 00 00 00` (invokes verb index 1) 74 | `02 00 02 00 02 00 00 00 00 00` (invokes verb index 2) 75 | 76 | ### 0x001a: PHS 1 77 | 78 | Further details unknown. 79 | 80 | The server sends back 0x003b in response. 81 | 82 | ## Packets (StoC) 83 | 84 | ### 0x0001: Handshake 85 | 86 | This is sent after the server receives the client's handshake. 87 | 88 | It is already encrypted (which indicates the key must be present in the communications up to/including it). 89 | 90 | It has 60 bytes of content. 91 | 92 | 1. Uint32LE byondVersion 93 | 2. Uint32LE minVersion 94 | 3. Uint8 isPermanentPort (1 if so, 0 otherwise) 95 | 4. Uint8 dmbFlagsHasEx 96 | 5. Uint8 unk (introduced in v433) 97 | 6. Padding: Read Uint32LE, add 0x71bd632f then AND 0x04008000 - if not 0, repeat. For example, 0x06beb95e terminates, 0x1ed688b0 doesn't. 98 | 7. Uint32LE addToEncryptionKey 99 | 8. Padding: Read Uint32LE, add 0x17db36e3 then AND 0x00402000 - if not 0, repeat. For example, 0x69b7216b terminates. 100 | 101 | addToEncryptionKey has to be added to the encryption key. 102 | 103 | The client sends back 0x001a in response. 104 | 105 | ### 0x000e: Cache List 106 | 107 | Further details unknown. 108 | 109 | WebClient expects 6 bytes but regular clients don't need any. Almost certainly a different format. 110 | 111 | This is the point where the server starts sending packets I can immediately verify it repeatedly sends. 112 | 113 | I don't intend to document everything right now, just get the webclient working. 114 | 115 | The communications (which are sometimes bi-direction) end with the server sending 0x0018 and then a batch of 0x0011. 116 | 117 | ### 0x0011: Register Verb Index 118 | 119 | This registers a verb index. 120 | 121 | It's important to note that these verbs are immediately usable. 122 | 123 | 1. Uint16LE index 124 | 2. String name 125 | 3. Unknown stuff (00 00 ff 00 04 00 for my test verbs but could be of any length) 126 | 127 | ### 0x0018: PHS 5 128 | 129 | Further details unknown. Empty in my tests. 130 | 131 | By this point the client is in-game, and gets sent verb indexes. 132 | 133 | ### 0x0026: Describe Atom Appearance 134 | 135 | 1. 4 bytes (??? subject to change ???) 136 | 2. String text 137 | 3. 6 bytes 138 | 4. The text character, but not sure how it's represented 139 | 140 | Further details unknown. 141 | 142 | This appears to be describing the appearance of an atom. 143 | 144 | ### 0x0027: Incoming Message 145 | 146 | This is sent from server to client. 147 | 148 | 1. String text 149 | 2. WebClient *suggests* that there is optional stuff here the existence of which is based on if there's room or not. 150 | 151 | ### 0x003b: PHS 2 152 | 153 | Further details unknown. 154 | 155 | This is followed by 0x00ef. 156 | 157 | ### 0x0072: Server Information 158 | 159 | Further details unknown. 160 | 161 | This contains a lot of stuff that looks like server information. 162 | 163 | ### 0x00ef: PHS 3 164 | 165 | Further details unknown. 166 | 167 | This is followed by 0x000e. 168 | 169 | -------------------------------------------------------------------------------- /protocol/WebClient.md: -------------------------------------------------------------------------------- 1 | # NOT REALLY A DESCRIPTION, but may be useful working notes 2 | 3 | The BYOND webclient is an HTTP server on the same port as the general BYOND server. 4 | 5 | ## The Current State Of The Official Software 6 | 7 | These are the transcribed StoC packets of a connection, with v513.1526: 8 | 9 | 1. Handshake packet (0x0001) (513, 498) 10 | 2. Winset packet (0x00e5), no controls, DMS files 'alert', 'any', 'bar', 'browser', 'button', 'child', 'color', 'dpad' -- (at this point I stopped, the most important note here is that defaultSkin was moved to the end) 11 | 3. Cache list packet (0x000e), 1 entry, 0x26b, cache ID is in fact the actual cache ID, weird string thing is empty. 12 | 4. Empty IDK packet (0x001a) - this is presumably the fancy auth thing that goes bonkers 13 | 5. Output packet (0x0027) - `Connection broken by server (error code 9)
\n` 14 | 15 | I feel it important to note that after the 0x001a, dealing with the stupid websocket mask nonsense shows the client's response: `00 1a 2d 00`. 16 | 17 | Clearly this is not satisfactory for our server overlord and we should bow down to it and blah blah blah. 18 | 19 | ## Paths 20 | 21 | The following paths are known to exist (that is, others may exist): 22 | 23 | ### /query 24 | 25 | Returns JSON of the form: 26 | 27 | ``` 28 | {"url": "http://localhost:53927/play", "version": "513.1526"} 29 | ``` 30 | 31 | ### /play 32 | 33 | This is the main webclient page. 34 | 35 | ### /res/* 36 | 37 | This is the content of the `web` BYOND distribution directory. 38 | 39 | ## WebSocket & Protocol Notes 40 | 41 | The server is also a WebSocket server (path `/`). 42 | 43 | The protocol has a different framing and a different handshake. 44 | 45 | In particular, framing-wise, the prefix is a big-endian UInt16 for the type, 46 | and that's all. No encryption, seemingly. 47 | 48 | The server then sends the initial handshake message. 49 | 50 | ## StoC 51 | 52 | ### 0x0001: Handshake (WS) 53 | 54 | 1. Uint32LE byondVersion 55 | 2. Uint32LE unkButProbablyMinVersion 56 | 57 | ### 0x00e5: Winset (WS) 58 | 59 | 1. Uint16LE controls 60 | 2. for each entry in controls, a complex structure blah blah blah 61 | 62 | then, while there is room at the end of the message: 63 | 64 | 1. Uint16LE strLen - if 0xFFFF, then a Uint32LE comes after to replace it 65 | 2. (that many string bytes) 66 | 67 | having any of these at all indicates the packet sets up the "main skin" of the webclient 68 | (i.e. the root of the weird HTML UI classes thing it has) 69 | each of these is such a class, see current state of software for further notes 70 | 71 | -------------------------------------------------------------------------------- /research-projects/Makefile: -------------------------------------------------------------------------------- 1 | OBJECTS = \ 2 | proc-inherit-quirks-1.dmb proc-inherit-quirks-2.dmb proc-inherit-quirks-3.dmb \ 3 | rand-inherit.dmb turf-atom-equivalence.dmb savefile.dmb world-channel.dmb \ 4 | modified-types.dmb a-very-long-output.dmb mapsurgery-game.dmb mapsurgery-map.dmb \ 5 | world-oddprops.dmb 6 | 7 | all: $(OBJECTS) 8 | .PHONY: clean 9 | clean: 10 | rm -f $(OBJECTS) 11 | 12 | %.dmb: %.dme 13 | DreamMaker $< 14 | -------------------------------------------------------------------------------- /research-projects/a-very-long-output.dme: -------------------------------------------------------------------------------- 1 | // A Very Long Output 2 | 3 | #define W16 " " 4 | #define W64 (W16 + W16 + W16 + W16) 5 | #define W256 (W64 + W64 + W64 + W64) 6 | 7 | #define S16 "@@@@@@@@@@@@@@@@" 8 | #define S64 (S16 + S16 + S16 + S16) 9 | #define S256 (S64 + S64 + S64 + S64) 10 | 11 | #define T16 "AAAAAAAAAAAAAAAA" 12 | #define T64 (T16 + T16 + T16 + T16) 13 | #define T256 (T64 + T64 + T64 + T64) 14 | 15 | /mob 16 | verb/x20() 17 | world << W256 18 | verb/x40() 19 | world << S256 20 | verb/x41() 21 | world << T256 22 | 23 | -------------------------------------------------------------------------------- /research-projects/a.dms: -------------------------------------------------------------------------------- 1 | // nope 2 | 3 | 4 | -------------------------------------------------------------------------------- /research-projects/b.dms: -------------------------------------------------------------------------------- 1 | // nope 2 | 3 | 4 | -------------------------------------------------------------------------------- /research-projects/chessboard.dm: -------------------------------------------------------------------------------- 1 | /turf/white_tile 2 | text = ":" 3 | 4 | /turf/grey_tile 5 | text = "." 6 | var/interesting = 1 7 | 8 | /turf/black_tile 9 | text = " " 10 | 11 | /mob 12 | text = "@" 13 | parent_type = /atom/movable 14 | 15 | /mob/reversi 16 | 17 | /mob/reversi/black 18 | text = "o" 19 | 20 | /mob/reversi/white 21 | text = "O" 22 | 23 | // Maps can only be included via #include - directly doesn't work. 24 | 25 | #include "chessboard.dmm" 26 | 27 | -------------------------------------------------------------------------------- /research-projects/chessboard.dmm: -------------------------------------------------------------------------------- 1 | "a" = (/turf/white_tile,/area) 2 | "b" = (/turf/black_tile,/area) 3 | (1, 1, 1) = {" 4 | abababab 5 | babababa 6 | abababab 7 | babababa 8 | abababab 9 | babababa 10 | abababab 11 | babababa 12 | "} 13 | 14 | -------------------------------------------------------------------------------- /research-projects/mapsurgery-game.dme: -------------------------------------------------------------------------------- 1 | #include "chessboard.dm" 2 | 3 | -------------------------------------------------------------------------------- /research-projects/mapsurgery-map.dme: -------------------------------------------------------------------------------- 1 | // just for editing the map 2 | 3 | /turf/white_tile 4 | 5 | /turf/grey_tile 6 | var/interesting = 1 7 | 8 | /turf/black_tile 9 | 10 | /mob/reversi 11 | 12 | /mob/reversi/black 13 | 14 | /mob/reversi/white 15 | 16 | #include "mapsurgery-map.dmm" 17 | 18 | -------------------------------------------------------------------------------- /research-projects/mapsurgery-map.dmm: -------------------------------------------------------------------------------- 1 | "a" = (/turf/white_tile,/area) 2 | "b" = (/turf/grey_tile,/area) 3 | "c" = (/turf/black_tile,/area) 4 | "d" = (/turf/grey_tile { text = "> " ; interesting = 1 },/area) 5 | "e" = (/mob/reversi/white,/turf/grey_tile,/area) 6 | "f" = (/mob/reversi/black,/turf/grey_tile,/area) 7 | (1, 1, 1) = {" 8 | aaaaaaac 9 | abbbbbbc 10 | abbaaabc 11 | abaefbbc 12 | abafebbc 13 | abbaaabc 14 | abbbbbdc 15 | cccccccc 16 | "} 17 | 18 | -------------------------------------------------------------------------------- /research-projects/modified-types.dme: -------------------------------------------------------------------------------- 1 | // Modified Types 2 | // As it turns out, the instance table has dedicated types. 3 | 4 | /quacker 5 | var/noise_text = "FAIL" 6 | proc/quack() 7 | world << noise_text 8 | 9 | /mob 10 | verb/runTest() 11 | new /quacker {noise_text = "PASS"} ().quack() 12 | 13 | -------------------------------------------------------------------------------- /research-projects/proc-inherit-quirks-1.dme: -------------------------------------------------------------------------------- 1 | // Proc Inheritance Quirks 1 2 | // Proc calls work in the exact same way as verb calls. 3 | // Proc calls are driven by display name. 4 | // So as these procs both have the display name "test", 5 | // test2 wins even in a call to test1. 6 | 7 | /datum 8 | proc/test1() 9 | set name = "test" 10 | world << "FAIL" 11 | proc/test2() 12 | set name = "test" 13 | world << "PASS" 14 | 15 | /mob 16 | verb/test() 17 | test1() 18 | 19 | -------------------------------------------------------------------------------- /research-projects/proc-inherit-quirks-2.dme: -------------------------------------------------------------------------------- 1 | // Proc Inheritance Quirks 2 2 | // Since proc calls work in the exact same way as verb calls, 3 | // proc calls are based on a list of procs, and the parent proc is 4 | // whichever version of that proc precedes it. 5 | 6 | /datum 7 | proc/test1() 8 | set name = "test" 9 | world << "To pass, this should be printed first." 10 | proc/test2() 11 | set name = "test" 12 | ..() 13 | world << "To pass, this should be printed second." 14 | 15 | /mob 16 | verb/test() 17 | test1() 18 | 19 | -------------------------------------------------------------------------------- /research-projects/proc-inherit-quirks-3.dme: -------------------------------------------------------------------------------- 1 | // Proc Inheritance Quirks 3 2 | // While ".procs" doesn't exist, ".verbs" does exist, and can prove several points: 3 | // 1. That verbs are the same type as procs - adding a proc to verbs works just fine 4 | // 2. That the verbs list acts the same way as demonstrated with procs. 5 | // (Note that an attempt to directly call a proc will make it a proc call, using proc parenthood.) 6 | 7 | /datum 8 | verb/test1() 9 | set name = "testB" 10 | world << "To pass, this should be printed first, and test2 should be run if and only if testA was run." 11 | world << "Furthermore, pressing testA multiple times must have no further effect than it being pressed once." 12 | 13 | /datum/holding_cell 14 | proc/test2() 15 | set name = "testB" 16 | ..() 17 | world << "This is test2 after the parent call." 18 | 19 | /mob 20 | verb/testA() 21 | world << "This is testA adding the test2 verb." 22 | src.verbs += /datum/holding_cell/proc/test2 23 | 24 | -------------------------------------------------------------------------------- /research-projects/rand-inherit.dme: -------------------------------------------------------------------------------- 1 | // Random Inheritance 2 | // The runtime appears to be not exactly strict about object inheritance. 3 | // And the DM compiler isn't exactly strict about it either. 4 | // For example, making the client extend /datum is apparently allowed. 5 | // As far as I can tell, DM objects completely ignore inheritance 6 | // outside of the effects on vars and procs. 7 | // The actual details of a class are split between: 8 | // 1. The class flags 9 | // 2. The type path used to create an instance of it (dynamically or statically) 10 | // Case in point: If you remove /pawn's parent, *nothing will happen.* 11 | 12 | /client 13 | parent_type = /datum 14 | 15 | var/atom/movable/quack = null 16 | 17 | /mob 18 | parent_type = /datum 19 | verb/doTheThing() 20 | quack.Move(locate(8, 5, 1)) 21 | 22 | /atom/movable 23 | parent_type = /datum 24 | 25 | /atom 26 | 27 | /pawn 28 | text = "p" 29 | parent_type = /atom/movable 30 | 31 | /world 32 | New() 33 | quack = new /pawn(locate(8, 7, 1)) 34 | 35 | // --- 36 | 37 | #include "chessboard.dm" 38 | 39 | -------------------------------------------------------------------------------- /research-projects/savefile.dme: -------------------------------------------------------------------------------- 1 | // Savefile 2 | 3 | /mob/canary 4 | var/fluffiness_text = "" 5 | 6 | /world 7 | New() 8 | var/savefile/target = new("savefile1.sav") 9 | // example floats 10 | target << 2 // 00 00 00 40 11 | target << 8 // 00 00 00 41 12 | target << 2 // 00 00 00 40 13 | target << 8 // 00 00 00 41 14 | // example strings 15 | target << "AAAA" // 41 41 41 41 16 | target << "BBBB" // 42 42 42 42 17 | target << "CCCC" // 43 43 43 43 18 | target << "DDDD" // 44 44 44 44 19 | target.Flush() 20 | 21 | // savefile using path for further research 22 | target = new("savefile2.sav") 23 | target["AAAA"] << 2 // 41 41 41 41 : 00 00 00 40 24 | target["BBBB"] << 2 25 | target.Flush() 26 | 27 | // savefile using datum for even more research 28 | target = new("savefile3.sav") 29 | var/mob/canary/canary = new /mob/canary() 30 | canary.fluffiness_text = "This canary is extremely fluffy." 31 | target << canary 32 | target.Flush() 33 | target.ExportText("/", file("savefile3.txt.sav")) 34 | /* 35 | * -- 1 -- 36 | * 37 | * 00000000 10 00 00 00 01 00 02 00 00 27 01 00 00 00 00 00 |.........'......| 38 | * | { 39 | * 00000010 00 00 00 00 00 3d 00 00 00 01 00 00 00 00 ff ff |.....=..........| 40 | * } | { 41 | * 00000020 ff ff 00 30 00 00 00 3e 43 4c 55 1e 63 70 79 82 |...0...>CLU.cpy.| 42 | * ^start 43 | * xorjump9 04 00 00 00 40 04 00 00 00 44 | * 00000030 ca 90 9d a6 af f8 c5 ca d3 dc a4 ef f3 00 48 53 |..............HS| 45 | * xorjump9 41 04 00 00 00 40 04 00 00 00 41 01 04 00 41 41 AA 46 | * 00000040 5a 65 2c 32 3f 0a 13 18 21 6d 71 7e c4 d3 da e1 |Ze,2?...!mq~....| 47 | * xorjump9 41 41 01 04 00 42 42 42 42 01 04 00 43 43 43 43 AA BBBB CCCC 48 | * 00000050 aa b0 bd 82 8b 9c a5 |.......| 49 | * xorjump9 01 04 00 44 44 44 44 DDDD 50 | * } 51 | * at the indicated ^start point (0x27 - len 0x30), the xorjump9 key is 0x3a (0x43 - 9). 52 | * the xorjump9 key for keys (not used here) is 0x53. 53 | * for further research on this, see the SAV document and savefile\_analyzer.lua 54 | */ 55 | 56 | -------------------------------------------------------------------------------- /research-projects/turf-atom-equivalence.dme: -------------------------------------------------------------------------------- 1 | // Turf/Atom Equivalence 2 | // So far, all indications are that turfs and immovable atoms are exactly the same. 3 | // This is an attempt to prove it. 4 | // This also notes an interesting timing problem with turf replacement. 5 | 6 | /mob 7 | verb/doTheThing() 8 | world << "before:" 9 | // have to delete previous content 10 | // because documentation isn't being reliable 11 | var/atom/ctmp = locate(1, 1, 1) 12 | world << src.loc 13 | del ctmp 14 | spawn(1) 15 | new /hole(locate(1, 1, 1)) 16 | spawn(1) 17 | world << "after:" 18 | world << src.loc 19 | world << "before should be a square" 20 | world << "after should be a hole, (implying the square was replaced with the hole)" 21 | 22 | /hole 23 | parent_type = /atom 24 | text = "O" 25 | 26 | // --- 27 | 28 | #include "chessboard.dm" 29 | 30 | -------------------------------------------------------------------------------- /research-projects/world-channel.dme: -------------------------------------------------------------------------------- 1 | // World Channel 2 | // Seems to just be "default" 3 | 4 | /mob 5 | Login() 6 | . = ..() 7 | world << world.channel 8 | 9 | -------------------------------------------------------------------------------- /research-projects/world-oddprops.dme: -------------------------------------------------------------------------------- 1 | // World odd properties 2 | 3 | /world 4 | domain = "this is an example of string" // Thanks to Willox 5 | tick_lag = 0.5 6 | hub = 1337 7 | 8 | /client 9 | script = 'a.dms' 10 | script = 'b.dms' 11 | script = "" 12 | command_text = "Outstandingly pokable." 13 | command_prompt = "Outstandingly undocumented." 14 | 15 | -------------------------------------------------------------------------------- /scripts/brute_force_xor_jump_9.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- This script brute-forces XOR-Jump-9 string encryption. 4 | 5 | local pf = io.open(..., "rb") 6 | local pfs = pf:read("*a") 7 | pf:close() 8 | 9 | local bit = bit or bit32 10 | 11 | local function dec(x, key) 12 | local jump = 9 13 | local rt = {} 14 | for j = 1, #x do 15 | rt[j] = string.char(bit.bxor(key, x:byte(j))) 16 | key = (key + jump) % 256 17 | end 18 | return table.concat(rt) 19 | end 20 | 21 | local vr = {} 22 | for i = 0, 255 do 23 | vr[i] = dec(pfs, i) 24 | io.stderr:write(i .. " computed\n") 25 | end 26 | 27 | local afail = {} 28 | for i = 0, 8 do afail[i] = true end 29 | for i = 11, 12 do afail[i] = true end 30 | for i = 14, 31 do afail[i] = true end 31 | for i = 127, 255 do afail[i] = true end 32 | 33 | local function decstr(str, b) 34 | local i = 0 35 | while str:byte(i + b) and (not afail[str:byte(i + b)]) do 36 | i = i + 1 37 | end 38 | if i == 0 then return end 39 | return str:sub(b, i + (b - 1)) 40 | end 41 | 42 | local st = 1 43 | local delays = {} 44 | while st < #pfs do 45 | if st % 16 == 0 then 46 | io.stderr:write((st / #pfs) .. "\n") 47 | end 48 | for key = 0, 255 do 49 | if delays[key] then 50 | delays[key] = delays[key] - 1 51 | if delays[key] == 0 then 52 | delays[key] = nil 53 | end 54 | else 55 | local str = decstr(vr[key], st) 56 | if str then 57 | io.write(string.format("%x/%x\n", st, key)) 58 | io.write(str .. "\n") 59 | io.flush() 60 | delays[key] = #str 61 | end 62 | end 63 | end 64 | st = st + 1 65 | end 66 | 67 | -------------------------------------------------------------------------------- /scripts/cycsub_r.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- ./cycsub_r.lua FILE 4 | 5 | local afile = ... 6 | 7 | local dat = io.open(afile, "rb"):read("*a") 8 | 9 | local key = "\x3c\x7c\x33\x5e\x0a\x71\x0d\x5b" 10 | 11 | local function cycsub_dec(buffer, key) 12 | local j = #buffer 13 | local out = "" 14 | local checksum = 0 15 | local keyIndex = 0 16 | while j >= 1 do 17 | local roundKey = bit.band(checksum + key:byte(keyIndex + 1), 0xFF) 18 | local bt = buffer:byte(j) 19 | bt = bit.band(bt - roundKey, 0xFF) 20 | out = string.char(bt) .. out 21 | checksum = bit.band(checksum + bt, 0xFF) 22 | keyIndex = (keyIndex + 1) % #key 23 | j = j - 1 24 | end 25 | return out 26 | end 27 | 28 | for i = 0, #dat - 1 do 29 | print("----") 30 | print(i) 31 | print("----") 32 | print(cycsub_dec(dat:sub(1, #dat - i), key)) 33 | end 34 | io.flush() 35 | 36 | -------------------------------------------------------------------------------- /scripts/general_xor.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- given an input file and an expected substring, 4 | -- shows the result of all XORs of the latter within the former 5 | -- on any XOR algorithm, this will show the keystream 6 | -- also shows the inter-byte jump, which is a great way of locating 7 | -- XORJUMP9, as it will show "09 09 09 09..." 8 | -- to make this easier, it will output " XORJUMP9" when this happens 9 | -- for a known target string (such as one you might generate using DreamMaker), 10 | -- this is much better than the brute-forcer 11 | 12 | local fn, ss, other = ... 13 | 14 | local alg = bit.bxor 15 | if other == "add" then 16 | alg = function (a, b) 17 | return bit.band(a - b, 255) 18 | end 19 | end 20 | 21 | local f = io.open(fn, "rb") 22 | local data = f:read("*a") 23 | 24 | for i = 1, #data - (#ss - 1) do 25 | print(string.format("%x", i)) 26 | local last 27 | local lasts = {} 28 | local isxj9 = true 29 | for j = 1, #ss do 30 | local cur = alg(data:byte(i + j - 1), ss:byte(j)) 31 | if not last then 32 | io.write(string.format("%02x", cur)) 33 | else 34 | io.write(string.format(" %02x", cur)) 35 | if bit.band(cur - last, 0xFF) ~= 9 then 36 | isxj9 = false 37 | end 38 | table.insert(lasts, string.format("%02x", bit.band(cur - last, 0xFF))) 39 | end 40 | last = cur 41 | end 42 | print() 43 | print(" " .. table.concat(lasts, " ")) 44 | if isxj9 then 45 | print(" XORJUMP9") 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /scripts/nqcrc.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- given an input file, outputs the NQCRC in hex 4 | 5 | local nqcrcTable = {} 6 | for i = 0, 255 do 7 | local value = bit.lshift(i, 0x18) 8 | for j = 0, 7 do 9 | local mod = 0 10 | if value < 0 then 11 | mod = 0xAF 12 | end 13 | value = bit.bxor(bit.lshift(value, 1), mod) 14 | end 15 | nqcrcTable[i] = value 16 | end 17 | 18 | local f = io.open(..., "rb") 19 | 20 | local hash = 0 21 | while true do 22 | local bt = f:read(1) 23 | if not bt then break end 24 | hash = bit.bxor(bit.lshift(hash, 8), nqcrcTable[bit.band(bit.bxor(bit.rshift(hash, 24), bt:byte()), 0xFF)]) 25 | end 26 | 27 | if hash < 0 then 28 | hash = hash + 0x100000000 29 | end 30 | print(string.format("%08x", hash)) 31 | 32 | -------------------------------------------------------------------------------- /scripts/rad_analyzer.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | local file = io.open(..., "rb") 4 | 5 | -- This script just shows RAD block structure. 6 | -- Might you be looking for the savefile analyzer? 7 | local function hex(a) 8 | io.write(string.format(" %02x", a)) 9 | end 10 | while true do 11 | local entryHead = file:read(5) 12 | if not entryHead then return end 13 | -- do we really need to check all 32-bits for an examination tool? yes? *fine* 14 | local entryLen = entryHead:byte(1) + (entryHead:byte(2) * 0x100) + (entryHead:byte(3) * 0x10000) + (entryHead:byte(4) * 0x1000000) 15 | print("chunk-alloc " .. entryHead:byte(5) .. " len: " .. entryLen) 16 | 17 | local entryData = "" 18 | io.write(" raw:") 19 | for i = 1, entryLen do 20 | local bt = file:read(1) 21 | entryData = entryData .. bt 22 | hex(bt:byte()) 23 | end 24 | print() 25 | print() 26 | end 27 | 28 | -------------------------------------------------------------------------------- /scripts/runsub_r.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- ./runsub_r.lua FILE POS KEY SUM 4 | -- this does not check checksum 5 | 6 | local afile, apos, akey, asum = ... 7 | 8 | afile = io.open(afile, "rb") 9 | afile:seek("set", apos) 10 | local input = afile:read("*a") 11 | afile:close() 12 | 13 | local key = tonumber(akey) 14 | local checksum = 0 15 | 16 | for i = 1, #input do 17 | local res = bit.band(input:byte(i) - (checksum + bit.rshift(key, checksum % 32)), 0xFF) 18 | io.write(string.char(res)) 19 | checksum = bit.band(checksum + res, 0xFF) 20 | end 21 | io.flush() 22 | 23 | -------------------------------------------------------------------------------- /scripts/runsub_t.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | -- useful for theorizing 4 | 5 | -- 00000010 0f af 55 81 4f bc 11 b0 88 d6 1e 5e b9 be 06 51 |..U.O......^...Q| 6 | -- 00000020 6b b7 73 6b 21 b7 69 |k.sk!.i| 7 | 8 | local inputX = "f9 5e 31 bb a0 b3 dd 93 0e ec fc a4 99 34 a2 f9 c5 4c b0 82 8c b1 64 5e fc eb 03 30 67 a3 e1 21 19 7e 51 db c0 d3 fd b3 2e 0c 1c c4 b9 54 c2 19 e5 6c d0 a2 ac d1 84 7e 1c 0b 23 50 87 c3 01 41 39 9e 71 fb e0 f3 1d d3 4e 2c 3c e4 d9 74 e2 39 05 8c f0 c2 cc f1 a4 9e 3c 2b 43 70 a7 e3 21 61 59 be 91 1b 00 13 3d f3 6e 4c 5c 04 f9 94 02 59 25 ac 10 e2 ec 11 c4 be 5c 4b 63 90 c7 03 41 81 79 de b1 3b 20 33 5d 13 8e 6c 7c 24 19 b4 22 79 45 cc 30 02 0c 31 e4 de 7c 6b 83 b0 e7 23 61 a1 99 fe d1 5b 40 53 7d 33 ae 8c 9c 44 39 d4 42 99 65 ec 50 22 2c 51 04 fe 9c 8b a3 d0 07 43 81 c1 b9 1e f1 7b 60 73 9d 53 ce ac bc 64 59 f4 62 b9 85 0c 70 42 4c 71 24 1e bc ab c3 f0 27 63 a1 e1 d9 3e 11 9b 80 93 bd 73 ee cc dc 84 79 14 82 d9 a5 2c 90 62 6c 91 44 3e dc cb e3 10 47 83 c1 01 c2 3b 0a" 9 | local input = "" 10 | for v in inputX:gmatch("[^ ]+") do 11 | input = input .. string.char(tonumber("0x" .. v)) 12 | end 13 | 14 | local key = 0x2ea95866 15 | local ver = 0x01140201 16 | local add = 0x73b76b51 17 | local minorVer = 0 18 | key = bit.band(key + ver + add, 0xFFFFFFFF) 19 | 20 | local checksum = 0 21 | 22 | for i = 1, #input - 1 do 23 | local res = bit.band(input:byte(i) - (checksum + bit.rshift(key, checksum % 32)), 0xFF) 24 | io.write(string.char(res)) 25 | checksum = bit.band(checksum + res, 0xFF) 26 | end 27 | io.flush() 28 | 29 | if checksum == input:byte(#input) then 30 | io.stderr:write("\nOK\n") 31 | else 32 | io.stderr:write("\nFAIL\n") 33 | end 34 | io.stderr:flush() 35 | 36 | -------------------------------------------------------------------------------- /scripts/savefile_analyzer.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | local file = io.open(..., "rb") 4 | 5 | -- This script handles the outer framing and encryption of a savefile. 6 | local isHeader = true 7 | local function hex(a) 8 | io.write(string.format(" %02x", a)) 9 | end 10 | while true do 11 | local entryHead = file:read(5) 12 | if not entryHead then return end 13 | if isHeader then 14 | print("header entry") 15 | else 16 | print("data entry") 17 | end 18 | io.write(" valid:") 19 | local valid = entryHead:byte(5) == 1 20 | hex(entryHead:byte(5)) 21 | print() 22 | -- do we really need to check all 32-bits for an examination tool? yes? *fine* 23 | local entryLen = entryHead:byte(1) + (entryHead:byte(2) * 0x100) + (entryHead:byte(3) * 0x10000) + (entryHead:byte(4) * 0x1000000) 24 | 25 | local entryData = "" 26 | io.write(" raw:") 27 | for i = 1, entryLen do 28 | local bt = file:read(1) 29 | entryData = entryData .. bt 30 | hex(bt:byte()) 31 | end 32 | print() 33 | 34 | if valid and not isHeader then 35 | -- data entry, try to decode 36 | local key = 0x53 37 | io.write(" key: ") 38 | for i = 1, entryData:byte(9) do 39 | io.write(string.char(bit.bxor(entryData:byte(9 + i), key))) 40 | key = (key + 9) % 256 41 | end 42 | print() 43 | local dataBase = 14 + entryData:byte(9) 44 | io.write(" dat:") 45 | local other = "" 46 | local key = 0x3A 47 | for i = dataBase, #entryData do 48 | local val = bit.bxor(entryData:byte(i), key) 49 | if val >= 32 and val ~= 127 then 50 | other = other .. string.char(val) .. " " 51 | else 52 | other = other .. ". " 53 | end 54 | hex(val) 55 | key = (key + 9) % 256 56 | end 57 | print() 58 | print(" dac: " .. other) 59 | end 60 | isHeader = false 61 | print() 62 | end 63 | 64 | --------------------------------------------------------------------------------