diff --git a/.gitignore b/.gitignore index d3d1e44e..8811a07a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,5 @@ app.*.map.json /android/app/release .vscode -extractedFonts -assets/bins/* \ No newline at end of file +assets/bins +assets/mcdFonts \ No newline at end of file diff --git a/lib/fileTypeUtils/dat/datHashGenerator.dart b/lib/fileTypeUtils/dat/datHashGenerator.dart index 7cd4ea3c..4800f170 100644 --- a/lib/fileTypeUtils/dat/datHashGenerator.dart +++ b/lib/fileTypeUtils/dat/datHashGenerator.dart @@ -38,15 +38,27 @@ class HashInfo { List> namesIndicesHashes = []; for (int i = 0; i < filenames.length; i++) - namesIndicesHashes.add([filenames[i], i, (crc32(filenames[i].toLowerCase()) & 0x7fffffff)]); - - namesIndicesHashes.sort((a, b) => a[2] >> preHashShift); + namesIndicesHashes.add([ + filenames[i], + i, + (crc32(filenames[i].toLowerCase()) & 0x7fffffff) + ]); + + namesIndicesHashes.sort((a, b) { + int kA = a[2] >> preHashShift; + int kB = b[2] >> preHashShift; + return kA.compareTo(kB); + }); hashes = namesIndicesHashes .map((e) => e[2] as int) .toList(); - hashes.sort((a, b) => a >> preHashShift); + hashes.sort((a, b) { + int kA = a >> preHashShift; + int kB = b >> preHashShift; + return kA.compareTo(kB); + }); for (int i = 0; i < namesIndicesHashes.length; i++) { if (bucketOffsets[namesIndicesHashes[i][2] >> preHashShift] == -1) diff --git a/lib/fileTypeUtils/mcd/mcdReader.dart b/lib/fileTypeUtils/mcd/mcdReader.dart index 9530de19..9eb343d3 100644 --- a/lib/fileTypeUtils/mcd/mcdReader.dart +++ b/lib/fileTypeUtils/mcd/mcdReader.dart @@ -1,24 +1,10 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; + import '../utils/ByteDataWrapper.dart'; -/* -struct Header -{ - uint32 messagesOffset; - uint32 messagesCount; - uint32 symbolsOffset; - uint32 symbolsCount; - uint32 glyphsOffset; - uint32 glyphsCount; - uint32 fontsOffset; - uint32 fontsCount; - uint32 eventsOffset; - uint32 eventsCount; - -} header; -*/ class McdFileHeader { final int messagesOffset; final int messagesCount; @@ -46,15 +32,21 @@ class McdFileHeader { fontsCount = bytes.readUint32(), eventsOffset = bytes.readUint32(), eventsCount = bytes.readUint32(); + + void write(ByteDataWrapper bytes) { + bytes.writeUint32(messagesOffset); + bytes.writeUint32(messagesCount); + bytes.writeUint32(symbolsOffset); + bytes.writeUint32(symbolsCount); + bytes.writeUint32(glyphsOffset); + bytes.writeUint32(glyphsCount); + bytes.writeUint32(fontsOffset); + bytes.writeUint32(fontsCount); + bytes.writeUint32(eventsOffset); + bytes.writeUint32(eventsCount); + } } -/* -struct Symbol { - uint16 fontId; - wchar_t code; - uint32 glyphId; -} -*/ class McdFileSymbol { final int fontId; final int charCode; @@ -64,9 +56,7 @@ class McdFileSymbol { late final McdFileFont _font; late final McdFileGlyph _glyph; - McdFileSymbol(this.fontId, this.charCode, this.glyphId, McdFile file) : - _font = file.fonts[fontId], - _glyph = file.glyphs[glyphId], + McdFileSymbol(this.fontId, this.charCode, this.glyphId, this._font, this._glyph) : char = String.fromCharCode(charCode); McdFileSymbol.read(ByteDataWrapper bytes, List fonts, List glyphs) : @@ -83,22 +73,14 @@ class McdFileSymbol { // print("Warning: Glyph kerning (${_glyph.below}, ${_glyph.horizontal}) does not match font kerning (${_font.below}, ${_font.horizontal})"); // } } -} -/* -struct Glyph { - uint32 textureId; - float u1; - float v1; - float u2; - float v2; - float width; - float height; - float above; - float below; - float horizontal; + void write(ByteDataWrapper bytes) { + bytes.writeUint16(fontId); + bytes.writeUint16(charCode); + bytes.writeUint32(glyphId); + } } -*/ + class McdFileGlyph { final int textureId; final double u1; @@ -125,17 +107,21 @@ class McdFileGlyph { above = bytes.readFloat32(), below = bytes.readFloat32(), horizontal = bytes.readFloat32(); + + void write(ByteDataWrapper bytes) { + bytes.writeUint32(textureId); + bytes.writeFloat32(u1); + bytes.writeFloat32(v1); + bytes.writeFloat32(u2); + bytes.writeFloat32(v2); + bytes.writeFloat32(width); + bytes.writeFloat32(height); + bytes.writeFloat32(above); + bytes.writeFloat32(below); + bytes.writeFloat32(horizontal); + } } -/* -struct Font { - uint32 id; - float width; - float height; - float below; - float horizontal; -} -*/ class McdFileFont { final int id; final double width; @@ -151,25 +137,34 @@ class McdFileFont { height = bytes.readFloat32(), below = bytes.readFloat32(), horizontal = bytes.readFloat32(); + + void write(ByteDataWrapper bytes) { + bytes.writeUint32(id); + bytes.writeFloat32(width); + bytes.writeFloat32(height); + bytes.writeFloat32(below); + bytes.writeFloat32(horizontal); + } } -/* -struct Letter { - uint16 code; - short positionOffset; -}; -*/ -class McdFileLetter { - final int code; - final int idx; +class McdFileLetterBase { + late final int code; + + int get byteSize => 2; +} +class McdFileLetter extends McdFileLetterBase { + late final int idx; final List _symbols; - McdFileLetter(this.code, this.idx, this._symbols); + McdFileLetter(int code, this.idx, this._symbols) { + super.code = code; + } - McdFileLetter.read(ByteDataWrapper bytes, this._symbols) : - code = bytes.readUint16(), + McdFileLetter.read(ByteDataWrapper bytes, this._symbols) { + code = bytes.readUint16(); idx = bytes.readInt16(); + } @override String toString() { @@ -189,27 +184,23 @@ class McdFileLetter { print(""); return ""; } -} -/* -struct Line { - uint32 lettersOffset; - uint32 padding; - uint32 lettersCount; - uint32 length2; - float below; - float horizontal; - - local int pos = FTell(); - FSeek(lettersOffset); + @override + int get byteSize => 4; - Letter letters[lettersCount]; + void write(ByteDataWrapper bytes) { + bytes.writeUint16(code); + bytes.writeInt16(idx); + } +} +class McdFileLetterTerminator extends McdFileLetterBase { + McdFileLetterTerminator() { + super.code = 0x8000; + } +} - FSeek(pos); -}; -*/ class McdFileLine { - final int lettersOffset; + int lettersOffset; final int padding; final int lettersCount; final int length2; @@ -219,7 +210,7 @@ class McdFileLine { late final int terminator; McdFileLine(this.lettersOffset, this.padding, this.lettersCount, this.length2, - this.below, this.horizontal, this.letters); + this.below, this.horizontal, this.letters, this.terminator); McdFileLine.read(ByteDataWrapper bytes, List symbols) : lettersOffset = bytes.readUint32(), @@ -243,26 +234,26 @@ class McdFileLine { String toString() { return letters.join(); } -} - -/* -struct Paragraph { - uint32 linesOffset; - uint32 linesCount; - uint32 vPos; - uint32 hPos; - uint32 fontId; - local int pos = FTell(); - FSeek(linesOffset); + void write(ByteDataWrapper bytes) { + bytes.writeUint32(lettersOffset); + bytes.writeUint32(padding); + bytes.writeUint32(lettersCount); + bytes.writeUint32(length2); + bytes.writeFloat32(below); + bytes.writeFloat32(horizontal); - Line lines[linesCount] ; + var pos = bytes.position; + bytes.position = lettersOffset; + for (var letter in letters) + letter.write(bytes); + bytes.writeUint16(terminator); + bytes.position = pos; + } +} - FSeek(pos); -}; -*/ class McdFileParagraph { - final int linesOffset; + int linesOffset; final int linesCount; final int vPos; final int hPos; @@ -291,25 +282,24 @@ class McdFileParagraph { String toString() { return lines.join("\n"); } -} -/* -struct Message { - uint32 paragraphsOffset; - uint32 paragraphsCount; - uint32 seqNumber; - uint32 eventId; - - local int pos = FTell(); - FSeek(paragraphsOffset); - - Paragraph p[paragraphsCount] ;; + void write(ByteDataWrapper bytes) { + bytes.writeUint32(linesOffset); + bytes.writeUint32(linesCount); + bytes.writeUint32(vPos); + bytes.writeUint32(hPos); + bytes.writeUint32(fontId); + + var pos = bytes.position; + bytes.position = linesOffset; + for (var line in lines) + line.write(bytes); + bytes.position = pos; + } +} - FSeek(pos); -}; -*/ class McdFileMessage { - final int paragraphsOffset; + int paragraphsOffset; final int paragraphsCount; final int seqNumber; final int eventId; @@ -336,22 +326,21 @@ class McdFileMessage { String toString() { return paragraphs.join("\n\n"); } -} -/* -struct Event { - uint32 id; - uint32 msgId; - char name[32]; + void write(ByteDataWrapper bytes) { + bytes.writeUint32(paragraphsOffset); + bytes.writeUint32(paragraphsCount); + bytes.writeUint32(seqNumber); + bytes.writeUint32(eventId); + + var pos = bytes.position; + bytes.position = paragraphsOffset; + for (var paragraph in paragraphs) + paragraph.write(bytes); + bytes.position = pos; + } +} - local int pos = FTell(); - FSeek(header.messagesOffset + msgId * 4 * 4); - - Message m; - - FSeek(pos); -}; -*/ class McdFileEvent { final int id; final int msgId; @@ -378,6 +367,13 @@ class McdFileEvent { "$msgLines\n" "}"; } + + void write(ByteDataWrapper bytes) { + bytes.writeUint32(id); + bytes.writeUint32(msgId); + var fullName = name.padRight(32, "\x00"); + bytes.writeString(fullName); + } } class McdFile { @@ -429,8 +425,39 @@ class McdFile { } } + McdFile.fromParts(this.header, this.messages, this.symbols, this.glyphs, this.fonts, + this.events); + static Future fromFile(String path) async { final bytes = await File(path).readAsBytes(); return McdFile.read(ByteDataWrapper(bytes.buffer)); } + + Future writeToFile(String path) async { + var fileSize = header.eventsOffset + events.length * 0x28; + var bytes = ByteDataWrapper(ByteData(fileSize).buffer); + + header.write(bytes); + bytes.position = header.messagesOffset; + for (var message in messages) + message.write(bytes); + + bytes.position = header.symbolsOffset; + for (var symbol in symbols) + symbol.write(bytes); + + bytes.position = header.glyphsOffset; + for (var glyph in glyphs) + glyph.write(bytes); + + bytes.position = header.fontsOffset; + for (var font in fonts) + font.write(bytes); + + bytes.position = header.eventsOffset; + for (var event in events) + event.write(bytes); + + await File(path).writeAsBytes(bytes.buffer.asUint8List()); + } } diff --git a/lib/stateManagement/openFileTypes.dart b/lib/stateManagement/openFileTypes.dart index 977071d3..991c210e 100644 --- a/lib/stateManagement/openFileTypes.dart +++ b/lib/stateManagement/openFileTypes.dart @@ -376,15 +376,18 @@ class McdFileData extends OpenFileData { return; _loadingState = LoadingState.loading; - mcdData = await McdData.fromMcdFile(path); + mcdData = await McdData.fromMcdFile(this, path); await super.load(); } - // @override - // Future save() async { - // await super.save(); - // } + @override + Future save() async { + await mcdData?.save(); + var datDir = dirname(path); + changedDatFiles.add(datDir); + await super.save(); + } @override void dispose() { diff --git a/lib/stateManagement/otherFileTypes/McdData.dart b/lib/stateManagement/otherFileTypes/McdData.dart index 66f05a36..9795c476 100644 --- a/lib/stateManagement/otherFileTypes/McdData.dart +++ b/lib/stateManagement/otherFileTypes/McdData.dart @@ -5,11 +5,30 @@ import 'dart:math'; import 'package:path/path.dart'; +import '../../fileTypeUtils/dat/datExtractor.dart'; import '../../fileTypeUtils/mcd/mcdReader.dart'; +import '../../fileTypeUtils/wta/wtaReader.dart'; +import '../../utils/assetDirFinder.dart'; import '../../utils/utils.dart'; import '../Property.dart'; import '../hasUuid.dart'; import '../nestedNotifier.dart'; +import '../openFileTypes.dart'; + +abstract class _McdFilePart { + McdFileData? file; + + _McdFilePart(this.file); + + void onDataChanged() { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + file?.contentNotifier.notifyListeners(); + file?.hasUnsavedChanges = true; + } + + void dispose() { + } +} class McdFontSymbol { final int code; @@ -18,18 +37,20 @@ class McdFontSymbol { final int y; final int width; final int height; + final int fontId; - McdFontSymbol(this.code, this.char, this.x, this.y, this.width, this.height); + McdFontSymbol(this.code, this.char, this.x, this.y, this.width, this.height, this.fontId); } class UsedFontSymbol { final int code; final String char; final int fontId; + final McdFontSymbol? fontSymbol; - UsedFontSymbol(this.code, this.char, [this.fontId = -1]); + UsedFontSymbol(this.code, this.char, this.fontId, this.fontSymbol); - UsedFontSymbol.withFont(UsedFontSymbol other, this.fontId) : + UsedFontSymbol.withFont(UsedFontSymbol other, this.fontId, this.fontSymbol) : code = other.code, char = other.char; @@ -39,8 +60,11 @@ class UsedFontSymbol { code == other.code && fontId == other.fontId; - @override - int get hashCode => Object.hash(code, fontId); + @override + int get hashCode => Object.hash(code, fontId); + + @override + String toString() => 'Sym($char, fontId: $fontId)'; } class McdFont { @@ -48,7 +72,7 @@ class McdFont { late final int fontWidth; late final int fontHeight; late final int fontBelow; - late final List supportedSymbols; + late final Map supportedSymbols; McdFont(this.fontId, this.fontWidth, this.fontHeight, this.fontBelow, this.supportedSymbols); } @@ -59,18 +83,22 @@ class McdGlobalFont extends McdFont { McdGlobalFont(this.atlasInfoPath, this.atlasTexturePath, super.fontId, super.fontWidth, super.fontHeight, super.fontBelow, super.supportedSymbols); - static Future fromInfoFile(String atlasInfoPath, String atlasTexturePath) async { + static Future fromInfoFile(String atlasInfoPath, String atlasTexturePath) async { var infoJson = jsonDecode(await File(atlasInfoPath).readAsString()); var fontId = infoJson["id"]; var fontWidth = infoJson["fontWidth"]; var fontHeight = infoJson["fontHeight"]; var fontBelow = infoJson["fontBelow"]; - var supportedSymbols = (infoJson["supportedSymbols"] as List) + var supportedSymbols = (infoJson["symbols"] as List) .map((e) => McdFontSymbol( - e["code"], e["char"], e["x"], e["y"], e["width"], e["height"]) - ) - .toList(); - return McdGlobalFont(atlasInfoPath, atlasTexturePath, fontId, fontWidth, fontHeight, fontBelow, supportedSymbols); + e["code"], e["char"], + e["x"].toInt(), e["y"].toInt(), + e["width"], e["height"], + fontId + )) + .map((e) => MapEntry(e.code, e)); + var supportedSymbolsMap = Map.fromEntries(supportedSymbols); + return McdGlobalFont(atlasInfoPath, atlasTexturePath, fontId, fontWidth, fontHeight, fontBelow, supportedSymbolsMap); } } @@ -92,53 +120,107 @@ class McdLocalFont extends McdFont { textWidth = (pixWidth / uvWidth).round(); textHeight = (pixHeight / uvHeight).round(); } - var supportedSymbols = List.generate(symbols.length, (i) => McdFontSymbol( + var supportedSymbolsList = List.generate(symbols.length, (i) => McdFontSymbol( symbols[i].charCode, symbols[i].char, (glyphs[i].u1 * textWidth).round(), (glyphs[i].v1 * textHeight).round(), glyphs[i].width.toInt(), glyphs[i].height.toInt(), + fontId )); - return McdLocalFont(fontId, textWidth, textHeight, font.below.toInt(), supportedSymbols); + var supportedSymbols = Map + .fromEntries(supportedSymbolsList.map((e) => MapEntry(e.code, e))); + return McdLocalFont(fontId, font.width.toInt(), font.height.toInt(), font.below.toInt(), supportedSymbols); } + + McdLocalFont.zero() : super(0, 0, 4, -6, {}); } -class McdLine with HasUuid { +class McdLine extends _McdFilePart with HasUuid { StringProp text; - McdLine(this.text); + McdLine(super.file, this.text) { + text.addListener(onDataChanged); + } - McdLine.fromMcd(McdFileLine mcdLine) : text = StringProp(mcdLine.toString()); + McdLine.fromMcd(super.file, McdFileLine mcdLine) + : text = StringProp(mcdLine.toString()) { + text.addListener(onDataChanged); + } + @override void dispose() { text.dispose(); + super.dispose(); } - Set getUsedSymbolCodes() { + Set getUsedSymbolCodes(Map fontSymbols) { var charMatcher = RegExp(r"<[^>]+>|[^ ≡]"); var symbols = charMatcher.allMatches(text.value) .map((m) => m.group(0)!) - .map((c) => UsedFontSymbol(c != "…" ? c.codeUnitAt(0) : 0x80, c)) + .map((c) { + var code = c != "…" ? c.codeUnitAt(0) : 0x80; + var sym = fontSymbols[code]; + return UsedFontSymbol(code, c, -1, sym); + }) .toSet(); return symbols; } + + List toLetters(int fontId, List symbols) { + var str = text.value; + List letters = []; + McdFileSymbol prevSymbol = symbols.first; + for (var i = 0; i < str.length; i++) { + var char = str[i]; + if (char == " ") + letters.add(McdFileLetter(0x8001, prevSymbol.fontId, const [])); + else if (char == "…") + letters.add(McdFileLetter(0x80, 0, const [])); + else if (char == "≡") + letters.add(McdFileLetter(0x8020, 9, const [])); + else if (char == "<" && str.substring(i, i + 5) == "") + letters.add(McdFileLetter(0x8020, 121, const [])); + else if (char == "<" && str.substring(i, i + 11) == " s.charCode == charCode && s.fontId == fontId); + if (symbolIndex == -1) + throw Exception("Unknown char: $char"); + letters.add(McdFileLetter(symbolIndex, 0, symbols)); + prevSymbol = symbols[symbolIndex]; + } + } + return letters; + } } -class McdParagraph with HasUuid { +class McdParagraph extends _McdFilePart with HasUuid { NumberProp vPos; McdFont font; ValueNestedNotifier lines; - McdParagraph(this.vPos, this.font, this.lines); + McdParagraph(super.file, this.vPos, this.font, this.lines) { + vPos.addListener(onDataChanged); + lines.addListener(onDataChanged); + } - McdParagraph.fromMcd(McdFileParagraph paragraph, List fonts) : + McdParagraph.fromMcd(super.file, McdFileParagraph paragraph, List fonts) : vPos = NumberProp(paragraph.vPos, true), font = fonts.firstWhere((f) => f.fontId == paragraph.fontId), - lines = ValueNestedNotifier(paragraph.lines.map((l) => McdLine.fromMcd(l)).toList()); + lines = ValueNestedNotifier( + paragraph.lines + .map((l) => McdLine.fromMcd(file, l)) + .toList() + ) { + vPos.addListener(onDataChanged); + lines.addListener(onDataChanged); + } void addLine() { - lines.add(McdLine(StringProp(""))); + lines.add(McdLine(file, StringProp(""))); } void removeLine(int index) { @@ -146,45 +228,58 @@ class McdParagraph with HasUuid { .dispose(); } + @override void dispose() { vPos.dispose(); for (var line in lines) line.dispose(); lines.dispose(); + super.dispose(); } Set getUsedSymbols() { Set symbols = {}; for (var line in lines) { var lineSymbols = line - .getUsedSymbolCodes() - .map((c) => UsedFontSymbol.withFont(c, font.fontId)); + .getUsedSymbolCodes(font.supportedSymbols) + .map((c) => UsedFontSymbol.withFont(c, font.fontId, c.fontSymbol)); symbols.addAll(lineSymbols); } return symbols; } } -class McdEvent with HasUuid { +class McdEvent extends _McdFilePart with HasUuid { HexProp eventId; StringProp name; NumberProp msgSeqNum; ValueNestedNotifier paragraphs; - McdEvent(this.eventId, this.name, this.msgSeqNum, this.paragraphs); + McdEvent(super.file, this.eventId, this.name, this.msgSeqNum, this.paragraphs) { + eventId.addListener(onDataChanged); + name.addListener(onDataChanged); + msgSeqNum.addListener(onDataChanged); + paragraphs.addListener(onDataChanged); + } - McdEvent.fromMcd(McdFileEvent event, List fonts) : + McdEvent.fromMcd(super.file, McdFileEvent event, List fonts) : eventId = HexProp(event.id), name = StringProp(event.name), msgSeqNum = NumberProp(event.message.seqNumber, true), paragraphs = ValueNestedNotifier( event.message.paragraphs - .map((p) => McdParagraph.fromMcd(p, fonts)) + .map((p) => McdParagraph.fromMcd(file, p, fonts)) .toList() - ); + ) { + eventId.addListener(onDataChanged); + name.addListener(onDataChanged); + msgSeqNum.addListener(onDataChanged); + paragraphs.addListener(onDataChanged); + } void addParagraph(McdLocalFont font) { paragraphs.add(McdParagraph( + file, NumberProp(0, true), font, ValueNestedNotifier([]) @@ -196,6 +291,7 @@ class McdEvent with HasUuid { .dispose(); } + @override void dispose() { eventId.dispose(); name.dispose(); @@ -203,6 +299,7 @@ class McdEvent with HasUuid { for (var paragraph in paragraphs) paragraph.dispose(); paragraphs.dispose(); + super.dispose(); } Set getUsedSymbols() { @@ -213,7 +310,7 @@ class McdEvent with HasUuid { } } -class McdData { +class McdData extends _McdFilePart { static ValueNestedNotifier availableFonts = ValueNestedNotifier([]); final StringProp? textureWtaPath; @@ -221,7 +318,10 @@ class McdData { ValueNestedNotifier usedFonts; ValueNestedNotifier events; - McdData(this.textureWtaPath, this.textureWtpPath, this.usedFonts, this.events); + McdData(super.file, this.textureWtaPath, this.textureWtpPath, this.usedFonts, this.events) { + usedFonts.addListener(onDataChanged); + events.addListener(onDataChanged); + } static Future searchTexFile(String initDir, String mcdName, String ext) async { String? texPath = join(initDir, mcdName + ext); @@ -229,6 +329,10 @@ class McdData { if (initDir.endsWith(".dat") && ext == ".wtp") { var dttDir = "${initDir.substring(0, initDir.length - 4)}.dtt"; texPath = join(dttDir, mcdName + ext); + if (await File(texPath).exists()) + return texPath; + var dttPath = join(dirname(dirname(dttDir)), basename(dttDir)); + await extractDatFiles(dttPath); if (await File(texPath).exists()) return texPath; texPath = join(initDir, mcdName + ext); @@ -240,7 +344,7 @@ class McdData { return null; } - static Future fromMcdFile(String mcdPath) async { + static Future fromMcdFile(McdFileData? file, String mcdPath) async { var datDir = dirname(mcdPath); var mcdName = basenameWithoutExtension(mcdPath); String? wtpPath = await searchTexFile(datDir, mcdName, ".wtp"); @@ -252,9 +356,10 @@ class McdData { mcd.fonts.map((f) => McdLocalFont.fromMcdFile(mcd, f.id)) ); - var events = mcd.events.map((e) => McdEvent.fromMcd(e, usedFonts)).toList(); + var events = mcd.events.map((e) => McdEvent.fromMcd(file, e, usedFonts)).toList(); return McdData( + file, wtaPath != null ? StringProp(wtaPath) : null, wtpPath != null ? StringProp(wtpPath) : null, ValueNestedNotifier(usedFonts), @@ -262,6 +367,7 @@ class McdData { ); } + @override void dispose() { textureWtaPath?.dispose(); textureWtpPath?.dispose(); @@ -269,10 +375,30 @@ class McdData { for (var event in events) event.dispose(); events.dispose(); + super.dispose(); + } + + Future loadAvailableFonts() async { + if (!await hasMcdFonts()) { + showToast("No MCD font assets found"); + return; + } + var fontsAssetsDir = join(assetsDir!, "mcdFonts"); + for (var fontId in fontIds) { + var fontDir = join(fontsAssetsDir, fontId); + var atlasInfoPath = join(fontDir, "_atlas.json"); + var atlasTexturePath = join(fontDir, "_atlas.png"); + var font = await McdGlobalFont.fromInfoFile( + atlasInfoPath, + atlasTexturePath + ); + availableFonts.add(font); + } } void addEvent() { events.add(McdEvent( + file, HexProp(events.isNotEmpty ? events.last.eventId.value + 1 : randomId()), StringProp("NEW_EVENT_NAME"), NumberProp(getLastSeqNum() + 1, true), @@ -291,6 +417,189 @@ class McdData { .reduce(max); } + Future save() async { + if (availableFonts.isEmpty) { + await loadAvailableFonts(); + if (availableFonts.isEmpty) { + showToast("No MCD font assets found"); + return; + } + } + if (textureWtaPath == null || textureWtpPath == null) { + showToast("No wta or wtp files found"); + return; + } + if (getLocalFontUnsupportedSymbols().isNotEmpty) { + showToast("Some symbols are not supported"); + print("Unsupported symbols: ${getLocalFontUnsupportedSymbols()}"); + return; + } + + var usedSymbols = getUsedSymbols().toList(); + usedSymbols.sort((a, b) { + var fontCmp = a.fontId.compareTo(b.fontId); + if (fontCmp != 0) + return fontCmp; + return a.code.compareTo(b.code); + }); + + // get export fonts + List usedFontIds = [0]; + for (var sym in usedSymbols) { + if (!usedFontIds.contains(sym.fontId)) + usedFontIds.add(sym.fontId); + } + var exportFonts = usedFontIds + .map((id) { + if (id == 0) + return McdLocalFont.zero(); + return usedFonts.firstWhere((f) => f.fontId == id); + }) + .map((f) => McdFileFont( + f.fontId, + f.fontWidth.toDouble(), f.fontHeight.toDouble(), + f.fontBelow.toDouble(), 0) + ) + .toList(); + var exportFontMap = { for (var f in exportFonts) f.id: f }; + + // glyphs + var wta = await WtaFile.readFromFile(textureWtaPath!.value); + var texId = wta.textureIdx.first; + var texSize = await getDdsFileSize(textureWtpPath!.value); + var exportGlyphs = List.generate(usedSymbols.length, (i) { + var sym = usedSymbols[i].fontSymbol; + if (sym == null) { + print("Symbol ${usedSymbols[i].code} not found"); + showToast("Symbol ${usedSymbols[i].code} not found"); + throw Exception("Symbol ${usedSymbols[i].code} not found"); + } + return McdFileGlyph( + texId, + sym.x / texSize.width, sym.y / texSize.height, + (sym.x + sym.width) / texSize.width, (sym.y + sym.height) / texSize.height, + sym.width.toDouble(), sym.height.toDouble(), + 0, exportFontMap[sym.fontId]!.below, 0 + ); + }); + + // symbols + var exportSymbols = List.generate(usedSymbols.length, (i) { + var sym = usedSymbols[i]; + return McdFileSymbol( + sym.fontId, + sym.code, + i, + exportFontMap[sym.fontId]!, + exportGlyphs[i] + ); + }); + + List exportEvents = []; + List exportMessages = []; + List exportParagraphs = []; + List exportLines = []; + List exportLetters = []; + + // messages and events + var sortedEvents = events.toList(); + sortedEvents.sort((a, b) => a.msgSeqNum.value.compareTo(b.msgSeqNum.value)); + for (int i = 0; i < sortedEvents.length; i++) { + var event = sortedEvents[i]; + var eventMsg = McdFileMessage( + -1, event.paragraphs.length, + event.msgSeqNum.value.toInt(), event.eventId.value, + [], + ); + exportMessages.add(eventMsg); + + var exportEvent = McdFileEvent( + event.eventId.value, i, + event.name.value, + eventMsg + ); + exportEvents.add(exportEvent); + } + + // paragraphs, lines, letters + for (int i = 0; i < exportMessages.length; i++) { + for (var paragraph in sortedEvents[i].paragraphs) { + List paragraphLines = []; + for (var line in paragraph.lines) { + var lineLetters = line.toLetters(paragraph.font.fontId, exportSymbols); + exportLetters.addAll(lineLetters); + var parLine = McdFileLine( + -1, 0, lineLetters.length * 2 + 1, lineLetters.length * 2 + 1, + paragraph.font.fontBelow.toDouble(), 0, lineLetters, + 0x8000 + ); + exportLetters.add(McdFileLetterTerminator()); + exportLines.add(parLine); + paragraphLines.add(parLine); + } + + var par = McdFileParagraph( + -1, paragraphLines.length, + paragraph.vPos.value.toInt(), 0, + paragraph.font.fontId, + paragraphLines + ); + exportParagraphs.add(par); + exportMessages[i].paragraphs.add(par); + } + } + + var lettersStart = 0x28; + var lettersEnd = lettersStart + exportLetters.fold(0, (sum, let) => sum + let.byteSize); + var afterLetterPadding = lettersEnd % 4 == 0 ? 4 : 2; + var msgStart = lettersEnd + afterLetterPadding; + var msgEnd = msgStart + exportMessages.length * 0x10; + var parStart = msgEnd + 4; + var parEnd = parStart + exportParagraphs.length * 0x14; + var lineStart = parEnd + 4; + var lineEnd = lineStart + exportLines.length * 0x18; + var symStart = lineEnd + 4; + var symEnd = symStart + exportSymbols.length * 0x8; + var glyphStart = symEnd + 4; + var glyphEnd = glyphStart + exportGlyphs.length * 0x28; + var fontStart = glyphEnd + 4; + var fontEnd = fontStart + exportFonts.length * 0x14; + var eventsStart = fontEnd + 4; + // var eventsEnd = eventsStart + exportEvents.length * 0x28; + + // update all offsets + var curLetterOffset = lettersStart; + var curParOffset = parStart; + var curLineOffset = lineStart; + for (var line in exportLines) { + line.lettersOffset = curLetterOffset; + var actualLetterCount = (line.lettersCount - 1) ~/ 2; + curLetterOffset += actualLetterCount * 0x4 + 2; + } + for (var msg in exportMessages) { + msg.paragraphsOffset = curParOffset; + curParOffset += msg.paragraphsCount * 0x14; + } + for (var par in exportParagraphs) { + par.linesOffset = curLineOffset; + curLineOffset += par.linesCount * 0x18; + } + var header = McdFileHeader( + msgStart, exportMessages.length, + symStart, exportSymbols.length, + glyphStart, exportGlyphs.length, + fontStart, exportFonts.length, + eventsStart, exportEvents.length + ); + + exportEvents.sort((a, b) => a.id.compareTo(b.id)); + + var mcdFile = McdFile.fromParts(header, exportMessages, exportSymbols, exportGlyphs, exportFonts, exportEvents); + await mcdFile.writeToFile(file!.path); + + print("Saved MCD file"); + } + Set getUsedSymbols() { Set symbols = {}; for (var event in events) @@ -304,6 +613,7 @@ class McdData { .where((usedSym) => !usedFonts .firstWhere((f) => f.fontId == usedSym.fontId) .supportedSymbols + .values .any((supSym) => supSym.code == usedSym.code)) .toSet(); } @@ -314,6 +624,7 @@ class McdData { .where((usedSym) => !availableFonts .firstWhere((f) => f.fontId == usedSym.fontId) .supportedSymbols + .values .any((supSym) => supSym.code == usedSym.code)) .toSet(); } diff --git a/lib/utils/assetDirFinder.dart b/lib/utils/assetDirFinder.dart index 64ad15d1..f8a21100 100644 --- a/lib/utils/assetDirFinder.dart +++ b/lib/utils/assetDirFinder.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; String? assetsDir; @@ -32,4 +33,66 @@ Future findAssetsDir() async { } print("Couldn't find assets dir"); return false; -} \ No newline at end of file +} + +bool _hasMrubyAssetsComplete = false; +bool _hasMrubyAssets = false; +Future hasMrubyAssets() async { + if (_hasMrubyAssetsComplete) + return _hasMrubyAssets; + await assetDirDone; + var mrubyDir = join(assetsDir!, "MrubyDecompiler"); + var mrubyFiles = await Directory(mrubyDir) + .list() + .map((f) => basename(f.path)) + .toList(); + _hasMrubyAssets = mrubyFiles.contains("__init__.py") && mrubyFiles.contains("bins"); + _hasMrubyAssetsComplete = true; + return _hasMrubyAssets; +} + +bool _hasMagickBinsComplete = false; +bool _hasMagickBins = false; +Future hasMagickBins() async { + if (_hasMagickBinsComplete) + return _hasMagickBins; + await assetDirDone; + var magickDir = join(assetsDir!, "MrubyDecompiler", "bins"); + var magickFiles = await Directory(magickDir) + .list() + .map((f) => basename(f.path)) + .toList(); + _hasMagickBins = magickFiles.contains("magick.exe") && magickFiles.contains("magick.exe.manifest"); + _hasMagickBinsComplete = true; + return _hasMagickBins; +} + +bool _hasMcdFontsComplete = false; +bool _hasMcdFonts = false; +const fontIds = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "19", "20", "35", "36", "37" ]; +Future hasMcdFonts() async { + if (_hasMcdFontsComplete) + return _hasMcdFonts; + await assetDirDone; + var fontsDir = join(assetsDir!, "mcdFonts"); + var fontDirs = await Directory(fontsDir) + .list() + .where((f) => f is Directory) + .map((f) => basename(f.path)) + .where((f) => fontIds.contains(f)) + .toList(); + fontDirs.sort((a, b) => int.parse(a).compareTo(int.parse(b))); + if (!listEquals(fontDirs, fontIds)) + return false; + var allFontsComplete = await Future.wait(fontDirs.map((f) async { + var fontDir = join(fontsDir, f); + var fontFiles = await Directory(fontDir) + .list() + .map((f) => basename(f.path)) + .toList(); + return fontFiles.contains("_atlas.png") && fontFiles.contains("_atlas.json"); + })); + _hasMcdFonts = allFontsComplete.every((f) => f); + _hasMcdFontsComplete = true; + return _hasMcdFonts; +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index d5c27236..240666ef 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -502,3 +502,21 @@ bool strEndsWithDat(String str) { bool get isDesktop => Platform.isWindows || Platform.isLinux || Platform.isMacOS; bool get isMobile => Platform.isAndroid || Platform.isIOS; + +class SizeInt { + final int width; + final int height; + + const SizeInt(this.width, this.height); + + @override + String toString() => "$width x $height"; +} +Future getDdsFileSize(String path) async { + var bytes = await File(path).readAsBytes(); + var reader = ByteDataWrapper(bytes.buffer); + reader.position = 0xc; + var height = reader.readUint32(); + var width = reader.readUint32(); + return SizeInt(width, height); +} diff --git a/pubspec.yaml b/pubspec.yaml index e02e25cc..b22155c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,23 @@ flutter: - assets/MrubyDecompiler/bins/linux/ - assets/MrubyDecompiler/bins/windows/ - assets/MrubyDecompiler/mrbToRb/ + - assets/mcdFonts/ + - assets/mcdFonts/1/ + - assets/mcdFonts/2/ + - assets/mcdFonts/3/ + - assets/mcdFonts/4/ + - assets/mcdFonts/5/ + - assets/mcdFonts/6/ + - assets/mcdFonts/7/ + - assets/mcdFonts/8/ + - assets/mcdFonts/9/ + - assets/mcdFonts/10/ + - assets/mcdFonts/11/ + - assets/mcdFonts/19/ + - assets/mcdFonts/20/ + - assets/mcdFonts/35/ + - assets/mcdFonts/36/ + - assets/mcdFonts/37/ fonts: - family: FiraCode