Alter music to read via normal data sources so plugins/mods can override it

Config utility copes with name/description attributes, nested/category options
.IRO support for 7H plugin
Slightly more concise Tolk output
This commit is contained in:
ficedula 2023-07-23 15:51:40 +01:00
parent 9bb83d967d
commit 7d6611e04a
19 changed files with 900 additions and 67 deletions

View File

@ -3,3 +3,4 @@ syntax: glob
*/obj/*
*/bin/*
*.user
PluginImplementations/output/*

View File

@ -7,6 +7,7 @@
using Ficedula.FF7;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
@ -48,6 +49,8 @@ namespace Braver {
} else
return null;
}
public override string ToString() => $"Pack";
}
public class LGPDataSource : DataSource {
@ -59,6 +62,8 @@ namespace Braver {
public override IEnumerable<string> Scan() => _lgp.Filenames;
public override Stream TryOpen(string file) => _lgp.TryOpen(file);
public override string ToString() => _lgp.ToString();
}
public class FileDataSource : DataSource {
@ -79,6 +84,8 @@ namespace Braver {
return new FileStream(fn, FileMode.Open, FileAccess.Read);
return null;
}
public override string ToString() => $"File source {_root}";
}
public class GameOptions {
@ -150,6 +157,7 @@ namespace Braver {
public void AddDataSource(string folder, DataSource source) {
if (!_data.TryGetValue(folder, out var list))
list = _data[folder] = new List<DataSource>();
Trace.WriteLine($"Adding data source for folder {folder}: {source}");
list.Add(source);
}
public string GetPath(string name) => _paths[name];

View File

@ -0,0 +1,29 @@
// This program and the accompanying materials are made available under the terms of the
// Eclipse Public License v2.0 which accompanies this distribution, and is available at
// https://www.eclipse.org/legal/epl-v20.html
//
// SPDX-License-Identifier: EPL-2.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Braver.Plugins {
[AttributeUsage(AttributeTargets.Property)]
public class ConfigPropertyAttribute : Attribute {
public string Name { get; set; }
public string Description { get; set; }
public ConfigPropertyAttribute(string name) {
Name = name;
}
public ConfigPropertyAttribute(string name, string description) {
Name = name;
Description = description;
}
}
}

View File

@ -28,9 +28,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Braver.Tolk", "PluginImplem
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BraverLauncher", "BraverLauncher\BraverLauncher.csproj", "{504E8DFB-B109-4A22-88E8-55E728903E2A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Braver.7HShim", "PluginImplementations\Braver.7HShim\Braver.7HShim.csproj", "{4A5088CD-48E8-494B-A82C-8212A5D49073}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Braver.7HShim", "PluginImplementations\Braver.7HShim\Braver.7HShim.csproj", "{4A5088CD-48E8-494B-A82C-8212A5D49073}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Braver.FFNxCompatibility", "PluginImplementations\Braver.FFNxCompatibility\Braver.FFNxCompatibility.csproj", "{0C0F23F0-D790-4102-BBD4-56774E4A089B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Braver.FFNxCompatibility", "PluginImplementations\Braver.FFNxCompatibility\Braver.FFNxCompatibility.csproj", "{0C0F23F0-D790-4102-BBD4-56774E4A089B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IrosArchive", "IrosArchive\IrosArchive.csproj", "{803429DC-A6B4-43C2-9F73-D6EA51EA6B99}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -86,6 +88,10 @@ Global
{0C0F23F0-D790-4102-BBD4-56774E4A089B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C0F23F0-D790-4102-BBD4-56774E4A089B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C0F23F0-D790-4102-BBD4-56774E4A089B}.Release|Any CPU.Build.0 = Release|Any CPU
{803429DC-A6B4-43C2-9F73-D6EA51EA6B99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{803429DC-A6B4-43C2-9F73-D6EA51EA6B99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{803429DC-A6B4-43C2-9F73-D6EA51EA6B99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{803429DC-A6B4-43C2-9F73-D6EA51EA6B99}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -19,16 +19,14 @@ namespace Braver {
public class Audio : IAudio {
private string _musicFolder;
private Channel<MusicCommand> _channel;
private Ficedula.FF7.Audio _sfxSource;
private FGame _game;
private byte _volume = 127;
private float _masterVolume;
public Audio(FGame game, string musicFolder, string soundFolder) {
public Audio(FGame game, string soundFolder) {
_game = game;
_musicFolder = musicFolder;
_masterVolume = game.GameOptions.MusicVolume;
_channel = Channel.CreateBounded<MusicCommand>(8);
Task.Run(RunMusic);
@ -153,15 +151,15 @@ namespace Braver {
void DoPlay(string track) {
var current = contexts.Peek();
DoStop();
string file = Path.Combine(_musicFolder, track + ".ogg");
if (!File.Exists(file)) {
System.Diagnostics.Trace.WriteLine($"Failed to find music track {file}");
var source = _game.TryOpen("vgmstream", track + ".ogg");
if (source == null) {
Trace.WriteLine($"Failed to find music track {track}.ogg");
return;
}
int loopStart = 0, loopEnd = 0;
try {
using (var reader = new NVorbis.VorbisReader(file)) {
using (var reader = new NVorbis.VorbisReader(source, false)) {
foreach(var tag in reader.Tags.All)
Trace.WriteLine($"Vorbis tag: {tag.Key} = {string.Join(", ", tag.Value)}");
loopStart = int.Parse(reader.Tags.GetTagSingle("LOOPSTART"));
@ -170,7 +168,8 @@ namespace Braver {
} catch (Exception ex) {
Trace.WriteLine($"Failed to parse vorbis tags: {ex}");
}
current.Vorbis = new NAudio.Vorbis.VorbisWaveReader(file);
source.Position = 0;
current.Vorbis = new NAudio.Vorbis.VorbisWaveReader(source, true);
current.WaveOut = new NAudio.Wave.WaveOut();
//current.WaveOut.Init(current.Vorbis);
current.Volume = new VolumeSampleProvider(new LoopProvider(current.Vorbis, loopStart, loopEnd));

View File

@ -122,7 +122,7 @@ namespace Braver {
throw new NotSupportedException($"Unrecognised data spec {spec}");
}
Audio = new Audio(this, _paths["MUSIC"], _paths["SFX"]);
Audio = new Audio(this, _paths["SFX"]);
Audio.Precache(Sfx.Cursor, true);
Audio.Precache(Sfx.Cancel, true);

View File

@ -36,7 +36,7 @@ DATA PACK? save %bdata%
DATA PACK? ui %bdata%
DATA PACK? wm %bdata%
PATH MUSIC %music%
DATA FILE? vgmstream %music%
PATH SFX %ff7%\data\sound
PATH FFMPEG %braver%\ffmpeg.exe
PATH MOVIES %movies%

View File

@ -66,12 +66,14 @@
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="lbPlugins" SelectionChanged="lbPlugins_SelectionChanged" />
<Grid x:Name="gPluginConfig" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
<ScrollViewer Grid.Column="1">
<Grid x:Name="gPluginConfig" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
</ScrollViewer>
</Grid>
</TabItem>
</TabControl>

View File

@ -14,6 +14,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
@ -132,10 +133,12 @@ namespace BraverLauncher {
void DoAdd(string name, Control c) {
gPluginConfig.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
if (!string.IsNullOrEmpty(name)) {
var lbl = new Label { Content = name };
var lbl = new Label { Content = name, Margin = new Thickness(3) };
Grid.SetRow(lbl, gPluginConfig.RowDefinitions.Count - 1);
gPluginConfig.Children.Add(lbl);
AutomationProperties.SetLabeledBy(c, lbl);
}
c.Margin = new Thickness(3);
Grid.SetRow(c, gPluginConfig.RowDefinitions.Count - 1);
Grid.SetColumn(c, 1);
gPluginConfig.Children.Add(c);
@ -157,24 +160,64 @@ namespace BraverLauncher {
DoAdd("", enabled);
enabled.Checked += (_o, _e) => config.Enabled = enabled.IsChecked ?? false;
foreach (var prop in plugin.Plugin.ConfigObject.GetType().GetProperties()) {
var v = config.Vars.FirstOrDefault(cv => cv.Name == prop.Name);
if (prop.PropertyType == typeof(string)) {
TextBox tb = new TextBox {
Text = v.Value ?? prop.GetValue(plugin.Plugin.ConfigObject)?.ToString() ?? string.Empty,
void DoObj(object o, IEnumerable<string> path, string category) {
if (!string.IsNullOrWhiteSpace(category)) {
gPluginConfig.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
var heading = new TextBlock {
Text = category,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 5, 0, 0),
};
DoAdd(prop.Name, tb);
tb.TextChanged += (_o, _e) => SetProp(prop.Name, tb.Text);
} else if (prop.PropertyType == typeof(bool)) {
CheckBox cb = new CheckBox {
Content = prop.Name,
IsChecked = (bool)prop.GetValue(plugin.Plugin.ConfigObject),
};
DoAdd("", cb);
cb.Checked += (_o, _e) => SetProp(prop.Name, (cb.IsChecked ?? false).ToString());
} else
throw new NotImplementedException();
Grid.SetColumnSpan(heading, 2);
Grid.SetRow(heading, gPluginConfig.RowDefinitions.Count - 1);
gPluginConfig.Children.Add(heading);
}
foreach (var prop in o.GetType().GetProperties()) {
string fullPropName = string.Join(".", path.Concat(new[] { prop.Name }));
var v = config.Vars.FirstOrDefault(cv => cv.Name == fullPropName);
var attr = prop.GetCustomAttributes(true).OfType<ConfigPropertyAttribute>().FirstOrDefault();
string name = attr?.Name ?? prop.Name,
desc = attr?.Description;
if (prop.PropertyType == typeof(string)) {
TextBox tb = new TextBox {
Text = v.Value ?? prop.GetValue(o)?.ToString() ?? string.Empty,
};
DoAdd(name, tb);
tb.TextChanged += (_o, _e) => SetProp(fullPropName, tb.Text);
} else if (prop.PropertyType == typeof(bool)) {
CheckBox cb = new CheckBox {
Content = name,
IsChecked = v == null ? (bool)prop.GetValue(o) : bool.Parse(v.Value),
};
DoAdd("", cb);
cb.Checked += (_o, _e) => SetProp(fullPropName, (cb.IsChecked ?? false).ToString());
} else if (prop.PropertyType.IsEnum) {
object val;
if (string.IsNullOrEmpty(v?.Value))
val = prop.GetValue(o);
else
val = Enum.Parse(prop.PropertyType, v?.Value);
ComboBox cb = new ComboBox {
ItemsSource = Enum.GetValues(prop.PropertyType),
SelectedValue = val,
};
DoAdd(name, cb);
cb.SelectionChanged += (_o, _e) => SetProp(fullPropName, cb.SelectedValue.ToString());
} else if (!prop.PropertyType.IsValueType) {
DoObj(
prop.GetValue(o),
path.Concat(new[] { prop.Name }),
(category + " " + name).Trim()
);
} else
throw new NotImplementedException();
}
}
DoObj(plugin.Plugin.ConfigObject, Enumerable.Empty<string>(), "");
}
}

View File

@ -1,7 +1,8 @@
{
"profiles": {
"BraverLauncher": {
"commandName": "Project"
"commandName": "Project",
"commandLineArgs": "/plugins:C:\\Users\\ficed\\Projects\\F7\\PluginImplementations\\output\\Debug"
}
}
}

View File

@ -97,5 +97,7 @@ namespace Ficedula.FF7 {
public void Dispose() {
_source.Dispose();
}
public override string ToString() => $"LGP {_source}";
}
}

385
IrosArchive/IrosArc.cs Normal file
View File

@ -0,0 +1,385 @@
/*
This source is subject to the Microsoft Public License. See LICENSE.TXT for details.
The original developer is Iros <irosff@outlook.com>
Modified by Ficedula to split out into a separate library not depending on anything else in 7H,
for read only access to IRO archives.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace IrosArchive {
public static class StreamUtil {
public static long ReadLong(this Stream s) {
byte[] b = new byte[8];
s.Read(b, 0, 8);
return BitConverter.ToInt64(b);
}
public static int ReadInt(this Stream s) {
byte[] b = new byte[4];
s.Read(b, 0, 4);
return BitConverter.ToInt32(b);
}
public static ushort ReadUShort(this Stream s) {
byte[] b = new byte[2];
s.Read(b, 0, 2);
return BitConverter.ToUInt16(b);
}
}
public class IrosArcException : Exception {
public IrosArcException(string msg) : base(msg) { }
}
[Flags]
public enum ArchiveFlags {
None = 0,
Patch = 0x1,
}
[Flags]
public enum FileFlags {
None = 0,
CompressLZS = 0x1,
CompressLZMA = 0x2,
COMPRESSION_FLAGS = 0xF,
#if RUDE
Obfuscate = 0x10000,
#else
RudeFlags = 0xff0000,
#endif
}
public enum CompressType {
Nothing = 0,
Everything,
ByExtension,
ByContent,
}
public class IrosArc : IDisposable {
public const int SIG = 0x534f5249;
public const int MAX_VERSION = 0x10002;
public const int MIN_VERSION = 0x10000;
internal const int SZ_OK = 0;
internal const int SZ_ERROR_DATA = 1;
internal const int SZ_ERROR_MEM = 2;
internal const int SZ_ERROR_CRC = 3;
internal const int SZ_ERROR_UNSUPPORTED = 4;
internal const int SZ_ERROR_PARAM = 5;
internal const int SZ_ERROR_INPUT_EOF = 6;
internal const int SZ_ERROR_OUTPUT_EOF = 7;
private static HashSet<string> _noCompressExt = new HashSet<string>(new[] {
".jpg", ".png", ".mp3", ".ogg"
}, StringComparer.InvariantCultureIgnoreCase);
private class ArcHeader {
public int Version { get; set; }
public ArchiveFlags Flags { get; set; }
public int Directory { get; set; }
public void Open(Stream s) {
if (s.ReadInt() != SIG) throw new IrosArcException("Signature mismatch");
Version = s.ReadInt();
Flags = (ArchiveFlags)s.ReadInt();
Directory = s.ReadInt();
if (Version < MIN_VERSION) throw new IrosArcException("Invalid header version " + Version.ToString());
if (Version > MAX_VERSION) throw new IrosArcException("Invalid header version " + Version.ToString());
}
public override string ToString() {
return String.Format("Version: {0}.{1} Directory at: {2} Flags: {3}", Version >> 16, Version & 0xffff, Directory, Flags);
}
}
private class DirectoryEntry {
public string Filename { get; set; }
public FileFlags Flags { get; set; }
public long Offset { get; set; }
public int Length { get; set; }
public void Open(Stream s, int version) {
long pos = s.Position;
ushort len = s.ReadUShort();
ushort flen = s.ReadUShort();
byte[] fn = new byte[flen];
s.Read(fn, 0, flen);
Filename = System.Text.Encoding.Unicode.GetString(fn);
Flags = (FileFlags)s.ReadInt();
if (version < 0x10001)
Offset = s.ReadInt();
else
Offset = s.ReadLong();
Length = s.ReadInt();
s.Position = pos + len;
}
public ushort GetSize() {
byte[] fndata = System.Text.Encoding.Unicode.GetBytes(Filename);
ushort len = (ushort)(fndata.Length + 4 + 16);
return len;
}
public override string ToString() {
return String.Format("File: {0} Offset: {1} Size: {2} Flags: {3}", Filename, Offset, Length, Flags);
}
}
private ArcHeader _header;
private List<DirectoryEntry> _entries;
private Dictionary<string, DirectoryEntry> _lookup;
private HashSet<string> _folderNames;
private FileStream _data;
private string _source;
private class CacheEntry {
public byte[] Data;
public DateTime LastAccess;
public string File;
}
private System.Collections.Concurrent.ConcurrentDictionary<long, CacheEntry> _cache = new System.Collections.Concurrent.ConcurrentDictionary<long, CacheEntry>();
private struct DataRecord {
public byte[] Data;
public bool Compressed;
}
private static DataRecord GetData(byte[] input, string filename, CompressType compress) {
if (compress == CompressType.Nothing) {
return new DataRecord() { Data = input };
}
if (compress == CompressType.ByExtension && _noCompressExt.Contains(Path.GetExtension(filename))) {
return new DataRecord() { Data = input };
}
var cdata = new MemoryStream();
//Lzs.Encode(new MemoryStream(input), cdata);
byte[] lprops;
using (var lzma = new SharpCompress.Compressors.LZMA.LzmaStream(new SharpCompress.Compressors.LZMA.LzmaEncoderProperties(), false, cdata)) {
lzma.Write(input, 0, input.Length);
lprops = lzma.Properties;
}
if (/*compress == CompressType.ByContent &&*/ (cdata.Length + lprops.Length + 8) > (input.Length * 10 / 8)) {
return new DataRecord() { Data = input };
}
byte[] data = new byte[cdata.Length + lprops.Length + 8];
Array.Copy(BitConverter.GetBytes(input.Length), data, 4);
Array.Copy(BitConverter.GetBytes(lprops.Length), 0, data, 4, 4);
Array.Copy(lprops, 0, data, 8, lprops.Length);
cdata.Position = 0;
cdata.Read(data, lprops.Length + 8, (int)cdata.Length);
return new DataRecord() { Data = data, Compressed = true };
}
public bool CheckValid() {
foreach (var entry in _entries) {
if ((entry.Offset + entry.Length) > _data.Length) return false;
}
return true;
}
public IrosArc(string filename, bool patchable = false, Action<int, int> progressAction = null) {
_source = filename;
var sw = new Stopwatch();
sw.Start();
if (patchable)
_data = new FileStream(filename, FileMode.Open, FileAccess.ReadWrite);
else
_data = new FileStream(filename, FileMode.Open, FileAccess.Read);
_header = new ArcHeader();
_header.Open(_data);
int numfiles;
_data.Position = _header.Directory;
do {
numfiles = _data.ReadInt();
if (numfiles == -1) {
_data.Position = _data.ReadLong();
}
} while (numfiles < 0);
_entries = new List<DirectoryEntry>();
_lookup = new Dictionary<string, DirectoryEntry>(StringComparer.InvariantCultureIgnoreCase);
_folderNames = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
for (int i = 0; i < numfiles; i++) {
progressAction?.Invoke(i, numfiles);
DirectoryEntry e = new DirectoryEntry();
e.Open(_data, _header.Version);
#if !RUDE
if ((e.Flags & FileFlags.RudeFlags) != 0) throw new IrosArcException(String.Format("Archive {0} entry {1} has invalid flags", filename, e.Filename));
#endif
_entries.Add(e);
_lookup[e.Filename] = e;
int lpos = e.Filename.LastIndexOf('\\');
if (lpos > 0) {
_folderNames.Add(e.Filename.Substring(0, lpos));
}
}
sw.Stop();
Trace.WriteLine($"IrosArc: opened {filename}, contains {_lookup.Count} files, took {sw.ElapsedMilliseconds} ms to parse");
}
public IEnumerable<string> AllFileNames() {
return _lookup.Keys;
}
public IEnumerable<string> AllFolderNames() {
return _folderNames.Select(s => s);
}
public bool HasFolder(string name) {
bool result = _folderNames.Contains(name);
if (result) {
Trace.WriteLine($"ARCHIVE: {_source} contains folder {name}");
}
return result;
}
public bool HasFile(string name) {
bool result = _lookup.ContainsKey(name);
if (result) {
Trace.WriteLine($"ARCHIVE: {_source} contains file {name}");
}
return result;
}
public int GetFileSize(string name) {
DirectoryEntry e;
if (_lookup.TryGetValue(name, out e)) {
switch (e.Flags & FileFlags.COMPRESSION_FLAGS) {
case FileFlags.CompressLZMA:
_data.Position = e.Offset;
return _data.ReadInt();
default:
case FileFlags.None:
return e.Length;
}
} else
return -1;
}
private void CleanCache() {
long[] remove = _cache
.ToArray()
.Where(kv => kv.Value.LastAccess < DateTime.Now.AddSeconds(-60))
.Select(kv => kv.Key)
.ToArray();
if (remove.Any()) {
Trace.WriteLine($"Removing {remove.Length} compressed files from cache: ");
CacheEntry _;
foreach (long r in remove) _cache.TryRemove(r, out _);
}
}
//private int _cacheCounter = 0;
private CacheEntry GetCache(DirectoryEntry e) {
CacheEntry ce;
if (!_cache.TryGetValue(e.Offset, out ce)) {
ce = new CacheEntry() { File = e.Filename };
byte[] data;
lock (_data) {
switch (e.Flags & FileFlags.COMPRESSION_FLAGS) {
case FileFlags.CompressLZS:
data = new byte[e.Length];
_data.Position = e.Offset;
_data.Read(data, 0, e.Length);
var ms = new MemoryStream(data);
var output = new MemoryStream();
Lzs.Decode(ms, output);
data = new byte[output.Length];
output.Position = 0;
output.Read(data, 0, data.Length);
ce.Data = data;
break;
case FileFlags.CompressLZMA:
_data.Position = e.Offset;
int decSize = _data.ReadInt(), propSize = _data.ReadInt();
byte[] props = new byte[propSize];
_data.Read(props, 0, props.Length);
byte[] cdata = new byte[e.Length - propSize - 8];
_data.Read(cdata, 0, cdata.Length);
data = new byte[decSize];
var lzma = new SharpCompress.Compressors.LZMA.LzmaStream(props, new MemoryStream(cdata));
lzma.Read(data, 0, data.Length);
/*int srcSize = cdata.Length;
switch (LzmaUncompress(data, ref decSize, cdata, ref srcSize, props, props.Length)) {
case SZ_OK:
//Woohoo!
break;
default:
throw new IrosArcException("Error decompressing " + e.Filename);
}*/
ce.Data = data;
break;
default:
throw new IrosArcException("Bad compression flags " + e.Flags.ToString());
}
}
_cache.AddOrUpdate(e.Offset, ce, (_, __) => ce);
}
ce.LastAccess = DateTime.Now;
CleanCache();
/*
if ((_cacheCounter++ % 100) == 0)
DebugLogger.WriteLine("IRO cache contents; " + String.Join(",", _cache.Values.Select(e => e.File)));
*/
return ce;
}
public byte[] GetBytes(string name) {
DirectoryEntry e;
if (_lookup.TryGetValue(name, out e)) {
if ((e.Flags & FileFlags.COMPRESSION_FLAGS) != 0)
return GetCache(e).Data;
else {
lock (_data) {
byte[] data = new byte[e.Length];
_data.Position = e.Offset;
_data.Read(data, 0, e.Length);
return data;
}
}
} else
return null;
}
public Stream GetData(string name) {
byte[] data = GetBytes(name);
return data == null ? null : new MemoryStream(data);
}
public void Dispose() {
if (_data != null) {
_data.Close();
_data = null;
}
}
public override string ToString() {
return "[IrosArchive " + _source + "]";
}
public IEnumerable<string> GetInformation() {
yield return _header.ToString();
foreach (var entry in _entries)
yield return entry.ToString();
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpCompress" Version="0.33.0" />
</ItemGroup>
</Project>

23
IrosArchive/LICENSE.txt Normal file
View File

@ -0,0 +1,23 @@
Microsoft Public License (MS-PL)
This license governs use of the accompanying software. If you use the software, you
accept this license. If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the
same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.

197
IrosArchive/Lzs.cs Normal file
View File

@ -0,0 +1,197 @@
/*
This source is subject to the Microsoft Public License. See LICENSE.TXT for details.
The original developer is Iros <irosff@outlook.com>
*/
namespace IrosArchive {
public static class Lzs {
private const int N = 4096;
private const int F = 18;
private const int THRESHOLD = 2;
private const int NIL = N;
public static void Encode(Stream input, Stream output) {
new EncodeContext().Encode(input, output);
}
public static void Decode(Stream input, Stream output) {
new EncodeContext().Decode(input, output);
}
private class EncodeContext {
public byte[] buffer = new byte[N + F];
public int MatchPos, MatchLen;
public int[] Lson = new int[N + 1];
public int[] Rson = new int[N + 257];
public int[] Dad = new int[N + 1];
public void InitTree() {
for (int i = N + 1; i <= N + 256; i++) Rson[i] = NIL;
for (int i = 0; i < N; i++) Dad[i] = NIL;
}
public void InsertNode(int r) {
int i, p, cmp;
int key = r;
cmp = 1;
p = N + 1 + buffer[key];
Rson[r] = Lson[r] = NIL;
MatchLen = 0;
while (true) {
if (cmp >= 0) {
if (Rson[p] != NIL)
p = Rson[p];
else {
Rson[p] = r;
Dad[r] = p;
return;
}
} else {
if (Lson[p] != NIL)
p = Lson[p];
else {
Lson[p] = r;
Dad[r] = p;
return;
}
}
for (i = 1; i < F; i++)
if ((cmp = buffer[key + i] - buffer[p + i]) != 0) break;
if (i > MatchLen) {
MatchPos = p;
if ((MatchLen = i) >= F) break;
}
}
Dad[r] = Dad[p]; Lson[r] = Lson[p]; Rson[r] = Rson[p];
Dad[Lson[p]] = r; Dad[Rson[p]] = r;
if (Rson[Dad[p]] == p)
Rson[Dad[p]] = r;
else
Lson[Dad[p]] = r;
Dad[p] = NIL;
}
public void DeleteNode(int p) {
int q;
if (Dad[p] == NIL) return;
if (Rson[p] == NIL)
q = Lson[p];
else if (Lson[p] == NIL)
q = Rson[p];
else {
q = Lson[p];
if (Rson[q] != NIL) {
do {
q = Rson[q];
} while (Rson[q] != NIL);
Rson[Dad[q]] = Lson[q]; Dad[Lson[q]] = Dad[q];
Lson[q] = Lson[p]; Dad[Lson[p]] = q;
}
Rson[q] = Rson[p]; Dad[Rson[p]] = q;
}
Dad[q] = Dad[p];
if (Rson[Dad[p]] == p)
Rson[Dad[p]] = q;
else
Lson[Dad[p]] = q;
Dad[p] = NIL;
}
public void Encode(Stream input, Stream output) /* was Encode(void) */
{
int i, c, len, r, s, last_match_length, code_buf_ptr;
byte[] code_buf = new byte[17];
byte mask;
InitTree(); /* initialize trees */
code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and
code_buf[0] works as eight flags, "1" representing that the unit
is an unencoded letter (1 byte), "0" a position-and-length pair
(2 bytes). Thus, eight units require at most 16 bytes of code. */
code_buf_ptr = mask = 1;
s = 0; r = N - F;
for (i = s; i < r; i++) buffer[i] = 0;
for (len = 0; len < F && (c = input.ReadByte()) != -1; len++)
buffer[r + len] = (byte)c; /* Read F bytes into the last F bytes of
the buffer */
if (len == 0) return; /* text of size zero */
for (i = 1; i <= F; i++) InsertNode(r - i); /* Insert the F strings,
each of which begins with one or more 'space' characters. Note
the order in which these strings are inserted. This way,
degenerate trees will be less likely to occur. */
InsertNode(r); /* Finally, insert the whole string just read. The
global variables match_length and match_position are set. */
do {
if (MatchLen > len) MatchLen = len; /* match_length
may be spuriously long near the end of text. */
if (MatchLen <= THRESHOLD) {
MatchLen = 1; /* Not long enough match. Send one byte. */
code_buf[0] |= mask; /* 'send one byte' flag */
code_buf[code_buf_ptr++] = buffer[r]; /* Send uncoded. */
} else {
code_buf[code_buf_ptr++] = (byte)MatchPos;
code_buf[code_buf_ptr++] = (byte)(((MatchPos >> 4) & 0xf0) | (MatchLen - (THRESHOLD + 1))); /* Send position and
length pair. Note match_length > THRESHOLD. */
}
if ((mask <<= 1) == 0) { /* Shift mask left one bit. */
for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */
output.WriteByte(code_buf[i]); /* code together */
code_buf[0] = 0; code_buf_ptr = mask = 1;
}
last_match_length = MatchLen;
for (i = 0; i < last_match_length &&
(c = input.ReadByte()) != -1; i++) {
DeleteNode(s); /* Delete old strings and */
buffer[s] = (byte)c; /* read new bytes */
if (s < F - 1) buffer[s + N] = (byte)c; /* If the position is
near the end of buffer, extend the buffer to make
string comparison easier. */
s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
/* Since this is a ring buffer, increment the position
modulo N. */
InsertNode(r); /* Register the string in text_buf[r..r+F-1] */
}
while (i++ < last_match_length) { /* After the end of text, */
DeleteNode(s); /* no need to read, but */
s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
if ((--len) != 0) InsertNode(r); /* buffer may not be empty. */
}
} while (len > 0); /* until length of string to be processed is zero */
if (code_buf_ptr > 1) { /* Send remaining code. */
for (i = 0; i < code_buf_ptr; i++) output.WriteByte(code_buf[i]);
}
return;
}
public void Decode(Stream input, Stream output) /* was Decode(void)
Just the reverse of Encode(). */
{
int i, j, k, r, c;
int flags;
for (i = 0; i < N - F; i++) buffer[i] = 0;
r = N - F; flags = 0;
for (; ; ) {
if (((flags >>= 1) & 256) == 0) {
if ((c = input.ReadByte()) == -1) break;
flags = c | 0xff00; /* uses higher byte cleverly */
} /* to count eight */
if ((flags & 1) != 0) {
if ((c = input.ReadByte()) == -1) break;
output.WriteByte((byte)c); buffer[r++] = (byte)c; r &= (N - 1);
} else {
if ((i = input.ReadByte()) == -1) break;
if ((j = input.ReadByte()) == -1) break;
i |= ((j & 0xf0) << 4); j = (j & 0x0f) + THRESHOLD;
for (k = 0; k <= j; k++) {
c = buffer[(i + k) & (N - 1)];
output.WriteByte((byte)c); buffer[r++] = (byte)c; r &= (N - 1);
}
}
}
return;
}
}
}
}

View File

@ -17,6 +17,7 @@
<Private>False</Private>
<CopyLocalSatelliteAssemblies>False</CopyLocalSatelliteAssemblies>
</ProjectReference>
<ProjectReference Include="..\..\IrosArchive\IrosArchive.csproj" />
</ItemGroup>
</Project>

View File

@ -6,9 +6,11 @@
using Braver.Plugins;
using Braver.Plugins.UI;
using IrosArchive;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
using System.Xml;
namespace Braver._7HShim {
@ -21,24 +23,31 @@ namespace Braver._7HShim {
protected void SetInt(string name, int i) => _settings[name] = i;
protected int GetInt(string name) => _settings.GetValueOrDefault(name);
private static char[] OP_CHARS = new[] { '=', '<', '>', '!' };
internal bool Evaluate(string comparison) {
string[] parts = comparison.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
throw new InvalidDataException($"Bad comparison: {comparison}");
int val1 = _settings.GetValueOrDefault(parts[0]),
val2 = int.Parse(parts[2]);
int opStart = comparison.IndexOfAny(OP_CHARS),
opEnd = comparison.LastIndexOfAny(OP_CHARS);
string operand0 = comparison.Substring(0, opStart).Trim(),
operand1 = comparison.Substring(opEnd + 1).Trim(),
op = comparison.Substring(opStart, opEnd - opStart + 1).Trim();
if (parts[1] == "=")
if (string.IsNullOrEmpty(operand0) || string.IsNullOrEmpty(operand1) || string.IsNullOrEmpty(op))
throw new InvalidDataException($"Bad comparison: {comparison}");
int val1 = _settings.GetValueOrDefault(operand0),
val2 = int.Parse(operand1);
if (op == "=")
return val1 == val2;
else if (parts[1] == "!=")
else if (op == "!=")
return val1 != val2;
else if (parts[1] == "<")
else if (op == "<")
return val1 < val2;
else if (parts[1] == ">")
else if (op == ">")
return val1 > val2;
else if (parts[1] == "<=")
else if (op == "<=")
return val1 <= val2;
else if (parts[1] == ">=")
else if (op == ">=")
return val1 >= val2;
else
throw new InvalidDataException($"Bad comparison: {comparison}");
@ -59,26 +68,31 @@ namespace Braver._7HShim {
public static SevenHConfig Build(XmlNode modinfo, ModuleBuilder module) {
var id = Guid.Parse(modinfo.SelectSingleNode("ID").InnerText);
var typ = module.DefineType("Config" + id.ToString("N"), System.Reflection.TypeAttributes.Class | System.Reflection.TypeAttributes.Public, typeof(SevenHConfig));
var typ = module.DefineType("Config" + id.ToString("N"), TypeAttributes.Class | TypeAttributes.Public, typeof(SevenHConfig));
var mGetB = typeof(SevenHConfig).GetMethod(nameof(GetBool), BindingFlags.NonPublic | BindingFlags.Instance);
var mSetB = typeof(SevenHConfig).GetMethod(nameof(SetBool), BindingFlags.NonPublic | BindingFlags.Instance);
var mGetI = typeof(SevenHConfig).GetMethod(nameof(GetInt), BindingFlags.NonPublic | BindingFlags.Instance);
var mSetI = typeof(SevenHConfig).GetMethod(nameof(SetInt), BindingFlags.NonPublic | BindingFlags.Instance);
void DoProp(string propName, Type propType) {
void DoProp(string propName, Type propType, string name, string description) {
MethodInfo mGet = propType == typeof(bool) ? mGetB : mGetI,
mSet = propType == typeof(bool) ? mSetB : mSetI;
var prop = typ.DefineProperty(
propName,
System.Reflection.PropertyAttributes.None,
PropertyAttributes.None,
propType, null
);
var attr = new CustomAttributeBuilder(
typeof(ConfigPropertyAttribute).GetConstructor(new[] { typeof(string), typeof(string) }),
new object[] { name, description }
);
prop.SetCustomAttribute(attr);
var getBuild = typ.DefineMethod(
"get_" + prop.Name,
System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.HideBySig | System.Reflection.MethodAttributes.SpecialName,
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName,
propType, null
);
var getIL = getBuild.GetILGenerator();
@ -91,7 +105,7 @@ namespace Braver._7HShim {
var setBuild = typ.DefineMethod(
"set_" + prop.Name,
System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.HideBySig | System.Reflection.MethodAttributes.SpecialName,
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName,
null, new[] { propType }
);
var setIL = setBuild.GetILGenerator();
@ -99,7 +113,7 @@ namespace Braver._7HShim {
setIL.Emit(OpCodes.Ldstr, prop.Name);
setIL.Emit(OpCodes.Ldarg_1);
if (propType.IsEnum)
setIL.Emit(OpCodes.Castclass, typeof(int));
setIL.Emit(OpCodes.Conv_I4);
setIL.Emit(OpCodes.Callvirt, mSet);
setIL.Emit(OpCodes.Ret);
prop.SetSetMethod(setBuild);
@ -107,11 +121,13 @@ namespace Braver._7HShim {
foreach (XmlNode xoption in modinfo.SelectNodes("ConfigOption")) {
string optType = xoption.SelectSingleNode("Type").InnerText;
string optName = xoption.SelectSingleNode("ID").InnerText;
string optID = xoption.SelectSingleNode("ID").InnerText;
string optName = xoption.SelectSingleNode("Name").InnerText;
string optDesc = xoption.SelectSingleNode("Description").InnerText;
if (optType.Equals("Bool", StringComparison.InvariantCultureIgnoreCase)) {
DoProp(optName, typeof(bool));
DoProp(optID, typeof(bool), optName, optDesc);
} else if (optType.Equals("List", StringComparison.InvariantCultureIgnoreCase)) {
var enumtype = module.DefineEnum("List_" + optName, System.Reflection.TypeAttributes.Public, typeof(int));
var enumtype = module.DefineEnum("List_" + optID, TypeAttributes.Public, typeof(int));
foreach(XmlNode xlistopt in xoption.SelectNodes("Option")) {
enumtype.DefineLiteral(
xlistopt.Attributes["Name"].Value.Replace(" ", "_"),
@ -119,7 +135,7 @@ namespace Braver._7HShim {
);
}
var builtEnum = enumtype.CreateType();
DoProp(optName, builtEnum);
DoProp(optID, builtEnum, optName, optDesc);
}
}
@ -128,11 +144,97 @@ namespace Braver._7HShim {
}
}
internal class SevenHMod {
public XmlDocument ModXml { get; set; }
internal abstract class SevenHMod {
public XmlDocument ModXml { get; protected set; }
public int ID { get; set; }
public SevenHConfig Config { get; set; }
public string Root { get; set; }
public abstract IEnumerable<string> GetSubFolders(string folder);
public abstract IEnumerable<string> GetFolders();
public abstract DataSource GetDataSource(string folder, string subfolder);
}
internal class SevenHIroMod : SevenHMod, IDisposable {
private IrosArchive.IrosArc _iro;
public SevenHIroMod(string iroFile) {
_iro = new IrosArchive.IrosArc(iroFile);
var doc = new XmlDocument();
using (var s = _iro.GetData("mod.xml"))
doc.Load(s);
ModXml = doc;
}
private class IroDataSource : DataSource {
private IrosArchive.IrosArc _iro;
private string _folder;
private string[] _filenames;
public IroDataSource(IrosArc iro, string folder) {
_iro = iro;
_folder = folder;
_filenames = iro.AllFileNames()
.Where(s => s.StartsWith(folder))
.Select(s => Path.GetFileName(s))
.ToArray();
}
public override IEnumerable<string> Scan() => _filenames;
public override Stream TryOpen(string file) => _iro.GetData(_folder + file);
public override string ToString() => $"IRO {_iro}";
}
public override DataSource GetDataSource(string folder, string subfolder) {
if (string.IsNullOrWhiteSpace(subfolder))
return new IroDataSource(_iro, folder + "\\");
else
return new IroDataSource(_iro, folder + "\\" + subfolder + "\\");
}
public override IEnumerable<string> GetFolders() {
return _iro.AllFolderNames()
.Where(s => !s.Contains('\\'));
}
public override IEnumerable<string> GetSubFolders(string folder) {
string prefix = folder + "\\";
return _iro.AllFolderNames()
.Where(s => s.StartsWith(prefix))
.Select(s => s.Substring(prefix.Length))
.Where(s => !s.Contains('\\'));
}
public void Dispose() {
_iro.Dispose();
}
}
internal class SevenHFileMod : SevenHMod {
private string _root;
public SevenHFileMod(string root) {
_root = root;
var doc = new XmlDocument();
doc.Load(Path.Combine(_root, "mod.xml"));
ModXml = doc;
}
public override DataSource GetDataSource(string folder, string subfolder) {
return new FileDataSource(Path.Combine(_root, folder, subfolder));
}
public override IEnumerable<string> GetFolders() {
return Directory.GetDirectories(_root)
.Select(fn => Path.GetFileName(fn));
}
public override IEnumerable<string> GetSubFolders(string folder) {
return Directory.GetDirectories(Path.Combine(_root, folder))
.Select(fn => Path.GetFileName(fn));
}
}
public class SevenHConfigCollection {
@ -155,6 +257,16 @@ namespace Braver._7HShim {
PropertyAttributes.None,
config.GetType(), null
);
var attr = new CustomAttributeBuilder(
typeof(ConfigPropertyAttribute).GetConstructor(new[] { typeof(string), typeof(string) }),
new object[] {
mod.ModXml.SelectSingleNode("/ModInfo/Name").InnerText,
mod.ModXml.SelectSingleNode("/ModInfo/Description").InnerText
}
);
prop.SetCustomAttribute(attr);
var getBuild = typ.DefineMethod(
"get_" + prop.Name,
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName,
@ -214,13 +326,16 @@ namespace Braver._7HShim {
if (File.Exists(modxml)) {
var doc = new XmlDocument();
doc.Load(modxml);
_mods.Add(new SevenHMod {
ModXml = doc,
_mods.Add(new SevenHFileMod(subdirectory) {
ID = _mods.Count + 1,
Root = subdirectory,
});
}
}
foreach(string iro in Directory.GetFiles(thisFolder, "*.iro")) {
_mods.Add(new SevenHIroMod(iro) {
ID = _mods.Count + 1
});
}
AssemblyName assemblyName = new AssemblyName("SevenHShim.DynamicConfig");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
@ -239,7 +354,10 @@ namespace Braver._7HShim {
public override void Init(BGame game) {
foreach(var mod in _mods) {
HashSet<string> remainingFolders = new HashSet<string>(mod.GetFolders(), StringComparer.InvariantCultureIgnoreCase);
foreach(XmlNode xfolder in mod.ModXml.SelectNodes("/ModInfo/ModFolder")) {
string folder = xfolder.Attributes["Folder"].Value;
remainingFolders.Remove(folder);
bool isActive;
if (xfolder.Attributes["ActiveWhen"] != null)
isActive = mod.Config.Evaluate(xfolder.Attributes["ActiveWhen"].Value);
@ -248,11 +366,12 @@ namespace Braver._7HShim {
else
isActive = true;
if (isActive) {
string folder = Path.Combine(mod.Root, xfolder.Attributes["Folder"].Value);
foreach (string datafolder in Directory.GetDirectories(folder))
game.AddDataSource(Path.GetFileName(datafolder), new FileDataSource(datafolder));
foreach (string datafolder in mod.GetSubFolders(folder))
game.AddDataSource(Path.GetFileName(datafolder), mod.GetDataSource(folder, datafolder));
}
}
foreach (string folder in remainingFolders)
game.AddDataSource(Path.GetFileName(folder), mod.GetDataSource(folder, ""));
}
}
}

View File

@ -13,6 +13,7 @@ using Tommy;
namespace Braver.FFNxCompatibility {
public class FFNxConfig {
[ConfigProperty("Enable Field Ambient Sounds")]
public bool FieldAmbientSounds { get; set; } = true;
}

View File

@ -16,8 +16,11 @@ using System.Windows.Forms;
namespace Braver.Tolk {
public class TolkConfig {
[ConfigProperty("Enable SAPI support")]
public bool EnableSAPI { get; set; } = true;
[ConfigProperty("Enable Footstep Sounds")]
public bool EnableFootsteps { get; set; } = true;
[ConfigProperty("Enable Focus sounds")]
public bool EnableFocusTracking { get; set; } = true;
}
@ -79,7 +82,7 @@ namespace Braver.Tolk {
private object _lastMenuContainer = null;
public void Menu(IEnumerable<string> items, int selected, object container) {
DavyKager.Tolk.Speak(
$"Menu {items.ElementAtOrDefault(selected)}, {selected + 1} of {items.Count()}",
$"{items.ElementAtOrDefault(selected)}, {selected + 1} of {items.Count()}",
_lastMenuContainer == container
);
_lastMenuContainer = container;