diff --git a/Makefile b/Makefile index 005560e64..8b09cbf63 100644 --- a/Makefile +++ b/Makefile @@ -49,12 +49,12 @@ RGBLINK ?= $(RGBDS)rgblink ### Build targets .SUFFIXES: -.PHONY: all crystal crystal11 crystal_au crystal_debug crystal11_debug clean tidy compare tools +.PHONY: all crystal crystal11 crystal_au crystal_debug crystal11_debug clean tidy compare tools text .SECONDEXPANSION: .PRECIOUS: .SECONDARY: -all: crystal +all: text crystal crystal_debug crystal: pokecrystal.gbc crystal11: pokecrystal11.gbc crystal_au: pokecrystal_au.gbc @@ -101,6 +101,9 @@ compare: $(roms) $(patches) tools: $(MAKE) -C tools/ +text: + python3 tools/text.py + RGBASMFLAGS = -Q8 -P includes.asm -Weverything -Wnumeric-string=2 -Wtruncation=1 # Create a sym/map for debug purposes if `make` run with `DEBUG=1` diff --git a/README.md b/README.md index 49a6f78f3..a6d897553 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ In pokecrystal-board **you will find**: In pokecrystal-board **you will *not* find**: - A ready-to-play game -- An engine that requires less ASM knowledge to use than the pokecrystal disassembly +- An engine that requires less ASM knowledge than the pokecrystal disassembly to develop features that don't yet exist - Guaranteed compatibility with extensions to pokecrystal developed by the community - Definitive GFX and SFX assets for the pokecrystal-board features diff --git a/docs/develop/ram_addresses.md b/docs/develop/ram_addresses.md index 92fb2e157..dfb3d3a80 100755 --- a/docs/develop/ram_addresses.md +++ b/docs/develop/ram_addresses.md @@ -25,7 +25,7 @@ - **wCurLevel**: initialized in LevelSelectionMenu (where it is also used), and stays static during the level. - **wDefaultLevelSelectionMenuLandmark**: used to know in which landmark to place the player when entering level selection menu. - **wLevelSelectionMenuEntryEventQueue**: which events have to be triggered the next time the player enters the level selection menu. - - **wLastUnlockedLevelsCount**, **wLastUnlockedLevels**: temporary list of unlocked levels during post-level screen + - **wLastUnlockedLevelsCount**, **wLastUnlockedLevels**, **wLastClearedLevelStage**: temporary list of unlocked and cleared levels during post-level screen - These addresses share memory region with string buffers from *wStringBuffer3* onwards. They are placed in memory in the following order. - **wTempSpaceStruct**: Temporary scope. Same structure as *wCurSpaceStruct* diff --git a/maps/DebugLevel5_Map1.asm b/maps/DebugLevel5_Map1.asm index 9835d287e..538f18ec7 100755 --- a/maps/DebugLevel5_Map1.asm +++ b/maps/DebugLevel5_Map1.asm @@ -59,14 +59,20 @@ DebugLevel5_Map1_MapEvents: done .YoungsterMikeyAfterText: - text "Becoming a good" +;>text +; You're a #MON trainer, right? +; Becoming a good trainer is really tough. +; I'm going to battle other people to get better. + text "You're a #MON" + line "trainer, right?" + para "Becoming a good" line "trainer is really" cont "tough." - - para "I'm going to bat-" - line "tle other people" - cont "to get better." + para "I'm going to battle" + line "other people to" + cont "get better." done +;text and ;text line is replaced by ;>text, signaling that this block requires no processing in a subsequent pass. +If the user wishes to request the formatting of the same block again after a modification, +they shall do it by replacing the opening ;>text with ;!>text and deleting the old converted text in the middle. +In the plain text, paragraphs shall use tab identation and be separated by line breaks. +;!>text shall be followed by prompt to finish the resulting text with the "prompt" text command. Otherwise it will be finished with "done" by default. + +For example the following block: + +;!>text +; You're a #MON trainer, right? +; Becoming a good trainer is really tough. +; I'm going to battle other people to get better. +;text +; You're a #MON trainer, right? +; Becoming a good trainer is really tough. +; I'm going to battle other people to get better. + text "You're a #MON" + line "trainer, right?" + para "Becoming a good" + line "trainer is really" + cont "tough." + para "I'm going to battle" + line "other people to" + cont "get better." + done +;": 7, +} + +def process_word(word): + """ + Return the length of a given word accounting for pokecrystal charmap and count an additional space character after the word. + """ + length = len(word) + for char, charlen in CHARMAP_LENGTH.items(): + length += (charlen - len(char)) * word.count(char) + if length > MAX_LINE_LENGTH: + exc = f"Found a word too long to split (above {MAX_LINE_LENGTH} characters)." + raise Exception(exc) + return length + 1 + +def process_paragraph(line): + """ + Split a paragraph provided in a single plain text line into multiple pokecrystal-sized lines. + """ + words = line.split() + word_pos = 0 + char_pos = 0 + word_breaks = [] + line_partitioned = [] + while word_pos < len(words): + char_pos += process_word(words[word_pos]) + if char_pos > (MAX_LINE_LENGTH + 1): + word_breaks.append(word_pos) + char_pos = 0 + continue + word_pos += 1 + start_word = 0 + for word_break in word_breaks: + line_partitioned.append(' '.join(words[start_word:word_break])) + start_word = word_break + line_partitioned.append(' '.join(words[start_word:])) + return line_partitioned + +def format_paragraph_lines(paragraph, is_first_paragraph): + """ + For each pokecrystal text line, prepend to it the corresponding pokecrystal text command and append to it \n + """ + line_length = len(paragraph) + cur_line_no = 1 + formatted_paragraph = "" + for line in paragraph: + formatted_line = "" + if cur_line_no == 1 and is_first_paragraph: + formatted_line += "\ttext " + elif cur_line_no % 2 and cur_line_no == line_length and line_length > 1: + formatted_line += "\tcont " + elif cur_line_no % 2: + formatted_line += "\tpara " + else: + formatted_line += "\tline " + formatted_line = formatted_line + '"' + line + '"' + "\n" + cur_line_no += 1 + formatted_paragraph += formatted_line + return formatted_paragraph + +def process_block(text_block, end_command): + is_first_paragraph = True + resulting_paragraph = "" + for paragraph in text_block: + if paragraph.startswith(TEXT_BLOCK_START): + continue + if paragraph.startswith(TEXT_BLOCK_END): + continue + paragraph = paragraph[2:-1] + # split one plain text line (a paragraph) into multiple pokecrystal text lines + paragraph = process_paragraph(paragraph) + # format the lines with pokecrystal text commands + resulting_paragraph += format_paragraph_lines(paragraph, is_first_paragraph) + is_first_paragraph = False + text_block = resulting_paragraph + "\t" + end_command + "\n" + return text_block + +def write_text_to_file(f, lines, text_block, line_no, text_start_index): + lines[text_start_index] = PROCESSED_TEXT_BLOCK_START + "\n" # Signal that this text was processed + lines.insert(line_no, text_block) + f.seek(0) + f.writelines(lines) + f.truncate() + +def main(): + filepaths = [] + for directory in TEXT_DIRS: + filepaths.extend([os.path.join(directory, filename) for filename in os.listdir(directory) if filename.endswith(".asm")]) + for filepath in filepaths: + with open(filepath, "r+") as f: + lines = f.readlines() + cur_file_index = -1 + text_start_index = -1 + for line in lines: + cur_file_index += 1 + if TEXT_BLOCK_START in line: + index = cur_file_index + text_start_index = index + text_block = [line] + while True: + index += 1 + next_line = lines[index] + text_block.append(next_line) + if next_line.startswith(TEXT_BLOCK_END): + end_command = "prompt" if " prompt" in next_line else "done" + break + if next_line.startswith("; ") and next_line.endswith("\n"): + continue + else: + exc = f"Invalid text block in file {filepath}, line {cur_file_index}. Make sure there is not already valid generated text in the middle. Aborting." + raise Exception(exc) + text_block = process_block(text_block, end_command) + write_text_to_file(f, lines, text_block, index, text_start_index) + +if __name__ == '__main__': + main()