Skip to content

Commit

Permalink
Added O2R Support (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
KiritoDv authored May 31, 2024
1 parent b81464f commit 5f296ac
Show file tree
Hide file tree
Showing 24 changed files with 493 additions and 392 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.history
.svn/
migrate_working_dir/
.vscode/

# IntelliJ related
*.iml
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![Retro Logo](https://user-images.githubusercontent.com/60364512/228030301-f2139d22-48da-412b-9862-8f72e471e89c.png#gh-dark-mode-only)
![Retro Logo](https://user-images.githubusercontent.com/60364512/228030301-f2139d22-48da-412b-9862-8f72e471e89c.png#gh-dark-mode-only)

![Retro Logo](https://user-images.githubusercontent.com/60364512/228030177-6b7a51f2-fe24-4ce4-8235-8d35f2526250.png#gh-light-mode-only)
An OTR generation tool.
An OTR / O2R generation tool.

## Getting Started

Expand Down
165 changes: 165 additions & 0 deletions lib/arc/arc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// ignore_for_file: sort_constructors_first, cascade_invocations

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:archive/archive.dart';
import 'package:flutter_storm/flutter_storm.dart';
import 'package:flutter_storm/flutter_storm_defines.dart';
import 'package:retro/utils/log.dart';

enum ArcMode {
otr, o2r
}

class Arc {
static ZipDecoder zdec = ZipDecoder();
static ZipEncoder zenc = ZipEncoder();

dynamic handle;
String path;

Arc(this.path) {
final mode = path.endsWith('.otr') ? ArcMode.otr : ArcMode.o2r;
final file = File(path);

switch (mode) {
case ArcMode.otr:
if(file.existsSync()) {
handle = MPQArchive.open(path, 0, 0);
} else {
handle = MPQArchive.create(path, MPQ_CREATE_SIGNATURE | MPQ_CREATE_ARCHIVE_V2, 65535);
}
break;
case ArcMode.o2r:
if(file.existsSync()) {
handle = zdec.decodeBytes(file.readAsBytesSync());
} else {
handle = Archive();
}
break;
}
}

Future<List<String>?> _listMPQFiles({ FutureOr<void> Function(String, Uint8List)? onFile }) async {
final files = <String>[];
try {
var fileFound = false;

final mpqArchive = handle as MPQArchive;
final hFind = FileFindResource();
mpqArchive.findFirstFile('*', hFind, null);

final fileName = hFind.fileName();
if (fileName != null && fileName != '(signature)' && fileName != '(listfile)' && fileName != '(attributes)') {
files.add(fileName);
if(onFile != null) {
final file = mpqArchive.openFileEx(fileName, 0);
await onFile(fileName, file.read(file.size()));
}
}

do {
try {
mpqArchive.findNextFile(hFind);
fileFound = true;

final fileName = hFind.fileName();
if (fileName == null ||
fileName == SIGNATURE_NAME ||
fileName == LISTFILE_NAME ||
fileName == ATTRIBUTES_NAME) {
continue;
}

log('File name: $fileName');
if (fileName != null && fileName != '(signature)' && fileName != '(listfile)' && fileName != '(attributes)') {
files.add(fileName);
if(onFile != null) {
final file = mpqArchive.openFileEx(fileName, 0);
await onFile(fileName, file.read(file.size()));
}
}
} on StormLibException catch (e) {
log('Failed to find next file: ${e.message}');
fileFound = false;
break;
} on Exception catch (e) {
log('Got an error: $e');
fileFound = false;
}
} while (fileFound);

hFind.close();
return files;
} on StormLibException catch (e) {
log('Failed to set locale: ${e.message}');
return null;
}
}

Future<List<String>?> _listZipFiles({ FutureOr<void> Function(String, Uint8List)? onFile }) async {
final files = <String>[];
final archive = handle as Archive;
for (final file in archive) {
files.add(file.name);
if(onFile != null) {
await onFile(file.name, file.content as Uint8List);
}
}
return files;
}

void _addMPQFile(String path, Uint8List data) {
final mpqArchive = handle as MPQArchive;
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
mpqArchive.createFile(path, timestamp, data.length, 0, 0)
..write(data, data.length, MPQ_COMPRESSION_ZLIB)
..finish();
}

void _addZipFile(String path, Uint8List data) {
final archive = handle as Archive;
archive.addFile(ArchiveFile(path, data.length, data));
}

void _closeMPQ() {
final mpqArchive = handle as MPQArchive;
mpqArchive.close();
}

void _closeZip() {
final archive = handle as Archive;
final file = File(path);
final fileBytes = zenc.encode(archive);
if(fileBytes == null) {
throw Exception('Failed to encode archive');
}
file.writeAsBytesSync(fileBytes);
}

Future<List<String>?> listItems({ FutureOr<void> Function(String, Uint8List)? onFile }) {
if(handle is MPQArchive) {
return _listMPQFiles(onFile: onFile);
} else {
return _listZipFiles(onFile: onFile);
}
}

void addFile(String path, Uint8List data) {
if(handle is MPQArchive) {
return _addMPQFile(path, data);
} else {
return _addZipFile(path, data);
}
}

void close() {
if(handle is MPQArchive) {
return _closeMPQ();
} else {
return _closeZip();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class _CreateFinishBottomBarModalState
)
],
)
: Text(i18n.createFinishScreen_generateOtr),
: Text(i18n.createFinishScreen_generateOtr, style: TextStyle(color: Colors.white)),
),
),
],
Expand Down
54 changes: 16 additions & 38 deletions lib/features/create/create_finish/create_finish_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter_storm/flutter_storm.dart';
import 'package:flutter_storm/flutter_storm_defines.dart';
import 'package:image/image.dart';
import 'package:path/path.dart' as dartp;
import 'package:retro/arc/arc.dart';
import 'package:retro/models/app_state.dart';
import 'package:retro/models/stage_entry.dart';
import 'package:retro/models/texture_manifest_entry.dart';
Expand Down Expand Up @@ -40,7 +41,7 @@ class CreateFinishViewModel with ChangeNotifier {
notifyListeners();
}

onTogglePrependAlt(newPrependAltValue) async {
Future<void> onTogglePrependAlt(bool newPrependAltValue) async {
prependAlt = newPrependAltValue;
notifyListeners();
}
Expand Down Expand Up @@ -149,7 +150,9 @@ class CreateFinishViewModel with ChangeNotifier {
Future<void> onGenerateOTR(Function onCompletion) async {
final outputFile = await FilePicker.platform.saveFile(
dialogTitle: 'Please select an output file:',
fileName: 'generated.otr',
fileName: 'generated.o2r',
type: FileType.custom,
allowedExtensions: ['otr', 'o2r'],
);

if (outputFile == null) {
Expand Down Expand Up @@ -209,42 +212,25 @@ class CreateFinishViewModel with ChangeNotifier {
}
}

Future<void> generateOTR(
Tuple4<HashMap<String, StageEntry>, String, SendPort, bool> params) async {
Future<void> generateOTR(Tuple4<HashMap<String, StageEntry>, String, SendPort, bool> params) async {
try {
MPQArchive? mpqArchive = MPQArchive.create(
params.item2, MPQ_CREATE_SIGNATURE | MPQ_CREATE_ARCHIVE_V2, 12288);
final arcFile = Arc(params.item2);

for (final entry in params.item1.entries) {
if (entry.value is CustomStageEntry) {
for (final file in (entry.value as CustomStageEntry).files) {
final fileLength = await file.length();
final fileData = await file.readAsBytes();
final fileName =
"${entry.key}/${p.normalize(file.path).split("/").last}";

final mpqFile = mpqArchive.createFile(
fileName,
DateTime.now().millisecondsSinceEpoch ~/ 1000,
fileLength,
0,
MPQ_FILE_COMPRESS);
mpqFile.write(fileData, fileLength, MPQ_COMPRESSION_ZLIB);
mpqFile.finish();
arcFile.addFile(fileName, fileData);
params.item3.send(1);
}
} else if (entry.value is CustomSequencesEntry) {
for (final pair in (entry.value as CustomSequencesEntry).pairs) {
final sequence = await compute(Sequence.fromSeqFile, pair);
final fileName = '${entry.key}/${sequence.path}';
final data = sequence.build();
final mpqFile = mpqArchive.createFile(
fileName,
DateTime.now().millisecondsSinceEpoch ~/ 1000,
data.length,
0,
MPQ_FILE_COMPRESS);
mpqFile.write(data, data.length, MPQ_COMPRESSION_ZLIB);
mpqFile.finish();
arcFile.addFile(fileName, data);
params.item3.send(1);
}
} else if (entry.value is CustomTexturesEntry) {
Expand All @@ -262,21 +248,13 @@ Future<void> generateOTR(
continue;
}

final mpqFile = mpqArchive.createFile(
texture.item1,
DateTime.now().millisecondsSinceEpoch ~/ 1000,
texture.item2!.length,
0,
MPQ_FILE_COMPRESS);
mpqFile.write(
texture.item2!, texture.item2!.length, MPQ_COMPRESSION_ZLIB);
mpqFile.finish();
arcFile.addFile(texture.item1, texture.item2!);
}
}
}

params.item3.send(true);
mpqArchive.close();
arcFile.close();
} on StormLibException catch (e) {
log(e.message);
}
Expand All @@ -296,8 +274,8 @@ Future<Tuple2<String, Uint8List?>> processTextureEntry(
return Tuple2((params.item3 ? 'alt/' : '') + fileName, data);
}

Future<Uint8List?> processJPEG(pair, String textureName) async {
final Uint8List imageData = await pair.item1.readAsBytes();
Future<Uint8List?> processJPEG(Tuple2<File, TextureManifestEntry> pair, String textureName) async {
final imageData = await pair.item1.readAsBytes();
final image = decodeJpg(imageData);

if (image == null) {
Expand Down Expand Up @@ -330,8 +308,8 @@ Future<Uint8List?> processPNG(
return null;
}

texture.textureType = pair.item2.textureType;
texture.isPalette = image.hasPalette &&
texture..textureType = pair.item2.textureType
..isPalette = image.hasPalette &&
(texture.textureType == TextureType.Palette4bpp ||
texture.textureType == TextureType.Palette8bpp);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'dart:collection';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:retro/features/create/create_finish/create_finish_viewmodel.dart';
import 'package:retro/features/create/create_replace_textures/create_replace_textures_viewmodel.dart';
import 'package:retro/utils/language_ext.dart';
import 'package:retro/models/texture_manifest_entry.dart';
import 'package:tuple/tuple.dart';

// ignore: non_constant_identifier_names
Widget FolderContent(
Expand Down Expand Up @@ -51,13 +55,14 @@ Widget FolderContent(
prototypeItem: const SizedBox(width: 0, height: 20),
itemBuilder: (context, index) {
final key = viewModel.processedFiles.keys.elementAt(index);
// ignore: avoid_dynamic_calls
return Text("${finishViewModel.prependAlt ? 'alt/' : ''}$key (${viewModel.processedFiles[key]?.length ?? 0} tex)");
},),),),
Padding(
padding: const EdgeInsets.only(top: 20),
child: ElevatedButton(
onPressed: viewModel.processedFiles.isNotEmpty ? () {
finishViewModel.onAddCustomTextureEntry(cast(viewModel.processedFiles));
finishViewModel.onAddCustomTextureEntry(viewModel.processedFiles as HashMap<String, List<Tuple2<File, TextureManifestEntry>>>);
viewModel.reset();
Navigator.of(context).popUntil(ModalRoute.withName('/create_selection'));
} : null,
Expand Down
Loading

0 comments on commit 5f296ac

Please sign in to comment.