diff --git a/.gitignore b/.gitignore index ae4e793..bcf8194 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ LanternExtractor/bin/ *.DS_Store packages/ .vs/ +.vscode/ diff --git a/LanternExtractor/ClientData/musictracks.txt b/LanternExtractor/ClientData/musictracks.txt deleted file mode 100644 index 196c1c3..0000000 --- a/LanternExtractor/ClientData/musictracks.txt +++ /dev/null @@ -1,595 +0,0 @@ -# Lantern Music Tracks - -#airplane -akanon-2 -airplane_0 -entrance_fanfare-1 -arpeggiated_runs -airplane_1 -heroism-1 - -#akanon -akanon -gfaydark-1 -gfaydark-2 -felwithe_2 -felwithe_0 -felwithe_1 -arena -gypsies - -#befallen -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - -#blackburrow -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - -#butcher -entrance_fanfare -entrance_fanfare_1 - -#cauldron -entrance_fanfare -entrance_fanfare_1 -entrance_fanfare_1-1 -entrance_fanfare-1 -sea - -#cobaltscar -cobaltscar -underwater - -#crushbone -gfaydark-4 -airplane_0 -entrance_fanfare-1 -eerie_3 -arpeggiated_runs -neriak_0-1 - -#crystal -erudsxing_0 -gfaydark-1 -cobaltscar -gfaydark - -#damage -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - -#damage1 -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - -#damage2 -damage_0 -damage_1 -damage_2 -damage_3 -damage_4 - -#eastkarana -cascade -eastkarana - -#eerie -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - -#erudnext -felwithe_0 -cascade -karana_river -entrance_fanfare_1-1 -bard -freportn -karana_river-1 -brass_harmonies - -#eudnint -cascade -karana_river -entrance_fanfare_1-1 -bard -freportn -karana_river-1 -brass_harmonies -felwithe_1 - -#erudsxing -erudsxing_0 -erudsxing_1 - -#fearplane -eerie_8 -eerie_9 -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_7 - -#felwithea -felwithe_0 -felwithe_1 -felwithe_2 - -#freporte -freeport -lionsmane -fishsale -cottage -gypsies -death_variant -eerie_4 -eerie_5 - -#freportn -harprun -gypsies-1 -gypsies -eqtheme -arena -brass_harmonies -akanon-1 -lionsmane -fishsale -bard -entrance_fanfare_1-1 -cottage -freportn -nightchords -string_fanfare -brass_fanfare - -#freportw -heroism -hogcaller -militia -arena -entrance_fanfare_1-1 -eastkarana -karana_river-1 - -#frozenshadow -eerie_7 -eerie_0 -lavastorm-1 -templeoflife -qeynos_0-1 -gfaydark - -#gfaydark -entrance_fanfare -entrance_fanfare_1 -entrance_fanfare_1-1 -entrance_fanfare-1 -gfaydark -eerie_9 - -#gl -attack_0 -attack_1 -attack_2 -death -airplane_0-1 -gl_5 -gl_6 -crouching -underwater -gl_9 -gl_10 -gl_11 -gl_12 -bard_intro -bard_main -gl_15 -gl_16 -gl_17 -gl_18 -gl_19 -sea -merchant -gfaydark-5 -guildmaster - -#grobb -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - -#guktop -eerie_0 -eerie_1 -eerie_5 -eerie_7 -arpeggiated_runs -neriak_0-1 -eerie_3 -maidensfancy -hogcaller -entrance_fanfare_1 - -#halas -felwithe_0 -felwithe_1 -felwithe_2 -arena -hogcaller -maidensfancy -lionsmane -fishsale -cottage -eerie_4 - -#hateplane -felwithe_0 -felwithe_2 -felwithe_1 -arpeggiated_runs -bard-2 -neriak_0-1 - -#innothule -entrance_fanfare -entrance_fanfare_1 -eerie_3 -eerie_2 - -#kael -kael_0 -kael_1 -attack_1 -arena - -#kaladima -entrance_fanfare_1 -arena -nightchords -cascade -hogcaller -lionsmane -freportn -heroism -brass_harmonies-1 - -#lavastorm -lavastorm -eerie_1 - -#lfaydark -cascade -gfaydark-2 -entrance_fanfare_1-1 -gypsies-1 -eerie_5 -militia - -#mistmoore -cascade -eastkarana -eerie_1 -entrance_fanfare -felwithe_1 -felwithe_2 -felwithe_0 -neriak_0-1 -karana_river -bard-2 -brass_harmonies -string_fanfare -freportn-1 -nightchords -gfaydark-3 -heroism-1 - -#najena -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 - -#nektulos -lavastorm-1 - -#neriaka -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - -#neriakb -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - -#neriakc -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_4 -eerie_5 -eerie_6 -eerie_7 -eerie_8 -eerie_9 -eerie_10 -eerie_11 -neriak_0 -neriak_1 -neriak_2 -arpeggiated_runs -fishsale -maidensfancy - -#northkarana -cascade -eastkarana - -#nro -nro -entrance_fanfare_1 -freeport -gypsies -eerie_7 - -#opener -opener_0 - -#opener2 -eastkarana -character_select - -#opener3 -opener_3 -character_select - -#opener4 -eqtheme -character_select - -#paw -eerie_0 -eerie_1 -crouching - -#pickchar -character_select-1 - -#qcat -eerie_0 -eerie_1 -crouching - -#qey2hh1 -cottage -maidensfancy -karana_river - -#qeynos -arena -qeynos_gates -eerie_0 -eerie_1 -templeoflife -bard -eqtheme -gypsies -lionsmane -fishsale -cottage -freeport -qeynos_0 - -#qeynos2 -arena -qeynos_gates -eerie_0 -eerie_1 -templeoflife -bard-1 - -#qeytoqrg -arena -qeytoqrg -eerie_0 -eerie_1 -templeoflife -cottage -maidensfancy -entrance_fanfare - -#qrg -arena -qeytoqrg -templeoflife -qeynos_gates_brass - -#rathemtn -cascade -eastkarana -gypsies-2 -entrance_fanfare_1 -entrance_fanfare_1-1 -eerie_2 -eerie_3 - -#rivervale -rivervale - -#runnyeye -eerie_0 -eerie_1 -crouching - -#skyshrine -gl_12 -bard_intro -skyshrine -beethoven6 -nightchords -sea -gfaydark -gfaydark-2 - -#soldungb -lavastorm -eerie_10 -eerie_11 -eerie_0 -eerie_1 -eerie_2 -eerie_3 -eerie_6-1 - -#southkarana -cascade -eastkarana -eerie_2 -entrance_fanfare -maidensfancy -akanon-1 - -#steamfont -gfaydark-4 -entrance_fanfare -entrance_fanfare_1-1 -akanon-1 -lavastorm -eerie_3 -eerie_5 - -#templeveeshan -templeveeshan -gfaydark-5 -attack_1 -attack_0 - -#thurgadina -templeveeshan -attack_1 -attack_0 -thurgadina_0 -templeoflife -thurgadina_1 - -#thurgadinb -thurgadina_1 -thurgadinb - -#tox -entrance_fanfare_1 -karana_river -gypsies-1 -cascade -karana_river-1 - -#unrest -eastkarana -eerie_2 -eerie_1 -entrance_fanfare -felwithe_1 -felwithe_2 -neriak_0-1 -karana_river -brass_harmonies -string_fanfare -freportn-1 -gfaydark-3 -heroism-1 - -#velketor -velketor_0 -velketor_1 - -#wakening -wakening -gfaydark \ No newline at end of file diff --git a/LanternExtractor/EQ/Archive/PfsArchive.cs b/LanternExtractor/EQ/Archive/PfsArchive.cs index d9cf23d..a073f00 100644 --- a/LanternExtractor/EQ/Archive/PfsArchive.cs +++ b/LanternExtractor/EQ/Archive/PfsArchive.cs @@ -79,7 +79,9 @@ public override bool Initialize() inflatedSize += inflatedLength; } - if (crc == 0x61580AC9) + // EQZip saved archives use 0xFFFFFFFFU for filenames + // https://github.com/Shendare/EQZip/blob/b181ec7658ea9880984d58271cbab924ab8dd702/EQArchive.cs#L517 + if (crc == 0x61580AC9 || (crc == 0xFFFFFFFFU && fileNames.Count == 0)) { var dictionaryStream = new MemoryStream(fileBytes); var dictionary = new BinaryReader(dictionaryStream); diff --git a/LanternExtractor/EQ/ArchiveExtractor.cs b/LanternExtractor/EQ/ArchiveExtractor.cs index 5b5a7d5..45166be 100644 --- a/LanternExtractor/EQ/ArchiveExtractor.cs +++ b/LanternExtractor/EQ/ArchiveExtractor.cs @@ -30,21 +30,21 @@ public static void Extract(string path, string rootFolder, ILogger logger, Setti if (settings.RawS3dExtract) { - archive.WriteAllFiles(Path.Combine(rootFolder + shortName, archiveName)); + archive.WriteAllFiles(Path.Combine(rootFolder, archiveName)); return; } // For non WLD files, we can just extract their contents // Used for pure texture archives (e.g. bmpwad.s3d) and sound archives (e.g. snd1.pfs) - // The difference between this and the raw export is that it will convert images to .png + // The difference between this and the raw export is that it will convert images to PNG if (!archive.IsWldArchive) { WriteS3dTextures(archive, rootFolder + shortName, logger); - if (EqFileHelper.IsSoundArchive(archiveName)) + if (EqFileHelper.IsUsedSoundArchive(archiveName)) { - var soundFolder = settings.ExportSoundsToSingleFolder ? "sounds" : shortName; - WriteS3dSounds(archive, rootFolder + soundFolder, logger); + WriteS3dSounds(archive, + Path.Combine(rootFolder, settings.ExportSoundsToSingleFolder ? "sounds" : shortName), logger); } return; @@ -68,9 +68,10 @@ public static void Extract(string path, string rootFolder, ILogger logger, Setti { ExtractArchiveSky(rootFolder, logger, settings, wldFileInArchive, shortName, archive); } - else if (EqFileHelper.IsCharactersArchive(archiveName)) + else if (EqFileHelper.IsCharacterArchive(archiveName)) { - ExtractArchiveCharacters(path, rootFolder, logger, settings, archiveName, wldFileInArchive, shortName, archive); + ExtractArchiveCharacters(path, rootFolder, logger, settings, archiveName, wldFileInArchive, shortName, + archive); } else if (EqFileHelper.IsObjectsArchive(archiveName)) { @@ -82,7 +83,6 @@ public static void Extract(string path, string rootFolder, ILogger logger, Setti } MissingTextureFixer.Fix(archiveName); - } private static void ExtractArchiveZone(string path, string rootFolder, ILogger logger, Settings settings, @@ -104,7 +104,8 @@ private static void ExtractArchiveZone(string path, string rootFolder, ILogger l if (litWldLightsFileInArchive != null) { var lightsWldFile = - new WldFileLights(litWldLightsFileInArchive, shortName, WldType.Lights, logger, settings, wldFileLit); + new WldFileLights(litWldLightsFileInArchive, shortName, WldType.Lights, logger, settings, + wldFileLit); lightsWldFile.Initialize(rootFolder); } } @@ -120,6 +121,7 @@ private static void ExtractArchiveZone(string path, string rootFolder, ILogger l wldFile.RootFolder = rootFolder; wldFile.ShortName = shortName; } + InitializeWldAndWriteTextures(wldFile, rootFolder, rootFolder + shortName + "/Zone/Textures/", archive, settings, logger); @@ -141,7 +143,7 @@ private static void ExtractArchiveZone(string path, string rootFolder, ILogger l zoneObjectsWldFile.Initialize(rootFolder); } - ExtractSoundData(shortName, rootFolder, settings); + ExtractSoundData(shortName, rootFolder, logger, settings); } private static void ExtractArchiveObjects(string path, string rootFolder, ILogger logger, Settings settings, @@ -200,8 +202,8 @@ private static void ExtractArchiveCharacters(string path, string rootFolder, ILo archive, settings, logger); } - private static void ExtractArchiveSky(string rootFolder, ILogger logger, Settings settings, ArchiveFile wldFileInArchive, - string shortName, ArchiveBase archive) + private static void ExtractArchiveSky(string rootFolder, ILogger logger, Settings settings, + ArchiveFile wldFileInArchive, string shortName, ArchiveBase archive) { var wldFile = new WldFileZone(wldFileInArchive, shortName, WldType.Sky, logger, settings); InitializeWldAndWriteTextures(wldFile, rootFolder, rootFolder + shortName + "/Textures/", @@ -213,10 +215,10 @@ private static void ExtractArchiveEquipment(string rootFolder, ILogger logger, S { var wldFile = new WldFileEquipment(wldFileInArchive, shortName, WldType.Equipment, logger, settings); var exportPath = rootFolder + - (settings.ExportEquipmentToSingleFolder && - settings.ModelExportFormat == ModelExportFormat.Intermediate - ? "equipment/Textures/" - : shortName + "/Textures/"); + (settings.ExportEquipmentToSingleFolder && + settings.ModelExportFormat == ModelExportFormat.Intermediate + ? "equipment/Textures/" + : shortName + "/Textures/"); InitializeWldAndWriteTextures(wldFile, rootFolder, exportPath, archive, settings, logger); } @@ -307,15 +309,24 @@ public static void WriteWldTextures(ArchiveBase archive, WldFile wldFile, string } } - private static void ExtractSoundData(string shortName, string rootFolder, Settings settings) + private static void ExtractSoundData(string shortName, string rootFolder, ILogger logger, Settings settings) { + var envAudio = EnvAudio.Instance; + var ealFilePath = Path.Combine(settings.EverQuestDirectory, "defaults.dat"); + if (!envAudio.Load(ealFilePath)) + { + envAudio.Load(Path.ChangeExtension(ealFilePath, ".eal")); + } + var sounds = new EffSndBnk(settings.EverQuestDirectory + shortName + "_sndbnk" + LanternStrings.SoundFormatExtension); sounds.Initialize(); + var soundEntries = new EffSounds( - settings.EverQuestDirectory + shortName + "_sounds" + LanternStrings.SoundFormatExtension, sounds); - soundEntries.Initialize(); + settings.EverQuestDirectory + shortName + "_sounds" + LanternStrings.SoundFormatExtension, + sounds, envAudio); + soundEntries.Initialize(logger); soundEntries.ExportSoundData(shortName, rootFolder); } } diff --git a/LanternExtractor/EQ/ClientDataCopier.cs b/LanternExtractor/EQ/ClientDataCopier.cs index dbdb957..39c4488 100644 --- a/LanternExtractor/EQ/ClientDataCopier.cs +++ b/LanternExtractor/EQ/ClientDataCopier.cs @@ -6,7 +6,7 @@ namespace LanternExtractor.EQ { public static class ClientDataCopier { - public const string ClientDataDirectory = "clientdata"; + private const string ClientDataDirectory = "clientdata"; public static void Copy(string fileName, string rootFolder, ILogger logger, Settings settings) { diff --git a/LanternExtractor/EQ/MusicCopier.cs b/LanternExtractor/EQ/MusicCopier.cs new file mode 100644 index 0000000..013e24c --- /dev/null +++ b/LanternExtractor/EQ/MusicCopier.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.Linq; +using LanternExtractor.Infrastructure.Logger; + +namespace LanternExtractor.EQ +{ + public static class MusicCopier + { + private const string MusicDirectory = "music"; + + public static void Copy(string shortname, ILogger logger, Settings settings) + { + if (shortname != "music" && shortname != "all") + { + return; + } + + if (!settings.CopyMusic) + { + return; + } + + var xmiFiles = Directory.GetFiles(settings.EverQuestDirectory, "*.*", SearchOption.AllDirectories) + .Where(EqFileHelper.IsMusicFile).ToList(); + var destinationFolder = "Exports/" + MusicDirectory; + + if (!Directory.Exists(destinationFolder)) + { + Directory.CreateDirectory(destinationFolder); + } + + foreach (var xmi in xmiFiles) + { + var fileName = Path.GetFileName(xmi); + var destination = Path.Combine(destinationFolder, fileName); + if (File.Exists(destination)) + { + continue; + } + + File.Copy(xmi, destination); + } + } + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/AudioInstance.cs b/LanternExtractor/EQ/Sound/AudioInstance.cs new file mode 100644 index 0000000..7b1bbd8 --- /dev/null +++ b/LanternExtractor/EQ/Sound/AudioInstance.cs @@ -0,0 +1,85 @@ +namespace LanternExtractor.EQ.Sound +{ + public abstract class AudioInstance + { + protected AudioInstance(AudioType type, float posX, float posY, float posZ, float radius) + { + AudioType = type; + PosX = posX; + PosY = posY; + PosZ = posZ; + Radius = radius; + } + + public AudioType AudioType { get; } + public float PosX { get; } + public float PosY { get; } + public float PosZ { get; } + public float Radius { get; } + } + + public class MusicInstance : AudioInstance + { + public MusicInstance(AudioType type, float posX, float posY, float posZ, float radius, int trackIndexDay, + int trackIndexNight, int loopCountDay, int loopCountNight, int fadeOutMs) : base(type, posX, posY, posZ, + radius) + { + TrackIndexDay = trackIndexDay; + TrackIndexNight = trackIndexNight; + LoopCountDay = loopCountDay; + LoopCountNight = loopCountNight; + FadeOutMs = fadeOutMs; + } + + public int TrackIndexDay { get; } + public int TrackIndexNight { get; } + public int LoopCountDay { get; } + public int LoopCountNight { get; } + public int FadeOutMs { get; } + } + + public abstract class SoundInstance : AudioInstance + { + public string Sound1 { get; } + public float Volume1 { get; } + public int Cooldown1 { get; } + public int CooldownRandom { get; } + + protected SoundInstance(AudioType type, float posX, float posY, float posZ, float radius, float volume1, + string sound1, int cooldown1, int cooldownRandom) : base(type, posX, posY, posZ, radius) + { + Sound1 = sound1; + Volume1 = volume1; + Cooldown1 = cooldown1; + CooldownRandom = cooldownRandom; + } + } + + public class SoundInstance2d : SoundInstance + { + public string Sound2 { get; } + public float Volume2 { get; } + public int Cooldown2 { get; } + + public SoundInstance2d(AudioType type, float posX, float posY, float posZ, float radius, float volume1, + string sound1, int cooldown1, string sound2, int cooldown2, int cooldownRandom, float volume2) : base(type, posX, posY, posZ, radius, volume1, + sound1, cooldown1, cooldownRandom) + { + Sound2 = sound2; + Cooldown2 = cooldown2; + Volume2 = volume2; + } + } + + public class SoundInstance3d : SoundInstance + { + public int Multiplier; + + public SoundInstance3d(AudioType type, float posX, float posY, float posZ, float radius, float volume, + string sound1, int cooldown1, int cooldownRandom, int multiplier) : base(type, posX, posY, posZ, radius, volume, sound1, + cooldown1, cooldownRandom) + { + Multiplier = multiplier; + } + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/AudioType.cs b/LanternExtractor/EQ/Sound/AudioType.cs new file mode 100644 index 0000000..f58e840 --- /dev/null +++ b/LanternExtractor/EQ/Sound/AudioType.cs @@ -0,0 +1,23 @@ +namespace LanternExtractor.EQ.Sound +{ + /// + /// Describes the way the sound is heard by the player + /// + public enum AudioType : byte + { + /// + /// Sounds that play at a constant volume + /// + Sound2d = 0, + + /// + /// Music instance. Can specify both day and night trackID. + /// + Music = 1, + + /// + /// Sounds that have a falloff - the farther the player is from the center, the quieter it becomes. + /// + Sound3d = 2, + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/ClientSounds.cs b/LanternExtractor/EQ/Sound/ClientSounds.cs new file mode 100644 index 0000000..1470ac2 --- /dev/null +++ b/LanternExtractor/EQ/Sound/ClientSounds.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace LanternExtractor.EQ.Sound +{ + public static class ClientSounds + { + // Hardcoded client sounds - verified that no other references exist in Trilogy client + private static Dictionary _clientSounds = new Dictionary + { + { 39, "death_me" }, + { 143, "thunder1" }, + { 144, "thunder2" }, + { 158, "wind_lp1" }, + { 159, "rainloop" }, + { 160, "torch_lp" }, + { 161, "watundlp" }, + }; + + public static string GetClientSound(int index) + { + return _clientSounds.TryGetValue(index, out var soundName) ? soundName : SoundConstants.Unknown; + } + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/EffSndBnk.cs b/LanternExtractor/EQ/Sound/EffSndBnk.cs index 2f758d3..9122f07 100644 --- a/LanternExtractor/EQ/Sound/EffSndBnk.cs +++ b/LanternExtractor/EQ/Sound/EffSndBnk.cs @@ -10,11 +10,9 @@ namespace LanternExtractor.EQ.Sound /// public class EffSndBnk { - public readonly List EmitSounds = new List(); - - public readonly List LoopSounds = new List(); - - private readonly string _soundFilePath; + private List _emitSounds = new List(); + private List _loopSounds = new List(); + private string _soundFilePath; public EffSndBnk(string soundFilePath) { @@ -23,15 +21,14 @@ public EffSndBnk(string soundFilePath) public void Initialize() { - List currentList = null; - if (!File.Exists(_soundFilePath)) { return; } + + List currentList = null; string fileText = File.ReadAllText(_soundFilePath); - List parsedLines = TextParser.ParseTextByNewline(fileText); if (parsedLines == null || parsedLines.Count == 0) @@ -41,15 +38,18 @@ public void Initialize() foreach (var line in parsedLines) { + if (string.IsNullOrEmpty(line)) + { + continue; + } + switch (line) { - case "": + case SoundConstants.Emit: + currentList = _emitSounds; continue; - case "EMIT": - currentList = EmitSounds; - continue; - case "LOOP": - currentList = LoopSounds; + case SoundConstants.Loop: + currentList = _loopSounds; continue; default: currentList?.Add(line); @@ -57,5 +57,25 @@ public void Initialize() } } } + + private string GetValueFromList(int index, ref List list) + { + if (index < 0 || index >= list.Count) + { + return SoundConstants.Unknown; + } + + return list[index]; + } + + public string GetEmitSound(int index) + { + return GetValueFromList(index, ref _emitSounds); + } + + public string GetLoopSound(int index) + { + return GetValueFromList(index, ref _loopSounds); + } } } \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/EffSounds.cs b/LanternExtractor/EQ/Sound/EffSounds.cs index 336b3a6..8bbd0da 100644 --- a/LanternExtractor/EQ/Sound/EffSounds.cs +++ b/LanternExtractor/EQ/Sound/EffSounds.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; +using LanternExtractor.Infrastructure.Logger; namespace LanternExtractor.EQ.Sound { @@ -12,44 +13,38 @@ namespace LanternExtractor.EQ.Sound /// public class EffSounds { + public static int EntryLengthInBytes = 84; + /// /// The sound bank referenced for sound names /// private readonly EffSndBnk _soundBank; - + + private readonly EnvAudio _envAudio; private readonly string _soundFilePath; - - private readonly List _soundEntries = new List(); - - private readonly List _musicTrackEntries = new List(); - - private const int EntryLengthInBytes = 84; - - public EffSounds(string soundFilePath, EffSndBnk soundBank) + private readonly List _audioInstances = new List(); + + public EffSounds(string soundFilePath, EffSndBnk soundBank, EnvAudio envAudio) { _soundFilePath = soundFilePath; _soundBank = soundBank; + _envAudio = envAudio; } - - public void Initialize() + + public void Initialize(ILogger logger) { if (_soundBank == null || !File.Exists(_soundFilePath)) { return; } - FileStream file = File.Open(_soundFilePath, FileMode.Open); - - string zoneShortName = Path.GetFileNameWithoutExtension(_soundFilePath).Split('_')[0]; - - LoadMusicTrackNames(zoneShortName); - + var file = File.Open(_soundFilePath, FileMode.Open); var reader = new BinaryReader(file); - - int fileLength = (int) reader.BaseStream.Length; + int fileLength = (int)reader.BaseStream.Length; if (fileLength % EntryLengthInBytes != 0) { + logger.LogError($"Invalid .eff file - size must be multiple of {EntryLengthInBytes}"); return; } @@ -57,233 +52,191 @@ public void Initialize() for (int i = 0; i < entryCount; ++i) { - var newSound = new SoundEntry(); - newSound.UnkRef00 = reader.ReadInt32(); - newSound.UnkRef04 = reader.ReadInt32(); - newSound.Reserved = reader.ReadInt32(); - newSound.Sequence = reader.ReadInt32(); + var basePosition = EntryLengthInBytes * i; + reader.BaseStream.Position = basePosition + 16; + float posX = reader.ReadSingle(); + float posY = reader.ReadSingle(); + float posZ = reader.ReadSingle(); + float radius = reader.ReadSingle(); - newSound.PosX = reader.ReadSingle(); - newSound.PosY = reader.ReadSingle(); - newSound.PosZ = reader.ReadSingle(); - newSound.Radius = reader.ReadSingle(); + reader.BaseStream.Position = basePosition + 56; - newSound.CooldownDay = reader.ReadInt32(); - newSound.CooldownNight = reader.ReadInt32(); - newSound.RandomDelay = reader.ReadInt32(); + var typeByte = reader.ReadByte(); - newSound.Unk44 = reader.ReadInt32(); - int soundId1 = reader.ReadInt32(); - int soundId2 = reader.ReadInt32(); - - byte soundType = reader.ReadByte(); - newSound.SoundType = (SoundType) soundType; - - if (soundType == 0 || soundType == 2 || soundType == 3) - { - // Find the sound names - EmissionType newSoundEmissionType = newSound.EmissionType; - newSound.SoundIdDay = GetSoundString(soundId1, _soundBank, ref newSoundEmissionType); - EmissionType soundEmissionType = newSound.EmissionType; - newSound.SoundIdNight = GetSoundString(soundId2, _soundBank, ref soundEmissionType); - } - else + if (!Enum.IsDefined(typeof(AudioType), typeByte)) { - newSound.SoundIdDay = GetMusicTrackName(soundId1); - newSound.SoundIdNight = GetMusicTrackName(soundId1); + logger.LogError($"Unable to parse sound type: {typeByte}"); + continue; } - newSound.UnkPad57 = reader.ReadByte(); - newSound.UnkPad58 = reader.ReadByte(); - newSound.UnkPad59 = reader.ReadByte(); - - newSound.AsDistance = reader.ReadInt32(); - newSound.UnkRange64 = reader.ReadInt32(); - newSound.FadeOutMs = reader.ReadInt32(); - newSound.UnkRange72 = reader.ReadInt32(); - newSound.FullVolRange = reader.ReadInt32(); - newSound.UnkRange80 = reader.ReadInt32(); + var type = (AudioType)typeByte; + reader.BaseStream.Position = basePosition + 48; + int soundId1 = reader.ReadInt32(); + string sound1 = GetSoundName(soundId1); - if (newSound.SoundIdDay != "" || newSound.SoundIdNight != "") + if (type == AudioType.Music) { - _soundEntries.Add(newSound); + int soundId2 = reader.ReadInt32(); + reader.BaseStream.Position = basePosition + 60; + int loopCountDay = reader.ReadInt32(); + int loopCountNight = reader.ReadInt32(); + int fadeOutMs = reader.ReadInt32(); + var musicInstance = new MusicInstance(type, posX, posY, posZ, radius, soundId1, soundId2, + loopCountDay, loopCountNight, fadeOutMs); + _audioInstances.Add(musicInstance); } - } - } - - private void LoadMusicTrackNames(string zoneShortName) - { - string[] trackLines = File.ReadAllLines("ClientData/musictracks.txt"); - - bool isTargetZone = false; - - foreach (string line in trackLines) - { - if (!isTargetZone) + else if (type == AudioType.Sound2d) { - if (line == "#" + zoneShortName) - { - isTargetZone = true; - } - - continue; + int soundId2 = reader.ReadInt32(); + string sound2 = GetSoundName(soundId2); + reader.BaseStream.Position = basePosition + 32; + int cooldown1 = reader.ReadInt32(); + int cooldown2 = reader.ReadInt32(); + int cooldownRandom = reader.ReadInt32(); + reader.BaseStream.Position = basePosition + 60; + int volume1Raw = reader.ReadInt32(); + float volume1 = GetEalSoundVolume(sound1, volume1Raw); + int volume2Raw = reader.ReadInt32(); + float volume2 = GetEalSoundVolume(sound2, volume2Raw); + var soundInstance = new SoundInstance2d(type, posX, posY, posZ, radius, volume1, sound1, cooldown1, + sound2, cooldown2, cooldownRandom, volume2); + _audioInstances.Add(soundInstance); } - - if (line == string.Empty) + else { - break; + reader.BaseStream.Position = basePosition + 32; + int cooldown1 = reader.ReadInt32(); + reader.BaseStream.Position = basePosition + 40; + int cooldownRandom = reader.ReadInt32(); + reader.BaseStream.Position = basePosition + 60; + int volumeRaw = reader.ReadInt32(); + float volume = GetEalSoundVolume(sound1, volumeRaw); + reader.BaseStream.Position = basePosition + 72; + int multiplier = reader.ReadInt32(); + var soundInstance = new SoundInstance3d(type, posX, posY, posZ, radius, volume, sound1, + cooldown1, cooldownRandom, multiplier); + _audioInstances.Add(soundInstance); } - - _musicTrackEntries.Add(line); } } - /// - /// Returns the name of the sound based on either the internal reference or the sound back definitions - /// The client uses specific integer ranges to identify the type. - /// There are also a handful of hardcoded sound ids. - /// - /// The id index of the sound - /// The sound bank in which to look - /// The emission type - /// The name of the sound - private string GetSoundString(int soundId, EffSndBnk soundBank, ref EmissionType soundType) + private string GetSoundName(int soundId) { - if (soundId == 0) + var emissionType = GetEmissionType(soundId); + switch (emissionType) { - return string.Empty; + case EmissionType.None: + return string.Empty; + case EmissionType.Emit: + return _soundBank.GetEmitSound(soundId - 1); + case EmissionType.Loop: + return _soundBank.GetLoopSound(soundId - 162); + case EmissionType.Internal: + return ClientSounds.GetClientSound(soundId); + default: + return SoundConstants.Unknown; } + } - if (soundId >= 1 && soundId < 32 && soundId < soundBank.EmitSounds.Count) + private float GetEalSoundVolume(string soundName, int volumeRaw) + { + if (volumeRaw > 0) { - soundType = EmissionType.Emit; - return soundBank.EmitSounds[soundId - 1]; + volumeRaw = -volumeRaw; } - // Hardcoded client sounds - verified that no other references exist in Trilogy client - if (soundId >= 32 && soundId < 162) - { - soundType = EmissionType.Internal; + return volumeRaw == 0 ? _envAudio.GetVolumeLinear(soundName) : _envAudio.GetVolumeLinear(volumeRaw); + } - switch (soundId) - { - case 39: - return "death_me"; - case 143: - return "thunder1"; - case 144: - return "thunder2"; - case 158: - return "wind_lp1"; - case 159: - return "rainloop"; - case 160: - return "torch_lp"; - case 161: - return "watundlp"; - } + private EmissionType GetEmissionType(int soundId) + { + if (soundId <= 0) + { + return EmissionType.None; } - if (soundId < 162 || soundId >= 162 + soundBank.LoopSounds.Count) + if (soundId < 32) { - return string.Empty; + return EmissionType.Emit; } - - soundType = EmissionType.Loop; - return soundBank.LoopSounds[soundId - 161 - 1]; + return soundId < 162 ? EmissionType.Internal : EmissionType.Loop; } - + public void ExportSoundData(string zoneName, string rootFolder) { - var soundExport = new StringBuilder(); + var sound2dExport = new StringBuilder(); + var sound3dExport = new StringBuilder(); var musicExport = new StringBuilder(); - foreach (SoundEntry entry in _soundEntries) + foreach (var entry in _audioInstances) { - if (entry.SoundType == SoundType.Music) + if (entry.AudioType == AudioType.Music) { - musicExport.Append(entry.PosX); - musicExport.Append(","); - musicExport.Append(entry.PosZ); - musicExport.Append(","); - musicExport.Append(entry.PosY); - musicExport.Append(","); - musicExport.Append(entry.Radius); - musicExport.Append(","); - musicExport.Append(entry.SoundIdDay); - musicExport.Append(","); - musicExport.Append(entry.SoundIdNight); - musicExport.Append(","); - musicExport.Append(entry.AsDistance); // Day loop count - musicExport.Append(","); - musicExport.Append(entry.UnkRange64); // Night loop count - musicExport.Append(","); - musicExport.Append(entry.FadeOutMs); - musicExport.AppendLine(); + if (!(entry is MusicInstance music)) + { + continue; + } + + musicExport.AppendLine(string.Join(",", music.PosX, music.PosZ, music.PosY, music.Radius, + music.TrackIndexDay, music.TrackIndexNight, music.LoopCountDay, music.LoopCountNight, + music.FadeOutMs)); + } + else if (entry.AudioType == AudioType.Sound2d) + { + if (!(entry is SoundInstance2d sound2d)) + { + continue; + } + + sound2dExport.AppendLine(string.Join(",", sound2d.PosX, sound2d.PosZ, + sound2d.PosY, sound2d.Radius, sound2d.Sound1, sound2d.Sound2, + sound2d.Cooldown1, sound2d.Cooldown2, sound2d.CooldownRandom, sound2d.Volume1, sound2d.Volume2)); } else { - soundExport.Append((int)entry.SoundType); - soundExport.Append(","); - soundExport.Append(entry.PosX); - soundExport.Append(","); - soundExport.Append(entry.PosZ); - soundExport.Append(","); - soundExport.Append(entry.PosY); - soundExport.Append(","); - soundExport.Append(entry.Radius); - soundExport.Append(","); - soundExport.Append(entry.SoundIdDay); - soundExport.Append(","); - soundExport.Append(entry.SoundIdNight); - soundExport.Append(","); - soundExport.Append(entry.CooldownDay); - soundExport.Append(","); - soundExport.Append(entry.CooldownNight); - soundExport.Append(","); - soundExport.Append(entry.RandomDelay); - soundExport.AppendLine(); + if (!(entry is SoundInstance3d sound3d)) + { + continue; + } + + sound3dExport.AppendLine(string.Join(",", sound3d.PosX, sound3d.PosZ, + sound3d.PosY, sound3d.Radius, sound3d.Sound1, sound3d.Cooldown1, sound3d.CooldownRandom, sound3d.Volume1, + sound3d.Multiplier)); } } - string exportPath = rootFolder + zoneName + "/Zone/"; + string exportPath = Path.Combine(rootFolder, zoneName, "Zone/"); - if (soundExport.Length != 0) + if (musicExport.Length > 0) { StringBuilder exportHeader = new StringBuilder(); - exportHeader.AppendLine(LanternStrings.ExportHeaderTitle + "Sound Instances"); - exportHeader.AppendLine("# Format: SoundType, PosX, PosY, PosZ, Radius, SoundIdDay, SoundIdNight, CooldownDay, CooldownNight, RandomDelay"); - + exportHeader.AppendLine(LanternStrings.ExportHeaderTitle + "Music Instances"); + exportHeader.AppendLine( + "# Format: PosX, PosY, PosZ, Radius, MusicIndexDay, MusicIndexNight, LoopCountDay, LoopCountNight, FadeOutMs"); Directory.CreateDirectory(exportPath); - File.WriteAllText(exportPath + "sound_instances.txt", exportHeader.ToString() + soundExport); + File.WriteAllText(exportPath + "music_instances.txt", exportHeader.ToString() + musicExport); } - if (musicExport.Length != 0) + if (sound2dExport.Length > 0) { StringBuilder exportHeader = new StringBuilder(); - exportHeader.AppendLine(LanternStrings.ExportHeaderTitle + "Music Instances"); - exportHeader.AppendLine("# Format: PosX, PosY, PosZ, Radius, SoundIdDay, SoundIdNight, DayLoopCount, NightLoopCount, FadeOutMs"); - + exportHeader.AppendLine(LanternStrings.ExportHeaderTitle + "Sound 2D Instances"); + exportHeader.AppendLine( + "# Format: PosX, PosY, PosZ, Radius, SoundNameDay, SoundNameNight, CooldownDay, CooldownNight, CooldownRandom, VolumeDay, VolumeNight"); Directory.CreateDirectory(exportPath); - File.WriteAllText(exportPath + "music_instances.txt", exportHeader.ToString() + musicExport); + File.WriteAllText(exportPath + "sound2d_instances.txt", exportHeader.ToString() + sound2dExport); } - } - /// - /// Gets the name of the music track if it exists - /// - /// The zone shortname - /// The index of the track - /// The name of the track - private string GetMusicTrackName(int index) - { - if (index < 0 || index >= _musicTrackEntries.Count) + if (sound3dExport.Length > 0) { - return "Unknown"; + StringBuilder exportHeader = new StringBuilder(); + exportHeader.AppendLine(LanternStrings.ExportHeaderTitle + "Sound 3D Instances"); + exportHeader.AppendLine( + "# Format: PosX, PosY, PosZ, Radius, SoundName, Cooldown, CooldownRandom, Volume, Multiplier"); + Directory.CreateDirectory(exportPath); + File.WriteAllText(exportPath + "sound3d_instances.txt", exportHeader.ToString() + sound3dExport); } - - return _musicTrackEntries[index]; } } -} +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/EmissionType.cs b/LanternExtractor/EQ/Sound/EmissionType.cs index fc604b7..ec0543f 100644 --- a/LanternExtractor/EQ/Sound/EmissionType.cs +++ b/LanternExtractor/EQ/Sound/EmissionType.cs @@ -2,19 +2,21 @@ { public enum EmissionType { + None = 0, + /// /// Emitted sounds - things like bird noises /// - Emit = 0, + Emit = 1, /// /// Looped sounds - things like the ocean or a lake /// - Loop = 1, + Loop = 2, /// /// Sounds that are internal to the client /// - Internal = 2 + Internal = 3 } } \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/EnvAudio.cs b/LanternExtractor/EQ/Sound/EnvAudio.cs new file mode 100644 index 0000000..a058fbc --- /dev/null +++ b/LanternExtractor/EQ/Sound/EnvAudio.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using EalTools; + +namespace LanternExtractor.EQ.Sound +{ + public class EnvAudio + { + public static EnvAudio Instance => _instance; + private static readonly EnvAudio _instance = new EnvAudio(); + + public EalData Data { get; private set; } + + private bool _loaded; + private string _ealFilePath; + private EalFile _ealFile; + private Dictionary _sourceLevels; + + static EnvAudio() + { + } + + private EnvAudio() + { + } + + public bool Load(string ealFilePath) + { + if (_ealFilePath == ealFilePath) + { + return _loaded; + } + + if (ealFilePath == null || !File.Exists(ealFilePath)) + { + return false; + } + + _ealFilePath = ealFilePath; + + // Allow other threads to open the same file for reading + _ealFile = new EalFile(new FileStream( + _ealFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)); + + if (!_ealFile.Initialize()) + { + return false; + } + + Data = _ealFile.Data; + + if (Data == null) + { + return false; + } + + _sourceLevels = Data.SourceModels.ToDictionary( + s => Path.GetFileNameWithoutExtension(s.Name).ToLower(), + s => s.SourceAttributes.EaxAttributes.DirectPathLevel + ); + + _loaded = true; + + return _loaded; + } + + private int GetVolumeEq(string soundFile) + { + var volume = 0; + _sourceLevels?.TryGetValue(soundFile, out volume); + return volume; + } + + public float GetVolumeLinear(string soundFile) + { + var volumeEq = GetVolumeEq(soundFile); + return GetVolumeLinear(volumeEq); + } + + public float GetVolumeLinear(int directAudioLevel) + { + var linear = MathF.Pow(10.0f, directAudioLevel / 2000.0f); + return Math.Clamp(linear, 0f, 1f); + } + } +} diff --git a/LanternExtractor/EQ/Sound/SoundConstants.cs b/LanternExtractor/EQ/Sound/SoundConstants.cs new file mode 100644 index 0000000..e4ff969 --- /dev/null +++ b/LanternExtractor/EQ/Sound/SoundConstants.cs @@ -0,0 +1,9 @@ +namespace LanternExtractor.EQ.Sound +{ + public class SoundConstants + { + public const string Emit = "EMIT"; + public const string Loop = "LOOP"; + public const string Unknown = "Unknown"; + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/SoundEntry.cs b/LanternExtractor/EQ/Sound/SoundEntry.cs deleted file mode 100644 index 1e13a52..0000000 --- a/LanternExtractor/EQ/Sound/SoundEntry.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace LanternExtractor.EQ.Sound -{ - /// - /// Contains information about a single sound instance in the world - /// Big thanks to Shendare from the EQEmu forums for sharing this information - /// More documentation will come as I verify all of this info - /// - public class SoundEntry - { - public EmissionType EmissionType { get; set; } - public int UnkRef00 { get; set; } - public int UnkRef04 { get; set; } - public int Reserved { get; set; } - public int Sequence { get; set; } - public float PosX { get; set; } - public float PosY { get; set; } - public float PosZ { get; set; } - public float Radius { get; set; } - public int CooldownDay { get; set; } - public int CooldownNight { get; set; } - public int RandomDelay { get; set; } - public int Unk44 { get; set; } - public string SoundIdDay { get; set; } - public string SoundIdNight { get; set; } - public SoundType SoundType { get; set; } - public byte UnkPad57 { get; set; } - public byte UnkPad58 { get; set; } - public byte UnkPad59 { get; set; } - public int AsDistance { get; set; } - public int UnkRange64 { get; set; } - public int FadeOutMs { get; set; } - public int UnkRange72 { get; set; } - public int FullVolRange { get; set; } - public int UnkRange80 { get; set; } - } -} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/SoundTest.cs b/LanternExtractor/EQ/Sound/SoundTest.cs new file mode 100644 index 0000000..b70bccf --- /dev/null +++ b/LanternExtractor/EQ/Sound/SoundTest.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Linq; + +namespace LanternExtractor.EQ.Sound +{ + /// + /// A small helper class useful for sound isolation and testing + /// + public static class SoundTest + { + public static void OutputSingleInstance(BinaryWriter writer, int index, string fileName) + { + writer.BaseStream.Position = 0; + var memoryStream = new MemoryStream(); + writer.BaseStream.CopyTo(memoryStream); + var bytes = memoryStream.ToArray().Skip(index * EffSounds.EntryLengthInBytes) + .Take(EffSounds.EntryLengthInBytes).ToArray(); + File.WriteAllBytes(fileName, bytes); + } + + public static void ModifyInstance(BinaryWriter writer, string fileName) + { + writer.BaseStream.Position = 16; // positions + writer.Write(0f); + writer.Write(0f); + writer.Write(50f); + + + writer.BaseStream.Position = 0; + var memoryStream = new MemoryStream(); + writer.BaseStream.CopyTo(memoryStream); + File.WriteAllBytes(fileName, memoryStream.ToArray()); + } + } +} \ No newline at end of file diff --git a/LanternExtractor/EQ/Sound/SoundType.cs b/LanternExtractor/EQ/Sound/SoundType.cs deleted file mode 100644 index 7743a76..0000000 --- a/LanternExtractor/EQ/Sound/SoundType.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace LanternExtractor.EQ.Sound -{ - /// - /// Describes the way the sound is heard by the player - /// - public enum SoundType : byte - { - /// - /// Sounds that play at a constant volume - /// - NoFalloff = 0, - - /// - /// Background music. Can specify both a daytime and nighttime music via the soundID - /// Music usually has a large fade out delay - /// - Music = 1, - - /// - /// Sounds that have a falloff - the further the player is from the center, the quieter it becomes - /// - Falloff = 2, - - /// - /// Sounds that stay at full volume until the user has wandered outside of the FullVolRange - /// - FullVolumeRange = 3, - } -} \ No newline at end of file diff --git a/LanternExtractor/EQ/Wld/Fragments/Mesh.cs b/LanternExtractor/EQ/Wld/Fragments/Mesh.cs index f8d7494..669ba5b 100644 --- a/LanternExtractor/EQ/Wld/Fragments/Mesh.cs +++ b/LanternExtractor/EQ/Wld/Fragments/Mesh.cs @@ -45,7 +45,7 @@ public class Mesh : WldFragment /// The vertices of the mesh /// public List Vertices { get; set; } - + /// /// The normals of the mesh /// @@ -55,7 +55,7 @@ public class Mesh : WldFragment /// The polygon indices of the mesh /// public List Indices { get; private set; } - + public List Colors { get; set; } /// @@ -123,9 +123,9 @@ public override void Initialize(int index, int size, byte[] data, // Seems to be related to lighting models? (torches, etc.) if (unknownDword1 != 0 || unknownDword2 != 0 || unknownDword3 != 0) { - + } - + MaxDistance = Reader.ReadSingle(); MinPosition = new vec3(Reader.ReadSingle(), Reader.ReadSingle(), Reader.ReadSingle()); MaxPosition = new vec3(Reader.ReadSingle(), Reader.ReadSingle(), Reader.ReadSingle()); @@ -145,7 +145,7 @@ public override void Initialize(int index, int size, byte[] data, short vertexTextureCount = Reader.ReadInt16(); short size9 = Reader.ReadInt16(); float scale = 1.0f / (1 << Reader.ReadInt16()); - + for (int i = 0; i < vertexCount; ++i) { Vertices.Add(new vec3(Reader.ReadInt16() * scale, Reader.ReadInt16() * scale, @@ -156,7 +156,7 @@ public override void Initialize(int index, int size, byte[] data, { if (isNewWldFormat) { - TextureUvCoordinates.Add(new vec2(Reader.ReadInt32() / 256.0f, Reader.ReadInt32() / 256.0f)); + TextureUvCoordinates.Add(new vec2(Reader.ReadSingle(), Reader.ReadSingle())); } else { @@ -179,7 +179,7 @@ public override void Initialize(int index, int size, byte[] data, int g = colorBytes[1]; int r = colorBytes[2]; int a = colorBytes[3]; - + Colors.Add(new Color( r, g, b, a)); } @@ -202,7 +202,7 @@ public override void Initialize(int index, int size, byte[] data, Vertex3 = Reader.ReadInt16(), }); } - + MobPieces = new Dictionary(); int mobStart = 0; @@ -224,7 +224,7 @@ public override void Initialize(int index, int size, byte[] data, MaterialGroups = new List(); StartTextureIndex = Int32.MaxValue; - + for (int i = 0; i < polygonTextureCount; ++i) { var group = new RenderGroup(); @@ -247,7 +247,7 @@ public override void Initialize(int index, int size, byte[] data, { Reader.BaseStream.Position += 12; } - + // In some rare cases, the number of uvs does not match the number of vertices if (Vertices.Count != TextureUvCoordinates.Count) { @@ -291,4 +291,4 @@ public void ClearCollision() ExportSeparateCollision = true; } } -} \ No newline at end of file +} diff --git a/LanternExtractor/EQ/Wld/Helpers/FragmentNameCleaner.cs b/LanternExtractor/EQ/Wld/Helpers/FragmentNameCleaner.cs index 8df4857..ac2cf36 100644 --- a/LanternExtractor/EQ/Wld/Helpers/FragmentNameCleaner.cs +++ b/LanternExtractor/EQ/Wld/Helpers/FragmentNameCleaner.cs @@ -28,13 +28,13 @@ public static string CleanName(WldFragment fragment, bool toLower = true) { cleanedName = cleanedName.Replace(_prefixes[fragment.GetType()], string.Empty); } - + if(toLower) { cleanedName = cleanedName.ToLower(); } - - return cleanedName; + + return cleanedName.Trim(); } } -} \ No newline at end of file +} diff --git a/LanternExtractor/EQ/Wld/WldFile.cs b/LanternExtractor/EQ/Wld/WldFile.cs index c116d7d..fd32127 100644 --- a/LanternExtractor/EQ/Wld/WldFile.cs +++ b/LanternExtractor/EQ/Wld/WldFile.cs @@ -23,11 +23,6 @@ public abstract class WldFile public WldType WldType => _wldType; - /// - /// The link between fragment types and fragment classes - /// - private Dictionary> _fragmentBuilder; - /// /// A link of indices to fragments /// @@ -159,8 +154,6 @@ public virtual bool Initialize(string rootFolder, bool exportData = true) ParseStringHash(WldStringDecoder.DecodeString(stringHash)); - long readPosition = 0; - for (int i = 0; i < fragmentCount; ++i) { uint fragSize = reader.ReadUInt32(); diff --git a/LanternExtractor/EqFileHelper.cs b/LanternExtractor/EqFileHelper.cs index 0fb62c9..6cd482c 100644 --- a/LanternExtractor/EqFileHelper.cs +++ b/LanternExtractor/EqFileHelper.cs @@ -6,57 +6,70 @@ namespace LanternExtractor { public static class EqFileHelper { - public static bool IsEquipmentArchive(string archiveName) + public static List GetValidEqFilePaths(string directory, string archiveName) { - return archiveName.StartsWith("gequip"); + archiveName = archiveName.ToLower(); + var eqFiles = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories); + List validFiles; + + switch (archiveName) + { + case "all": + validFiles = GetAllValidFiles(eqFiles); + break; + case "zones": + validFiles = GetValidZoneFiles(eqFiles); + break; + case "characters": + validFiles = GetValidCharacterFiles(eqFiles); + break; + case "equipment": + validFiles = GetValidEquipmentFiles(eqFiles); + break; + case "sounds": + validFiles = GetValidSoundFiles(eqFiles); + break; + default: + { + validFiles = GetValidFiles(archiveName, directory); + break; + } + } + + return validFiles; } - public static bool IsCharactersArchive(string archiveName) + private static List GetValidEquipmentFiles(string[] eqFiles) { - return archiveName.Contains("_chr") || archiveName.StartsWith("chequip") || archiveName.Contains("_amr"); + return eqFiles.Where(x => IsEquipmentArchive(Path.GetFileName(x))).ToList(); } - public static bool IsObjectsArchive(string archiveName) + private static List GetAllValidFiles(string[] eqFiles) { - return archiveName.Contains("_obj"); + return eqFiles.Where(x => IsValidArchive(Path.GetFileName(x))).ToList(); } - public static bool IsSkyArchive(string archiveName) + private static List GetValidZoneFiles(string[] eqFiles) { - return archiveName == "sky"; + return eqFiles.Where(x => IsZoneArchive(Path.GetFileName(x))).ToList(); } - public static bool IsSoundArchive(string archiveName) + private static List GetValidCharacterFiles(string[] eqFiles) { - return archiveName.StartsWith("snd"); + return eqFiles.Where(x => IsCharacterArchive(Path.GetFileName(x))).ToList(); } - public static bool IsClientDataFile(string archiveName) + private static List GetValidSoundFiles(string[] eqFiles) { - return archiveName == "clientdata"; + return eqFiles.Where(x => IsSoundArchive(Path.GetFileName(x))).ToList(); } - public static List GetValidEqFilePaths(string directory, string archiveName) + private static List GetValidFiles(string archiveName, string directory) { - archiveName = archiveName.ToLower(); - var validFiles = new List(); - - if (archiveName == "all") - { - validFiles = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories) - .Where(s => (s.EndsWith(".s3d") || s.EndsWith(".pfs") || s.EndsWith(".t3d")) && !s.Contains("chequip") && - !s.EndsWith("_lit.s3d")).ToList(); - } - else if (archiveName == "equipment") - { - validFiles = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories) - .Where(s => (s.EndsWith(".s3d") || s.EndsWith(".pfs") || s.EndsWith(".t3d")) && s.Contains("gequip")).ToList(); - } - else if (archiveName.EndsWith(".s3d") || archiveName.EndsWith(".pfs") || archiveName.EndsWith(".t3d")) + if (archiveName.EndsWith(".s3d") || archiveName.EndsWith(".pfs") || archiveName.EndsWith(".t3d")) { - string archivePath = directory + archiveName; - + string archivePath = Path.Combine(directory, archiveName); if (File.Exists(archivePath)) { validFiles.Add(archivePath); @@ -64,13 +77,11 @@ public static List GetValidEqFilePaths(string directory, string archiveN } else { - // If it's the shortname of a PFS file, we assume it's standalone - used for sound files - string archivePath = directory + archiveName + LanternStrings.PfsFormatExtension; + string archivePath = Path.Combine(directory, archiveName, LanternStrings.PfsFormatExtension); if (File.Exists(archivePath)) { validFiles.Add(archivePath); - return validFiles; } @@ -80,28 +91,28 @@ public static List GetValidEqFilePaths(string directory, string archiveN archiveExtension = LanternStrings.T3dFormatExtension; } - // Try and find all associated files with the shortname - can theoretically be a non zone file - string mainArchivePath = directory + archiveName + archiveExtension; + // Try and find all associated files with the shortname - can theoretically be a non-zone file + string mainArchivePath = Path.Combine(directory, archiveName, archiveExtension); if (File.Exists(mainArchivePath)) { validFiles.Add(mainArchivePath); } // Some zones have additional object archives for things added past their initial release - // None of them contain fragments that are linked to other related archives. - string extensionObjectsArchivePath = directory + archiveName + "_2_obj" + archiveExtension; + // No archives contain cross archive fragment references + string extensionObjectsArchivePath = Path.Combine(directory, $"{archiveName}_2_obj", archiveExtension); if (File.Exists(extensionObjectsArchivePath)) { validFiles.Add(extensionObjectsArchivePath); } - string objectsArchivePath = directory + archiveName + "_obj" + archiveExtension; + string objectsArchivePath = Path.Combine(directory, $"{archiveName}_obj", archiveExtension); if (File.Exists(objectsArchivePath)) { validFiles.Add(objectsArchivePath); } - string charactersArchivePath = directory + archiveName + "_chr" + archiveExtension; + string charactersArchivePath = Path.Combine(directory, $"{archiveName}_chr", archiveExtension); if (File.Exists(charactersArchivePath)) { validFiles.Add(charactersArchivePath); @@ -110,7 +121,7 @@ public static List GetValidEqFilePaths(string directory, string archiveN // Some zones have additional character archives for things added past their initial release // None of them contain fragments that are linked to other related archives. // "qeynos" must be excluded because both qeynos and qeynos2 are used as shortnames - string extensionCharactersArchivePath = directory + archiveName + "2_chr" + archiveExtension; + string extensionCharactersArchivePath = Path.Combine(directory, $"{archiveName}2_chr", archiveExtension); if (File.Exists(extensionCharactersArchivePath) && archiveName != "qeynos") { validFiles.Add(extensionCharactersArchivePath); @@ -132,5 +143,79 @@ public static string ObjArchivePath(string archivePath) return archivePath; } + + private static bool IsValidArchive(string archiveName) + { + return archiveName.EndsWith(".s3d") || archiveName.EndsWith(".t3d") || archiveName.EndsWith(".pfs") + && !archiveName.Contains("chequip") && !archiveName.EndsWith("_lit.s3d"); + } + + private static bool IsZoneArchive(string archiveName) + { + return IsValidArchive(archiveName) && !IsEquipmentArchive(archiveName) && !IsSkyArchive(archiveName) && + !IsBitmapArchive(archiveName) && !IsCharacterArchive(archiveName); + } + + public static bool IsEquipmentArchive(string archiveName) + { + return archiveName.StartsWith("gequip"); + } + + public static bool IsCharacterArchive(string archiveName) + { + return archiveName.Contains("_chr") || archiveName.StartsWith("chequip") || + archiveName.Contains("_amr"); + } + + public static bool IsObjectsArchive(string archiveName) + { + return archiveName.Contains("_obj"); + } + + public static bool IsSkyArchive(string archiveName) + { + return archiveName == "sky"; + } + + public static bool IsBitmapArchive(string archiveName) + { + return archiveName.StartsWith("bmpwad"); + } + + public static bool IsSoundArchive(string archiveName) + { + return archiveName.StartsWith("snd"); + } + + public static bool IsClientDataFile(string archiveName) + { + return archiveName == "clientdata"; + } + + public static bool IsMusicFile(string filename) + { + return filename.EndsWith(".xmi"); + } + + public static bool IsSpecialCaseExtraction(string archiveName) + { + return archiveName == "clientdata" || archiveName == "music"; + } + + public static bool IsUsedSoundArchive(string archiveName) + { + if (!IsSoundArchive(archiveName)) + { + return false; + } + + // Trilogy client does not use archives higher than snd9 + if (int.TryParse(archiveName.Substring(archiveName.Length - 2), out _)) + { + return false; + } + + return true; + } } } diff --git a/LanternExtractor/Infrastructure/EqBmp.cs b/LanternExtractor/Infrastructure/EqBmp.cs new file mode 100644 index 0000000..cc043ae --- /dev/null +++ b/LanternExtractor/Infrastructure/EqBmp.cs @@ -0,0 +1,121 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +namespace LanternExtractor.Infrastructure +{ + /// + /// Wrapper over Bitmap with various hacks to make libgdiplus + /// consistently create transparent pngs on MacOS & Linux + /// + public class EqBmp + { + private static readonly bool _needsGdipHacks = Environment.OSVersion.Platform == PlatformID.MacOSX || + Environment.OSVersion.Platform == PlatformID.Unix; + private static bool _hasCheckedForPaletteFlagsField; + private static System.Reflection.FieldInfo _paletteFlagsField = null; + + public PixelFormat PixelFormat => _bitmap.PixelFormat; + + private readonly Bitmap _bitmap; + private readonly ColorPalette _palette; + + public EqBmp(Stream stream) + { + SetPaletteFlagsField(); + + _bitmap = new Bitmap(stream); + _palette = _bitmap.Palette; + } + + public void WritePng(string outputFilePath) + { + _bitmap.Save(outputFilePath, ImageFormat.Png); + } + + public void MakeMagentaTransparent() + { + _bitmap.MakeTransparent(Color.Magenta); + if (_needsGdipHacks) + { + // https://github.com/ mono/libgdiplus/commit/bf9a1440b7bfea704bf2cb771f5c2b5c09e7bcfa + _bitmap.MakeTransparent(Color.FromArgb(0, Color.Magenta)); + } + } + + public void MakePaletteTransparent(int transparentIndex) + { + if (_needsGdipHacks) + { + // https://github.com/mono/libgdiplus/issues/702 + _paletteFlagsField?.SetValue(_palette, _palette.Flags | (int)PaletteFlags.HasAlpha); + } + + var transparentColor = GetTransparentPaletteColor(); + _palette.Entries[transparentIndex] = transparentColor; + _bitmap.Palette = _palette; + + if (_needsGdipHacks) + { + // Due to a bug with the libgdiplus implementation of System.Drawing, setting a color palette + // entry to transparent does not work. The workaround is to ensure that the transparent + // key is unique and then use MakeTransparent() + _bitmap.MakeTransparent(transparentColor); + } + } + + private Color GetTransparentPaletteColor() + { + var transparencyColor = Color.FromArgb(0, 0, 0, 0); + + if (!_needsGdipHacks) + { + return transparencyColor; + } + + var random = new Random(); + var foundUnique = false; + + while (!foundUnique) + { + foundUnique = _palette.Entries.All(e => e != transparencyColor); + transparencyColor = Color.FromArgb(0, random.Next(256), random.Next(256), random.Next(256)); + } + + return transparencyColor; + } + + // https://github.com/Robmaister/SharpFont/blob/422bdab059dd8e594b4b061a3b53152e71342ce2/Source/SharpFont.GDI/FTBitmapExtensions.cs + // https://github.com/Robmaister/SharpFont/pull/136 + private static void SetPaletteFlagsField() + { + if (!_needsGdipHacks || _hasCheckedForPaletteFlagsField) + { + return; + } + + _hasCheckedForPaletteFlagsField = true; + + // The field needed may be named "flags" or "_flags", dependin on the version of Mono. To be thorough, check for the first Name that contains "lags". + var fields = typeof(ColorPalette).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + for (int i = 0; i < fields.Length; i++) + { + if (fields[i].Name.Contains("lags")) + { + _paletteFlagsField = fields[i]; + break; + } + } + } + + enum PaletteFlags + { + HasAlpha = 0x0001, + GrayScale = 0x0002, + HalfTone = 0x0004, + } + } +} diff --git a/LanternExtractor/Infrastructure/ImageWriter.cs b/LanternExtractor/Infrastructure/ImageWriter.cs index ac9471f..2625509 100644 --- a/LanternExtractor/Infrastructure/ImageWriter.cs +++ b/LanternExtractor/Infrastructure/ImageWriter.cs @@ -31,8 +31,6 @@ public static void WriteImageAsPng(byte[] bytes, string filePath, string fileNam private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName, bool isMasked, bool rotate, ILogger logger) { - var byteStream = new MemoryStream(bytes); - if (string.IsNullOrEmpty(filePath)) { return; @@ -40,11 +38,12 @@ private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName Directory.CreateDirectory(filePath); - Bitmap image; + EqBmp image; + var byteStream = new MemoryStream(bytes); try { - image = new Bitmap(byteStream); + image = new EqBmp(byteStream); } catch (Exception e) { @@ -60,76 +59,21 @@ private static void WriteBmpAsPng(byte[] bytes, string filePath, string fileName fileName = "canwall1.png"; } - Bitmap cloneBitmap; - - if (isMasked) + switch (image.PixelFormat) { - cloneBitmap = image.Clone(new Rectangle(0, 0, image.Width, image.Height), - PixelFormat.Format8bppIndexed); - - int paletteIndex = GetPaletteIndex(fileName); - var palette = cloneBitmap.Palette; - - if (Environment.OSVersion.Platform != PlatformID.MacOSX && - Environment.OSVersion.Platform != PlatformID.Unix) - { - palette.Entries[paletteIndex] = Color.FromArgb(0, 0, 0, 0); - cloneBitmap.Palette = palette; - } - else - { - // Due to a bug with the MacOS implementation of System.Drawing, setting a color palette value to - // transparent does not work. The workaround is to ensure that the first palette value (the transparent - // key) is unique and then use MakeTransparent() - Color transparencyColor = palette.Entries[paletteIndex]; - bool isUnique = false; - - while (!isUnique) - { - isUnique = true; - - for (var i = 1; i < cloneBitmap.Palette.Entries.Length; i++) - { - Color paletteValue = cloneBitmap.Palette.Entries[i]; - - if (paletteValue == transparencyColor) - { - Random random = new Random(); - transparencyColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); - isUnique = false; - break; - } - } - } - - palette.Entries[paletteIndex] = transparencyColor; - cloneBitmap.Palette = palette; - cloneBitmap.MakeTransparent(transparencyColor); - - // For some reason, this now has to be done to ensure the pixels are actually set to transparent - // Another head scratching MacOS bug - for (int i = 0; i < cloneBitmap.Width; ++i) + case PixelFormat.Format8bppIndexed: + if (isMasked) { - for (int j = 0; j < cloneBitmap.Height; ++j) - { - if (cloneBitmap.GetPixel(i, j) == transparencyColor) - { - cloneBitmap.SetPixel(i, j, Color.FromArgb(0, 0, 0, 0)); - } - } + var paletteIndex = GetPaletteIndex(fileName); + image.MakePaletteTransparent(paletteIndex); } - } - } - else - { - cloneBitmap = image.Clone(new Rectangle(0, 0, image.Width, image.Height), PixelFormat.Format32bppArgb); - if (image.PixelFormat != PixelFormat.Format8bppIndexed) - { - cloneBitmap.MakeTransparent(Color.Magenta); - } + break; + default: + image.MakeMagentaTransparent(); + break; } - cloneBitmap.Save(Path.Combine(filePath, fileName), ImageFormat.Png); + image.WritePng(Path.Combine(filePath, fileName)); } private static void WriteDdsAsPng(byte[] bytes, string filePath, string fileName) diff --git a/LanternExtractor/LanternExtractor.cs b/LanternExtractor/LanternExtractor.cs index 8afceb7..4dacf7b 100644 --- a/LanternExtractor/LanternExtractor.cs +++ b/LanternExtractor/LanternExtractor.cs @@ -53,11 +53,10 @@ private static void Main(string[] args) } var archiveName = args[0]; - List eqFiles = EqFileHelper.GetValidEqFilePaths(_settings.EverQuestDirectory, archiveName); eqFiles.Sort(); - if (eqFiles.Count == 0 && !EqFileHelper.IsClientDataFile(archiveName)) + if (eqFiles.Count == 0 && !EqFileHelper.IsSpecialCaseExtraction(archiveName)) { Console.WriteLine("No valid EQ files found for: '" + archiveName + "' at path: " + _settings.EverQuestDirectory); @@ -89,7 +88,9 @@ private static void Main(string[] args) ArchiveExtractor.Extract(file, "Exports/", _logger, _settings); } } + ClientDataCopier.Copy(archiveName, "Exports/", _logger, _settings); + MusicCopier.Copy(archiveName, _logger, _settings); Console.WriteLine($"Extraction complete ({(DateTime.Now - start).TotalSeconds:.00}s)"); } diff --git a/LanternExtractor/LanternExtractor.csproj b/LanternExtractor/LanternExtractor.csproj index c41337e..67a1e45 100644 --- a/LanternExtractor/LanternExtractor.csproj +++ b/LanternExtractor/LanternExtractor.csproj @@ -1,4 +1,4 @@ - + Exe net6.0 @@ -22,14 +22,12 @@ PreserveNewest - - PreserveNewest - PreserveNewest + 0.9.8 diff --git a/LanternExtractor/LanternStrings.cs b/LanternExtractor/LanternStrings.cs index bdab7f9..0517317 100644 --- a/LanternExtractor/LanternStrings.cs +++ b/LanternExtractor/LanternStrings.cs @@ -5,7 +5,7 @@ namespace LanternExtractor /// public static class LanternStrings { - public const string ExportHeaderTitle = "# Lantern Extractor 0.2 - "; + public const string ExportHeaderTitle = "# Lantern Extractor 0.1.7 - "; public const string ExportHeaderFormat = "# Format: "; public const string ObjMaterialHeader = "mtllib "; diff --git a/LanternExtractor/ModelExportFormat.cs b/LanternExtractor/ModelExportFormat.cs new file mode 100644 index 0000000..c13527c --- /dev/null +++ b/LanternExtractor/ModelExportFormat.cs @@ -0,0 +1,9 @@ +namespace LanternExtractor +{ + public enum ModelExportFormat + { + Intermediate = 0, + Obj = 1, + GlTF = 2 + } +} \ No newline at end of file diff --git a/LanternExtractor/Settings.cs b/LanternExtractor/Settings.cs index 7c94159..5aac280 100644 --- a/LanternExtractor/Settings.cs +++ b/LanternExtractor/Settings.cs @@ -1,25 +1,15 @@ using System; -using System.Collections.Generic; using System.IO; using LanternExtractor.Infrastructure; using LanternExtractor.Infrastructure.Logger; - namespace LanternExtractor { - public enum ModelExportFormat - { - Intermediate = 0, - Obj = 1, - GlTF = 2 - } - /// /// Simple class that parses settings for the extractor /// public class Settings { - /// /// The logger reference for debug output /// @@ -55,20 +45,11 @@ public class Settings /// Sets the desired model export format /// public ModelExportFormat ModelExportFormat { get; private set; } - - /// - /// Sets the desired model export format - /// + public bool ExportCharactersToSingleFolder { get; private set; } - - /// - /// Sets the desired model export format - /// + public bool ExportEquipmentToSingleFolder { get; private set; } - - /// - /// Export all sound files to a single folder - /// + public bool ExportSoundsToSingleFolder { get; private set; } /// @@ -101,6 +82,11 @@ public class Settings /// Additional files that should be copied when extracting with `all` or `clientdata` /// public string ClientDataToCopy { get; private set; } + + /// + /// If enabled, XMI files will be copied to the 'Exports/Music' folder + /// + public bool CopyMusic { get; private set; } /// /// The verbosity of the logger @@ -147,78 +133,88 @@ public void Initialize() return; } - if (parsedSettings.ContainsKey("EverQuestDirectory")) + if (parsedSettings.TryGetValue("EverQuestDirectory", out var setting)) { - EverQuestDirectory = parsedSettings["EverQuestDirectory"]; + EverQuestDirectory = setting; // Ensure the path ends with a / EverQuestDirectory = Path.GetFullPath(EverQuestDirectory + "/"); } - if (parsedSettings.ContainsKey("RawS3DExtract")) + if (parsedSettings.TryGetValue("RawS3DExtract", out var parsedSetting)) { - RawS3dExtract = Convert.ToBoolean(parsedSettings["RawS3DExtract"]); + RawS3dExtract = Convert.ToBoolean(parsedSetting); } - if (parsedSettings.ContainsKey("ExportZoneMeshGroups")) + if (parsedSettings.TryGetValue("ExportZoneMeshGroups", out var setting1)) { - ExportZoneMeshGroups = Convert.ToBoolean(parsedSettings["ExportZoneMeshGroups"]); + ExportZoneMeshGroups = Convert.ToBoolean(setting1); } - if (parsedSettings.ContainsKey("ExportHiddenGeometry")) + if (parsedSettings.TryGetValue("ExportHiddenGeometry", out var parsedSetting1)) { - ExportHiddenGeometry = Convert.ToBoolean(parsedSettings["ExportHiddenGeometry"]); + ExportHiddenGeometry = Convert.ToBoolean(parsedSetting1); } - if (parsedSettings.ContainsKey("ExportZoneWithObjects")) + if (parsedSettings.TryGetValue("ExportZoneWithObjects", out var setting2)) { - ExportZoneWithObjects = Convert.ToBoolean(parsedSettings["ExportZoneWithObjects"]); + ExportZoneWithObjects = Convert.ToBoolean(setting2); } - if (parsedSettings.ContainsKey("ModelExportFormat")) + if (parsedSettings.TryGetValue("ModelExportFormat", out var parsedSetting2)) { - var exportFormatSetting = (ModelExportFormat)Convert.ToInt32(parsedSettings["ModelExportFormat"]); + var exportFormatSetting = (ModelExportFormat)Convert.ToInt32(parsedSetting2); ModelExportFormat = exportFormatSetting; } - if (parsedSettings.ContainsKey("ExportCharacterToSingleFolder")) + if (parsedSettings.TryGetValue("ExportCharacterToSingleFolder", out var setting3)) { - ExportCharactersToSingleFolder = Convert.ToBoolean(parsedSettings["ExportCharacterToSingleFolder"]); + ExportCharactersToSingleFolder = Convert.ToBoolean(setting3); } - if (parsedSettings.ContainsKey("ExportEquipmentToSingleFolder")) + if (parsedSettings.TryGetValue("ExportEquipmentToSingleFolder", out var parsedSetting3)) { - ExportEquipmentToSingleFolder = Convert.ToBoolean(parsedSettings["ExportEquipmentToSingleFolder"]); + ExportEquipmentToSingleFolder = Convert.ToBoolean(parsedSetting3); } - - if (parsedSettings.ContainsKey("ExportSoundsToSingleFolder")) + + if (parsedSettings.TryGetValue("ExportSoundsToSingleFolder", out var setting4)) { - ExportSoundsToSingleFolder = Convert.ToBoolean(parsedSettings["ExportSoundsToSingleFolder"]); + ExportSoundsToSingleFolder = Convert.ToBoolean(setting4); } - if (parsedSettings.ContainsKey("ExportAllAnimationFrames")) + if (parsedSettings.TryGetValue("ExportAllAnimationFrames", out var parsedSetting4)) { - ExportAllAnimationFrames = Convert.ToBoolean(parsedSettings["ExportAllAnimationFrames"]); + ExportAllAnimationFrames = Convert.ToBoolean(parsedSetting4); } - if (parsedSettings.ContainsKey("ExportGltfVertexColors")) + if (parsedSettings.TryGetValue("ExportGltfVertexColors", out var setting5)) { - ExportGltfVertexColors = Convert.ToBoolean(parsedSettings["ExportGltfVertexColors"]); + ExportGltfVertexColors = Convert.ToBoolean(setting5); } - if (parsedSettings.ContainsKey("ExportGltfInGlbFormat")) + if (parsedSettings.TryGetValue("ExportGltfInGlbFormat", out var parsedSetting5)) { - ExportGltfInGlbFormat = Convert.ToBoolean(parsedSettings["ExportGltfInGlbFormat"]); + ExportGltfInGlbFormat = Convert.ToBoolean(parsedSetting5); } - if (parsedSettings.ContainsKey("ClientDataToCopy")) + if (parsedSettings.TryGetValue("ClientDataToCopy", out var setting6)) + { + ClientDataToCopy = setting6; + } + + if (parsedSettings.TryGetValue("ClientDataToCopy", out var parsedSetting6)) + { + ClientDataToCopy = parsedSetting6; + } + + if (parsedSettings.TryGetValue("CopyMusic", out var setting7)) { - ClientDataToCopy = parsedSettings["ClientDataToCopy"]; + CopyMusic = Convert.ToBoolean(setting7); } - if (parsedSettings.ContainsKey("LoggerVerbosity")) + if (parsedSettings.TryGetValue("LoggerVerbosity", out var parsedSetting7)) { - LoggerVerbosity = Convert.ToInt32(parsedSettings["LoggerVerbosity"]); + LoggerVerbosity = Convert.ToInt32(parsedSetting7); } } } diff --git a/LanternExtractor/settings.txt b/LanternExtractor/settings.txt index 9e40c22..4de0147 100644 --- a/LanternExtractor/settings.txt +++ b/LanternExtractor/settings.txt @@ -6,8 +6,8 @@ EverQuestDirectory = C:/EverQuest # Extract the contents of the S3D archive as is? -# If true, this will export all contents as they exist in the archives to their original formats. -# If false, this will extract contents, process WLD files and convert images to .png +# If true, this will export all contents as they exist in the archives in their original formats. +# If false, this will extract contents, process WLD files and convert images to PNG. RawS3DExtract = false # Sets the desired export format for models @@ -37,7 +37,7 @@ ExportCharacterToSingleFolder = true ExportEquipmentToSingleFolder = true # If true, will export all sounds to the 'sounds' folder -# Note, some sounds with conflicting names will be overwritten +# Some sounds with conflicting names will be overwritten ExportSoundsToSingleFolder = true # If true and using OBJ export format, this will dump an OBJ for each animation frame. @@ -69,6 +69,10 @@ ExportGltfInGlbFormat = false # (Intermediate) ClientDataToCopy = spells.eff,spdat.eff +# If enabled, client XMI files will be copied to the 'Exports/Music' folder +# XMI files can be used in the Lantern client +CopyMusic = true + # The minimum verbosity of the logger # 0 = info, 1 = warnings, 2 = errors LoggerVerbosity = 2