; MIT License ; ; Copyright (c) 2018-2019 Eldred Habert ; Originally hosted at https://github.com/ISSOtm/rgbds-structs ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in all ; copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ; SOFTWARE. ; !!! WARNING ABOUT READABILITY OF THIS CODE !!! ; ; RGBDS, being the venerable/old/decrepit (pick on depending on mood) assembler that it is, requires ; all label, variable etc. definitions to be on column 0. As in, no whitespace allowed (otherwise, syntax error) ; Meanwhile, these macros tend to use a lot of nesting (requiring indenting for readability), ; as well as variable definitions (requiring none to work). ; As you can probably tell, those two conflict and result in very poor readability ; Sadly, there is nothing I can do against that short of using a special preprocessor, ; which I refuse to do for usability's sake. ; You have all my apologies, how little they may matter, if you are trying to read this code ; I still did my best to use explicit comments and variable names, hope they will help! ; strreplace variable_name, original_char, new_char strreplace: MACRO DOT_POS = STRIN("{\1}", \2) IF DOT_POS != 0 TMP equs STRCAT(STRSUB("{\1}", 1, DOT_POS + (-1)), STRCAT(\3, STRSUB("{\1}", DOT_POS + 1, STRLEN("{\1}") - DOT_POS))) PURGE \1 \1 equs "{TMP}" PURGE TMP strreplace \1, \2, \3 ENDC IF DEF(DOT_POS) PURGE DOT_POS ENDC ENDM ; rgbds_structs_version version_string ; Call with the expected version string to ensure you're using a compatible version ; Example: rgbds_structs_version 1.0.0 rgbds_structs_version: MACRO CURRENT_VERSION equs "1,2,1" EXPECTED_VERSION equs "\1" strreplace EXPECTED_VERSION, ".", "\," check_ver: MACRO IF \1 != \4 || \2 > \5 || \3 > \6 PURGE EXPECTED_VERSION ENDC ENDM CHECK_VER_CALL equs "check_ver {EXPECTED_VERSION},{CURRENT_VERSION}" CHECK_VER_CALL IF !DEF(EXPECTED_VERSION) strreplace CURRENT_VERSION, "\,", "." FAIL "RGBDS-structs version \1 is required, which is incompatible with current version {CURRENT_VERSION}" ENDC PURGE CHECK_VER_CALL PURGE check_ver PURGE CURRENT_VERSION PURGE EXPECTED_VERSION ENDM ; struct struct_name ; Begins a struct declaration struct: MACRO IF DEF(NB_FIELDS) FAIL "Please close struct definitions using `end_struct`" ENDC STRUCT_NAME equs "\1" NB_FIELDS = 0 RSRESET ENDM ; end_struct ; Ends a struct declaration end_struct: MACRO ; Set nb of fields STRUCT_NB_FIELDS equs "{STRUCT_NAME}_nb_fields" STRUCT_NB_FIELDS = NB_FIELDS PURGE STRUCT_NB_FIELDS ; Set size of struct STRUCT_SIZEOF equs "sizeof_{STRUCT_NAME}" STRUCT_SIZEOF RB 0 PURGE STRUCT_SIZEOF PURGE NB_FIELDS PURGE STRUCT_NAME ENDM ; get_nth_field_info field_id ; Defines EQUS strings pertaining to a struct's Nth field ; For internal use, please do not use externally get_nth_field_info: MACRO ; Field's name STRUCT_FIELD equs "{STRUCT_NAME}_field{d:\1}" STRUCT_FIELD_NAME equs "{STRUCT_FIELD}_name" STRUCT_FIELD_TYPE equs "{STRUCT_FIELD}_type" STRUCT_FIELD_NBEL equs "{STRUCT_FIELD}_nb_el" ; Number of elements STRUCT_FIELD_SIZE equs "{STRUCT_FIELD}_size" ; sizeof(type) * nb_el ENDM ; new_field nb_elems, rs_type, field_name ; For internal use, please do not use externally new_field: MACRO IF !DEF(STRUCT_NAME) || !DEF(NB_FIELDS) FAIL "Please start defining a struct, using `define_struct`" ENDC get_nth_field_info NB_FIELDS ; Set field name (keep in mind `STRUCT_FIELD_NAME` is *itself* an EQUS!) STRUCT_FIELD_NAME equs "\"\3\"" PURGE STRUCT_FIELD_NAME ; Set field offset STRUCT_FIELD \2 (\1) ; Alias this in a human-comprehensive manner STRUCT_FIELD_NAME equs "{STRUCT_NAME}_\3" STRUCT_FIELD_NAME = STRUCT_FIELD ; Compute field size CURRENT_RS RB 0 STRUCT_FIELD_SIZE = CURRENT_RS - STRUCT_FIELD ; Set properties STRUCT_FIELD_NBEL = \1 STRUCT_FIELD_TYPE equs STRSUB("\2", 2, 1) PURGE STRUCT_FIELD PURGE STRUCT_FIELD_NAME PURGE STRUCT_FIELD_TYPE PURGE STRUCT_FIELD_NBEL PURGE STRUCT_FIELD_SIZE PURGE CURRENT_RS NB_FIELDS = NB_FIELDS + 1 ENDM ; bytes nb_bytes, field_name ; Defines a field of N bytes bytes: MACRO new_field \1, RB, \2 ENDM ; words nb_words, field_name ; Defines a field of N*2 bytes words: MACRO new_field \1, RW, \2 ENDM ; longs nb_longs, field_name ; Defines a field of N*4 bytes longs: MACRO new_field \1, RL, \2 ENDM ; dstruct struct_type, INSTANCE_NAME[, ...] ; Allocates space for a struct in memory ; If no further arguments are supplied, the space is simply allocated (using `ds`) ; Otherwise, the data is written to memory using the appropriate types ; For example, a struct defined with `bytes 1, Field1` and `words 3, Field2` would have four extra arguments, one byte then three words. dstruct: MACRO NB_FIELDS equs "\1_nb_fields" IF !DEF(NB_FIELDS) FAIL "Struct \1 isn't defined!" ELIF _NARG != 2 && _NARG != NB_FIELDS + 2 ; We must have either a RAM declaration (no data args) or a ROM one (RAM args + data args) EXPECTED_NARG = 2 + NB_FIELDS FAIL "Invalid number of arguments, expected 2 or {d:EXPECTED_NARG} but got {d:_NARG}" ENDC ; Define the two fields required by `get_nth_field_info` STRUCT_NAME equs "\1" ; Which struct `get_nth_field_info` should pull info about INSTANCE_NAME equs "\2" ; The instance's base name ; RGBASM always expands `\X` macro args, so `IF _NARG > 2 && STRIN("\3", "=")` will error out when there are only 2 args ; Therefore, the condition is checked here (we can't nest the `IF`s over there because that doesn't translate well to `ELSE`) IS_NAMED_INVOCATION = 0 IF _NARG > 2 IF STRIN("\3", "=") IS_NAMED_INVOCATION = 1 ENDC ENDC IF IS_NAMED_INVOCATION ; This is a named instantiation, translate that to an ordered one ; This is needed because data has to be laid out in order, so some translation is needed anyways ; And finally, it's better to re-use the existing code at the cost of a single nested macro, I believe MACRO_CALL equs "dstruct \1, \2" ; This will be used later, but define it now because `SHIFT` will be run ; In practice `SHIFT` has no effect outside of one when invoked inside of a REPT block, but I hope this behavior is changed (causes a problem elsewhere) ARG_NUM = 3 REPT NB_FIELDS ; Find out which argument the current one is CUR_ARG equs "\3" ; Remove all whitespace to obtain something like ".name=value" (whitespace are unnecessary and complexify parsing) strreplace CUR_ARG, " ", "" strreplace CUR_ARG, "\t", "" EQUAL_POS = STRIN("{CUR_ARG}", "=") IF EQUAL_POS == 0 FAIL "Argument #{ARG_NUM} (\3) does not contain an equal sign in this named instantiation" ELIF STRCMP(STRSUB("{CUR_ARG}", 1, 1), ".") FAIL "Argument #{ARG_NUM} (\3) does not start with a period" ENDC FIELD_ID = -1 CUR_FIELD_ID = 0 REPT NB_FIELDS ; Get the name of the Nth field and compare TMP equs "{STRUCT_NAME}_field{d:CUR_FIELD_ID}_name" CUR_FIELD_NAME equs TMP PURGE TMP IF !STRCMP(STRUPR("{CUR_FIELD_NAME}"), STRUPR(STRSUB("{CUR_ARG}", 2, EQUAL_POS - 2))) ; Match found! IF FIELD_ID == -1 FIELD_ID = CUR_FIELD_ID ELSE TMP equs "{STRUCT_NAME}_field{d:CUR_FIELD_ID}_name" CONFLICTING_FIELD_NAME equs TMP PURGE TMP FAIL "Fields {CUR_FIELD_NAME} and {CONFLICTING_FIELD_NAME} have conflicting names (case-insensitive), cannot perform named instantiation" ENDC ENDC PURGE CUR_FIELD_NAME CUR_FIELD_ID = CUR_FIELD_ID + 1 ENDR PURGE CUR_FIELD_ID IF FIELD_ID == -1 FAIL "Argument #{d:ARG_NUM} (\3) does not match any field of the struct" ENDC INITIALIZER_NAME equs "FIELD_{d:FIELD_ID}_INITIALIZER" INITIALIZER_NAME equs STRSUB("{CUR_ARG}", EQUAL_POS + 1, STRLEN("{CUR_ARG}") - EQUAL_POS) PURGE INITIALIZER_NAME ; Go to next arg ARG_NUM = ARG_NUM + 1 SHIFT PURGE CUR_ARG ENDR ; Now that we matched each named initializer to their order, invoke the macro again but without names FIELD_ID = 0 REPT NB_FIELDS TMP equs "{MACRO_CALL}" PURGE MACRO_CALL INITIALIZER_VALUE equs "{FIELD_{d:FIELD_ID}_INITIALIZER}" DELETE_INITIALIZER equs "PURGE FIELD_{d:FIELD_ID}_INITIALIZER" DELETE_INITIALIZER PURGE DELETE_INITIALIZER MACRO_CALL equs "{TMP}, {INITIALIZER_VALUE}" PURGE TMP PURGE INITIALIZER_VALUE FIELD_ID = FIELD_ID + 1 ENDR PURGE FIELD_ID ; Clean up vars for nested invocation, otherwise some `equs` will be expanded PURGE INSTANCE_NAME PURGE STRUCT_NAME PURGE IS_NAMED_INVOCATION PURGE NB_FIELDS MACRO_CALL ; Now do call the macro PURGE MACRO_CALL ELSE INSTANCE_NAME:: ; Declare the struct's root ; Define instance's properties from struct's \2_nb_fields = NB_FIELDS sizeof_\2 = sizeof_\1 ; Start defining fields FIELD_ID = 0 REPT NB_FIELDS get_nth_field_info FIELD_ID FIELD_NAME equs STRCAT("{INSTANCE_NAME}_", STRUCT_FIELD_NAME) FIELD_NAME:: ; We have defined a label, but now we also need the data backing it ; There are basically two options: IF _NARG == 2 ; RAM definition, no data ds STRUCT_FIELD_SIZE ELSE DATA_TYPE equs STRCAT("D", {{STRUCT_FIELD_TYPE}}) REPT STRUCT_FIELD_NBEL DATA_TYPE \3 SHIFT ENDR PURGE DATA_TYPE ENDC ; Clean up vars for next iteration PURGE STRUCT_FIELD PURGE STRUCT_FIELD_NAME PURGE STRUCT_FIELD_TYPE PURGE STRUCT_FIELD_NBEL PURGE STRUCT_FIELD_SIZE PURGE FIELD_NAME FIELD_ID = FIELD_ID + 1 ENDR ; Clean up PURGE FIELD_ID ; Make sure to keep what's here in sync with cleanup at the end of a named invocation PURGE INSTANCE_NAME PURGE STRUCT_NAME PURGE IS_NAMED_INVOCATION PURGE NB_FIELDS ENDC ENDM ; dstructs nb_structs, struct_type, INSTANCE_NAME ; Allocates space for an array of structs in memory ; Each struct will have the index appended to its name **as hex** ; (for example: `dstructs 32, NPC, wNPC` will define wNPC0, wNPC1, and so on until wNPC1F) ; This is a limitation because RGBASM does not provide an easy way to get the decimal representation of a number ; Does not support data declarations because I think each struct should be defined individually for that purpose dstructs: MACRO STRUCT_ID = 0 REPT \1 dstruct \2, \3{X:STRUCT_ID} STRUCT_ID = STRUCT_ID + 1 ENDR PURGE STRUCT_ID ENDM