├── .gitignore ├── test ├── GlobalUsings.cs ├── Resources │ ├── D2S │ │ └── 1.15 │ │ │ ├── Druid.d2s │ │ │ ├── Amazon.d2s │ │ │ ├── Assassin.d2s │ │ │ ├── Paladin.d2s │ │ │ ├── Barbarian.d2s │ │ │ ├── Sorceress.d2s │ │ │ ├── DannyIsGreat.d2s │ │ │ └── Necromancer.d2s │ └── D2I │ │ └── 1.15 │ │ └── SharedStash_SoftCore.d2i ├── D2ITest.cs ├── D2SLibTests.csproj ├── D2STest.cs ├── BitReader_Old.cs ├── BitReaderTests.cs └── BitWriter_Old.cs ├── benchmarks ├── D2SLib_Nuget.Benchmark │ ├── Program.cs │ └── D2SLib_Nuget.Benchmark.csproj ├── D2SLib_Local.Benchmark │ ├── Program.cs │ ├── LoadGame.cs │ ├── D2SLib_Local.Benchmark.csproj │ └── BenchmarkConfig.cs └── Results.txt ├── Makefile ├── src ├── D2SLib.csproj.user ├── MetaData.cs ├── Model │ ├── Data │ │ ├── ItemStatCostData.cs │ │ ├── DataColumn.cs │ │ ├── ItemsData.cs │ │ └── DataFile.cs │ ├── Save │ │ ├── D2I.cs │ │ ├── Golem.cs │ │ ├── Status.cs │ │ ├── Attributes.cs │ │ ├── Header.cs │ │ ├── Mercenary.cs │ │ ├── Locations.cs │ │ ├── Corpses.cs │ │ ├── Skills.cs │ │ ├── Appearances.cs │ │ ├── NPCDialogs.cs │ │ ├── D2S.cs │ │ ├── Waypoints.cs │ │ ├── Quests.cs │ │ └── Items.cs │ └── Huffman │ │ ├── Node.cs │ │ └── HuffmanTree.cs ├── IO │ ├── IBitReader.cs │ ├── IBitWriter.cs │ ├── BitReader.cs │ ├── BitWriter.cs │ └── InternalBitArray.cs ├── D2SLib.csproj ├── ResourceFilesData.cs ├── Core.cs └── Resources │ └── ItemStatCost.txt ├── LICENSE ├── README.md ├── .gitattributes └── D2SLib.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | Properties 5 | TestResults -------------------------------------------------------------------------------- /test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Druid.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Druid.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Amazon.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Amazon.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Assassin.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Assassin.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Paladin.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Paladin.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Barbarian.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Barbarian.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Sorceress.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Sorceress.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/DannyIsGreat.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/DannyIsGreat.d2s -------------------------------------------------------------------------------- /test/Resources/D2S/1.15/Necromancer.d2s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2S/1.15/Necromancer.d2s -------------------------------------------------------------------------------- /test/Resources/D2I/1.15/SharedStash_SoftCore.d2i: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dschu012/D2SLib/HEAD/test/Resources/D2I/1.15/SharedStash_SoftCore.d2i -------------------------------------------------------------------------------- /benchmarks/D2SLib_Nuget.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #todo... better method 2 | nuget: 3 | dotnet nuget push D2SLib.1.0.2.nupkg --api-key $NUGET_APIKEY --source https://api.nuget.org/v3/index.json 4 | -------------------------------------------------------------------------------- /src/D2SLib.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/MetaData.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.Model.Data; 2 | 3 | namespace D2SLib; 4 | 5 | public sealed class MetaData 6 | { 7 | public MetaData(ItemStatCostData itemsStatCost, ItemsData itemsData) 8 | { 9 | ItemStatCostData = itemsStatCost; 10 | ItemsData = itemsData; 11 | } 12 | 13 | public ItemStatCostData ItemStatCostData { get; } 14 | public ItemsData ItemsData { get; } 15 | } 16 | -------------------------------------------------------------------------------- /benchmarks/D2SLib_Local.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 4 | 5 | // When running memory profiler, comment out the above and uncomment the following. 6 | 7 | //var lg = new D2SLib_Benchmark.LoadGame(); 8 | //lg.GlobalSetup(); 9 | 10 | //for (int i = 0; i < 10_000; i++) 11 | //{ 12 | // lg.SaveOnly(); 13 | //} 14 | -------------------------------------------------------------------------------- /test/D2ITest.cs: -------------------------------------------------------------------------------- 1 | using D2SLib; 2 | using D2SLib.Model.Save; 3 | 4 | namespace D2SLibTests 5 | { 6 | [TestClass] 7 | public class D2ITest 8 | { 9 | 10 | [TestMethod] 11 | public void VerifyCanReadSharedStash115() 12 | { 13 | //0x61 == 1.15 14 | D2I stash = Core.ReadD2I(File.ReadAllBytes(@"Resources\D2I\1.15\SharedStash_SoftCore.d2i"), 0x61); 15 | Assert.IsTrue(stash.ItemList.Count == 8); 16 | Assert.IsTrue(stash.ItemList.Items[0].Code == "rng "); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Model/Data/ItemStatCostData.cs: -------------------------------------------------------------------------------- 1 | namespace D2SLib.Model.Data; 2 | 3 | public sealed class ItemStatCostData : DataFile 4 | { 5 | public DataRow? GetById(int id) => GetByColumnAndValue("ID", id); 6 | public DataRow? GetByStat(string stat) => GetByColumnAndValue("Stat", stat); 7 | 8 | public static ItemStatCostData Read(Stream data) 9 | { 10 | var itemStatCost = new ItemStatCostData(); 11 | itemStatCost.ReadData(data); 12 | return itemStatCost; 13 | } 14 | 15 | public static ItemStatCostData Read(string file) 16 | { 17 | using Stream stream = File.OpenRead(file); 18 | return Read(stream); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/D2SLib_Local.Benchmark/LoadGame.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using D2SLib; 3 | using D2SLib.Model.Save; 4 | 5 | namespace D2SLib_Benchmark; 6 | 7 | [Config(typeof(BenchmarkConfig))] 8 | public class LoadGame 9 | { 10 | private byte[] _saveData = Array.Empty(); 11 | private D2S? _saveGame; 12 | 13 | [Benchmark] 14 | public void LoadOnly() 15 | { 16 | _ = Core.ReadD2S(_saveData); 17 | } 18 | 19 | [Benchmark] 20 | public void SaveOnly() 21 | { 22 | _ = Core.WriteD2S(_saveGame!); 23 | } 24 | 25 | [GlobalSetup] 26 | public void GlobalSetup() 27 | { 28 | _saveData = File.ReadAllBytes(@"Resources\D2S\1.15\DannyIsGreat.d2s"); 29 | _saveGame = Core.ReadD2S(_saveData); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /benchmarks/D2SLib_Local.Benchmark/D2SLib_Local.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0;netcoreapp3.1 6 | 10 7 | enable 8 | enable 9 | D2SLib_Benchmark 10 | 11 | 12 | 13 | 14 | Resources\%(RecursiveDir)\%(FileName)%(Extension) 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/IO/IBitReader.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace D2SLib.IO; 4 | 5 | public interface IBitReader 6 | { 7 | int Position { get; } 8 | 9 | void Align(); 10 | bool ReadBit(); 11 | byte[] ReadBits(int numberOfBits); 12 | IMemoryOwner ReadBitsPooled(int numberOfBits); 13 | int ReadBits(int numberOfBits, Span output); 14 | byte ReadByte(); 15 | byte ReadByte(int bits); 16 | byte[] ReadBytes(int numberOfBytes); 17 | IMemoryOwner ReadBytesPooled(int numberOfBytes); 18 | int ReadBytes(int numberOfBytes, Span output); 19 | int ReadBytes(Span output); 20 | int ReadInt32(); 21 | int ReadInt32(int bits); 22 | string ReadString(int byteCount); 23 | ushort ReadUInt16(); 24 | ushort ReadUInt16(int bits); 25 | uint ReadUInt32(); 26 | uint ReadUInt32(int bits); 27 | void Seek(int bytePostion); 28 | void SeekBits(int bitPosition); 29 | void AdvanceBits(int bits); 30 | } -------------------------------------------------------------------------------- /benchmarks/D2SLib_Local.Benchmark/BenchmarkConfig.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Environments; 4 | using BenchmarkDotNet.Exporters; 5 | using BenchmarkDotNet.Jobs; 6 | using BenchmarkDotNet.Loggers; 7 | 8 | namespace D2SLib_Benchmark; 9 | 10 | internal class BenchmarkConfig : ManualConfig 11 | { 12 | public BenchmarkConfig() 13 | { 14 | AddJob(Job.Default 15 | .WithRuntime(CoreRuntime.Core60) 16 | .WithPlatform(Platform.X64) 17 | .WithJit(Jit.RyuJit)); 18 | AddJob(Job.Default 19 | .WithRuntime(CoreRuntime.Core31) 20 | .WithPlatform(Platform.X64) 21 | .WithJit(Jit.RyuJit)); 22 | AddDiagnoser(MemoryDiagnoser.Default); 23 | //AddExporter(CsvMeasurementsExporter.Default); 24 | //AddExporter(HtmlExporter.Default); 25 | AddExporter(MarkdownExporter.GitHub); 26 | AddLogger(ConsoleLogger.Default); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Model/Save/D2I.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public sealed class D2I : IDisposable 6 | { 7 | private D2I(IBitReader reader, uint version) 8 | { 9 | ItemList = ItemList.Read(reader, version); 10 | } 11 | 12 | public ItemList ItemList { get; } 13 | 14 | public void Write(IBitWriter writer, uint version) 15 | { 16 | ItemList.Write(writer, version); 17 | } 18 | 19 | public static D2I Read(IBitReader reader, uint version) => new(reader, version); 20 | 21 | public static D2I Read(ReadOnlySpan bytes, uint version) 22 | { 23 | using var reader = new BitReader(bytes); 24 | return new D2I(reader, version); 25 | } 26 | 27 | public static byte[] Write(D2I d2i, uint version) 28 | { 29 | using var writer = new BitWriter(); 30 | d2i.Write(writer, version); 31 | return writer.ToArray(); 32 | } 33 | 34 | public void Dispose() => ItemList?.Dispose(); 35 | } 36 | -------------------------------------------------------------------------------- /benchmarks/D2SLib_Nuget.Benchmark/D2SLib_Nuget.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0;netcoreapp3.1 6 | 10 7 | enable 8 | enable 9 | D2SLib_Benchmark 10 | 11 | 12 | 13 | 14 | Resources\%(RecursiveDir)\%(FileName)%(Extension) 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dschu012 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/D2SLibTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net6.0 5 | latest 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/IO/IBitWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace D2SLib.IO; 4 | 5 | public interface IBitWriter 6 | { 7 | int Length { get; } 8 | int Position { get; } 9 | 10 | void Align(); 11 | void Dispose(); 12 | void Seek(int bytePostion); 13 | void SeekBits(int bitPosition); 14 | void Skip(int bytes); 15 | void SkipBits(int numberOfBits); 16 | byte[] ToArray(); 17 | IMemoryOwner ToPooledArray(); 18 | int GetBytes(Span output); 19 | void WriteBit(bool value); 20 | void WriteBits(IList bits); 21 | void WriteBits(IList bits, int numberOfBits); 22 | void WriteByte(byte value); 23 | void WriteByte(byte value, int size); 24 | void WriteBytes(ReadOnlySpan value); 25 | void WriteBytes(ReadOnlySpan value, int numberOfBits); 26 | void WriteInt32(int value); 27 | void WriteInt32(int value, int numberOfBits); 28 | void WriteString(ReadOnlySpan s, int length); 29 | void WriteUInt16(ushort value); 30 | void WriteUInt16(ushort value, int numberOfBits); 31 | void WriteUInt32(uint value); 32 | void WriteUInt32(uint value, int numberOfBits); 33 | } -------------------------------------------------------------------------------- /src/Model/Save/Golem.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public class Golem 6 | { 7 | private Golem(IBitReader reader, uint version) 8 | { 9 | Header = reader.ReadUInt16(); 10 | Exists = reader.ReadByte() == 1; 11 | if (Exists) 12 | { 13 | Item = Item.Read(reader, version); 14 | } 15 | } 16 | 17 | public ushort? Header { get; set; } 18 | public bool Exists { get; set; } 19 | public Item? Item { get; set; } 20 | 21 | public void Write(IBitWriter writer, uint version) 22 | { 23 | writer.WriteUInt16(Header ?? 0x666B); 24 | writer.WriteByte((byte)(Exists ? 1 : 0)); 25 | if (Exists) 26 | { 27 | Item?.Write(writer, version); 28 | } 29 | } 30 | 31 | public static Golem Read(IBitReader reader, uint version) 32 | { 33 | var golem = new Golem(reader, version); 34 | return golem; 35 | } 36 | 37 | [Obsolete("Try the non-allocating overload!")] 38 | public static byte[] Write(Golem golem, uint version) 39 | { 40 | using var writer = new BitWriter(); 41 | golem.Write(writer, version); 42 | return writer.ToArray(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/Huffman/Node.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Huffman; 4 | 5 | internal class Node 6 | { 7 | public char Symbol { get; set; } 8 | public int Frequency { get; set; } 9 | public Node? Right { get; set; } 10 | public Node? Left { get; set; } 11 | 12 | internal InternalBitArray? Traverse(char symbol, InternalBitArray data) 13 | { 14 | if (IsLeaf()) 15 | { 16 | return symbol.Equals(Symbol) ? data : null; 17 | } 18 | else 19 | { 20 | if (Left is not null) 21 | { 22 | data.Add(false); 23 | var left = Left.Traverse(symbol, data); 24 | if (left is null) 25 | data.Length--; 26 | else 27 | return data; 28 | } 29 | 30 | if (Right is not null) 31 | { 32 | data.Add(true); 33 | var right = Right.Traverse(symbol, data); 34 | if (right is null) 35 | data.Length--; 36 | else 37 | return data; 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | 44 | public bool IsLeaf() => Left is null && Right is null; 45 | } 46 | -------------------------------------------------------------------------------- /benchmarks/Results.txt: -------------------------------------------------------------------------------- 1 | Original: 2 | 3 | | Method | Runtime | Mean | Error | StdDev | Median | Gen 0 | Allocated | 4 | |--------- |-------------- |---------:|--------:|---------:|---------:|---------:|----------:| 5 | | LoadOnly | .NET 6.0 | 168.7 ms | 3.35 ms | 7.36 ms | 169.5 ms | - | 1 MB | 6 | | SaveOnly | .NET 6.0 | 304.1 ms | 8.64 ms | 25.49 ms | 316.7 ms | 500.0000 | 2 MB | 7 | | LoadOnly | .NET Core 3.1 | 169.7 ms | 3.37 ms | 7.68 ms | 170.3 ms | - | 1 MB | 8 | | SaveOnly | .NET Core 3.1 | 304.2 ms | 8.02 ms | 23.15 ms | 316.4 ms | 500.0000 | 2 MB | 9 | 10 | Revised: 11 | 12 | | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated | 13 | |--------- |-------------- |-----------:|--------:|--------:|--------:|--------:|----------:| 14 | | LoadOnly | .NET 6.0 | 735.6 us | 4.74 us | 4.20 us | 50.7813 | 11.7188 | 237 KB | 15 | | SaveOnly | .NET 6.0 | 1,223.6 us | 7.57 us | 8.42 us | 44.9219 | - | 215 KB | 16 | | LoadOnly | .NET Core 3.1 | 880.1 us | 3.63 us | 3.22 us | 50.7813 | 11.7188 | 237 KB | 17 | | SaveOnly | .NET Core 3.1 | 1,426.6 us | 8.43 us | 7.88 us | 44.9219 | - | 215 KB | 18 | 19 | 20 | Load: 229 times faster, 77% less memory used. 21 | Save: 249 times faster, 90% less memory used. -------------------------------------------------------------------------------- /src/D2SLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net6.0 5 | enable 6 | enable 7 | latest 8 | 9 | dschu012 10 | d2s d2i diablo-ii diablo2 d2r 11 | Read and write saves from Diablo 2. Supports versions 1.10 through Diablo II: Resurrected (1.15). Supports both d2s (player saves) and d2i (shared stash). 12 | true 13 | https://github.com/dschu012/D2SLib 14 | git 15 | 1.0.2 16 | True 17 | latest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Model/Save/Status.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace D2SLib.Model.Save; 5 | 6 | public sealed class Status : IDisposable 7 | { 8 | private InternalBitArray _flags; 9 | public Status(byte flags) 10 | { 11 | _flags = new InternalBitArray(stackalloc byte[] { flags }); 12 | } 13 | 14 | [JsonIgnore] 15 | public IList Flags => _flags; 16 | public bool IsHardcore { get => Flags[2]; set => Flags[2] = value; } 17 | public bool IsDead { get => Flags[3]; set => Flags[3] = value; } 18 | public bool IsExpansion { get => Flags[5]; set => Flags[5] = value; } 19 | public bool IsLadder { get => Flags[6]; set => Flags[6] = value; } 20 | 21 | public void Write(IBitWriter writer) 22 | { 23 | var bits = (InternalBitArray)Flags; 24 | writer.WriteBits(bits); 25 | } 26 | 27 | public static Status Read(byte bytes) 28 | { 29 | var status = new Status(bytes); 30 | return status; 31 | } 32 | 33 | [Obsolete("Try the non-allocating overload!")] 34 | public static byte[] Write(Status status) 35 | { 36 | using var writer = new BitWriter(); 37 | status.Write(writer); 38 | return writer.ToArray(); 39 | } 40 | 41 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 42 | } 43 | -------------------------------------------------------------------------------- /src/ResourceFilesData.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.Model.Data; 2 | using System.Reflection; 3 | 4 | namespace D2SLib; 5 | 6 | public sealed class ResourceFilesData 7 | { 8 | private ResourceFilesData() 9 | { 10 | ArmorData armorData; 11 | WeaponsData weaponsData; 12 | MiscData miscData; 13 | ItemStatCostData itemStatCostData; 14 | 15 | using (Stream s = GetResource("ItemStatCost.txt")) 16 | { 17 | itemStatCostData = ItemStatCostData.Read(s); 18 | } 19 | using (Stream s = GetResource("Armor.txt")) 20 | { 21 | armorData = ArmorData.Read(s); 22 | } 23 | using (Stream s = GetResource("Weapons.txt")) 24 | { 25 | weaponsData = WeaponsData.Read(s); 26 | } 27 | using (Stream s = GetResource("Misc.txt")) 28 | { 29 | miscData = MiscData.Read(s); 30 | } 31 | 32 | MetaData = new MetaData(itemStatCostData, new ItemsData(armorData, weaponsData, miscData)); 33 | } 34 | 35 | public static ResourceFilesData Instance { get; } = new(); 36 | 37 | public MetaData MetaData { get; set; } 38 | 39 | private static Stream GetResource(string file) 40 | { 41 | var assembly = Assembly.GetExecutingAssembly(); 42 | return assembly.GetManifestResourceStream($"D2SLib.Resources.{file}") 43 | ?? throw new InvalidOperationException($"{file} was not found in embedded resources."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### D2SLib 2 | 3 | Simple C# library for reading and writing Diablo 2 saves. Supports version 1.10 through Diablo II: Resurrected (1.15). Supports reading both d2s (player saves) and d2i (shared stash) files. 4 | 5 | 6 | ### Usage 7 | Use [Nuget](https://www.nuget.org/packages/D2SLib/) to add D2SLib to your project. 8 | 9 | ``` 10 | using D2SLib; 11 | using D2SLib.Model.Save; 12 | 13 | .... 14 | //read a save 15 | D2S character = Core.ReadD2S(File.ReadAllBytes(@"Resources\D2S\1.15\DannyIsGreat.d2s")); 16 | 17 | //outputs: DannyIsGreat 18 | Console.WriteLine(character.Name); 19 | 20 | //convert 1.10-1.114d save to d2r 1.15 21 | character.Header.Version = 0x61; 22 | 23 | //set all skills to have 20 points 24 | character.ClassSkills.Skills.ForEach(skill => skill.Points = 20); 25 | 26 | //add lvl 31 conviction arua to the first statlist on the first item in your chars inventory 27 | character.PlayerItemList.Items[0].StatLists[0].Stats.Add(new ItemStat { Stat = "item_aura", Param = 123, Value = 31 }); 28 | 29 | //write save 30 | File.WriteAllBytes(Environment.ExpandEnvironmentVariables($"%userprofile%/Saved Games/Diablo II Resurrected Tech Alpha/{character.Name}.d2s"), Core.WriteD2S(character)); 31 | 32 | ``` 33 | 34 | How to seed the library with your own TXT files 35 | ``` 36 | TXT txt = new TXT(); 37 | txt.ItemStatCostTXT = ItemStatCostTXT.Read(@"ItemStatCost.txt"); 38 | txt.ItemsTXT.ArmorTXT = ArmorTXT.Read(@"Armor.txt"); 39 | txt.ItemsTXT.WeaponsTXT = WeaponsTXT.Read(@"Weapons.txt"); 40 | txt.ItemsTXT.MiscTXT = MiscTXT.Read(@"Misc.txt"); 41 | Core.TXT = txt; 42 | D2S character = Core.ReadD2S(File.ReadAllBytes(@"DannyIsGreat.d2s")); 43 | ``` 44 | 45 | ##### Useful Links: 46 | * https://github.com/d07RiV/d07riv.github.io/blob/master/d2r.html (credits to d07riv for reversing the item code on D2R) 47 | * https://github.com/nokka/d2s 48 | * https://github.com/krisives/d2s-format 49 | * http://paul.siramy.free.fr/d2ref/eng/ 50 | * http://user.xmission.com/~trevin/DiabloIIv1.09_File_Format.shtml 51 | * https://github.com/nickshanks/Alkor 52 | * https://github.com/HarpyWar/d2s-character-editor 53 | -------------------------------------------------------------------------------- /src/Model/Save/Attributes.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | //variable size. depends on # of attributes 6 | public class Attributes 7 | { 8 | public ushort? Header { get; set; } 9 | public Dictionary Stats { get; } = new Dictionary(); 10 | 11 | public static Attributes Read(IBitReader reader) 12 | { 13 | var itemStatCost = Core.MetaData.ItemStatCostData; 14 | var attributes = new Attributes 15 | { 16 | Header = reader.ReadUInt16() 17 | }; 18 | ushort id = reader.ReadUInt16(9); 19 | while (id != 0x1ff) 20 | { 21 | var property = itemStatCost.GetById(id); 22 | int attribute = reader.ReadInt32(property?["CSvBits"].ToInt32() ?? 0); 23 | int valShift = property?["ValShift"].ToInt32() ?? 0; 24 | if (valShift > 0) 25 | { 26 | attribute >>= valShift; 27 | } 28 | attributes.Stats.Add(property?["Stat"].Value ?? string.Empty, attribute); 29 | id = reader.ReadUInt16(9); 30 | } 31 | reader.Align(); 32 | return attributes; 33 | } 34 | 35 | public void Write(IBitWriter writer) 36 | { 37 | var itemStatCost = Core.MetaData.ItemStatCostData; 38 | writer.WriteUInt16(Header ?? 0x6667); 39 | foreach (var entry in Stats) 40 | { 41 | var property = itemStatCost.GetByStat(entry.Key); 42 | writer.WriteUInt16(property?["ID"].ToUInt16() ?? 0, 9); 43 | int attribute = entry.Value; 44 | int valShift = property?["ValShift"].ToInt32() ?? 0; 45 | if (valShift > 0) 46 | { 47 | attribute <<= valShift; 48 | } 49 | writer.WriteInt32(attribute, property?["CSvBits"].ToInt32() ?? 0); 50 | } 51 | writer.WriteUInt16(0x1ff, 9); 52 | writer.Align(); 53 | } 54 | 55 | [Obsolete("Try the non-allocating overload!")] 56 | public static byte[] Write(Attributes attributes) 57 | { 58 | using var writer = new BitWriter(); 59 | attributes.Write(writer); 60 | return writer.ToArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/Save/Header.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Buffers.Binary; 3 | 4 | namespace D2SLib.Model.Save; 5 | 6 | public class Header 7 | { 8 | //0x0000 9 | public uint? Magic { get; set; } 10 | //0x0004 11 | public uint Version { get; set; } 12 | //0x0008 13 | public uint Filesize { get; set; } 14 | //0x000c 15 | public uint Checksum { get; set; } 16 | 17 | public void Write(IBitWriter writer) 18 | { 19 | writer.WriteUInt32(Magic ?? 0xAA55AA55); 20 | writer.WriteUInt32(Version); 21 | writer.WriteUInt32(Filesize); 22 | writer.WriteUInt32(Checksum); 23 | } 24 | 25 | public static Header Read(IBitReader reader) 26 | { 27 | var header = new Header 28 | { 29 | Magic = reader.ReadUInt32(), 30 | Version = reader.ReadUInt32(), 31 | Filesize = reader.ReadUInt32(), 32 | Checksum = reader.ReadUInt32() 33 | }; 34 | return header; 35 | } 36 | 37 | [Obsolete("Try the direct-read overload!")] 38 | public static Header Read(ReadOnlySpan bytes) 39 | { 40 | using var reader = new BitReader(bytes); 41 | return Read(reader); 42 | } 43 | 44 | [Obsolete("Try the non-allocating overload!")] 45 | public static byte[] Write(Header header) 46 | { 47 | using var writer = new BitWriter(); 48 | header.Write(writer); 49 | return writer.ToArray(); 50 | } 51 | 52 | public static void Fix(Span bytes) 53 | { 54 | FixSize(bytes); 55 | FixChecksum(bytes); 56 | } 57 | 58 | public static void FixSize(Span bytes) 59 | { 60 | Span length = stackalloc byte[sizeof(uint)]; 61 | BinaryPrimitives.WriteUInt32LittleEndian(length, (uint)bytes.Length); 62 | length.CopyTo(bytes[0x8..]); 63 | } 64 | 65 | public static void FixChecksum(Span bytes) 66 | { 67 | bytes[0xc..].Clear(); 68 | int checksum = 0; 69 | for (int i = 0; i < bytes.Length; i++) 70 | { 71 | checksum = bytes[i] + (checksum * 2) + (checksum < 0 ? 1 : 0); 72 | } 73 | Span csb = stackalloc byte[sizeof(int)]; 74 | BinaryPrimitives.WriteInt32LittleEndian(csb, checksum); 75 | csb.CopyTo(bytes[0xc..]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/D2STest.cs: -------------------------------------------------------------------------------- 1 | using D2SLib; 2 | using D2SLib.Model.Save; 3 | using System.Diagnostics; 4 | using System.Text.Json; 5 | 6 | namespace D2SLibTests; 7 | 8 | [TestClass] 9 | public class D2STest 10 | { 11 | [TestMethod] 12 | public void VerifyCanReadSimple115Save() 13 | { 14 | D2S character = Core.ReadD2S(File.ReadAllBytes(@"Resources\D2S\1.15\Amazon.d2s")); 15 | Assert.IsTrue(character.Name == "Amazon"); 16 | Assert.IsTrue(character.ClassId == 0x0); 17 | 18 | LogCharacter(character); 19 | } 20 | 21 | [TestMethod] 22 | public void VerifyCanReadComplex115Save() 23 | { 24 | D2S character = Core.ReadD2S(File.ReadAllBytes(@"Resources\D2S\1.15\DannyIsGreat.d2s")); 25 | Assert.IsTrue(character.Name == "DannyIsGreat"); 26 | Assert.IsTrue(character.ClassId == 0x1); 27 | 28 | LogCharacter(character); 29 | } 30 | 31 | [TestMethod] 32 | public void VerifyCanWriteComplex115Save() 33 | { 34 | byte[] input = File.ReadAllBytes(@"Resources\D2S\1.15\DannyIsGreat.d2s"); 35 | D2S character = Core.ReadD2S(input); 36 | byte[] ret = Core.WriteD2S(character); 37 | //File.WriteAllBytes(Environment.ExpandEnvironmentVariables($"%userprofile%/Saved Games/Diablo II Resurrected Tech Alpha/{character.Name}.d2s"), ret); 38 | Assert.AreEqual(input.Length, ret.Length); 39 | 40 | // This test fails with "element at index 12 differs" (checksum) but that was true in original code 41 | //CollectionAssert.AreEqual(input, ret); 42 | } 43 | 44 | [Conditional("DEBUG")] 45 | private static void LogCharacter(D2S character, string? label = null) 46 | { 47 | if (label is not null) 48 | { 49 | Console.Write(label); 50 | Console.WriteLine(':'); 51 | } 52 | 53 | Console.WriteLine( 54 | JsonSerializer.Serialize(character, 55 | new JsonSerializerOptions 56 | { 57 | WriteIndented = true, 58 | #if NET6_0_OR_GREATER 59 | DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, 60 | #else 61 | IgnoreNullValues = true, 62 | #endif 63 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 64 | })); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/Core.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using D2SLib.Model.Save; 3 | using Microsoft.Toolkit.HighPerformance.Buffers; 4 | 5 | namespace D2SLib; 6 | 7 | public class Core 8 | { 9 | private static MetaData? _metaData = null; 10 | public static MetaData MetaData 11 | { 12 | get => _metaData ?? ResourceFilesData.Instance.MetaData; 13 | set => _metaData = value; 14 | } 15 | 16 | public static D2S ReadD2S(string path) => D2S.Read(File.ReadAllBytes(path)); 17 | 18 | public static async Task ReadD2SAsync(string path, CancellationToken ct = default) 19 | { 20 | var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false); 21 | return D2S.Read(bytes); 22 | } 23 | 24 | public static D2S ReadD2S(ReadOnlySpan bytes) => D2S.Read(bytes); 25 | 26 | public static Item ReadItem(string path, uint version) => ReadItem(File.ReadAllBytes(path), version); 27 | 28 | public static async Task ReadItemAsync(string path, uint version, CancellationToken ct = default) 29 | { 30 | var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false); 31 | return Item.Read(bytes, version); 32 | } 33 | 34 | public static Item ReadItem(ReadOnlySpan bytes, uint version) => Item.Read(bytes, version); 35 | 36 | public static D2I ReadD2I(string path, uint version) => D2I.Read(File.ReadAllBytes(path), version); 37 | 38 | public static async Task ReadD2IAsync(string path, uint version, CancellationToken ct = default) 39 | { 40 | var bytes = await File.ReadAllBytesAsync(path, ct).ConfigureAwait(false); 41 | return D2I.Read(bytes, version); 42 | } 43 | 44 | public static D2I ReadD2I(ReadOnlySpan bytes, uint version) => D2I.Read(bytes, version); 45 | 46 | public static MemoryOwner WriteD2SPooled(D2S d2s) => D2S.WritePooled(d2s); 47 | 48 | public static byte[] WriteD2S(D2S d2s) => D2S.Write(d2s); 49 | 50 | public static MemoryOwner WriteItemPooled(Item item, uint version) 51 | { 52 | using var writer = new BitWriter(); 53 | item.Write(writer, version); 54 | return writer.ToPooledArray(); 55 | } 56 | 57 | public static byte[] WriteItem(Item item, uint version) => Item.Write(item, version); 58 | 59 | public static MemoryOwner WriteD2IPooled(D2I d2i, uint version) 60 | { 61 | using var writer = new BitWriter(); 62 | d2i.Write(writer, version); 63 | return writer.ToPooledArray(); 64 | } 65 | 66 | public static byte[] WriteD2I(D2I d2i, uint version) => D2I.Write(d2i, version); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /test/BitReader_Old.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace D2SLibTests; 4 | 5 | public class BitReader_Old : IDisposable 6 | { 7 | private BitArray _bits; 8 | public int Position { get; private set; } 9 | 10 | public BitReader_Old(byte[] bytes) 11 | { 12 | Position = 0; 13 | _bits = new BitArray(bytes); 14 | } 15 | public bool ReadBit() => _bits[Position++]; 16 | 17 | public byte[] ReadBits(int numberOfBits) 18 | { 19 | byte[] bytes = new byte[(numberOfBits - 1) / 8 + 1]; 20 | int byteIndex = 0; 21 | int bitIndex = 0; 22 | for (int i = 0; i < numberOfBits; i++) 23 | { 24 | if (_bits[Position + i]) 25 | { 26 | bytes[byteIndex] |= (byte)(1 << bitIndex); 27 | } 28 | bitIndex++; 29 | if (bitIndex == 8) 30 | { 31 | byteIndex++; 32 | bitIndex = 0; 33 | } 34 | } 35 | Position += numberOfBits; 36 | return bytes; 37 | } 38 | 39 | public byte[] ReadBytes(int numberOfBytes) => ReadBits(numberOfBytes * 8); 40 | 41 | public byte ReadByte(int bits) 42 | { 43 | byte[] bytes = ReadBits(bits); 44 | Array.Resize(ref bytes, 1); 45 | return bytes[0]; 46 | } 47 | 48 | public byte ReadByte() => ReadBytes(1)[0]; 49 | 50 | public ushort ReadUInt16(int bits) 51 | { 52 | byte[] bytes = ReadBits(bits); 53 | Array.Resize(ref bytes, 2); 54 | return BitConverter.ToUInt16(bytes, 0); ; 55 | } 56 | 57 | public ushort ReadUInt16() => BitConverter.ToUInt16(ReadBytes(2), 0); 58 | 59 | public uint ReadUInt32(int bits) 60 | { 61 | byte[] bytes = ReadBits(bits); 62 | Array.Resize(ref bytes, 4); 63 | return BitConverter.ToUInt32(bytes, 0); 64 | } 65 | 66 | public uint ReadUInt32() => BitConverter.ToUInt32(ReadBytes(4), 0); 67 | 68 | public int ReadInt32(int bits) 69 | { 70 | byte[] bytes = ReadBits(bits); 71 | Array.Resize(ref bytes, 4); 72 | return BitConverter.ToInt32(bytes, 0); 73 | } 74 | 75 | public int ReadInt32() => BitConverter.ToInt32(ReadBytes(4), 0); 76 | 77 | public string ReadString(int bytes) => System.Text.Encoding.ASCII.GetString(ReadBytes(bytes)).Trim('\0'); 78 | 79 | public void SeekBits(int bitPosition) => Position = bitPosition; 80 | public void Seek(int bytePostion) => SeekBits(bytePostion * 8); 81 | 82 | public void Align() => Position = (Position + 7) & ~7; 83 | 84 | public void Dispose() => _bits = null!; 85 | } 86 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Model/Save/Mercenary.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public sealed class Mercenary 6 | { 7 | //is this right? 8 | public ushort IsDead { get; set; } 9 | public uint Id { get; set; } 10 | public ushort NameId { get; set; } 11 | public ushort TypeId { get; set; } 12 | public uint Experience { get; set; } 13 | 14 | public void Write(IBitWriter writer) 15 | { 16 | writer.WriteUInt16(IsDead); 17 | writer.WriteUInt32(Id); 18 | writer.WriteUInt16(NameId); 19 | writer.WriteUInt16(TypeId); 20 | writer.WriteUInt32(Experience); 21 | } 22 | 23 | public static Mercenary Read(IBitReader reader) 24 | { 25 | var mercenary = new Mercenary 26 | { 27 | IsDead = reader.ReadUInt16(), 28 | Id = reader.ReadUInt32(), 29 | NameId = reader.ReadUInt16(), 30 | TypeId = reader.ReadUInt16(), 31 | Experience = reader.ReadUInt32() 32 | }; 33 | return mercenary; 34 | } 35 | 36 | [Obsolete("Try the direct-read overload!")] 37 | public static Mercenary Read(ReadOnlySpan bytes) 38 | { 39 | using var reader = new BitReader(bytes); 40 | return Read(reader); 41 | } 42 | 43 | [Obsolete("Try the non-allocating overload!")] 44 | public static byte[] Write(Mercenary mercenary) 45 | { 46 | using var writer = new BitWriter(); 47 | mercenary.Write(writer); 48 | return writer.ToArray(); 49 | } 50 | } 51 | 52 | public sealed class MercenaryItemList : IDisposable 53 | { 54 | public ushort? Header { get; set; } 55 | public ItemList? ItemList { get; private set; } 56 | 57 | public void Write(IBitWriter writer, Mercenary mercenary, uint version) 58 | { 59 | writer.WriteUInt16(Header ?? 0x666A); 60 | if (mercenary.Id != 0) 61 | { 62 | ItemList?.Write(writer, version); 63 | } 64 | } 65 | 66 | public static MercenaryItemList Read(IBitReader reader, Mercenary mercenary, uint version) 67 | { 68 | var mercenaryItemList = new MercenaryItemList 69 | { 70 | Header = reader.ReadUInt16() 71 | }; 72 | if (mercenary.Id != 0) 73 | { 74 | mercenaryItemList.ItemList = ItemList.Read(reader, version); 75 | } 76 | return mercenaryItemList; 77 | } 78 | 79 | [Obsolete("Try the non-allocating overload!")] 80 | public static byte[] Write(MercenaryItemList mercenaryItemList, Mercenary mercenary, uint version) 81 | { 82 | using var writer = new BitWriter(); 83 | mercenaryItemList.Write(writer, mercenary, version); 84 | return writer.ToArray(); 85 | } 86 | 87 | public void Dispose() => ItemList?.Dispose(); 88 | } 89 | -------------------------------------------------------------------------------- /src/Model/Save/Locations.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace D2SLib.Model.Save; 5 | 6 | public class Locations 7 | { 8 | private readonly Location[] _locations = new Location[3]; 9 | 10 | public Location Normal { get => _locations[0]; set => _locations[0] = value; } 11 | public Location Nightmare { get => _locations[1]; set => _locations[1] = value; } 12 | public Location Hell { get => _locations[2]; set => _locations[2] = value; } 13 | 14 | public void Write(IBitWriter writer) 15 | { 16 | for (int i = 0; i < _locations.Length; i++) 17 | { 18 | _locations[i].Write(writer); 19 | } 20 | } 21 | 22 | public static Locations Read(IBitReader reader) 23 | { 24 | var locations = new Locations(); 25 | var places = locations._locations; 26 | for (int i = 0; i < places.Length; i++) 27 | { 28 | places[i] = Location.Read(reader); 29 | } 30 | return locations; 31 | } 32 | 33 | [Obsolete("Try the direct-read overload!")] 34 | public static Locations Read(ReadOnlySpan bytes) 35 | { 36 | using var reader = new BitReader(bytes); 37 | return Read(reader); 38 | } 39 | 40 | [Obsolete("Try the non-allocating overload!")] 41 | public static byte[] Write(Locations locations) 42 | { 43 | using var writer = new BitWriter(); 44 | locations.Write(writer); 45 | return writer.ToArray(); 46 | } 47 | } 48 | 49 | public readonly struct Location : IEquatable 50 | { 51 | public Location(bool active, byte act) 52 | { 53 | Active = active; 54 | Act = act; 55 | } 56 | 57 | public readonly bool Active { get; } 58 | public readonly byte Act { get; } 59 | 60 | public void Write(IBitWriter writer) 61 | { 62 | byte b = 0x0; 63 | if (Active) 64 | { 65 | b |= 0x7; 66 | } 67 | 68 | b |= (byte)(Act - 1); 69 | 70 | writer.WriteByte(b); 71 | } 72 | 73 | public static Location Read(IBitReader reader) 74 | { 75 | byte b = reader.ReadByte(); 76 | return new Location( 77 | active: (b >> 7) == 1, 78 | act: (byte)((b & 0x5) + 1) 79 | ); 80 | } 81 | 82 | public bool Equals(Location other) 83 | { 84 | return Active == other.Active 85 | && Act == other.Act; 86 | } 87 | 88 | public override bool Equals([NotNullWhen(true)] object? obj) => obj is Location other && Equals(other); 89 | public override int GetHashCode() => HashCode.Combine(Active, Act); 90 | 91 | public static bool operator ==(Location left, Location right) => left.Equals(right); 92 | 93 | public static bool operator !=(Location left, Location right) => !left.Equals(right); 94 | } 95 | -------------------------------------------------------------------------------- /src/Model/Save/Corpses.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public sealed class CorpseList : IDisposable 6 | { 7 | public CorpseList(ushort? header, ushort count) 8 | { 9 | Header = header; 10 | Count = count; 11 | Corpses = new List(count); 12 | } 13 | 14 | public ushort? Header { get; set; } 15 | public ushort Count { get; set; } 16 | public List Corpses { get; } 17 | 18 | public void Write(IBitWriter writer, uint version) 19 | { 20 | writer.WriteUInt16(Header ?? 0x4D4A); 21 | writer.WriteUInt16(Count); 22 | for (int i = 0; i < Count; i++) 23 | { 24 | Corpses[i].Write(writer, version); 25 | } 26 | } 27 | 28 | public static CorpseList Read(IBitReader reader, uint version) 29 | { 30 | var corpseList = new CorpseList( 31 | header: reader.ReadUInt16(), 32 | count: reader.ReadUInt16() 33 | ); 34 | for (int i = 0; i < corpseList.Count; i++) 35 | { 36 | corpseList.Corpses.Add(Corpse.Read(reader, version)); 37 | } 38 | return corpseList; 39 | } 40 | 41 | [Obsolete("Try the non-allocating overload!")] 42 | public static byte[] Write(CorpseList corpseList, uint version) 43 | { 44 | using var writer = new BitWriter(); 45 | corpseList.Write(writer, version); 46 | return writer.ToArray(); 47 | } 48 | 49 | public void Dispose() 50 | { 51 | foreach (var corpse in Corpses) 52 | { 53 | corpse?.Dispose(); 54 | } 55 | Corpses.Clear(); 56 | } 57 | } 58 | 59 | public sealed class Corpse : IDisposable 60 | { 61 | private Corpse(IBitReader reader, uint version) 62 | { 63 | Unk0x0 = reader.ReadUInt32(); 64 | X = reader.ReadUInt32(); 65 | Y = reader.ReadUInt32(); 66 | ItemList = ItemList.Read(reader, version); 67 | } 68 | 69 | public uint? Unk0x0 { get; set; } 70 | public uint X { get; set; } 71 | public uint Y { get; set; } 72 | public ItemList ItemList { get; } 73 | 74 | public void Write(IBitWriter writer, uint version) 75 | { 76 | writer.WriteUInt32(Unk0x0 ?? 0x0); 77 | writer.WriteUInt32(X); 78 | writer.WriteUInt32(Y); 79 | ItemList.Write(writer, version); 80 | } 81 | 82 | public static Corpse Read(IBitReader reader, uint version) 83 | { 84 | var corpse = new Corpse(reader, version); 85 | return corpse; 86 | } 87 | 88 | [Obsolete("Try the non-allocating overload!")] 89 | public static byte[] Write(Corpse corpse, uint version) 90 | { 91 | using var writer = new BitWriter(); 92 | corpse.Write(writer, version); 93 | return writer.ToArray(); 94 | } 95 | 96 | public void Dispose() => ItemList.Dispose(); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /D2SLib.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D2SLib", "src\D2SLib.csproj", "{BA0A2A07-363C-4284-87C9-71F1349F0583}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D2SLibTests", "test\D2SLibTests.csproj", "{F2F223AD-BCE4-47B3-BAF8-DA2D533FD05F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D2SLib_Local.Benchmark", "benchmarks\D2SLib_Local.Benchmark\D2SLib_Local.Benchmark.csproj", "{3F72860B-D08C-45EE-9E1B-17888E103B62}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "D2SLib_Nuget.Benchmark", "benchmarks\D2SLib_Nuget.Benchmark\D2SLib_Nuget.Benchmark.csproj", "{7D3FEEAB-7752-4753-9EC0-EF4E7F72EB74}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FCAB7929-60A6-4DA7-9C96-0BE97B160694}" 15 | ProjectSection(SolutionItems) = preProject 16 | benchmarks\Results.txt = benchmarks\Results.txt 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {BA0A2A07-363C-4284-87C9-71F1349F0583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {BA0A2A07-363C-4284-87C9-71F1349F0583}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {BA0A2A07-363C-4284-87C9-71F1349F0583}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {BA0A2A07-363C-4284-87C9-71F1349F0583}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {F2F223AD-BCE4-47B3-BAF8-DA2D533FD05F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {F2F223AD-BCE4-47B3-BAF8-DA2D533FD05F}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {F2F223AD-BCE4-47B3-BAF8-DA2D533FD05F}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {F2F223AD-BCE4-47B3-BAF8-DA2D533FD05F}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {3F72860B-D08C-45EE-9E1B-17888E103B62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {3F72860B-D08C-45EE-9E1B-17888E103B62}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {3F72860B-D08C-45EE-9E1B-17888E103B62}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {3F72860B-D08C-45EE-9E1B-17888E103B62}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {7D3FEEAB-7752-4753-9EC0-EF4E7F72EB74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {7D3FEEAB-7752-4753-9EC0-EF4E7F72EB74}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {7D3FEEAB-7752-4753-9EC0-EF4E7F72EB74}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {7D3FEEAB-7752-4753-9EC0-EF4E7F72EB74}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {1F294F48-D408-43E4-BED3-97DF83E20574} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/Model/Data/DataColumn.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace D2SLib.Model.Data; 6 | 7 | [DebuggerDisplay("{Name} ({Count} rows)")] 8 | public abstract class DataColumn 9 | { 10 | protected DataColumn(string name) 11 | { 12 | Name = name; 13 | } 14 | 15 | public string Name { get; } 16 | public abstract int Count { get; } 17 | 18 | public abstract void AddEmptyValue(); 19 | public abstract string GetString(int rowIndex); 20 | public abstract int GetInt32(int rowIndex); 21 | public abstract ushort GetUInt16(int rowIndex); 22 | public abstract bool GetBoolean(int rowIndex); 23 | public override string ToString() => Name; 24 | } 25 | 26 | public abstract class DataColumn : DataColumn 27 | { 28 | private readonly List _data; 29 | 30 | public DataColumn(string name) : base(name) 31 | { 32 | _data = new(); 33 | } 34 | 35 | public T this[int i] 36 | { 37 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 38 | get => _data[i]; 39 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 40 | set => _data[i] = value; 41 | } 42 | 43 | public override sealed int Count => _data.Count; 44 | public int IndexOf(T value) => _data.IndexOf(value); 45 | public void AddValue(T value) => _data.Add(value); 46 | 47 | public ReadOnlySpan Values 48 | { 49 | get 50 | { 51 | #if NET6_0_OR_GREATER 52 | return CollectionsMarshal.AsSpan(_data); 53 | #else 54 | return _data.ToArray(); 55 | #endif 56 | } 57 | } 58 | } 59 | 60 | public sealed class StringDataColumn : DataColumn 61 | { 62 | public StringDataColumn(string name) : base(name) { } 63 | public override int GetInt32(int rowIndex) => 0; 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public override string GetString(int rowIndex) => this[rowIndex]; 67 | 68 | public override ushort GetUInt16(int rowIndex) => 0; 69 | public override bool GetBoolean(int rowIndex) => false; 70 | public override void AddEmptyValue() => AddValue(string.Empty); 71 | } 72 | 73 | public sealed class Int32DataColumn : DataColumn 74 | { 75 | public Int32DataColumn(string name) : base(name) { } 76 | 77 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 78 | public override int GetInt32(int rowIndex) => this[rowIndex]; 79 | 80 | public override string GetString(int rowIndex) => this[rowIndex].ToString(); 81 | public override ushort GetUInt16(int rowIndex) => (ushort)this[rowIndex]; 82 | public override bool GetBoolean(int rowIndex) => this[rowIndex] != 0; 83 | public override void AddEmptyValue() => AddValue(0); 84 | } 85 | 86 | public sealed class UInt16DataColumn : DataColumn 87 | { 88 | public UInt16DataColumn(string name) : base(name) { } 89 | public override int GetInt32(int rowIndex) => this[rowIndex]; 90 | public override string GetString(int rowIndex) => this[rowIndex].ToString(); 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | public override ushort GetUInt16(int rowIndex) => this[rowIndex]; 94 | public override bool GetBoolean(int rowIndex) => this[rowIndex] != 0; 95 | public override void AddEmptyValue() => AddValue(0); 96 | } -------------------------------------------------------------------------------- /src/Model/Huffman/HuffmanTree.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Huffman; 4 | 5 | //hardcoded.... 6 | internal class HuffmanTree 7 | { 8 | public Node? Root { get; set; } 9 | 10 | public static readonly IReadOnlyDictionary TABLE = new Dictionary(38) 11 | { 12 | {'0', "11111011"}, 13 | {' ', "10"}, 14 | {'1', "1111100"}, 15 | {'2', "001100"}, 16 | {'3', "1101101"}, 17 | {'4', "11111010"}, 18 | {'5', "00010110"}, 19 | {'6', "1101111"}, 20 | {'7', "01111"}, 21 | {'8', "000100"}, 22 | {'9', "01110"}, 23 | {'a', "11110"}, 24 | {'b', "0101"}, 25 | {'c', "01000"}, 26 | {'d', "110001"}, 27 | {'e', "110000"}, 28 | {'f', "010011"}, 29 | {'g', "11010"}, 30 | {'h', "00011"}, 31 | {'i', "1111110"}, 32 | {'j', "000101110"}, 33 | {'k', "010010"}, 34 | {'l', "11101"}, 35 | {'m', "01101"}, 36 | {'n', "001101"}, 37 | {'o', "1111111"}, 38 | {'p', "11001"}, 39 | {'q', "11011001"}, 40 | {'r', "11100"}, 41 | {'s', "0010"}, 42 | {'t', "01100"}, 43 | {'u', "00001"}, 44 | {'v', "1101110"}, 45 | {'w', "00000"}, 46 | {'x', "00111"}, 47 | {'y', "0001010"}, 48 | {'z', "11011000"} 49 | }; 50 | 51 | //todo find a way to build this like d2? 52 | public void Build() 53 | { 54 | Root = new Node(); 55 | foreach (var entry in TABLE) 56 | { 57 | var current = Root; 58 | foreach (char bit in entry.Value.AsSpan()) 59 | { 60 | if (bit == '1') 61 | { 62 | if (current.Right == null) 63 | { 64 | current.Right = new Node(); 65 | } 66 | current = current.Right; 67 | } 68 | else if (bit == '0') 69 | { 70 | if (current.Left == null) 71 | { 72 | current.Left = new Node(); 73 | } 74 | current = current.Left; 75 | } 76 | } 77 | current.Symbol = entry.Key; 78 | } 79 | } 80 | 81 | public InternalBitArray EncodeChar(char source) 82 | { 83 | var encodedSymbol = Root?.Traverse(source, new InternalBitArray(0)); 84 | if (encodedSymbol is null) 85 | throw new InvalidOperationException("Could not encode with an empty tree."); 86 | return encodedSymbol; 87 | } 88 | 89 | public char DecodeChar(IBitReader reader) 90 | { 91 | var current = Root; 92 | while (!(current?.IsLeaf() ?? true)) 93 | { 94 | if (reader.ReadBit()) 95 | { 96 | if (current.Right is not null) 97 | { 98 | current = current.Right; 99 | } 100 | } 101 | else 102 | { 103 | if (current.Left is not null) 104 | { 105 | current = current.Left; 106 | } 107 | } 108 | } 109 | return current?.Symbol ?? '\0'; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Model/Save/Skills.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public class ClassSkills 6 | { 7 | private const int SKILL_COUNT = 30; 8 | 9 | private static readonly uint[] SKILL_OFFSETS = { 6, 36, 66, 96, 126, 221, 251 }; 10 | public ushort? Header { get; set; } 11 | public List Skills { get; } = new List(SKILL_COUNT); 12 | 13 | public ClassSkill this[int i] => Skills[i]; 14 | 15 | public void Write(IBitWriter writer) 16 | { 17 | writer.WriteUInt16(Header ?? 0x6669); 18 | for (int i = 0; i < SKILL_COUNT; i++) 19 | { 20 | Skills[i].Write(writer); 21 | } 22 | } 23 | 24 | public static ClassSkills Read(IBitReader reader, int playerClass) 25 | { 26 | var classSkills = new ClassSkills 27 | { 28 | Header = reader.ReadUInt16() 29 | }; 30 | uint offset = SKILL_OFFSETS[playerClass]; 31 | for (uint i = 0; i < SKILL_COUNT; i++) 32 | { 33 | var skill = ClassSkill.Read(offset + i, reader.ReadByte()); 34 | classSkills.Skills.Add(skill); 35 | } 36 | return classSkills; 37 | } 38 | 39 | [Obsolete("Try the direct-read overload!")] 40 | public static ClassSkills Read(ReadOnlySpan bytes, int playerClass) 41 | { 42 | using var reader = new BitReader(bytes); 43 | return Read(reader, playerClass); 44 | } 45 | 46 | [Obsolete("Try the non-allocating overload!")] 47 | public static byte[] Write(ClassSkills classSkills) 48 | { 49 | using var writer = new BitWriter(); 50 | classSkills.Write(writer); 51 | return writer.ToArray(); 52 | } 53 | } 54 | 55 | public class ClassSkill 56 | { 57 | public uint Id { get; set; } 58 | public byte Points { get; set; } 59 | 60 | public void Write(IBitWriter writer) => writer.WriteByte(Points); 61 | 62 | public static ClassSkill Read(uint id, byte points) 63 | { 64 | var classSkill = new ClassSkill 65 | { 66 | Id = id, 67 | Points = points 68 | }; 69 | return classSkill; 70 | } 71 | 72 | [Obsolete("Try the non-allocating overload!")] 73 | public static byte[] Write(ClassSkill classSkill) 74 | { 75 | using var writer = new BitWriter(); 76 | classSkill.Write(writer); 77 | return writer.ToArray(); 78 | } 79 | } 80 | 81 | //header skill 82 | public class Skill 83 | { 84 | public uint Id { get; set; } 85 | 86 | public void Write(IBitWriter writer) => writer.WriteUInt32(Id); 87 | 88 | public static Skill Read(IBitReader reader) 89 | { 90 | var skill = new Skill 91 | { 92 | Id = reader.ReadUInt32() 93 | }; 94 | return skill; 95 | } 96 | 97 | [Obsolete("Try the direct-read overload!")] 98 | public static Skill Read(ReadOnlySpan bytes) 99 | { 100 | var skill = new Skill 101 | { 102 | Id = BitConverter.ToUInt32(bytes) 103 | }; 104 | return skill; 105 | } 106 | 107 | [Obsolete("Try the non-allocating overload!")] 108 | public static byte[] Write(Skill skill) 109 | { 110 | using var writer = new BitWriter(); 111 | skill.Write(writer); 112 | return writer.ToArray(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Model/Data/ItemsData.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.Model.Huffman; 2 | 3 | namespace D2SLib.Model.Data; 4 | 5 | //collections or ArmorData MiscData WeaponsData with helper methods 6 | public sealed class ItemsData 7 | { 8 | public ItemsData(ArmorData armorData, WeaponsData weaponsData, MiscData miscData) 9 | { 10 | ArmorData = armorData; 11 | WeaponsData = weaponsData; 12 | MiscData = miscData; 13 | } 14 | 15 | public ArmorData ArmorData { get; } 16 | public WeaponsData WeaponsData { get; } 17 | public MiscData MiscData { get; } 18 | 19 | private HuffmanTree? _itemCodeTree = null; 20 | internal HuffmanTree ItemCodeTree 21 | { 22 | get => _itemCodeTree ??= InitializeHuffmanTree(); 23 | set => _itemCodeTree = value; 24 | } 25 | 26 | public DataRow? this[string code] => GetByCode(code); 27 | 28 | public DataRow? GetByCode(string code) 29 | { 30 | return ArmorData[code] 31 | ?? WeaponsData[code] 32 | ?? MiscData[code]; 33 | } 34 | 35 | public bool IsArmor(string code) => ArmorData[code] is not null; 36 | 37 | public bool IsWeapon(string code) => WeaponsData[code] is not null; 38 | 39 | public bool IsMisc(string code) => MiscData[code] is not null; 40 | 41 | private HuffmanTree InitializeHuffmanTree() 42 | { 43 | /* 44 | List items = new List(); 45 | foreach(var row in ArmorData.Rows) 46 | { 47 | items.Add(row["code"]); 48 | } 49 | foreach (var row in WeaponsData.Rows) 50 | { 51 | items.Add(row["code"]); 52 | } 53 | foreach (var row in MiscData.Rows) 54 | { 55 | items.Add(row["code"]); 56 | } 57 | */ 58 | var itemCodeTree = new HuffmanTree(); 59 | itemCodeTree.Build(); 60 | return itemCodeTree; 61 | } 62 | } 63 | 64 | public sealed class ArmorData : DataFile 65 | { 66 | public DataRow? this[string code] => GetByColumnAndValue("code", code); 67 | 68 | public static ArmorData Read(Stream data) 69 | { 70 | var armor = new ArmorData(); 71 | armor.ReadData(data); 72 | return armor; 73 | } 74 | 75 | public static ArmorData Read(string file) 76 | { 77 | using Stream stream = File.OpenRead(file); 78 | return Read(stream); 79 | } 80 | } 81 | 82 | public sealed class WeaponsData : DataFile 83 | { 84 | public DataRow? this[string code] => GetByColumnAndValue("code", code); 85 | 86 | public static WeaponsData Read(Stream data) 87 | { 88 | var weapons = new WeaponsData(); 89 | weapons.ReadData(data); 90 | return weapons; 91 | } 92 | 93 | public static WeaponsData Read(string file) 94 | { 95 | using Stream stream = File.OpenRead(file); 96 | return Read(stream); 97 | } 98 | } 99 | 100 | public sealed class MiscData : DataFile 101 | { 102 | public DataRow? this[string code] => GetByColumnAndValue("code", code); 103 | 104 | public static MiscData Read(Stream data) 105 | { 106 | var misc = new MiscData(); 107 | misc.ReadData(data); 108 | return misc; 109 | } 110 | 111 | public static MiscData Read(string file) 112 | { 113 | using Stream stream = File.OpenRead(file); 114 | return Read(stream); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/BitReaderTests.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Text; 3 | using static System.Buffers.Binary.BinaryPrimitives; 4 | 5 | namespace D2SLibTests; 6 | 7 | [TestClass] 8 | public sealed class BitReaderTests 9 | { 10 | [TestMethod] 11 | public void CanReadBits() 12 | { 13 | byte[] bytes = new byte[32]; 14 | new Random(1337).NextBytes(bytes); 15 | using var bro = new BitReader_Old(bytes); 16 | using var br = new BitReader(bytes); 17 | 18 | var oldBits = bro.ReadBits(17); 19 | var newBits = br.ReadBits(17); 20 | 21 | for (int i = 0; i < oldBits.Length; i++) 22 | { 23 | Console.Write(Convert.ToString(oldBits[i], 2).PadLeft(8, '0')); 24 | Console.Write(' '); 25 | } 26 | Console.WriteLine(); 27 | 28 | for (int i = 0; i < newBits.Length; i++) 29 | { 30 | Console.Write(Convert.ToString(newBits[i], 2).PadLeft(8, '0')); 31 | Console.Write(' '); 32 | } 33 | Console.WriteLine(); 34 | 35 | CollectionAssert.AreEqual(oldBits, newBits); 36 | Assert.AreEqual(bro.Position, br.Position); 37 | } 38 | 39 | [TestMethod] 40 | public void CanReadByte() 41 | { 42 | byte[] bytes = new byte[] { 137 }; 43 | using var bro = new BitReader_Old(bytes); 44 | using var br = new BitReader(bytes); 45 | 46 | Assert.AreEqual(bro.ReadByte(), br.ReadByte()); 47 | Assert.AreEqual(bro.Position, br.Position); 48 | } 49 | 50 | [TestMethod] 51 | public void CanReadBytes() 52 | { 53 | byte[] bytes = new byte[97]; 54 | new Random(1337).NextBytes(bytes); 55 | using var bro = new BitReader_Old(bytes); 56 | using var br = new BitReader(bytes); 57 | 58 | bro.SeekBits(5); 59 | br.SeekBits(5); 60 | 61 | var oldBits = bro.ReadBytes(95); 62 | var newBits = br.ReadBytes(95); 63 | 64 | CollectionAssert.AreEqual(oldBits, newBits); 65 | Assert.AreEqual(bro.Position, br.Position); 66 | } 67 | 68 | [TestMethod] 69 | public void CanReadInt32() 70 | { 71 | byte[] bytes = new byte[sizeof(int)]; 72 | WriteInt32LittleEndian(bytes, 1370048); 73 | using var bro = new BitReader_Old(bytes); 74 | using var br = new BitReader(bytes); 75 | 76 | Assert.AreEqual(bro.ReadInt32(), br.ReadInt32()); 77 | Assert.AreEqual(bro.Position, br.Position); 78 | } 79 | 80 | [TestMethod] 81 | public void CanReadUInt32() 82 | { 83 | byte[] bytes = new byte[sizeof(uint)]; 84 | WriteUInt32LittleEndian(bytes, 1370048); 85 | using var bro = new BitReader_Old(bytes); 86 | using var br = new BitReader(bytes); 87 | 88 | Assert.AreEqual(bro.ReadUInt32(), br.ReadUInt32()); 89 | Assert.AreEqual(bro.Position, br.Position); 90 | } 91 | 92 | [TestMethod] 93 | public void CanReadUInt16() 94 | { 95 | byte[] bytes = new byte[sizeof(ushort)]; 96 | WriteUInt16LittleEndian(bytes, 7401); 97 | using var bro = new BitReader_Old(bytes); 98 | using var br = new BitReader(bytes); 99 | 100 | Assert.AreEqual(bro.ReadUInt16(), br.ReadUInt16()); 101 | Assert.AreEqual(bro.Position, br.Position); 102 | } 103 | 104 | [TestMethod] 105 | public void CanReadString() 106 | { 107 | byte[] bytes = Encoding.ASCII.GetBytes("test"); 108 | using var bro = new BitReader_Old(bytes); 109 | using var br = new BitReader(bytes); 110 | 111 | Assert.AreEqual(bro.ReadString(4), br.ReadString(4)); 112 | Assert.AreEqual(bro.Position, br.Position); 113 | } 114 | } -------------------------------------------------------------------------------- /test/BitWriter_Old.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace D2SLibTests; 4 | 5 | public class BitWriter_Old : IDisposable 6 | { 7 | private BitArray _bits; 8 | 9 | private int _position = 0; 10 | public int Position 11 | { 12 | get => _position; 13 | private set 14 | { 15 | if (value > Length) 16 | { 17 | Length = value; 18 | } 19 | _position = value; 20 | } 21 | } 22 | public int Length { get; private set; } 23 | public BitWriter_Old(int initialCapacity) 24 | { 25 | _bits = new BitArray(initialCapacity); 26 | Position = 0; 27 | } 28 | 29 | public BitWriter_Old() : this(1024) 30 | { 31 | } 32 | 33 | public void WriteBit(bool value) 34 | { 35 | // grow if necessary 36 | while (Position >= _bits.Length) 37 | { 38 | if (_bits.Length == 0) 39 | { 40 | _bits.Length = 1024; 41 | } 42 | else 43 | { 44 | _bits.Length = _bits.Length * 2; 45 | } 46 | } 47 | _bits[Position++] = value; 48 | } 49 | 50 | public void WriteBits(BitArray bits, int numberOfBits) 51 | { 52 | for (int i = 0; i < numberOfBits; i++) 53 | { 54 | WriteBit(bits[i]); 55 | } 56 | } 57 | public void WriteBytes(byte[] value, int numberOfBits) 58 | { 59 | Array.Resize(ref value, (numberOfBits - 1) / 8 + 1); 60 | var bits = new BitArray(value); 61 | WriteBits(bits, numberOfBits); 62 | } 63 | public void WriteBytes(byte[] value) 64 | { 65 | var bits = new BitArray(value); 66 | WriteBits(bits, value.Length * 8); 67 | } 68 | 69 | public void WriteByte(byte value, int size) => WriteBytes(new byte[] { value }, size); 70 | 71 | public void WriteByte(byte value) => WriteBytes(new byte[] { value }, 8); 72 | 73 | public void WriteUInt16(ushort value, int numberOfBits) => WriteBytes(BitConverter.GetBytes(value), numberOfBits); 74 | 75 | public void WriteUInt16(ushort value) => WriteBytes(BitConverter.GetBytes(value), 16); 76 | public void WriteUInt32(uint value, int numberOfBits) => WriteBytes(BitConverter.GetBytes(value), numberOfBits); 77 | public void WriteUInt32(uint value) => WriteBytes(BitConverter.GetBytes(value), 32); 78 | public void WriteInt32(int value, int numberOfBits) => WriteBytes(BitConverter.GetBytes(value), numberOfBits); 79 | public void WriteInt32(int value) => WriteBytes(BitConverter.GetBytes(value), 32); 80 | public void WriteString(string s, int length) => WriteBytes(System.Text.Encoding.ASCII.GetBytes(s), length * 8); 81 | public byte[] ToArray() 82 | { 83 | byte[] bytes = new byte[((Length - 1) / 8) + 1]; 84 | int byteIndex = 0; 85 | int bitIndex = 0; 86 | for (int i = 0; i < Length; ++i) 87 | { 88 | if (_bits[i]) 89 | { 90 | bytes[byteIndex] |= (byte)(1 << bitIndex); 91 | } 92 | ++bitIndex; 93 | if (bitIndex >= 8) 94 | { 95 | ++byteIndex; 96 | bitIndex = 0; 97 | } 98 | } 99 | return bytes; 100 | } 101 | public void SkipBits(int numberOfBits) => Position += numberOfBits; 102 | public void Skip(int bytes) => SkipBits(bytes * 8); 103 | public void SeekBits(int bitPosition) => Position = bitPosition; 104 | public void Seek(int bytePostion) => SeekBits(bytePostion * 8); 105 | public void Align() => Position = (Position + 7) & ~7; 106 | public void Dispose() => _bits = null!; 107 | } 108 | -------------------------------------------------------------------------------- /src/Model/Save/Appearances.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace D2SLib.Model.Save; 5 | 6 | public class Appearances 7 | { 8 | private readonly Appearance[] _parts = new Appearance[16]; 9 | 10 | public Appearance Head { get => _parts[0]; set => _parts[0] = value; } 11 | public Appearance Torso { get => _parts[1]; set => _parts[1] = value; } 12 | public Appearance Legs { get => _parts[2]; set => _parts[2] = value; } 13 | public Appearance RightArm { get => _parts[3]; set => _parts[3] = value; } 14 | public Appearance LeftArm { get => _parts[4]; set => _parts[4] = value; } 15 | public Appearance RightHand { get => _parts[5]; set => _parts[5] = value; } 16 | public Appearance LeftHand { get => _parts[6]; set => _parts[6] = value; } 17 | public Appearance Shield { get => _parts[7]; set => _parts[7] = value; } 18 | public Appearance Special1 { get => _parts[8]; set => _parts[8] = value; } 19 | public Appearance Special2 { get => _parts[9]; set => _parts[9] = value; } 20 | public Appearance Special3 { get => _parts[10]; set => _parts[10] = value; } 21 | public Appearance Special4 { get => _parts[11]; set => _parts[11] = value; } 22 | public Appearance Special5 { get => _parts[12]; set => _parts[12] = value; } 23 | public Appearance Special6 { get => _parts[13]; set => _parts[13] = value; } 24 | public Appearance Special7 { get => _parts[14]; set => _parts[14] = value; } 25 | public Appearance Special8 { get => _parts[15]; set => _parts[15] = value; } 26 | 27 | public void Write(IBitWriter writer) 28 | { 29 | for (int i = 0; i < _parts.Length; i++) 30 | { 31 | _parts[i].Write(writer); 32 | } 33 | } 34 | 35 | public static Appearances Read(IBitReader reader) 36 | { 37 | var appearances = new Appearances(); 38 | var parts = appearances._parts; 39 | 40 | for (int i = 0; i < parts.Length; i++) 41 | { 42 | parts[i] = new Appearance(reader); 43 | } 44 | 45 | return appearances; 46 | } 47 | 48 | [Obsolete("Try the direct-read overload!")] 49 | public static Appearances Read(ReadOnlySpan bytes) 50 | { 51 | using var reader = new BitReader(bytes); 52 | return Read(reader); 53 | } 54 | 55 | [Obsolete("Try the non-allocating overload!")] 56 | public static byte[] Write(Appearances appearances) 57 | { 58 | using var writer = new BitWriter(); 59 | appearances.Write(writer); 60 | return writer.ToArray(); 61 | } 62 | 63 | } 64 | 65 | public readonly struct Appearance : IEquatable 66 | { 67 | public Appearance(byte graphic, byte tint) 68 | { 69 | Graphic = graphic; 70 | Tint = tint; 71 | } 72 | 73 | public Appearance(IBitReader reader) 74 | { 75 | Graphic = reader.ReadByte(); 76 | Tint = reader.ReadByte(); 77 | } 78 | 79 | public readonly byte Graphic { get; } 80 | public readonly byte Tint { get; } 81 | 82 | public void Write(IBitWriter writer) 83 | { 84 | writer.WriteByte(Graphic); 85 | writer.WriteByte(Tint); 86 | } 87 | 88 | public bool Equals([AllowNull] Appearance other) 89 | { 90 | return Graphic == other.Graphic 91 | && Tint == other.Tint; 92 | } 93 | 94 | public override bool Equals(object? obj) => obj is Appearance app && Equals(app); 95 | 96 | public override int GetHashCode() => HashCode.Combine(Graphic, Tint); 97 | 98 | public static bool operator ==(Appearance left, Appearance right) => left.Equals(right); 99 | 100 | public static bool operator !=(Appearance left, Appearance right) => !left.Equals(right); 101 | } 102 | -------------------------------------------------------------------------------- /src/IO/BitReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Toolkit.HighPerformance.Buffers; 2 | using System.Buffers; 3 | using System.Text; 4 | using static System.Buffers.Binary.BinaryPrimitives; 5 | using static D2SLib.IO.InternalBitArray; 6 | 7 | namespace D2SLib.IO; 8 | 9 | public sealed class BitReader : IBitReader, IDisposable 10 | { 11 | private const int STACK_MAX = 0xff; 12 | 13 | private InternalBitArray _bits; 14 | public int Position { get; private set; } 15 | 16 | public BitReader(ReadOnlySpan bytes) 17 | { 18 | Position = 0; 19 | _bits = new InternalBitArray(bytes); 20 | } 21 | 22 | public bool ReadBit() => _bits[Position++]; 23 | 24 | public byte[] ReadBits(int numberOfBits) 25 | { 26 | byte[] bytes = new byte[GetByteArrayLengthFromBitLength(numberOfBits)]; 27 | ReadBits(numberOfBits, bytes); 28 | return bytes; 29 | } 30 | 31 | public MemoryOwner ReadBitsPooled(int numberOfBits) 32 | { 33 | var bytes = MemoryOwner.Allocate(GetByteArrayLengthFromBitLength(numberOfBits)); 34 | ReadBits(numberOfBits, bytes.Span); 35 | return bytes; 36 | } 37 | 38 | IMemoryOwner IBitReader.ReadBitsPooled(int numberOfBits) 39 | => ReadBitsPooled(numberOfBits); 40 | 41 | public int ReadBits(int numberOfBits, Span output) 42 | { 43 | int byteCount = GetByteArrayLengthFromBitLength(numberOfBits); 44 | 45 | if (output.Length < byteCount) 46 | throw new ArgumentOutOfRangeException(nameof(output)); 47 | 48 | int byteIndex = 0; 49 | int bitIndex = 0; 50 | for (int i = 0; i < numberOfBits; i++) 51 | { 52 | if (_bits[Position + i]) 53 | { 54 | output[byteIndex] |= (byte)(1 << bitIndex); 55 | } 56 | bitIndex++; 57 | if (bitIndex == 8) 58 | { 59 | byteIndex++; 60 | bitIndex = 0; 61 | } 62 | } 63 | Position += numberOfBits; 64 | 65 | return byteCount; 66 | } 67 | 68 | 69 | public int ReadBytes(int numberOfBytes, Span output) 70 | => ReadBits(numberOfBytes * 8, output); 71 | 72 | public int ReadBytes(Span output) 73 | => ReadBits(output.Length * 8, output); 74 | 75 | public byte[] ReadBytes(int numberOfBytes) 76 | => ReadBits(numberOfBytes * 8); 77 | 78 | public MemoryOwner ReadBytesPooled(int numberOfBytes) 79 | => ReadBitsPooled(numberOfBytes * 8); 80 | 81 | IMemoryOwner IBitReader.ReadBytesPooled(int numberOfBytes) 82 | => ReadBitsPooled(numberOfBytes * 8); 83 | 84 | public byte ReadByte(int bits) 85 | { 86 | if ((uint)bits > 8) throw new ArgumentOutOfRangeException(nameof(bits)); 87 | Span bytes = stackalloc byte[1]; 88 | bytes.Clear(); 89 | int bytesRead = ReadBits(bits, bytes); 90 | return bytes[0]; 91 | } 92 | 93 | public byte ReadByte() => ReadByte(8); 94 | 95 | public ushort ReadUInt16(int bits) 96 | { 97 | if ((uint)bits > sizeof(ushort) * 8) throw new ArgumentOutOfRangeException(nameof(bits)); 98 | Span bytes = stackalloc byte[sizeof(ushort)]; 99 | bytes.Clear(); 100 | ReadBits(bits, bytes); 101 | return ReadUInt16LittleEndian(bytes); 102 | } 103 | 104 | public ushort ReadUInt16() => ReadUInt16(sizeof(ushort) * 8); 105 | 106 | public uint ReadUInt32(int bits) 107 | { 108 | if ((uint)bits > sizeof(uint) * 8) throw new ArgumentOutOfRangeException(nameof(bits)); 109 | Span bytes = stackalloc byte[sizeof(uint)]; 110 | bytes.Clear(); 111 | ReadBits(bits, bytes); 112 | return ReadUInt32LittleEndian(bytes); 113 | } 114 | 115 | public uint ReadUInt32() => ReadUInt32(sizeof(uint) * 8); 116 | 117 | public int ReadInt32(int bits) 118 | { 119 | if ((uint)bits > sizeof(int) * 8) throw new ArgumentOutOfRangeException(nameof(bits)); 120 | Span bytes = stackalloc byte[sizeof(int)]; 121 | bytes.Clear(); 122 | ReadBits(bits, bytes); 123 | return ReadInt32LittleEndian(bytes); 124 | } 125 | 126 | public int ReadInt32() => ReadInt32(sizeof(int) * 8); 127 | 128 | public string ReadString(int byteCount) 129 | { 130 | using var pooledBytes = byteCount > STACK_MAX ? SpanOwner.Allocate(byteCount) : SpanOwner.Empty; 131 | Span bytes = byteCount > STACK_MAX ? pooledBytes.Span : stackalloc byte[byteCount]; 132 | bytes.Clear(); 133 | int readBytes = ReadBytes(bytes); 134 | bytes = bytes[..readBytes]; 135 | return Encoding.ASCII.GetString(bytes.Trim((byte)0)); 136 | } 137 | 138 | public void AdvanceBits(int bits) => Position += bits; 139 | public void SeekBits(int bitPosition) => Position = bitPosition; 140 | public void Seek(int bytePostion) => SeekBits(bytePostion * 8); 141 | 142 | public void Align() => Position = (Position + 7) & ~7; 143 | 144 | public void Dispose() => Interlocked.Exchange(ref _bits!, null)?.Dispose(); 145 | } 146 | -------------------------------------------------------------------------------- /src/IO/BitWriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Toolkit.HighPerformance.Buffers; 2 | using System.Buffers; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | using static D2SLib.IO.InternalBitArray; 6 | 7 | namespace D2SLib.IO; 8 | 9 | public sealed class BitWriter : IBitWriter, IDisposable 10 | { 11 | private const int STACK_MAX = 0xff; 12 | 13 | private InternalBitArray _bits; 14 | 15 | private int _position = 0; 16 | public int Position 17 | { 18 | get => _position; 19 | private set 20 | { 21 | if (value > Length) 22 | { 23 | Length = value; 24 | } 25 | _position = value; 26 | } 27 | } 28 | 29 | public int Length { get; private set; } 30 | 31 | public BitWriter(int initialCapacity) 32 | { 33 | _bits = new InternalBitArray(initialCapacity); 34 | Position = 0; 35 | } 36 | 37 | public BitWriter() : this(1024) 38 | { 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public void WriteBit(bool value) 43 | { 44 | if (_position >= _bits.Length) 45 | { 46 | Grow(); 47 | } 48 | _bits[Position++] = value; 49 | } 50 | 51 | [MethodImpl(MethodImplOptions.NoInlining)] 52 | private void Grow() 53 | { 54 | while (_position >= _bits.Length) 55 | { 56 | if (_bits.Length == 0) 57 | { 58 | _bits.Length = 1024; 59 | } 60 | else 61 | { 62 | _bits.Length *= 2; 63 | } 64 | } 65 | } 66 | 67 | public void WriteBits(IList bits) => WriteBits(bits, bits.Count); 68 | 69 | public void WriteBits(IList bits, int numberOfBits) 70 | { 71 | for (int i = 0; i < numberOfBits; i++) 72 | { 73 | WriteBit(bits[i]); 74 | } 75 | } 76 | 77 | public void WriteBytes(ReadOnlySpan value) 78 | { 79 | using var bits = new InternalBitArray(value); 80 | WriteBits(bits); 81 | } 82 | 83 | public void WriteBytes(ReadOnlySpan value, int numberOfBits) 84 | { 85 | using var bits = new InternalBitArray(value) { Length = numberOfBits }; 86 | WriteBits(bits, numberOfBits); 87 | } 88 | 89 | public void WriteByte(byte value, int size) => WriteBytes(stackalloc byte[] { value }, size); 90 | 91 | public void WriteByte(byte value) => WriteBytes(stackalloc byte[] { value }, 8); 92 | 93 | public void WriteUInt16(ushort value, int numberOfBits) 94 | { 95 | Span bytes = stackalloc byte[sizeof(ushort)]; 96 | BitConverter.TryWriteBytes(bytes, value); 97 | WriteBytes(bytes, numberOfBits); 98 | } 99 | public void WriteUInt16(ushort value) => WriteUInt16(value, sizeof(ushort) * 8); 100 | 101 | public void WriteUInt32(uint value, int numberOfBits) 102 | { 103 | Span bytes = stackalloc byte[sizeof(uint)]; 104 | BitConverter.TryWriteBytes(bytes, value); 105 | WriteBytes(bytes, numberOfBits); 106 | } 107 | public void WriteUInt32(uint value) => WriteUInt32(value, sizeof(uint) * 8); 108 | 109 | public void WriteInt32(int value, int numberOfBits) 110 | { 111 | Span bytes = stackalloc byte[sizeof(int)]; 112 | BitConverter.TryWriteBytes(bytes, value); 113 | WriteBytes(bytes, numberOfBits); 114 | } 115 | public void WriteInt32(int value) => WriteInt32(value, sizeof(int) * 8); 116 | 117 | public void WriteString(ReadOnlySpan s, int length) //=> WriteBytes(System.Text.Encoding.ASCII.GetBytes(s), length * 8); 118 | { 119 | Span bytes = length > STACK_MAX ? new byte[length] : stackalloc byte[length]; 120 | Encoding.ASCII.GetBytes(s.Length > length ? s[..length] : s, bytes); 121 | WriteBytes(bytes, length * 8); 122 | } 123 | 124 | public byte[] ToArray() 125 | { 126 | byte[] bytes = new byte[GetByteArrayLengthFromBitLength(Length)]; 127 | InternalGetBytes(bytes); 128 | return bytes; 129 | } 130 | 131 | public MemoryOwner ToPooledArray() 132 | { 133 | var bytes = MemoryOwner.Allocate(GetByteArrayLengthFromBitLength(Length)); 134 | InternalGetBytes(bytes.Span); 135 | return bytes; 136 | } 137 | 138 | IMemoryOwner IBitWriter.ToPooledArray() => ToPooledArray(); 139 | 140 | public int GetBytes(Span output) 141 | { 142 | int byteLength = GetByteArrayLengthFromBitLength(Length); 143 | 144 | if (byteLength > output.Length) 145 | throw new ArgumentOutOfRangeException(nameof(output)); 146 | 147 | InternalGetBytes(output); 148 | 149 | return byteLength; 150 | } 151 | 152 | // assumes calling method has sized output correctly 153 | private void InternalGetBytes(Span output) 154 | { 155 | int byteIndex = 0; 156 | int bitIndex = 0; 157 | for (int i = 0; i < Length; ++i) 158 | { 159 | if (_bits[i]) 160 | { 161 | output[byteIndex] |= (byte)(1 << bitIndex); 162 | } 163 | ++bitIndex; 164 | if (bitIndex >= 8) 165 | { 166 | ++byteIndex; 167 | bitIndex = 0; 168 | } 169 | } 170 | } 171 | 172 | public void SkipBits(int numberOfBits) => Position += numberOfBits; 173 | public void Skip(int bytes) => SkipBits(bytes * 8); 174 | public void SeekBits(int bitPosition) => Position = bitPosition; 175 | public void Seek(int bytePostion) => SeekBits(bytePostion * 8); 176 | public void Align() => Position = (Position + 7) & ~7; 177 | public void Dispose() => Interlocked.Exchange(ref _bits!, null)?.Dispose(); 178 | } 179 | -------------------------------------------------------------------------------- /src/Model/Save/NPCDialogs.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public sealed class NPCDialogSection 6 | { 7 | private readonly NPCDialogDifficulty[] _difficulties = new NPCDialogDifficulty[3]; 8 | 9 | //0x02c9 [npc header identifier = 0x01, 0x77 ".w"] 10 | public ushort? Header { get; set; } 11 | //0x02ca [npc header length = 0x34] 12 | public ushort? Length { get; set; } 13 | public NPCDialogDifficulty Normal => _difficulties[0]; 14 | public NPCDialogDifficulty Nightmare => _difficulties[1]; 15 | public NPCDialogDifficulty Hell => _difficulties[2]; 16 | 17 | public void Write(IBitWriter writer) 18 | { 19 | writer.WriteUInt16(Header ?? 0x7701); 20 | writer.WriteUInt16(Length ?? 0x34); 21 | 22 | int start = writer.Position; 23 | 24 | for (int i = 0; i < _difficulties.Length; i++) 25 | { 26 | _difficulties[i].Write(writer); 27 | } 28 | 29 | writer.SeekBits(start + (0x30 * 8)); 30 | } 31 | 32 | public static NPCDialogSection Read(IBitReader reader) 33 | { 34 | var npcDialogSection = new NPCDialogSection 35 | { 36 | Header = reader.ReadUInt16(), 37 | Length = reader.ReadUInt16() 38 | }; 39 | 40 | Span bytes = stackalloc byte[0x30]; 41 | reader.ReadBytes(bytes); 42 | using var bits = new InternalBitArray(bytes); 43 | 44 | for (int i = 0; i < npcDialogSection._difficulties.Length; i++) 45 | { 46 | npcDialogSection._difficulties[i] = NPCDialogDifficulty.Read(bits); 47 | } 48 | 49 | return npcDialogSection; 50 | } 51 | 52 | [Obsolete("Try the direct-read overload!")] 53 | public static NPCDialogSection Read(ReadOnlySpan bytes) 54 | { 55 | using var reader = new BitReader(bytes); 56 | return Read(reader); 57 | } 58 | 59 | [Obsolete("Try the non-allocating overload!")] 60 | public static byte[] Write(NPCDialogSection npcDialogSection) 61 | { 62 | using var writer = new BitWriter(); 63 | npcDialogSection.Write(writer); 64 | return writer.ToArray(); 65 | } 66 | } 67 | 68 | //8 bytes per difficulty for Intro for each Difficulty followed by 8 bytes per difficulty for Congrats for each difficulty 69 | public sealed class NPCDialogDifficulty 70 | { 71 | private readonly NPCDialogData[] _dialogs = new NPCDialogData[41]; 72 | 73 | private NPCDialogDifficulty() { } 74 | 75 | public NPCDialogData WarrivActII => _dialogs[0]; 76 | public NPCDialogData Unk0x0001 => _dialogs[1]; 77 | public NPCDialogData Charsi => _dialogs[2]; 78 | public NPCDialogData WarrivActI => _dialogs[3]; 79 | public NPCDialogData Kashya => _dialogs[4]; 80 | public NPCDialogData Akara => _dialogs[5]; 81 | public NPCDialogData Gheed => _dialogs[6]; 82 | public NPCDialogData Unk0x0007 => _dialogs[7]; 83 | public NPCDialogData Greiz => _dialogs[8]; 84 | public NPCDialogData Jerhyn => _dialogs[9]; 85 | public NPCDialogData MeshifActII => _dialogs[10]; 86 | public NPCDialogData Geglash => _dialogs[11]; 87 | public NPCDialogData Lysander => _dialogs[12]; 88 | public NPCDialogData Fara => _dialogs[13]; 89 | public NPCDialogData Drogan => _dialogs[14]; 90 | public NPCDialogData Unk0x000F => _dialogs[15]; 91 | public NPCDialogData Alkor => _dialogs[16]; 92 | public NPCDialogData Hratli => _dialogs[17]; 93 | public NPCDialogData Ashera => _dialogs[18]; 94 | public NPCDialogData Unk0x0013 => _dialogs[19]; 95 | public NPCDialogData Unk0x0014 => _dialogs[20]; 96 | public NPCDialogData CainActIII => _dialogs[21]; 97 | public NPCDialogData Unk0x0016 => _dialogs[22]; 98 | public NPCDialogData Elzix => _dialogs[23]; 99 | public NPCDialogData Malah => _dialogs[24]; 100 | public NPCDialogData Anya => _dialogs[25]; 101 | public NPCDialogData Unk0x001A => _dialogs[26]; 102 | public NPCDialogData Natalya => _dialogs[27]; 103 | public NPCDialogData MeshifActIII => _dialogs[28]; 104 | public NPCDialogData Unk0x001D => _dialogs[29]; 105 | public NPCDialogData Unk0x001F => _dialogs[30]; 106 | public NPCDialogData Ormus => _dialogs[31]; 107 | public NPCDialogData Unk0x0021 => _dialogs[32]; 108 | public NPCDialogData Unk0x0022 => _dialogs[33]; 109 | public NPCDialogData Unk0x0023 => _dialogs[34]; 110 | public NPCDialogData Unk0x0024 => _dialogs[35]; 111 | public NPCDialogData Unk0x0025 => _dialogs[36]; 112 | public NPCDialogData CainActV => _dialogs[37]; 113 | public NPCDialogData Qualkehk => _dialogs[38]; 114 | public NPCDialogData Nihlathak => _dialogs[39]; 115 | public NPCDialogData Unk0x0029 => _dialogs[40]; 116 | 117 | // 23 bits here unused 118 | 119 | public void Write(IBitWriter writer) 120 | { 121 | for (int i = 0; i < _dialogs.Length; i++) 122 | { 123 | var data = _dialogs[i]; 124 | int position = writer.Position; 125 | writer.WriteBit(data.Introduction); 126 | writer.SeekBits(position + (0x18 * 8)); 127 | writer.WriteBit(data.Congratulations); 128 | writer.SeekBits(position + 1); 129 | } 130 | } 131 | 132 | internal static NPCDialogDifficulty Read(InternalBitArray bits) 133 | { 134 | var output = new NPCDialogDifficulty(); 135 | 136 | for (int i = 0; i < output._dialogs.Length; i++) 137 | { 138 | var data = new NPCDialogData 139 | { 140 | Introduction = bits[i], 141 | Congratulations = bits[i + (0x18 * 8)] 142 | }; 143 | output._dialogs[i] = data; 144 | } 145 | 146 | return output; 147 | } 148 | } 149 | 150 | public sealed class NPCDialogData 151 | { 152 | public bool Introduction { get; set; } 153 | public bool Congratulations { get; set; } 154 | } 155 | -------------------------------------------------------------------------------- /src/Model/Save/D2S.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using Microsoft.Toolkit.HighPerformance.Buffers; 3 | using System.Diagnostics; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace D2SLib.Model.Save; 7 | 8 | public sealed class D2S : IDisposable 9 | { 10 | private D2S(IBitReader reader) 11 | { 12 | Header = Header.Read(reader); 13 | ActiveWeapon = reader.ReadUInt32(); 14 | Name = reader.ReadString(16); 15 | Status = Status.Read(reader.ReadByte()); 16 | Progression = reader.ReadByte(); 17 | Unk0x0026 = reader.ReadBytes(2); 18 | ClassId = reader.ReadByte(); 19 | Unk0x0029 = reader.ReadBytes(2); 20 | Level = reader.ReadByte(); 21 | Created = reader.ReadUInt32(); 22 | LastPlayed = reader.ReadUInt32(); 23 | Unk0x0034 = reader.ReadBytes(4); 24 | AssignedSkills = Enumerable.Range(0, 16).Select(_ => Skill.Read(reader)).ToArray(); 25 | LeftSkill = Skill.Read(reader); 26 | RightSkill = Skill.Read(reader); 27 | LeftSwapSkill = Skill.Read(reader); 28 | RightSwapSkill = Skill.Read(reader); 29 | Appearances = Appearances.Read(reader); 30 | Location = Locations.Read(reader); 31 | MapId = reader.ReadUInt32(); 32 | Unk0x00af = reader.ReadBytes(2); 33 | Mercenary = Mercenary.Read(reader); 34 | RealmData = reader.ReadBytes(140); 35 | Quests = QuestsSection.Read(reader); 36 | Waypoints = WaypointsSection.Read(reader); 37 | NPCDialog = NPCDialogSection.Read(reader); 38 | Attributes = Attributes.Read(reader); 39 | 40 | ClassSkills = ClassSkills.Read(reader, ClassId); 41 | PlayerItemList = ItemList.Read(reader, Header.Version); 42 | PlayerCorpses = CorpseList.Read(reader, Header.Version); 43 | 44 | if (Status.IsExpansion) 45 | { 46 | MercenaryItemList = MercenaryItemList.Read(reader, Mercenary, Header.Version); 47 | Golem = Golem.Read(reader, Header.Version); 48 | } 49 | } 50 | 51 | //0x0000 52 | public Header Header { get; set; } 53 | //0x0010 54 | public uint ActiveWeapon { get; set; } 55 | //0x0014 sizeof(16) 56 | public string Name { get; set; } 57 | //0x0024 58 | public Status Status { get; set; } 59 | //0x0025 60 | [JsonIgnore] 61 | public byte Progression { get; set; } 62 | //0x0026 [unk = 0x0, 0x0] 63 | [JsonIgnore] 64 | public byte[]? Unk0x0026 { get; set; } 65 | //0x0028 66 | public byte ClassId { get; set; } 67 | //0x0029 [unk = 0x10, 0x1E] 68 | [JsonIgnore] 69 | public byte[]? Unk0x0029 { get; set; } 70 | //0x002b 71 | public byte Level { get; set; } 72 | //0x002c 73 | public uint Created { get; set; } 74 | //0x0030 75 | public uint LastPlayed { get; set; } 76 | //0x0034 [unk = 0xff, 0xff, 0xff, 0xff] 77 | [JsonIgnore] 78 | public byte[]? Unk0x0034 { get; set; } 79 | //0x0038 80 | public Skill[] AssignedSkills { get; set; } 81 | //0x0078 82 | public Skill LeftSkill { get; set; } 83 | //0x007c 84 | public Skill RightSkill { get; set; } 85 | //0x0080 86 | public Skill LeftSwapSkill { get; set; } 87 | //0x0084 88 | public Skill RightSwapSkill { get; set; } 89 | //0x0088 [char menu appearance] 90 | public Appearances Appearances { get; set; } 91 | //0x00a8 92 | public Locations Location { get; set; } 93 | //0x00ab 94 | public uint MapId { get; set; } 95 | //0x00af [unk = 0x0, 0x0] 96 | [JsonIgnore] 97 | public byte[]? Unk0x00af { get; set; } 98 | //0x00b1 99 | public Mercenary Mercenary { get; set; } 100 | //0x00bf [unk = 0x0] (server related data) 101 | [JsonIgnore] 102 | public byte[]? RealmData { get; set; } 103 | //0x014b 104 | public QuestsSection Quests { get; set; } 105 | //0x0279 106 | public WaypointsSection Waypoints { get; set; } 107 | //0x02c9 108 | public NPCDialogSection NPCDialog { get; set; } 109 | //0x2fc 110 | public Attributes Attributes { get; set; } 111 | 112 | public ClassSkills ClassSkills { get; set; } 113 | 114 | public ItemList PlayerItemList { get; set; } 115 | public CorpseList PlayerCorpses { get; set; } 116 | public MercenaryItemList? MercenaryItemList { get; set; } 117 | public Golem? Golem { get; set; } 118 | 119 | public void Write(IBitWriter writer) 120 | { 121 | Header.Write(writer); 122 | writer.WriteUInt32(ActiveWeapon); 123 | writer.WriteString(Name, 16); 124 | Status.Write(writer); 125 | writer.WriteByte(Progression); 126 | //Unk0x0026 127 | writer.WriteBytes(Unk0x0026 ?? new byte[2]); 128 | writer.WriteByte(ClassId); 129 | //Unk0x0029 130 | writer.WriteBytes(Unk0x0029 ?? stackalloc byte[] { 0x10, 0x1e }); 131 | writer.WriteByte(Level); 132 | writer.WriteUInt32(Created); 133 | writer.WriteUInt32(LastPlayed); 134 | //Unk0x0034 135 | writer.WriteBytes(Unk0x0034 ?? stackalloc byte[] { 0xff, 0xff, 0xff, 0xff }); 136 | for (int i = 0; i < 16; i++) 137 | { 138 | AssignedSkills[i].Write(writer); 139 | } 140 | LeftSkill.Write(writer); 141 | RightSkill.Write(writer); 142 | LeftSwapSkill.Write(writer); 143 | RightSwapSkill.Write(writer); 144 | Appearances.Write(writer); 145 | Location.Write(writer); 146 | writer.WriteUInt32(MapId); 147 | //0x00af [unk = 0x0, 0x0] 148 | writer.WriteBytes(Unk0x00af ?? new byte[2]); 149 | Mercenary.Write(writer); 150 | //0x00bf [unk = 0x0] (server related data) 151 | writer.WriteBytes(RealmData ?? new byte[140]); 152 | Quests.Write(writer); 153 | Waypoints.Write(writer); 154 | NPCDialog.Write(writer); 155 | Attributes.Write(writer); 156 | ClassSkills.Write(writer); 157 | PlayerItemList.Write(writer, Header.Version); 158 | PlayerCorpses.Write(writer, Header.Version); 159 | if (Status.IsExpansion) 160 | { 161 | MercenaryItemList?.Write(writer, Mercenary, Header.Version); 162 | Golem?.Write(writer, Header.Version); 163 | } 164 | } 165 | 166 | public static D2S Read(ReadOnlySpan bytes) 167 | { 168 | using var reader = new BitReader(bytes); 169 | var d2s = new D2S(reader); 170 | Debug.Assert(reader.Position == (bytes.Length * 8)); 171 | return d2s; 172 | } 173 | 174 | public static MemoryOwner WritePooled(D2S d2s) 175 | { 176 | using var writer = new BitWriter(); 177 | d2s.Write(writer); 178 | var bytes = writer.ToPooledArray(); 179 | Header.Fix(bytes.Span); 180 | return bytes; 181 | } 182 | 183 | public static byte[] Write(D2S d2s) 184 | { 185 | using var writer = new BitWriter(); 186 | d2s.Write(writer); 187 | byte[] bytes = writer.ToArray(); 188 | Header.Fix(bytes); 189 | return bytes; 190 | } 191 | 192 | public void Dispose() 193 | { 194 | Waypoints.Dispose(); 195 | Status.Dispose(); 196 | Quests.Dispose(); 197 | PlayerItemList.Dispose(); 198 | PlayerCorpses.Dispose(); 199 | MercenaryItemList?.Dispose(); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Model/Data/DataFile.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Toolkit.HighPerformance; 2 | using Microsoft.Toolkit.HighPerformance.Buffers; 3 | using System.Diagnostics; 4 | 5 | namespace D2SLib.Model.Data; 6 | 7 | public abstract class DataFile 8 | { 9 | private DataColumn[] _columns = Array.Empty(); 10 | private readonly Dictionary _columnsLookup = new(); 11 | 12 | public IReadOnlyDictionary ColumnNames => _columnsLookup; 13 | public ReadOnlySpan Columns => _columns; 14 | public int Count => _columns.Length == 0 ? 0 : _columns[0].Count; 15 | 16 | public DataRow this[int rowIndex] 17 | { 18 | get 19 | { 20 | if ((uint)rowIndex >= (uint)(_columns.Length == 0 ? 0 : _columns[0].Count)) 21 | throw new ArgumentOutOfRangeException(nameof(rowIndex)); 22 | return new DataRow(this, rowIndex); 23 | } 24 | } 25 | 26 | public IEnumerable GetRows() 27 | { 28 | if (_columns.Length > 0) 29 | { 30 | for (int i = 0, len = _columns[0].Count; i < len; i++) 31 | { 32 | yield return new DataRow(this, i); 33 | } 34 | } 35 | } 36 | 37 | protected void ReadData(Stream data) 38 | { 39 | _columns = Array.Empty(); 40 | _columnsLookup.Clear(); 41 | 42 | using var reader = new StreamReader(data); 43 | 44 | var colNames = new List(); 45 | 46 | int colIndex = 0; 47 | int rowIndex = 0; 48 | string? line = null; 49 | while ((line = reader.ReadLine()) is not null) 50 | { 51 | var curLine = line.AsSpan(); 52 | colIndex = 0; 53 | 54 | if (_columns.Length == 0) 55 | { 56 | // parse column names from header line 57 | foreach (var colName in curLine.Tokenize('\t')) 58 | { 59 | colNames.Add(StringPool.Shared.GetOrAdd(colName)); 60 | _columnsLookup.TryAdd(colNames[colIndex], colIndex); 61 | colIndex++; 62 | } 63 | 64 | _columns = new DataColumn[colIndex]; 65 | continue; 66 | } 67 | 68 | // add data to existing columns 69 | foreach (var value in curLine.Tokenize('\t')) 70 | { 71 | var col = _columns[colIndex] ??= new StringDataColumn(colNames[colIndex]); 72 | 73 | if (value.IsEmpty) 74 | { 75 | col.AddEmptyValue(); 76 | } 77 | else if (ushort.TryParse(value, out var ushortVal)) 78 | { 79 | switch (col) 80 | { 81 | case UInt16DataColumn ushortCol: 82 | ushortCol.AddValue(ushortVal); 83 | break; 84 | case Int32DataColumn intCol: 85 | intCol.AddValue(ushortVal); 86 | break; 87 | case StringDataColumn stringCol: 88 | { 89 | // we need to upgrade this column to a UInt16DataColum (if previous rows were blank) 90 | var newCol = new UInt16DataColumn(stringCol.Name); 91 | foreach (var v in stringCol.Values) 92 | { 93 | if (string.IsNullOrEmpty(v)) 94 | newCol.AddEmptyValue(); 95 | else 96 | throw new InvalidOperationException($"Trying to add the number {ushortVal} to a string column with previous values (row {rowIndex + 1}, col {colIndex + 1})"); 97 | } 98 | newCol.AddValue(ushortVal); 99 | _columns[colIndex] = newCol; 100 | } 101 | break; 102 | default: 103 | throw new InvalidOperationException("Unsupported column type."); 104 | } 105 | } 106 | else if (int.TryParse(value, out var intVal)) 107 | { 108 | switch (col) 109 | { 110 | case Int32DataColumn intCol: 111 | intCol.AddValue(intVal); 112 | break; 113 | case UInt16DataColumn ushortCol: 114 | { 115 | // we need to upgrade this column to an Int32DataColumn to hold this data 116 | var newCol = new Int32DataColumn(ushortCol.Name); 117 | foreach (var v in ushortCol.Values) 118 | { 119 | newCol.AddValue(v); 120 | } 121 | newCol.AddValue(intVal); 122 | _columns[colIndex] = newCol; 123 | } 124 | break; 125 | case StringDataColumn stringCol: 126 | { 127 | // we need to upgrade this column to an Int32DataColumn (if previous rows were blank) 128 | var newCol = new Int32DataColumn(stringCol.Name); 129 | foreach (var v in stringCol.Values) 130 | { 131 | if (string.IsNullOrEmpty(v)) 132 | newCol.AddEmptyValue(); 133 | else 134 | throw new InvalidOperationException($"Trying to add the number {intVal} to a string column with previous values (row {rowIndex + 1}, col {colIndex + 1})"); 135 | } 136 | newCol.AddValue(intVal); 137 | _columns[colIndex] = newCol; 138 | } 139 | break; 140 | default: 141 | throw new InvalidOperationException("Unsupported column type."); 142 | } 143 | } 144 | else // string value 145 | { 146 | if (col is StringDataColumn stringCol) 147 | { 148 | stringCol.AddValue(StringPool.Shared.GetOrAdd(value)); 149 | } 150 | else 151 | { 152 | if (value.Trim().IsEmpty) 153 | { 154 | col.AddEmptyValue(); 155 | } 156 | else 157 | { 158 | throw new InvalidOperationException($"Non-empty string '{value.ToString()}' being added to a {col.GetType().Name} column (row {rowIndex + 1}, col {colIndex + 1})"); 159 | } 160 | } 161 | } 162 | 163 | colIndex++; 164 | } 165 | 166 | // if any columns didn't have a row added, add empty values 167 | while (colIndex < _columns.Length) 168 | { 169 | _columns[colIndex++].AddEmptyValue(); 170 | } 171 | 172 | rowIndex++; 173 | } 174 | } 175 | 176 | public DataRow? GetByColumnAndValue(string name, ReadOnlySpan value) 177 | { 178 | if (int.TryParse(value, out int parsed)) 179 | { 180 | return GetByColumnAndValue(name, parsed); 181 | } 182 | 183 | if (ColumnNames.TryGetValue(name, out var colIdx)) 184 | { 185 | var col = _columns[colIdx]; 186 | value = value.Trim(); 187 | 188 | for (int i = 0; i < col.Count; i++) 189 | { 190 | if (value.Equals(col.GetString(i).AsSpan().Trim(), StringComparison.Ordinal)) 191 | { 192 | return new DataRow(this, i); 193 | } 194 | } 195 | } 196 | return null; 197 | } 198 | 199 | public DataRow? GetByColumnAndValue(string name, int value) 200 | { 201 | if (ColumnNames.TryGetValue(name, out var colIdx)) 202 | { 203 | var col = _columns[colIdx]; 204 | 205 | for (int i = 0; i < col.Count; i++) 206 | { 207 | if (col.GetInt32(i) == value) 208 | { 209 | return new DataRow(this, i); 210 | } 211 | } 212 | } 213 | return null; 214 | } 215 | } 216 | 217 | [DebuggerDisplay("Row {RowIndex}")] 218 | public sealed class DataRow 219 | { 220 | private readonly DataFile _data; 221 | 222 | public DataRow(DataFile data, int rowIndex) 223 | { 224 | _data = data; 225 | RowIndex = rowIndex; 226 | } 227 | 228 | public int RowIndex { get; } 229 | 230 | public DataCell this[int colIndex] => new(_data, colIndex, RowIndex); 231 | public DataCell this[string colName] => new(_data, _data.ColumnNames[colName], RowIndex); 232 | } 233 | 234 | [DebuggerDisplay("Cell (row {RowIndex}, col {ColIndex})")] 235 | public sealed class DataCell 236 | { 237 | private readonly DataFile _data; 238 | 239 | public DataCell(DataFile data, int colIndex, int rowIndex) 240 | { 241 | _data = data; 242 | ColIndex = colIndex; 243 | RowIndex = rowIndex; 244 | } 245 | 246 | public int RowIndex { get; } 247 | public int ColIndex { get; } 248 | 249 | public string Value => _data.Columns[ColIndex].GetString(RowIndex); 250 | public int ToInt32() => _data.Columns[ColIndex].GetInt32(RowIndex); 251 | public ushort ToUInt16() => _data.Columns[ColIndex].GetUInt16(RowIndex); 252 | public bool ToBool() => _data.Columns[ColIndex].GetBoolean(RowIndex); 253 | } 254 | 255 | -------------------------------------------------------------------------------- /src/Model/Save/Waypoints.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | 3 | namespace D2SLib.Model.Save; 4 | 5 | public sealed class WaypointsSection : IDisposable 6 | { 7 | private readonly WaypointsDifficulty[] _difficulties = new WaypointsDifficulty[3]; 8 | 9 | //0x0279 [waypoint data = 0x57, 0x53 "WS"] 10 | public ushort? Header { get; set; } 11 | //0x027b [waypoint header version = 0x1, 0x0, 0x0, 0x0] 12 | public uint? Version { get; set; } 13 | //0x027f [waypoint header length = 0x50, 0x0] 14 | public ushort? Length { get; set; } 15 | public WaypointsDifficulty Normal => _difficulties[0]; 16 | public WaypointsDifficulty Nightmare => _difficulties[1]; 17 | public WaypointsDifficulty Hell => _difficulties[2]; 18 | 19 | public void Write(IBitWriter writer) 20 | { 21 | writer.WriteUInt16(Header ?? 0x5357); 22 | writer.WriteUInt32(Version ?? 0x1); 23 | writer.WriteUInt16(Length ?? 0x50); 24 | 25 | for (int i = 0; i < _difficulties.Length; i++) 26 | { 27 | _difficulties[i].Write(writer); 28 | } 29 | } 30 | 31 | public static WaypointsSection Read(IBitReader reader) 32 | { 33 | var waypointsSection = new WaypointsSection 34 | { 35 | Header = reader.ReadUInt16(), 36 | Version = reader.ReadUInt32(), 37 | Length = reader.ReadUInt16() 38 | }; 39 | 40 | for (int i = 0; i < waypointsSection._difficulties.Length; i++) 41 | { 42 | waypointsSection._difficulties[i] = WaypointsDifficulty.Read(reader); 43 | } 44 | 45 | return waypointsSection; 46 | } 47 | 48 | [Obsolete("Try the direct-read overload!")] 49 | public static WaypointsSection Read(ReadOnlySpan bytes) 50 | { 51 | using var reader = new BitReader(bytes); 52 | return Read(reader); 53 | } 54 | 55 | [Obsolete("Try the non-allocating overload!")] 56 | public static byte[] Write(WaypointsSection waypointsSection) 57 | { 58 | using var writer = new BitWriter(); 59 | waypointsSection.Write(writer); 60 | return writer.ToArray(); 61 | } 62 | 63 | public void Dispose() 64 | { 65 | for (int i = 0; i < _difficulties.Length; i++) 66 | { 67 | Interlocked.Exchange(ref _difficulties[i]!, null)?.Dispose(); 68 | } 69 | } 70 | } 71 | 72 | public sealed class WaypointsDifficulty : IDisposable 73 | { 74 | private WaypointsDifficulty(IBitReader reader) 75 | { 76 | Header = reader.ReadUInt16(); 77 | ActI = ActIWaypoints.Read(reader); 78 | ActII = ActIIWaypoints.Read(reader); 79 | ActIII = ActIIIWaypoints.Read(reader); 80 | ActIV = ActIVWaypoints.Read(reader); 81 | ActV = ActVWaypoints.Read(reader); 82 | 83 | reader.Align(); 84 | reader.AdvanceBits(17 * 8); 85 | } 86 | 87 | //[0x02, 0x01] 88 | public ushort? Header { get; set; } 89 | public ActIWaypoints ActI { get; set; } 90 | public ActIIWaypoints ActII { get; set; } 91 | public ActIIIWaypoints ActIII { get; set; } 92 | public ActIVWaypoints ActIV { get; set; } 93 | public ActVWaypoints ActV { get; set; } 94 | 95 | public void Write(IBitWriter writer) 96 | { 97 | writer.WriteUInt16(Header ?? 0x102); 98 | 99 | int startPos = writer.Position; 100 | ActI.Write(writer); 101 | ActII.Write(writer); 102 | ActIII.Write(writer); 103 | ActIV.Write(writer); 104 | ActV.Write(writer); 105 | int endPos = writer.Position; 106 | 107 | writer.Align(); 108 | Span padding = stackalloc byte[13]; 109 | padding.Clear(); 110 | writer.WriteBytes(padding); 111 | } 112 | 113 | public static WaypointsDifficulty Read(IBitReader reader) 114 | { 115 | var waypointsDifficulty = new WaypointsDifficulty(reader); 116 | return waypointsDifficulty; 117 | } 118 | 119 | [Obsolete("Try the direct-read overload!")] 120 | public static WaypointsDifficulty Read(ReadOnlySpan bytes) 121 | { 122 | using var reader = new BitReader(bytes); 123 | return Read(reader); 124 | } 125 | 126 | [Obsolete("Try the non-allocating overload!")] 127 | public static byte[] Write(WaypointsDifficulty waypointsDifficulty) 128 | { 129 | using var writer = new BitWriter(); 130 | waypointsDifficulty.Write(writer); 131 | return writer.ToArray(); 132 | } 133 | 134 | public void Dispose() 135 | { 136 | ActI.Dispose(); 137 | ActII.Dispose(); 138 | ActIII.Dispose(); 139 | ActIV.Dispose(); 140 | ActV.Dispose(); 141 | } 142 | } 143 | 144 | public sealed class ActIWaypoints : IDisposable 145 | { 146 | private InternalBitArray _flags; 147 | private ActIWaypoints(InternalBitArray flags) => _flags = flags; 148 | 149 | public bool RogueEncampement { get => _flags[0]; set => _flags[0] = value; } 150 | public bool ColdPlains { get => _flags[1]; set => _flags[1] = value; } 151 | public bool StonyField { get => _flags[2]; set => _flags[2] = value; } 152 | public bool DarkWoods { get => _flags[3]; set => _flags[3] = value; } 153 | public bool BlackMarsh { get => _flags[4]; set => _flags[4] = value; } 154 | public bool OuterCloister { get => _flags[5]; set => _flags[5] = value; } 155 | public bool JailLvl1 { get => _flags[6]; set => _flags[6] = value; } 156 | public bool InnerCloister { get => _flags[7]; set => _flags[7] = value; } 157 | public bool CatacombsLvl2 { get => _flags[8]; set => _flags[8] = value; } 158 | 159 | public void Write(IBitWriter writer) 160 | { 161 | foreach (var flag in _flags) 162 | { 163 | writer.WriteBit(flag); 164 | } 165 | } 166 | 167 | public static ActIWaypoints Read(IBitReader reader) 168 | { 169 | Span bytes = stackalloc byte[2]; 170 | reader.ReadBits(9, bytes); 171 | var bits = new InternalBitArray(bytes); 172 | return new ActIWaypoints(bits); 173 | } 174 | 175 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 176 | } 177 | 178 | public sealed class ActIIWaypoints : IDisposable 179 | { 180 | private InternalBitArray _flags; 181 | private ActIIWaypoints(InternalBitArray flags) => _flags = flags; 182 | 183 | public bool LutGholein { get => _flags[0]; set => _flags[0] = value; } 184 | public bool SewersLvl2 { get => _flags[1]; set => _flags[1] = value; } 185 | public bool DryHills { get => _flags[2]; set => _flags[2] = value; } 186 | public bool HallsOfTheDeadLvl2 { get => _flags[3]; set => _flags[3] = value; } 187 | public bool FarOasis { get => _flags[4]; set => _flags[4] = value; } 188 | public bool LostCity { get => _flags[5]; set => _flags[5] = value; } 189 | public bool PalaceCellarLvl1 { get => _flags[6]; set => _flags[6] = value; } 190 | public bool ArcaneSanctuary { get => _flags[7]; set => _flags[7] = value; } 191 | public bool CanyonOfTheMagi { get => _flags[8]; set => _flags[8] = value; } 192 | 193 | public void Write(IBitWriter writer) 194 | { 195 | foreach (var flag in _flags) 196 | { 197 | writer.WriteBit(flag); 198 | } 199 | } 200 | 201 | public static ActIIWaypoints Read(IBitReader reader) 202 | { 203 | Span bytes = stackalloc byte[2]; 204 | reader.ReadBits(9, bytes); 205 | var bits = new InternalBitArray(bytes); 206 | return new ActIIWaypoints(bits); 207 | } 208 | 209 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 210 | } 211 | 212 | public sealed class ActIIIWaypoints : IDisposable 213 | { 214 | private InternalBitArray _flags; 215 | private ActIIIWaypoints(InternalBitArray flags) => _flags = flags; 216 | 217 | public bool KurastDocks { get => _flags[0]; set => _flags[0] = value; } 218 | public bool SpiderForest { get => _flags[1]; set => _flags[1] = value; } 219 | public bool GreatMarsh { get => _flags[2]; set => _flags[2] = value; } 220 | public bool FlayerJungle { get => _flags[3]; set => _flags[3] = value; } 221 | public bool LowerKurast { get => _flags[4]; set => _flags[4] = value; } 222 | public bool KurastBazaar { get => _flags[5]; set => _flags[5] = value; } 223 | public bool UpperKurast { get => _flags[6]; set => _flags[6] = value; } 224 | public bool Travincal { get => _flags[7]; set => _flags[7] = value; } 225 | public bool DuranceOfHateLvl2 { get => _flags[8]; set => _flags[8] = value; } 226 | 227 | public void Write(IBitWriter writer) 228 | { 229 | foreach (var flag in _flags) 230 | { 231 | writer.WriteBit(flag); 232 | } 233 | } 234 | 235 | public static ActIIIWaypoints Read(IBitReader reader) 236 | { 237 | Span bytes = stackalloc byte[2]; 238 | reader.ReadBits(9, bytes); 239 | var bits = new InternalBitArray(bytes); 240 | return new ActIIIWaypoints(bits); 241 | } 242 | 243 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 244 | } 245 | 246 | public sealed class ActIVWaypoints : IDisposable 247 | { 248 | private InternalBitArray _flags; 249 | private ActIVWaypoints(InternalBitArray flags) => _flags = flags; 250 | 251 | public bool ThePandemoniumFortress { get => _flags[0]; set => _flags[0] = value; } 252 | public bool CityOfTheDamned { get => _flags[1]; set => _flags[1] = value; } 253 | public bool RiverOfFlame { get => _flags[2]; set => _flags[2] = value; } 254 | 255 | public void Write(IBitWriter writer) 256 | { 257 | foreach (var flag in _flags) 258 | { 259 | writer.WriteBit(flag); 260 | } 261 | } 262 | 263 | public static ActIVWaypoints Read(IBitReader reader) 264 | { 265 | Span bytes = stackalloc byte[1]; 266 | reader.ReadBits(3, bytes); 267 | var bits = new InternalBitArray(bytes); 268 | return new ActIVWaypoints(bits); 269 | } 270 | 271 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 272 | } 273 | 274 | public sealed class ActVWaypoints : IDisposable 275 | { 276 | private InternalBitArray _flags; 277 | private ActVWaypoints(InternalBitArray flags) => _flags = flags; 278 | 279 | public bool Harrogath { get => _flags[0]; set => _flags[0] = value; } 280 | public bool FrigidHighlands { get => _flags[1]; set => _flags[1] = value; } 281 | public bool ArreatPlateau { get => _flags[2]; set => _flags[2] = value; } 282 | public bool CrystallinePassage { get => _flags[3]; set => _flags[3] = value; } 283 | public bool HallsOfPain { get => _flags[4]; set => _flags[4] = value; } 284 | public bool GlacialTrail { get => _flags[5]; set => _flags[5] = value; } 285 | public bool FrozenTundra { get => _flags[6]; set => _flags[6] = value; } 286 | public bool TheAncientsWay { get => _flags[7]; set => _flags[7] = value; } 287 | public bool WorldstoneKeepLvl2 { get => _flags[8]; set => _flags[8] = value; } 288 | 289 | public void Write(IBitWriter writer) 290 | { 291 | foreach (var flag in _flags) 292 | { 293 | writer.WriteBit(flag); 294 | } 295 | } 296 | 297 | public static ActVWaypoints Read(IBitReader reader) 298 | { 299 | Span bytes = stackalloc byte[2]; 300 | reader.ReadBits(9, bytes); 301 | var bits = new InternalBitArray(bytes); 302 | return new ActVWaypoints(bits); 303 | } 304 | 305 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 306 | } 307 | -------------------------------------------------------------------------------- /src/Model/Save/Quests.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using System.Diagnostics; 3 | 4 | namespace D2SLib.Model.Save; 5 | 6 | public sealed class QuestsSection : IDisposable 7 | { 8 | private readonly QuestsDifficulty[] _difficulties = new QuestsDifficulty[3]; 9 | 10 | //0x014b [unk = 0x1, 0x0, 0x0, 0x0] 11 | public uint? Magic { get; set; } 12 | //0x014f [quests header identifier = 0x57, 0x6f, 0x6f, 0x21 "Woo!"] 13 | public uint? Header { get; set; } 14 | //0x0153 [version = 0x6, 0x0, 0x0, 0x0] 15 | public uint? Version { get; set; } 16 | //0x0153 [quests header length = 0x2a, 0x1] 17 | public ushort? Length { get; set; } 18 | 19 | public QuestsDifficulty Normal => _difficulties[0]; 20 | public QuestsDifficulty Nightmare => _difficulties[1]; 21 | public QuestsDifficulty Hell => _difficulties[2]; 22 | 23 | public void Write(IBitWriter writer) 24 | { 25 | writer.WriteUInt32(Magic ?? 0x1); 26 | writer.WriteUInt32(Header ?? 0x216F6F57); 27 | writer.WriteUInt32(Version ?? 0x6); 28 | writer.WriteUInt16(Length ?? 0x12A); 29 | 30 | for (int i = 0; i < _difficulties.Length; i++) 31 | { 32 | _difficulties[i].Write(writer); 33 | } 34 | } 35 | 36 | public static QuestsSection Read(IBitReader reader) 37 | { 38 | var questSection = new QuestsSection 39 | { 40 | Magic = reader.ReadUInt32(), 41 | Header = reader.ReadUInt32(), 42 | Version = reader.ReadUInt32(), 43 | Length = reader.ReadUInt16() 44 | }; 45 | 46 | for (int i = 0; i < questSection._difficulties.Length; i++) 47 | { 48 | questSection._difficulties[i] = QuestsDifficulty.Read(reader); 49 | } 50 | 51 | return questSection; 52 | } 53 | 54 | [Obsolete("Try the direct-read overload!")] 55 | public static QuestsSection Read(byte[] bytes) 56 | { 57 | using var reader = new BitReader(bytes); 58 | return Read(reader); 59 | } 60 | 61 | [Obsolete("Try the non-allocating overload!")] 62 | public static byte[] Write(QuestsSection questSection) 63 | { 64 | using var writer = new BitWriter(); 65 | questSection.Write(writer); 66 | return writer.ToArray(); 67 | } 68 | 69 | public void Dispose() 70 | { 71 | for (int i = 0; i < _difficulties.Length; i++) 72 | { 73 | Interlocked.Exchange(ref _difficulties[i]!, null)?.Dispose(); 74 | } 75 | } 76 | } 77 | 78 | public sealed class QuestsDifficulty : IDisposable 79 | { 80 | private QuestsDifficulty(IBitReader reader) 81 | { 82 | ActI = ActIQuests.Read(reader); 83 | ActII = ActIIQuests.Read(reader); 84 | ActIII = ActIIIQuests.Read(reader); 85 | ActIV = ActIVQuests.Read(reader); 86 | ActV = ActVQuests.Read(reader); 87 | } 88 | 89 | public ActIQuests ActI { get; set; } 90 | public ActIIQuests ActII { get; set; } 91 | public ActIIIQuests ActIII { get; set; } 92 | public ActIVQuests ActIV { get; set; } 93 | public ActVQuests ActV { get; set; } 94 | 95 | public void Write(IBitWriter writer) 96 | { 97 | ActI.Write(writer); 98 | ActII.Write(writer); 99 | ActIII.Write(writer); 100 | ActIV.Write(writer); 101 | ActV.Write(writer); 102 | } 103 | 104 | public static QuestsDifficulty Read(IBitReader reader) 105 | { 106 | var qd = new QuestsDifficulty(reader); 107 | return qd; 108 | } 109 | 110 | [Obsolete("Try the direct-read overload!")] 111 | public static QuestsDifficulty Read(ReadOnlySpan bytes) 112 | { 113 | using var reader = new BitReader(bytes); 114 | return Read(reader); 115 | } 116 | 117 | [Obsolete("Try the non-allocating overload!")] 118 | public static byte[] Write(QuestsDifficulty questsDifficulty) 119 | { 120 | using var writer = new BitWriter(); 121 | questsDifficulty.Write(writer); 122 | Debug.Assert(writer.Position == 96 * 8); 123 | return writer.ToArray(); 124 | } 125 | 126 | public void Dispose() 127 | { 128 | ActI.Dispose(); 129 | ActII.Dispose(); 130 | ActIII.Dispose(); 131 | ActIV.Dispose(); 132 | ActV.Dispose(); 133 | } 134 | } 135 | 136 | 137 | public sealed class Quest : IDisposable 138 | { 139 | private InternalBitArray _flags; 140 | 141 | private Quest(InternalBitArray flags) => _flags = flags; 142 | 143 | public bool RewardGranted { get => _flags[0]; set => _flags[0] = value; } 144 | public bool RewardPending { get => _flags[1]; set => _flags[1] = value; } 145 | public bool Started { get => _flags[2]; set => _flags[2] = value; } 146 | public bool LeftTown { get => _flags[3]; set => _flags[3] = value; } 147 | public bool EnterArea { get => _flags[4]; set => _flags[4] = value; } 148 | public bool Custom1 { get => _flags[5]; set => _flags[5] = value; } 149 | public bool Custom2 { get => _flags[6]; set => _flags[6] = value; } 150 | public bool Custom3 { get => _flags[7]; set => _flags[7] = value; } 151 | public bool Custom4 { get => _flags[8]; set => _flags[8] = value; } 152 | public bool Custom5 { get => _flags[9]; set => _flags[9] = value; } 153 | public bool Custom6 { get => _flags[10]; set => _flags[10] = value; } 154 | public bool Custom7 { get => _flags[11]; set => _flags[11] = value; } 155 | public bool QuestLog { get => _flags[12]; set => _flags[12] = value; } 156 | public bool PrimaryGoalAchieved { get => _flags[13]; set => _flags[13] = value; } 157 | public bool CompletedNow { get => _flags[14]; set => _flags[14] = value; } 158 | public bool CompletedBefore { get => _flags[15]; set => _flags[15] = value; } 159 | 160 | public void Write(IBitWriter writer) 161 | { 162 | ushort flags = 0x0; 163 | ushort i = 1; 164 | foreach (var flag in _flags) 165 | { 166 | if (flag) 167 | { 168 | flags |= i; 169 | } 170 | i <<= 1; 171 | } 172 | writer.WriteUInt16(flags); 173 | } 174 | 175 | public static Quest Read(IBitReader reader) 176 | { 177 | Span bytes = stackalloc byte[2]; 178 | reader.ReadBytes(bytes); 179 | var bits = new InternalBitArray(bytes); 180 | return new Quest(bits); 181 | } 182 | 183 | [Obsolete("Try the direct-read overload!")] 184 | public static Quest Read(ReadOnlySpan bytes) 185 | { 186 | var bits = new InternalBitArray(bytes); 187 | return new Quest(bits); 188 | } 189 | 190 | [Obsolete("Try the non-allocating overload!")] 191 | public static byte[] Write(Quest quest) 192 | { 193 | using var writer = new BitWriter(); 194 | quest.Write(writer); 195 | return writer.ToArray(); 196 | } 197 | 198 | public void Dispose() => Interlocked.Exchange(ref _flags!, null)?.Dispose(); 199 | } 200 | 201 | public sealed class ActIQuests : IDisposable 202 | { 203 | private readonly Quest[] _quests = new Quest[8]; 204 | 205 | public Quest Introduction => _quests[0]; 206 | public Quest DenOfEvil => _quests[1]; 207 | public Quest SistersBurialGrounds => _quests[2]; 208 | public Quest ToolsOfTheTrade => _quests[3]; 209 | public Quest TheSearchForCain => _quests[4]; 210 | public Quest TheForgottenTower => _quests[5]; 211 | public Quest SistersToTheSlaughter => _quests[6]; 212 | public Quest Completion => _quests[7]; 213 | 214 | public void Write(IBitWriter writer) 215 | { 216 | for (int i = 0; i < _quests.Length; i++) 217 | { 218 | _quests[i].Write(writer); 219 | } 220 | } 221 | 222 | public static ActIQuests Read(IBitReader reader) 223 | { 224 | var quests = new ActIQuests(); 225 | for (int i = 0; i < quests._quests.Length; i++) 226 | { 227 | quests._quests[i] = Quest.Read(reader); 228 | } 229 | return quests; 230 | } 231 | 232 | public void Dispose() 233 | { 234 | for (int i = 0; i < _quests.Length; i++) 235 | { 236 | Interlocked.Exchange(ref _quests[i]!, null)?.Dispose(); 237 | } 238 | } 239 | } 240 | 241 | public sealed class ActIIQuests : IDisposable 242 | { 243 | private readonly Quest[] _quests = new Quest[8]; 244 | 245 | public Quest Introduction => _quests[0]; 246 | public Quest RadamentsLair => _quests[1]; 247 | public Quest TheHoradricStaff => _quests[2]; 248 | public Quest TaintedSun => _quests[3]; 249 | public Quest ArcaneSanctuary => _quests[4]; 250 | public Quest TheSummoner => _quests[5]; 251 | public Quest TheSevenTombs => _quests[6]; 252 | public Quest Completion => _quests[7]; 253 | 254 | public void Write(IBitWriter writer) 255 | { 256 | for (int i = 0; i < _quests.Length; i++) 257 | { 258 | _quests[i].Write(writer); 259 | } 260 | } 261 | 262 | public static ActIIQuests Read(IBitReader reader) 263 | { 264 | var quests = new ActIIQuests(); 265 | for (int i = 0; i < quests._quests.Length; i++) 266 | { 267 | quests._quests[i] = Quest.Read(reader); 268 | } 269 | return quests; 270 | } 271 | 272 | public void Dispose() 273 | { 274 | for (int i = 0; i < _quests.Length; i++) 275 | { 276 | Interlocked.Exchange(ref _quests[i]!, null)?.Dispose(); 277 | } 278 | } 279 | } 280 | 281 | public sealed class ActIIIQuests : IDisposable 282 | { 283 | private readonly Quest[] _quests = new Quest[8]; 284 | 285 | public Quest Introduction => _quests[0]; 286 | public Quest LamEsensTome => _quests[1]; 287 | public Quest KhalimsWill => _quests[2]; 288 | public Quest BladeOfTheOldReligion => _quests[3]; 289 | public Quest TheGoldenBird => _quests[4]; 290 | public Quest TheBlackenedTemple => _quests[5]; 291 | public Quest TheGuardian => _quests[6]; 292 | public Quest Completion => _quests[7]; 293 | 294 | public void Write(IBitWriter writer) 295 | { 296 | for (int i = 0; i < _quests.Length; i++) 297 | { 298 | _quests[i].Write(writer); 299 | } 300 | } 301 | 302 | public static ActIIIQuests Read(IBitReader reader) 303 | { 304 | var quests = new ActIIIQuests(); 305 | for (int i = 0; i < quests._quests.Length; i++) 306 | { 307 | quests._quests[i] = Quest.Read(reader); 308 | } 309 | return quests; 310 | } 311 | 312 | public void Dispose() 313 | { 314 | for (int i = 0; i < _quests.Length; i++) 315 | { 316 | Interlocked.Exchange(ref _quests[i]!, null)?.Dispose(); 317 | } 318 | } 319 | } 320 | 321 | public sealed class ActIVQuests : IDisposable 322 | { 323 | private readonly Quest[] _quests = new Quest[8]; 324 | 325 | public Quest Introduction => _quests[0]; 326 | public Quest TheFallenAngel => _quests[1]; 327 | public Quest TerrorsEnd => _quests[2]; 328 | public Quest Hellforge => _quests[3]; 329 | public Quest Completion => _quests[4]; 330 | 331 | //3 shorts at the end of ActIV completion. presumably for extra quests never used. 332 | public Quest Extra1 => _quests[5]; 333 | public Quest Extra2 => _quests[6]; 334 | public Quest Extra3 => _quests[7]; 335 | 336 | public void Write(IBitWriter writer) 337 | { 338 | for (int i = 0; i < _quests.Length; i++) 339 | { 340 | _quests[i].Write(writer); 341 | } 342 | } 343 | 344 | public static ActIVQuests Read(IBitReader reader) 345 | { 346 | var quests = new ActIVQuests(); 347 | for (int i = 0; i < quests._quests.Length; i++) 348 | { 349 | quests._quests[i] = Quest.Read(reader); 350 | } 351 | return quests; 352 | } 353 | 354 | public void Dispose() 355 | { 356 | for (int i = 0; i < _quests.Length; i++) 357 | { 358 | Interlocked.Exchange(ref _quests[i]!, null)?.Dispose(); 359 | } 360 | } 361 | } 362 | 363 | public sealed class ActVQuests : IDisposable 364 | { 365 | private readonly Quest[] _quests = new Quest[16]; 366 | 367 | public Quest Introduction => _quests[0]; 368 | //2 shorts after ActV introduction. presumably for extra quests never used. 369 | public Quest Extra1 => _quests[1]; 370 | public Quest Extra2 => _quests[2]; 371 | public Quest SiegeOnHarrogath => _quests[3]; 372 | public Quest RescueOnMountArreat => _quests[4]; 373 | public Quest PrisonOfIce => _quests[5]; 374 | public Quest BetrayalOfHarrogath => _quests[6]; 375 | public Quest RiteOfPassage => _quests[7]; 376 | public Quest EveOfDestruction => _quests[8]; 377 | public Quest Completion => _quests[9]; 378 | //6 shorts after ActV completion. presumably for extra quests never used. 379 | public Quest Extra3 => _quests[10]; 380 | public Quest Extra4 => _quests[11]; 381 | public Quest Extra5 => _quests[12]; 382 | public Quest Extra6 => _quests[13]; 383 | public Quest Extra7 => _quests[14]; 384 | public Quest Extra8 => _quests[15]; 385 | 386 | public void Write(IBitWriter writer) 387 | { 388 | for (int i = 0; i < _quests.Length; i++) 389 | { 390 | _quests[i].Write(writer); 391 | } 392 | } 393 | 394 | public static ActVQuests Read(IBitReader reader) 395 | { 396 | var quests = new ActVQuests(); 397 | for (int i = 0; i < quests._quests.Length; i++) 398 | { 399 | quests._quests[i] = Quest.Read(reader); 400 | } 401 | return quests; 402 | } 403 | 404 | public void Dispose() 405 | { 406 | for (int i = 0; i < _quests.Length; i++) 407 | { 408 | Interlocked.Exchange(ref _quests[i]!, null)?.Dispose(); 409 | } 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/IO/InternalBitArray.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Adapted from here, to allow for span-based constructor. Removed unused/unsafe code. 5 | // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections/src/System/Collections/BitArray.cs 6 | 7 | using Microsoft.Toolkit.HighPerformance; 8 | using System.Buffers; 9 | using System.Buffers.Binary; 10 | using System.Collections; 11 | using System.Diagnostics; 12 | using System.Runtime.CompilerServices; 13 | 14 | namespace D2SLib.IO; 15 | 16 | // A vector of bits. Use this to store bits efficiently 17 | [Serializable] 18 | internal sealed class InternalBitArray : IList, ICloneable, IDisposable 19 | { 20 | private int[] m_array; // Do not rename (binary serialization) 21 | private int m_length; // Do not rename (binary serialization) 22 | private int _version; // Do not rename (binary serialization) 23 | 24 | private const int _ShrinkThreshold = 256; 25 | 26 | /*========================================================================= 27 | ** Allocates space to hold length bit values. All of the values in the bit 28 | ** array are set to false. 29 | ** 30 | ** Exceptions: ArgumentException if length < 0. 31 | =========================================================================*/ 32 | public InternalBitArray(int length) 33 | : this(length, false) 34 | { 35 | } 36 | 37 | /*========================================================================= 38 | ** Allocates space to hold length bit values. All of the values in the bit 39 | ** array are set to defaultValue. 40 | ** 41 | ** Exceptions: ArgumentOutOfRangeException if length < 0. 42 | =========================================================================*/ 43 | public InternalBitArray(int length, bool defaultValue) 44 | { 45 | if (length < 0) 46 | { 47 | throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be non-negative"); 48 | } 49 | 50 | int arrayLength = GetInt32ArrayLengthFromBitLength(length); 51 | m_array = ArrayPool.Shared.Rent(arrayLength); 52 | m_length = length; 53 | 54 | if (defaultValue) 55 | { 56 | var span = m_array.AsSpan(0, arrayLength); 57 | span.Fill(-1); 58 | 59 | // clear high bit values in the last int 60 | Div32Rem(length, out int extraBits); 61 | if (extraBits > 0) 62 | { 63 | span[^1] = (1 << extraBits) - 1; 64 | } 65 | } 66 | 67 | _version = 0; 68 | } 69 | 70 | /*========================================================================= 71 | ** Allocates space to hold the bit values in bytes. bytes[0] represents 72 | ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte 73 | ** represents the lowest index value; bytes[0] & 1 represents bit 0, 74 | ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. 75 | ** 76 | ** Exceptions: ArgumentException if bytes == null. 77 | =========================================================================*/ 78 | public InternalBitArray(ReadOnlySpan bytes) 79 | { 80 | // this value is chosen to prevent overflow when computing m_length. 81 | // m_length is of type int32 and is exposed as a property, so 82 | // type of m_length can't be changed to accommodate. 83 | if (bytes.Length > int.MaxValue / BitsPerByte) 84 | { 85 | throw new ArgumentException("Too many bytes!", nameof(bytes)); 86 | } 87 | 88 | m_array = ArrayPool.Shared.Rent(GetInt32ArrayLengthFromByteLength(bytes.Length)); 89 | m_length = bytes.Length * BitsPerByte; 90 | 91 | uint totalCount = (uint)bytes.Length / 4; 92 | 93 | for (int i = 0; i < totalCount; i++) 94 | { 95 | m_array[i] = BinaryPrimitives.ReadInt32LittleEndian(bytes); 96 | bytes = bytes[4..]; 97 | } 98 | 99 | Debug.Assert(bytes.Length is >= 0 and < 4); 100 | 101 | int last = 0; 102 | switch (bytes.Length) 103 | { 104 | case 3: 105 | last = bytes[2] << 16; 106 | goto case 2; 107 | // fall through 108 | case 2: 109 | last |= bytes[1] << 8; 110 | goto case 1; 111 | // fall through 112 | case 1: 113 | m_array[totalCount] = last | bytes[0]; 114 | break; 115 | } 116 | 117 | _version = 0; 118 | } 119 | 120 | /*========================================================================= 121 | ** Allocates a new BitArray with the same length and bit values as bits. 122 | ** 123 | ** Exceptions: ArgumentException if bits == null. 124 | =========================================================================*/ 125 | public InternalBitArray(InternalBitArray bits) 126 | { 127 | if (bits == null) 128 | { 129 | throw new ArgumentNullException(nameof(bits)); 130 | } 131 | 132 | int arrayLength = GetInt32ArrayLengthFromBitLength(bits.m_length); 133 | 134 | m_array = ArrayPool.Shared.Rent(arrayLength); 135 | 136 | Debug.Assert(bits.m_array.Length <= arrayLength); 137 | 138 | Array.Copy(bits.m_array, m_array, arrayLength); 139 | m_length = bits.m_length; 140 | 141 | _version = bits._version; 142 | } 143 | 144 | public bool this[int index] 145 | { 146 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 147 | get => Get(index); 148 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 149 | set => Set(index, value); 150 | } 151 | 152 | /*========================================================================= 153 | ** Returns the bit value at position index. 154 | ** 155 | ** Exceptions: ArgumentOutOfRangeException if index < 0 or 156 | ** index >= GetLength(). 157 | =========================================================================*/ 158 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 159 | public bool Get(int index) 160 | { 161 | if ((uint)index >= (uint)m_length) 162 | { 163 | ThrowArgumentOutOfRangeException(index); 164 | } 165 | 166 | return (m_array[index >> 5] & (1 << index)) != 0; 167 | } 168 | 169 | /*========================================================================= 170 | ** Sets the bit value at position index to value. 171 | ** 172 | ** Exceptions: ArgumentOutOfRangeException if index < 0 or 173 | ** index >= GetLength(). 174 | =========================================================================*/ 175 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 176 | public void Set(int index, bool value) 177 | { 178 | if ((uint)index >= (uint)m_length) 179 | { 180 | ThrowArgumentOutOfRangeException(index); 181 | } 182 | 183 | int bitMask = 1 << index; 184 | ref int segment = ref m_array[index >> 5]; 185 | 186 | if (value) 187 | { 188 | segment |= bitMask; 189 | } 190 | else 191 | { 192 | segment &= ~bitMask; 193 | } 194 | 195 | _version++; 196 | } 197 | 198 | public void Add(bool value) 199 | { 200 | int idx = Length++; 201 | Set(idx, value); 202 | } 203 | 204 | /*========================================================================= 205 | ** Sets all the bit values to value. 206 | =========================================================================*/ 207 | public void SetAll(bool value) 208 | { 209 | int arrayLength = GetInt32ArrayLengthFromBitLength(Length); 210 | var span = m_array.AsSpan(0, arrayLength); 211 | if (value) 212 | { 213 | span.Fill(-1); 214 | 215 | // clear high bit values in the last int 216 | Div32Rem(m_length, out int extraBits); 217 | if (extraBits > 0) 218 | { 219 | span[^1] &= (1 << extraBits) - 1; 220 | } 221 | } 222 | else 223 | { 224 | span.Clear(); 225 | } 226 | 227 | _version++; 228 | } 229 | 230 | public int Length 231 | { 232 | get => m_length; 233 | set 234 | { 235 | if (value < 0) 236 | { 237 | throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be non-negative."); 238 | } 239 | 240 | int newints = GetInt32ArrayLengthFromBitLength(value); 241 | if (newints > m_array.Length || newints + _ShrinkThreshold < m_array.Length) 242 | { 243 | // grow or shrink (if wasting more than _ShrinkThreshold ints) 244 | ArrayPool.Shared.Resize(ref m_array!, newints); 245 | } 246 | 247 | if (value > m_length) 248 | { 249 | // clear high bit values in the last int 250 | int last = (m_length - 1) >> BitShiftPerInt32; 251 | Div32Rem(m_length, out int bits); 252 | if (bits > 0) 253 | { 254 | m_array[last] &= (1 << bits) - 1; 255 | } 256 | 257 | // clear remaining int values 258 | m_array.AsSpan(last + 1, newints - last - 1).Clear(); 259 | } 260 | 261 | m_length = value; 262 | _version++; 263 | } 264 | } 265 | 266 | int ICollection.Count => m_length; 267 | 268 | bool ICollection.IsReadOnly => false; 269 | 270 | public object Clone() => new InternalBitArray(this); 271 | 272 | public IEnumerator GetEnumerator() => new BitArrayEnumeratorSimple(this); 273 | 274 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 275 | 276 | // XPerY=n means that n Xs can be stored in 1 Y. 277 | private const int BitsPerInt32 = 32; 278 | private const int BitsPerByte = 8; 279 | 280 | private const int BitShiftPerInt32 = 5; 281 | private const int BitShiftPerByte = 3; 282 | private const int BitShiftForBytesPerInt32 = 2; 283 | 284 | /// 285 | /// Used for conversion between different representations of bit array. 286 | /// Returns (n + (32 - 1)) / 32, rearranged to avoid arithmetic overflow. 287 | /// For example, in the bit to int case, the straightforward calc would 288 | /// be (n + 31) / 32, but that would cause overflow. So instead it's 289 | /// rearranged to ((n - 1) / 32) + 1. 290 | /// Due to sign extension, we don't need to special case for n == 0, if we use 291 | /// bitwise operations (since ((n - 1) >> 5) + 1 = 0). 292 | /// This doesn't hold true for ((n - 1) / 32) + 1, which equals 1. 293 | /// 294 | /// Usage: 295 | /// GetArrayLength(77): returns how many ints must be 296 | /// allocated to store 77 bits. 297 | /// 298 | /// 299 | /// how many ints are required to store n bytes 300 | private static int GetInt32ArrayLengthFromBitLength(int n) 301 | { 302 | Debug.Assert(n >= 0); 303 | return (int)((uint)(n - 1 + (1 << BitShiftPerInt32)) >> BitShiftPerInt32); 304 | } 305 | 306 | private static int GetInt32ArrayLengthFromByteLength(int n) 307 | { 308 | Debug.Assert(n >= 0); 309 | // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 2) + 1 = 0 310 | // This doesn't hold true for ((n - 1) / 4) + 1, which equals 1. 311 | return (int)((uint)(n - 1 + (1 << BitShiftForBytesPerInt32)) >> BitShiftForBytesPerInt32); 312 | } 313 | 314 | internal static int GetByteArrayLengthFromBitLength(int n) 315 | { 316 | Debug.Assert(n >= 0); 317 | // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 3) + 1 = 0 318 | // This doesn't hold true for ((n - 1) / 8) + 1, which equals 1. 319 | return (int)((uint)(n - 1 + (1 << BitShiftPerByte)) >> BitShiftPerByte); 320 | } 321 | 322 | private static int Div32Rem(int number, out int remainder) 323 | { 324 | uint quotient = (uint)number / 32; 325 | remainder = number & (32 - 1); // equivalent to number % 32, since 32 is a power of 2 326 | return (int)quotient; 327 | } 328 | 329 | private static void ThrowArgumentOutOfRangeException(int index) => throw new ArgumentOutOfRangeException(nameof(index), index, "Index was out of range."); 330 | 331 | int IList.IndexOf(bool item) => throw new NotImplementedException(); 332 | void IList.Insert(int index, bool item) => throw new NotImplementedException(); 333 | void IList.RemoveAt(int index) => throw new NotImplementedException(); 334 | void ICollection.Add(bool item) => throw new NotImplementedException(); 335 | public void Clear() => SetAll(false); 336 | bool ICollection.Contains(bool item) => throw new NotImplementedException(); 337 | void ICollection.CopyTo(bool[] array, int arrayIndex) => throw new NotImplementedException(); 338 | bool ICollection.Remove(bool item) => throw new NotImplementedException(); 339 | 340 | public void Dispose() 341 | { 342 | if (m_array.Length > 0) 343 | { 344 | ArrayPool.Shared.Return(m_array); 345 | m_array = Array.Empty(); 346 | m_length = 0; 347 | } 348 | } 349 | 350 | private sealed class BitArrayEnumeratorSimple : IEnumerator, ICloneable 351 | { 352 | private readonly InternalBitArray _bitArray; 353 | private int _index; 354 | private readonly int _version; 355 | private bool _currentElement; 356 | 357 | internal BitArrayEnumeratorSimple(InternalBitArray bitArray) 358 | { 359 | _bitArray = bitArray; 360 | _index = -1; 361 | _version = bitArray._version; 362 | } 363 | 364 | public object Clone() => MemberwiseClone(); 365 | 366 | public bool MoveNext() 367 | { 368 | if (_version != _bitArray._version) 369 | { 370 | throw new InvalidOperationException("Enumeration failed: collection was modified."); 371 | } 372 | 373 | if (_index < (_bitArray.m_length - 1)) 374 | { 375 | _index++; 376 | _currentElement = _bitArray.Get(_index); 377 | return true; 378 | } 379 | else 380 | { 381 | _index = _bitArray.m_length; 382 | } 383 | 384 | return false; 385 | } 386 | 387 | public bool Current 388 | { 389 | get 390 | { 391 | if ((uint)_index >= (uint)_bitArray.m_length) 392 | { 393 | throw GetInvalidOperationException(_index); 394 | } 395 | 396 | return _currentElement; 397 | } 398 | } 399 | 400 | object IEnumerator.Current => Current; 401 | 402 | public void Reset() 403 | { 404 | if (_version != _bitArray._version) 405 | { 406 | throw new InvalidOperationException("Enumeration failed: collection was modified."); 407 | } 408 | 409 | _index = -1; 410 | } 411 | 412 | private InvalidOperationException GetInvalidOperationException(int index) 413 | { 414 | if (index == -1) 415 | { 416 | return new InvalidOperationException("Enumeration not started."); 417 | } 418 | else 419 | { 420 | Debug.Assert(index >= _bitArray.m_length); 421 | return new InvalidOperationException("Enumeration ended."); 422 | } 423 | } 424 | 425 | void IDisposable.Dispose() { } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /src/Model/Save/Items.cs: -------------------------------------------------------------------------------- 1 | using D2SLib.IO; 2 | using D2SLib.Model.Data; 3 | using System.Text; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace D2SLib.Model.Save; 7 | 8 | public enum ItemMode : byte 9 | { 10 | Stored = 0x0, 11 | Equipped = 0x1, 12 | Belt = 0x2, 13 | Buffer = 0x4, 14 | Socket = 0x6, 15 | } 16 | 17 | public enum ItemLocation : byte 18 | { 19 | None, 20 | Head, 21 | Neck, 22 | Torso, 23 | RightHand, 24 | LeftHand, 25 | RightFinger, 26 | LeftFinger, 27 | Waist, 28 | Feet, 29 | Gloves, 30 | SwapRight, 31 | SwapLeft 32 | } 33 | 34 | public enum ItemQuality : byte 35 | { 36 | Inferior = 0x1, 37 | Normal, 38 | Superior, 39 | Magic, 40 | Set, 41 | Rare, 42 | Unique, 43 | Craft, 44 | Tempered 45 | } 46 | 47 | public sealed class ItemList : IDisposable 48 | { 49 | private ItemList(ushort header, ushort count) 50 | { 51 | Header = header; 52 | Count = count; 53 | Items = new List(count); 54 | } 55 | 56 | public ushort? Header { get; set; } 57 | public ushort Count { get; set; } 58 | public List Items { get; } 59 | 60 | public void Write(IBitWriter writer, uint version) 61 | { 62 | writer.WriteUInt16(Header ?? 0x4D4A); 63 | writer.WriteUInt16(Count); 64 | for (int i = 0; i < Count; i++) 65 | { 66 | Items[i].Write(writer, version); 67 | } 68 | } 69 | 70 | public static ItemList Read(IBitReader reader, uint version) 71 | { 72 | var itemList = new ItemList( 73 | header: reader.ReadUInt16(), 74 | count: reader.ReadUInt16() 75 | ); 76 | for (int i = 0; i < itemList.Count; i++) 77 | { 78 | itemList.Items.Add(Item.Read(reader, version)); 79 | } 80 | return itemList; 81 | } 82 | 83 | [Obsolete("Try the non-allocating overload!")] 84 | public static byte[] Write(ItemList itemList, uint version) 85 | { 86 | using var writer = new BitWriter(); 87 | itemList.Write(writer, version); 88 | return writer.ToArray(); 89 | } 90 | 91 | public void Dispose() 92 | { 93 | foreach (var item in Items) 94 | { 95 | item?.Dispose(); 96 | } 97 | Items.Clear(); 98 | } 99 | } 100 | 101 | public sealed class Item : IDisposable 102 | { 103 | private InternalBitArray _flags = new(4); 104 | 105 | public ushort? Header { get; set; } 106 | 107 | [JsonIgnore] 108 | public IList Flags 109 | { 110 | get => _flags; 111 | set 112 | { 113 | if (value is InternalBitArray flags) 114 | { 115 | _flags?.Dispose(); 116 | _flags = flags; 117 | } 118 | else 119 | { 120 | throw new ArgumentException("Flags were not of expected type."); 121 | } 122 | } 123 | } 124 | 125 | public string? Version { get; set; } 126 | public ItemMode Mode { get; set; } 127 | public ItemLocation Location { get; set; } 128 | public byte X { get; set; } 129 | public byte Y { get; set; } 130 | public byte Page { get; set; } 131 | public byte EarLevel { get; set; } 132 | public string PlayerName { get; set; } = string.Empty; //used for personalized or ears 133 | public string Code { get; set; } = string.Empty; 134 | public byte NumberOfSocketedItems { get; set; } 135 | public byte TotalNumberOfSockets { get; set; } 136 | public List SocketedItems { get; set; } = new(); 137 | public uint Id { get; set; } 138 | public byte ItemLevel { get; set; } 139 | public ItemQuality Quality { get; set; } 140 | public bool HasMultipleGraphics { get; set; } 141 | public byte GraphicId { get; set; } 142 | public bool IsAutoAffix { get; set; } 143 | public ushort AutoAffixId { get; set; } //? 144 | public uint FileIndex { get; set; } 145 | public ushort[] MagicPrefixIds { get; set; } = new ushort[3]; 146 | public ushort[] MagicSuffixIds { get; set; } = new ushort[3]; 147 | public ushort RarePrefixId { get; set; } 148 | public ushort RareSuffixId { get; set; } 149 | public uint RunewordId { get; set; } 150 | [JsonIgnore] 151 | public bool HasRealmData { get; set; } 152 | [JsonIgnore] 153 | public uint[] RealmData { get; set; } = new uint[3]; 154 | public ushort Armor { get; set; } 155 | public ushort MaxDurability { get; set; } 156 | public ushort Durability { get; set; } 157 | public ushort Quantity { get; set; } 158 | public byte SetItemMask { get; set; } 159 | public List StatLists { get; } = new List(); 160 | public bool IsIdentified { get => _flags[4]; set => _flags[4] = value; } 161 | public bool IsSocketed { get => _flags[11]; set => _flags[11] = value; } 162 | public bool IsNew { get => _flags[13]; set => _flags[13] = value; } 163 | public bool IsEar { get => _flags[16]; set => _flags[16] = value; } 164 | public bool IsStarterItem { get => _flags[17]; set => _flags[17] = value; } 165 | public bool IsCompact { get => _flags[21]; set => _flags[21] = value; } 166 | public bool IsEthereal { get => _flags[22]; set => _flags[22] = value; } 167 | public bool IsPersonalized { get => _flags[24]; set => _flags[24] = value; } 168 | public bool IsRuneword { get => _flags[26]; set => _flags[26] = value; } 169 | 170 | public void Write(IBitWriter writer, uint version) 171 | { 172 | if (version <= 0x60) 173 | { 174 | writer.WriteUInt16(Header ?? 0x4D4A); 175 | } 176 | WriteCompact(writer, this, version); 177 | if (!IsCompact) 178 | { 179 | WriteComplete(writer, this, version); 180 | } 181 | writer.Align(); 182 | for (int i = 0; i < NumberOfSocketedItems; i++) 183 | { 184 | SocketedItems[i].Write(writer, version); 185 | } 186 | } 187 | 188 | public static Item Read(ReadOnlySpan bytes, uint version) 189 | { 190 | using var reader = new BitReader(bytes); 191 | return Read(reader, version); 192 | } 193 | 194 | public static Item Read(IBitReader reader, uint version) 195 | { 196 | var item = new Item(); 197 | if (version <= 0x60) 198 | { 199 | item.Header = reader.ReadUInt16(); 200 | } 201 | ReadCompact(reader, item, version); 202 | if (!item.IsCompact) 203 | { 204 | ReadComplete(reader, item, version); 205 | } 206 | reader.Align(); 207 | for (int i = 0; i < item.NumberOfSocketedItems; i++) 208 | { 209 | item.SocketedItems.Add(Read(reader, version)); 210 | } 211 | return item; 212 | } 213 | 214 | public static byte[] Write(Item item, uint version) 215 | { 216 | using var writer = new BitWriter(); 217 | item.Write(writer, version); 218 | return writer.ToArray(); 219 | } 220 | 221 | private static string ReadPlayerName(IBitReader reader) 222 | { 223 | Span name = stackalloc char[15]; 224 | for (int i = 0; i < name.Length; i++) 225 | { 226 | name[i] = (char)reader.ReadByte(7); 227 | if (name[i] == '\0') 228 | { 229 | break; 230 | } 231 | } 232 | return new string(name); 233 | } 234 | 235 | private static void WritePlayerName(IBitWriter writer, string name) 236 | { 237 | var nameChars = name.AsSpan().TrimEnd('\0'); 238 | Span bytes = stackalloc byte[nameChars.Length]; 239 | int byteCount = Encoding.ASCII.GetBytes(nameChars, bytes); 240 | bytes = bytes[..byteCount]; 241 | for (int i = 0; i < bytes.Length; i++) 242 | { 243 | writer.WriteByte(bytes[i], 7); 244 | } 245 | writer.WriteByte((byte)'\0', 7); 246 | } 247 | 248 | private static void ReadCompact(IBitReader reader, Item item, uint version) 249 | { 250 | Span bytes = stackalloc byte[4]; 251 | reader.ReadBytes(bytes); 252 | item.Flags = new InternalBitArray(bytes); 253 | if (version <= 0x60) 254 | { 255 | item.Version = Convert.ToString(reader.ReadUInt16(10), 10); 256 | } 257 | else if (version >= 0x61) 258 | { 259 | item.Version = Convert.ToString(reader.ReadUInt16(3), 2); 260 | } 261 | item.Mode = (ItemMode)reader.ReadByte(3); 262 | item.Location = (ItemLocation)reader.ReadByte(4); 263 | item.X = reader.ReadByte(4); 264 | item.Y = reader.ReadByte(4); 265 | item.Page = reader.ReadByte(3); 266 | if (item.IsEar) 267 | { 268 | item.FileIndex = reader.ReadByte(3); 269 | item.EarLevel = reader.ReadByte(7); 270 | item.PlayerName = ReadPlayerName(reader); 271 | } 272 | else 273 | { 274 | item.Code = string.Empty; 275 | if (version <= 0x60) 276 | { 277 | item.Code = reader.ReadString(4); 278 | } 279 | else if (version >= 0x61) 280 | { 281 | for (int i = 0; i < 4; i++) 282 | { 283 | item.Code += Core.MetaData.ItemsData.ItemCodeTree.DecodeChar(reader); 284 | } 285 | } 286 | item.NumberOfSocketedItems = reader.ReadByte(item.IsCompact ? 1 : 3); 287 | } 288 | } 289 | 290 | private static void WriteCompact(IBitWriter writer, Item item, uint version) 291 | { 292 | if (item.Flags is not InternalBitArray flags) 293 | { 294 | flags = new InternalBitArray(32) 295 | { 296 | [04] = item.IsIdentified, 297 | [11] = item.IsSocketed, 298 | [13] = item.IsNew, 299 | [16] = item.IsEar, 300 | [17] = item.IsStarterItem, 301 | [21] = item.IsCompact, 302 | [22] = item.IsEthereal, 303 | [24] = item.IsPersonalized, 304 | [26] = item.IsRuneword 305 | }; 306 | } 307 | writer.WriteBits(flags); 308 | if (version <= 0x60) 309 | { 310 | //todo. how do we handle 1.15 version to 1.14. maybe this should be a string 311 | writer.WriteUInt16(Convert.ToUInt16(item.Version, 10), 10); 312 | } 313 | else if (version >= 0x61) 314 | { 315 | writer.WriteUInt16(Convert.ToUInt16(item.Version, 2), 3); 316 | } 317 | writer.WriteByte((byte)item.Mode, 3); 318 | writer.WriteByte((byte)item.Location, 4); 319 | writer.WriteByte(item.X, 4); 320 | writer.WriteByte(item.Y, 4); 321 | writer.WriteByte(item.Page, 3); 322 | if (item.IsEar) 323 | { 324 | writer.WriteUInt32(item.FileIndex, 3); 325 | writer.WriteByte(item.EarLevel, 7); 326 | WritePlayerName(writer, item.PlayerName); 327 | } 328 | else 329 | { 330 | var itemCode = item.Code.PadRight(4, ' '); 331 | Span code = stackalloc byte[itemCode.Length]; 332 | Encoding.ASCII.GetBytes(itemCode, code); 333 | if (version <= 0x60) 334 | { 335 | writer.WriteBytes(code); 336 | } 337 | else if (version >= 0x61) 338 | { 339 | var codeTree = Core.MetaData.ItemsData.ItemCodeTree; 340 | for (int i = 0; i < 4; i++) 341 | { 342 | using var bits = codeTree.EncodeChar((char)code[i]); 343 | foreach (bool bit in bits) 344 | { 345 | writer.WriteBit(bit); 346 | } 347 | } 348 | } 349 | writer.WriteByte(item.NumberOfSocketedItems, item.IsCompact ? 1 : 3); 350 | } 351 | } 352 | 353 | private static void ReadComplete(IBitReader reader, Item item, uint version) 354 | { 355 | item.Id = reader.ReadUInt32(); 356 | item.ItemLevel = reader.ReadByte(7); 357 | item.Quality = (ItemQuality)reader.ReadByte(4); 358 | item.HasMultipleGraphics = reader.ReadBit(); 359 | if (item.HasMultipleGraphics) 360 | { 361 | item.GraphicId = reader.ReadByte(3); 362 | } 363 | item.IsAutoAffix = reader.ReadBit(); 364 | if (item.IsAutoAffix) 365 | { 366 | item.AutoAffixId = reader.ReadUInt16(11); 367 | } 368 | switch (item.Quality) 369 | { 370 | case ItemQuality.Normal: 371 | break; 372 | case ItemQuality.Inferior: 373 | case ItemQuality.Superior: 374 | item.FileIndex = reader.ReadUInt16(3); 375 | break; 376 | case ItemQuality.Magic: 377 | item.MagicPrefixIds[0] = reader.ReadUInt16(11); 378 | item.MagicSuffixIds[0] = reader.ReadUInt16(11); 379 | break; 380 | case ItemQuality.Rare: 381 | case ItemQuality.Craft: 382 | item.RarePrefixId = reader.ReadUInt16(8); 383 | item.RareSuffixId = reader.ReadUInt16(8); 384 | for (int i = 0; i < 3; i++) 385 | { 386 | if (reader.ReadBit()) 387 | { 388 | item.MagicPrefixIds[i] = reader.ReadUInt16(11); 389 | } 390 | if (reader.ReadBit()) 391 | { 392 | item.MagicSuffixIds[i] = reader.ReadUInt16(11); 393 | } 394 | } 395 | break; 396 | case ItemQuality.Set: 397 | case ItemQuality.Unique: 398 | item.FileIndex = reader.ReadUInt16(12); 399 | break; 400 | } 401 | ushort propertyLists = 0; 402 | if (item.IsRuneword) 403 | { 404 | item.RunewordId = reader.ReadUInt32(12); 405 | propertyLists |= (ushort)(1 << (reader.ReadUInt16(4) + 1)); 406 | } 407 | if (item.IsPersonalized) 408 | { 409 | item.PlayerName = ReadPlayerName(reader); 410 | } 411 | var trimmedCode = item.Code.AsSpan().TrimEnd(); 412 | if (trimmedCode.SequenceEqual("tbk") || trimmedCode.SequenceEqual("ibk")) 413 | { 414 | item.MagicSuffixIds[0] = reader.ReadByte(5); 415 | } 416 | item.HasRealmData = reader.ReadBit(); 417 | if (item.HasRealmData) 418 | { 419 | //reader.ReadBits(96); 420 | reader.AdvanceBits(96); 421 | } 422 | var itemStatCost = Core.MetaData.ItemStatCostData; 423 | var row = Core.MetaData.ItemsData.GetByCode(item.Code); 424 | bool isArmor = Core.MetaData.ItemsData.IsArmor(item.Code); 425 | bool isWeapon = Core.MetaData.ItemsData.IsWeapon(item.Code); 426 | bool isStackable = row?["stackable"].ToBool() ?? false; 427 | if (isArmor) 428 | { 429 | item.Armor = (ushort)(reader.ReadUInt16(11) + itemStatCost.GetByStat("armorclass")?["Save Add"].ToUInt16() ?? 0); 430 | } 431 | if (isArmor || isWeapon) 432 | { 433 | var maxDurabilityStat = itemStatCost.GetByStat("maxdurability"); 434 | var durabilityStat = itemStatCost.GetByStat("maxdurability"); 435 | item.MaxDurability = (ushort)(reader.ReadUInt16(maxDurabilityStat?["Save Bits"].ToInt32() ?? 0) + maxDurabilityStat?["Save Add"].ToUInt16() ?? 0); 436 | if (item.MaxDurability > 0) 437 | { 438 | item.Durability = (ushort)(reader.ReadUInt16(durabilityStat?["Save Bits"].ToInt32() ?? 0) + durabilityStat?["Save Add"].ToUInt16() ?? 0); 439 | //what is this? 440 | reader.ReadBit(); 441 | } 442 | } 443 | if (isStackable) 444 | { 445 | item.Quantity = reader.ReadUInt16(9); 446 | } 447 | if (item.IsSocketed) 448 | { 449 | item.TotalNumberOfSockets = reader.ReadByte(4); 450 | } 451 | item.SetItemMask = 0; 452 | if (item.Quality == ItemQuality.Set) 453 | { 454 | item.SetItemMask = reader.ReadByte(5); 455 | propertyLists |= item.SetItemMask; 456 | } 457 | item.StatLists.Add(ItemStatList.Read(reader)); 458 | for (int i = 1; i <= 64; i <<= 1) 459 | { 460 | if ((propertyLists & i) != 0) 461 | { 462 | item.StatLists.Add(ItemStatList.Read(reader)); 463 | } 464 | } 465 | } 466 | 467 | private static void WriteComplete(IBitWriter writer, Item item, uint version) 468 | { 469 | writer.WriteUInt32(item.Id); 470 | writer.WriteByte(item.ItemLevel, 7); 471 | writer.WriteByte((byte)item.Quality, 4); 472 | writer.WriteBit(item.HasMultipleGraphics); 473 | if (item.HasMultipleGraphics) 474 | { 475 | writer.WriteByte(item.GraphicId, 3); 476 | } 477 | writer.WriteBit(item.IsAutoAffix); 478 | if (item.IsAutoAffix) 479 | { 480 | writer.WriteUInt16(item.AutoAffixId, 11); 481 | } 482 | switch (item.Quality) 483 | { 484 | case ItemQuality.Normal: 485 | break; 486 | case ItemQuality.Inferior: 487 | case ItemQuality.Superior: 488 | writer.WriteUInt32(item.FileIndex, 3); 489 | break; 490 | case ItemQuality.Magic: 491 | writer.WriteUInt16(item.MagicPrefixIds[0], 11); 492 | writer.WriteUInt16(item.MagicSuffixIds[0], 11); 493 | break; 494 | case ItemQuality.Rare: 495 | case ItemQuality.Craft: 496 | writer.WriteUInt16(item.RarePrefixId, 8); 497 | writer.WriteUInt16(item.RareSuffixId, 8); 498 | for (int i = 0; i < 3; i++) 499 | { 500 | bool hasPrefix = item.MagicPrefixIds[i] > 0; 501 | bool hasSuffix = item.MagicSuffixIds[i] > 0; 502 | writer.WriteBit(hasPrefix); 503 | if (hasPrefix) 504 | { 505 | writer.WriteUInt16(item.MagicPrefixIds[i], 11); 506 | } 507 | writer.WriteBit(hasSuffix); 508 | if (hasSuffix) 509 | { 510 | writer.WriteUInt16(item.MagicSuffixIds[i], 11); 511 | } 512 | } 513 | break; 514 | case ItemQuality.Set: 515 | case ItemQuality.Unique: 516 | writer.WriteUInt32(item.FileIndex, 12); 517 | break; 518 | } 519 | ushort propertyLists = 0; 520 | if (item.IsRuneword) 521 | { 522 | writer.WriteUInt32(item.RunewordId, 12); 523 | propertyLists |= 1 << 6; 524 | writer.WriteUInt16(5, 4); 525 | } 526 | if (item.IsPersonalized) 527 | { 528 | WritePlayerName(writer, item.PlayerName); 529 | } 530 | var trimmedCode = item.Code.AsSpan().Trim(); 531 | if (trimmedCode.SequenceEqual("tbk") || trimmedCode.SequenceEqual("ibk")) 532 | { 533 | writer.WriteUInt16(item.MagicSuffixIds[0], 5); 534 | } 535 | writer.WriteBit(item.HasRealmData); 536 | if (item.HasRealmData) 537 | { 538 | //todo 96 bits 539 | } 540 | var itemStatCost = Core.MetaData.ItemStatCostData; 541 | var row = Core.MetaData.ItemsData.GetByCode(item.Code); 542 | bool isArmor = Core.MetaData.ItemsData.IsArmor(item.Code); 543 | bool isWeapon = Core.MetaData.ItemsData.IsWeapon(item.Code); 544 | bool isStackable = row?["stackable"].ToBool() ?? false; 545 | if (isArmor) 546 | { 547 | writer.WriteUInt16((ushort)(item.Armor - itemStatCost.GetByStat("armorclass")?["Save Add"].ToUInt16() ?? 0), 11); 548 | } 549 | if (isArmor || isWeapon) 550 | { 551 | var maxDurabilityStat = itemStatCost.GetByStat("maxdurability"); 552 | var durabilityStat = itemStatCost.GetByStat("maxdurability"); 553 | writer.WriteUInt16((ushort)(item.MaxDurability - maxDurabilityStat?["Save Add"].ToUInt16() ?? 0), maxDurabilityStat?["Save Bits"].ToInt32() ?? 0); 554 | if (item.MaxDurability > 0) 555 | { 556 | writer.WriteUInt16((ushort)(item.Durability - durabilityStat?["Save Add"].ToUInt16() ?? 0), durabilityStat?["Save Bits"].ToInt32() ?? 0); 557 | ////what is this? 558 | writer.WriteBit(false); 559 | } 560 | } 561 | if (isStackable) 562 | { 563 | writer.WriteUInt16(item.Quantity, 9); 564 | } 565 | if (item.IsSocketed) 566 | { 567 | writer.WriteByte(item.TotalNumberOfSockets, 4); 568 | } 569 | if (item.Quality == ItemQuality.Set) 570 | { 571 | writer.WriteByte(item.SetItemMask, 5); 572 | propertyLists |= item.SetItemMask; 573 | } 574 | ItemStatList.Write(writer, item.StatLists[0]); 575 | int idx = 1; 576 | for (int i = 1; i <= 64; i <<= 1) 577 | { 578 | if ((propertyLists & i) != 0) 579 | { 580 | ItemStatList.Write(writer, item.StatLists[idx++]); 581 | } 582 | } 583 | } 584 | 585 | public void Dispose() 586 | { 587 | Interlocked.Exchange(ref _flags!, null)?.Dispose(); 588 | foreach (var item in SocketedItems) 589 | { 590 | item?.Dispose(); 591 | } 592 | SocketedItems.Clear(); 593 | } 594 | } 595 | 596 | public class ItemStatList 597 | { 598 | private const ushort magicmindam = 52; 599 | private const ushort item_maxdamage_percent = 17; 600 | private const ushort firemindam = 48; 601 | private const ushort lightmindam = 50; 602 | private const ushort coldmindam = 54; 603 | private const ushort poisonmindam = 57; 604 | 605 | public List Stats { get; set; } = new(); 606 | 607 | public static ItemStatList Read(IBitReader reader) 608 | { 609 | var itemStatList = new ItemStatList(); 610 | ushort id = reader.ReadUInt16(9); 611 | while (id != 0x1ff) 612 | { 613 | itemStatList.Stats.Add(ItemStat.Read(reader, id)); 614 | //https://github.com/ThePhrozenKeep/D2MOO/blob/master/source/D2Common/src/Items/Items.cpp#L7332 615 | if (id is magicmindam or item_maxdamage_percent or firemindam or lightmindam) 616 | { 617 | itemStatList.Stats.Add(ItemStat.Read(reader, (ushort)(id + 1))); 618 | } 619 | else if (id is coldmindam or poisonmindam) 620 | { 621 | itemStatList.Stats.Add(ItemStat.Read(reader, (ushort)(id + 1))); 622 | itemStatList.Stats.Add(ItemStat.Read(reader, (ushort)(id + 2))); 623 | } 624 | id = reader.ReadUInt16(9); 625 | } 626 | return itemStatList; 627 | } 628 | 629 | public static void Write(IBitWriter writer, ItemStatList itemStatList) 630 | { 631 | for (int i = 0; i < itemStatList.Stats.Count; i++) 632 | { 633 | var stat = itemStatList.Stats[i]; 634 | var property = ItemStat.GetStatRow(stat); 635 | ushort id = property?["ID"].ToUInt16() ?? 0; 636 | writer.WriteUInt16(id, 9); 637 | ItemStat.Write(writer, stat); 638 | 639 | //assume these stats are in order... 640 | //https://github.com/ThePhrozenKeep/D2MOO/blob/master/source/D2Common/src/Items/Items.cpp#L7332 641 | if (id is magicmindam or item_maxdamage_percent or firemindam or lightmindam) 642 | { 643 | ItemStat.Write(writer, itemStatList.Stats[++i]); 644 | } 645 | else if (id is coldmindam or poisonmindam) 646 | { 647 | ItemStat.Write(writer, itemStatList.Stats[++i]); 648 | ItemStat.Write(writer, itemStatList.Stats[++i]); 649 | } 650 | } 651 | writer.WriteUInt16(0x1ff, 9); 652 | } 653 | 654 | } 655 | 656 | public class ItemStat 657 | { 658 | public ushort? Id { get; set; } 659 | public string Stat { get; set; } = string.Empty; 660 | public int? SkillTab { get; set; } 661 | public int? SkillId { get; set; } 662 | public int? SkillLevel { get; set; } 663 | public int? MaxCharges { get; set; } 664 | public int? Param { get; set; } 665 | public int Value { get; set; } 666 | 667 | public static ItemStat Read(IBitReader reader, ushort id) 668 | { 669 | var itemStat = new ItemStat(); 670 | var property = Core.MetaData.ItemStatCostData.GetById(id); 671 | if (property == null) 672 | { 673 | throw new Exception($"No ItemStatCost record found for id: {id} at bit {reader.Position - 9}"); 674 | } 675 | itemStat.Id = id; 676 | itemStat.Stat = property["Stat"].Value; 677 | int saveParamBitCount = property["Save Param Bits"].ToInt32(); 678 | int encode = property["Encode"].ToInt32(); 679 | if (saveParamBitCount != 0) 680 | { 681 | int saveParam = reader.ReadInt32(saveParamBitCount); 682 | //todo is there a better way to identify skill tab stats. 683 | switch (property["descfunc"].ToInt32()) 684 | { 685 | case 14: //+[value] to [skilltab] Skill Levels ([class] Only) : stat id 188 686 | itemStat.SkillTab = saveParam & 0x7; 687 | itemStat.SkillLevel = (saveParam >> 3) & 0x1fff; 688 | break; 689 | default: 690 | break; 691 | } 692 | switch (encode) 693 | { 694 | case 2: //chance to cast skill 695 | case 3: //skill charges 696 | itemStat.SkillLevel = saveParam & 0x3f; 697 | itemStat.SkillId = (saveParam >> 6) & 0x3ff; 698 | break; 699 | case 1: 700 | case 4: //by times 701 | default: 702 | itemStat.Param = saveParam; 703 | break; 704 | } 705 | } 706 | int saveBits = reader.ReadInt32(property["Save Bits"].ToInt32()); 707 | saveBits -= property["Save Add"].ToInt32(); 708 | switch (encode) 709 | { 710 | case 3: //skill charges 711 | itemStat.MaxCharges = (saveBits >> 8) & 0xff; 712 | itemStat.Value = saveBits & 0xff; 713 | break; 714 | default: 715 | itemStat.Value = saveBits; 716 | break; 717 | } 718 | return itemStat; 719 | } 720 | 721 | public static void Write(IBitWriter writer, ItemStat stat) 722 | { 723 | var property = GetStatRow(stat); 724 | if (property is null) 725 | { 726 | throw new ArgumentException($"No ItemStatCost record found for id: {stat.Id}", nameof(stat)); 727 | } 728 | int saveParamBitCount = property["Save Param Bits"].ToInt32(); 729 | int encode = property["Encode"].ToInt32(); 730 | if (saveParamBitCount != 0) 731 | { 732 | if (stat.Param != null) 733 | { 734 | writer.WriteInt32((int)stat.Param, saveParamBitCount); 735 | } 736 | else 737 | { 738 | int saveParamBits = 0; 739 | switch (property["descfunc"].ToInt32()) 740 | { 741 | case 14: //+[value] to [skilltab] Skill Levels ([class] Only) : stat id 188 742 | saveParamBits |= (stat.SkillTab ?? 0 & 0x7); 743 | saveParamBits |= ((stat.SkillLevel ?? 0 & 0x1fff) << 3); 744 | break; 745 | default: 746 | break; 747 | } 748 | switch (encode) 749 | { 750 | case 2: //chance to cast skill 751 | case 3: //skill charges 752 | saveParamBits |= (stat.SkillLevel ?? 0 & 0x3f); 753 | saveParamBits |= ((stat.SkillId ?? 0 & 0x3ff) << 6); 754 | break; 755 | case 4: //by times 756 | case 1: 757 | default: 758 | break; 759 | } 760 | //always use param if it is there. 761 | if (stat.Param != null) 762 | { 763 | saveParamBits = (int)stat.Param; 764 | } 765 | writer.WriteInt32(saveParamBits, saveParamBitCount); 766 | } 767 | } 768 | int saveBits = stat.Value; 769 | saveBits += property["Save Add"].ToInt32(); 770 | switch (encode) 771 | { 772 | case 3: //skill charges 773 | saveBits &= 0xff; 774 | saveBits |= ((stat.MaxCharges ?? 0 & 0xff) << 8); 775 | break; 776 | default: 777 | break; 778 | } 779 | writer.WriteInt32(saveBits, property["Save Bits"].ToInt32()); 780 | } 781 | 782 | public static DataRow? GetStatRow(ItemStat stat) 783 | { 784 | return stat.Id is ushort statId 785 | ? Core.MetaData.ItemStatCostData.GetById(statId) 786 | : Core.MetaData.ItemStatCostData.GetByStat(stat.Stat); 787 | } 788 | } 789 | -------------------------------------------------------------------------------- /src/Resources/ItemStatCost.txt: -------------------------------------------------------------------------------- 1 | Stat ID Send Other Signed Send Bits Send Param Bits UpdateAnimRate Saved CSvSigned CSvBits CSvParam fCallback fMin MinAccr Encode Add Multiply Divide ValShift 1.09-Save Bits 1.09-Save Add Save Bits Save Add Save Param Bits keepzero op op param op base op stat1 op stat2 op stat3 direct maxstat itemspecific damagerelated itemevent1 itemeventfunc1 itemevent2 itemeventfunc2 descpriority descfunc descval descstrpos descstrneg descstr2 dgrp dgrpfunc dgrpval dgrpstrpos dgrpstrneg dgrpstr2 stuff *eol 2 | strength 0 1 11 1 0 10 1 1 125 55 1024 7 32 8 32 67 1 1 ModStr1a ModStr1a 1 1 1 Moditem2allattrib Moditem2allattrib 6 0 3 | energy 1 11 1 0 10 1 1 100 55 1024 7 32 7 32 8 maxmana 61 1 1 ModStr1d ModStr1d 1 1 1 Moditem2allattrib Moditem2allattrib 0 4 | dexterity 2 1 11 1 0 10 1 1 125 55 1024 7 32 7 32 65 1 1 ModStr1b ModStr1b 1 1 1 Moditem2allattrib Moditem2allattrib 0 5 | vitality 3 11 1 0 10 1 1 100 55 1024 7 32 7 32 9 maxhp maxstamina 63 1 1 ModStr1c ModStr1c 1 1 1 Moditem2allattrib Moditem2allattrib 0 6 | statpts 4 9 1 0 10 1024 0 7 | newskills 5 9 1 0 8 1024 0 8 | hitpoints 6 32 1 0 21 1024 8 1 maxhp 0 9 | maxhp 7 32 1 0 21 1 1 1 56 20 1024 8 8 32 9 32 59 1 1 ModStr1u ModStr1u 0 10 | mana 8 32 1 0 21 1024 8 1 1 maxmana 0 11 | maxmana 9 32 1 0 21 1 1 0 81 20 1024 8 8 32 8 32 55 1 1 ModStr1e ModStr1e 0 12 | stamina 10 32 1 0 21 1024 8 1 1 maxstamina 0 13 | maxstamina 11 32 1 0 21 1 1 0 75 20 1024 8 8 32 8 32 51 1 1 ModStr5d ModStr5d 0 14 | level 12 9 1 0 7 1024 0 15 | experience 13 32 1 0 32 1024 0 16 | gold 14 32 1 0 25 1024 0 17 | goldbank 15 32 1 0 25 1024 0 18 | item_armor_percent 16 1 11 47 20 1024 9 0 9 0 13 armorclass 74 4 1 Modstr2v Modstr2v 0 19 | item_maxdamage_percent 17 1 11 45 20 1024 9 0 9 0 13 maxdamage secondary_maxdamage item_throw_maxdamage 1 129 3 0 ModStr2j ModStr2j 0 20 | item_mindamage_percent 18 1 11 45 20 1024 9 0 9 0 13 mindamage secondary_mindamage item_throw_mindamage 1 130 3 0 ModStr2k ModStr2k 0 21 | tohit 19 1 16 15 10 1024 10 10 1 115 1 1 ModStr1h ModStr1h 0 22 | toblock 20 1 10 89 204 1024 6 0 6 0 134 2 1 ModStr3g ModStr3g 0 23 | mindamage 21 1 16 122 25 1024 6 0 6 0 1 127 1 1 ModStr1g ModStr1g 0 24 | maxdamage 22 1 16 94 16 1024 7 0 7 0 1 126 1 1 ModStr1f ModStr1f 0 25 | secondary_mindamage 23 1 16 97 15 1024 6 0 6 0 1 124 1 1 ModStr1g ModStr1g 0 26 | secondary_maxdamage 24 1 16 85 11 1024 7 0 7 0 1 123 1 1 ModStr1f ModStr1f 0 27 | damagepercent 25 1 12 45 40 1024 8 0 8 0 1 0 28 | manarecovery 26 1024 8 0 8 0 0 29 | manarecoverybonus 27 1 16 1024 8 0 8 0 52 2 2 ModStr4g ModStr4g 0 30 | staminarecoverybonus 28 1 16 1024 8 0 8 0 48 2 2 ModStr3v ModStr3v 0 31 | lastexp 29 32 1024 0 32 | nextexp 30 32 1024 0 33 | armorclass 31 1 16 17 10 1024 10 10 11 10 71 1 1 ModStr1i ModStr1i 0 34 | armorclass_vs_missile 32 1 16 11 5 1024 8 0 9 0 69 1 1 ModStr6a ModStr6a 0 35 | armorclass_vs_hth 33 1 16 13 7 1024 8 0 8 0 70 1 1 ModStr6b ModStr6b 0 36 | normal_damage_reduction 34 1 10 188 200 1024 6 0 6 0 22 3 2 ModStr2u ModStr2u 0 37 | magic_damage_reduction 35 1 10 397 340 1024 6 0 6 0 21 3 2 ModStr2t ModStr2t 0 38 | damageresist 36 1 9 152 68 1024 8 0 8 0 22 2 2 ModStr2u ModStr2u 0 39 | magicresist 37 1 9 164 68 1024 8 0 8 0 41 4 2 ModStr1m ModStr1m 0 40 | maxmagicresist 38 1 9 1091 409 1024 5 0 5 0 46 4 1 ModStr5x ModStr5x 0 41 | fireresist 39 1 9 43 20 1024 8 0 8 50 36 4 2 ModStr1j ModStr1j 2 19 strModAllResistances strModAllResistances 0 42 | maxfireresist 40 1 9 584 256 1024 5 0 5 0 42 4 1 ModStr5u ModStr5u 0 43 | lightresist 41 1 9 43 20 1024 8 0 8 50 38 4 2 ModStr1l ModStr1l 2 19 strModAllResistances strModAllResistances 0 44 | maxlightresist 42 1 9 584 256 1024 5 0 5 0 43 4 1 ModStr5w ModStr5w 0 45 | coldresist 43 1 9 43 20 1024 8 0 8 50 40 4 2 ModStr1k ModStr1k 2 19 strModAllResistances strModAllResistances 0 46 | maxcoldresist 44 1 9 584 256 1024 5 0 5 0 44 4 1 ModStr5v ModStr5v 0 47 | poisonresist 45 1 9 43 20 1024 8 0 8 50 34 4 2 ModStr1n ModStr1n 2 19 strModAllResistances strModAllResistances 0 48 | maxpoisonresist 46 1 9 526 256 1024 5 0 5 0 45 4 1 ModStr5y ModStr5y 0 49 | damageaura 47 1 16 1024 0 50 | firemindam 48 1 16 11 10 1024 8 0 8 0 1 102 1 1 ModStr1p ModStr1p 0 51 | firemaxdam 49 1 16 19 10 1024 8 0 9 0 1 101 1 1 ModStr1o ModStr1o 0 52 | lightmindam 50 1 16 12 10 1024 6 0 6 0 1 99 1 1 ModStr1r ModStr1r 0 53 | lightmaxdam 51 1 16 17 10 1024 9 0 10 0 1 98 1 1 ModStr1q ModStr1q 0 54 | magicmindam 52 1 16 196 20 1024 6 0 8 0 1 104 1 1 strModMagicDamage strModMagicDamage 0 55 | magicmaxdam 53 1 16 183 20 1024 7 0 9 0 1 103 1 1 strModMagicDamage strModMagicDamage 0 56 | coldmindam 54 1 16 451 512 1024 6 0 8 0 1 96 1 1 ModStr1t ModStr1t 0 57 | coldmaxdam 55 1 16 128 340 1024 8 0 9 0 1 95 1 1 ModStr1s ModStr1s 0 58 | coldlength 56 1 16 77 4 1024 8 0 8 0 1 0 59 | poisonmindam 57 1 19 12 28 1024 9 0 10 0 1 92 1 1 ModStr4i ModStr4i 0 60 | poisonmaxdam 58 1 19 11 34 1024 9 0 10 0 1 91 1 1 ModStr4h ModStr4h 0 61 | poisonlength 59 1 16 0 4 1024 8 0 9 0 1 0 62 | lifedrainmindam 60 1 16 1044 341 1024 7 0 7 0 1 88 2 1 ModStr2z ModStr2z 0 63 | lifedrainmaxdam 61 1 16 1024 1 0 64 | manadrainmindam 62 1 16 1179 341 1024 7 0 7 0 1 89 2 1 ModStr2y ModStr2y 0 65 | manadrainmaxdam 63 1 16 1024 1 0 66 | stamdrainmindam 64 1 16 1024 1 0 67 | stamdrainmaxdam 65 1 16 1024 1 0 68 | stunlength 66 16 1024 1 0 69 | velocitypercent 67 1 1 10 1 1024 7 30 7 30 0 70 | attackrate 68 1 1 10 1 1024 7 30 7 30 1 0 71 | other_animrate 69 1 1 10 1 1024 0 72 | quantity 70 1 16 1024 1 1 0 73 | value 71 1 9 1024 8 100 8 100 1 0 74 | durability 72 1 9 1024 8 0 9 0 1 maxdurability 1 0 75 | maxdurability 73 1 9 9 4 1024 8 0 8 0 1 0 76 | hpregen 74 451 410 1024 6 30 6 30 56 1 2 ModStr2l ModStr2w 0 77 | item_maxdurability_percent 75 1 7 117 10 1024 7 20 7 20 13 maxdurability 3 2 2 ModStr2i ModStr2i 0 78 | item_maxhp_percent 76 1 16 32093 204 1024 6 10 6 10 11 maxhp 58 2 2 ModStr2g ModStr2g 0 79 | item_maxmana_percent 77 1 16 56452 204 1024 6 10 6 10 11 maxmana 54 2 2 ModStr2h ModStr2h 0 80 | item_attackertakesdamage 78 1 16 1 112 128 1024 7 0 7 0 damagedinmelee 6 13 3 2 ModStr1v ModStr1v 0 81 | item_goldbonus 79 1 10 187 34 1024 9 100 9 100 10 2 1 ModStr1w ModStr1w 0 82 | item_magicbonus 80 1 9 577 102 1024 8 100 8 100 8 2 1 ModStr1x ModStr1x 0 83 | item_knockback 81 1 8 1 105 0 1024 7 0 7 0 1 domeleedamage 7 domissiledamage 7 76 3 0 ModStr1y ModStr1y 0 84 | item_timeduration 82 1 10 1024 9 20 9 20 0 85 | item_addclassskills 83 1 4 3 1 49523 1560 1024 3 0 3 0 3 150 13 1 ModStr3a ModStr3a 0 86 | unsentparam1 84 1024 3 0 0 87 | item_addexperience 85 1 9 36015 519 1024 3 0 9 50 11 4 1 Moditem2ExpG Moditem2ExpG 0 88 | item_healafterkill 86 1 7 1 30 101 1024 3 0 7 0 kill 28 16 1 1 ModitemHPaK ModitemHPaK 0 89 | item_reducedprices 87 8 18957 203 1024 3 0 7 0 8 2 2 ModitemRedVendP ModitemRedVendP 0 90 | item_doubleherbduration 88 1 2 1024 1 0 1 0 0 91 | item_lightradius 89 1 5 1 15 51 1024 4 4 4 4 6 1 1 ModStr3f ModStr3f 0 92 | item_lightcolor 90 1 24 1 155 0 1024 5 0 24 0 0 93 | item_req_percent 91 1 8 26 -34 1024 8 100 8 100 0 4 2 ModStr3h ModStr3h 0 94 | item_levelreq 92 1024 6 20 7 0 95 | item_fasterattackrate 93 1 1 9 1042 156 1024 7 20 7 20 1 145 4 1 ModStr4m ModStr4m 0 96 | item_levelreqpct 94 1024 7 20 7 64 13 item_levelreq 0 97 | lastblockframe 95 1024 6 20 0 98 | item_fastermovevelocity 96 1 1 9 4083 156 1024 7 20 7 20 148 4 1 ModStr4s ModStr4s 0 99 | item_nonclassskill 97 1 15 9 1 1 181 327 1024 7 20 6 0 9 81 28 0 100 | state 98 1 1 8 1 415 64 1024 6 20 1 8 0 101 | item_fastergethitrate 99 1 1 9 1065 72 1024 7 20 7 20 139 4 1 ModStr4p ModStr4p 0 102 | monster_playercount 100 1024 7 20 0 103 | skill_poison_override_length 101 8 1024 6 20 0 104 | item_fasterblockrate 102 1 1 9 1484 72 1024 7 20 7 20 136 4 1 ModStr4y ModStr4y 0 105 | skill_bypass_undead 103 1 1024 7 20 0 106 | skill_bypass_demons 104 1 1024 6 20 0 107 | item_fastercastrate 105 1 1 9 3876 156 1024 7 20 7 20 142 4 1 ModStr4v ModStr4v 0 108 | skill_bypass_beasts 106 1 1024 7 20 0 109 | item_singleskill 107 1 15 9 1 1 181 256 1024 14 0 3 0 9 81 27 0 110 | item_restinpeace 108 1 1 1987 0 1024 14 0 1 0 1 kill 29 81 3 0 ModitemSMRIP ModitemSMRIP 0 111 | curse_resistance 109 9 159 33 1024 14 0 9 0 0 112 | item_poisonlengthresist 110 1 9 27 10 1024 8 20 8 20 18 2 2 ModStr3r ModStr3r 0 113 | item_normaldamage 111 1 8 94 100 1024 7 20 9 20 1 122 1 2 ModStr5b ModStr5b 0 114 | item_howl 112 1 8 1 55 10 1024 7 -1 7 -1 1 domeleedamage 8 domissiledamage 8 79 5 2 ModStr3u ModStr3u 0 115 | item_stupidity 113 1 8 1 332 1024 1024 7 0 7 0 1 domeleedamage 9 domissiledamage 9 80 12 2 ModStr6d ModStr6d 0 116 | item_damagetomana 114 1 7 1 43 20 1024 6 0 6 0 damagedinmelee 13 damagedbymissile 13 11 2 1 ModStr3w ModStr3w 0 117 | item_ignoretargetac 115 1 2 1088 1024 1024 1 0 1 0 1 119 3 0 ModStr3y ModStr3y 0 118 | item_fractionaltargetac 116 1 8 67 20 1024 7 0 7 0 1 118 20 1 ModStr5o ModStr5o 0 119 | item_preventheal 117 1 8 48 50 1024 7 0 7 0 1 81 3 0 ModStr4a ModStr4a 0 120 | item_halffreezeduration 118 1 2 5096 988 1024 1 0 1 0 19 3 0 ModStr4b ModStr4b 0 121 | item_tohit_percent 119 1 12 981 40 1024 9 20 9 20 1 117 2 1 ModStr4c ModStr4c 0 122 | item_damagetargetac 120 1 8 24 -20 1024 7 128 7 128 1 75 1 1 ModStr4d ModStr4d 0 123 | item_demondamage_percent 121 1 12 19 12 1024 9 20 9 20 1 112 4 1 ModStr4e ModStr4e 0 124 | item_undeaddamage_percent 122 1 12 13 12 1024 9 20 9 20 1 108 4 1 ModStr4f ModStr4f 0 125 | item_demon_tohit 123 1 13 15 7 1024 10 128 10 128 1 110 1 1 ModStr4j ModStr4j 0 126 | item_undead_tohit 124 1 13 11 7 1024 10 128 10 128 1 106 1 1 ModStr4k ModStr4k 0 127 | item_throwable 125 1 2 82 1024 1024 1 0 1 0 5 3 0 ModStr5a ModStr5a 0 128 | item_elemskill 126 1 3 3 1 76 1024 1024 4 0 3 0 3 157 1 1 ModStr3i ModStr3i 0 129 | item_allskills 127 1 8 1 15123 4096 1024 3 0 3 0 158 1 1 ModStr3k ModStr3k 0 130 | item_attackertakeslightdamage 128 1 6 1 4 102 1024 5 0 5 0 damagedinmelee 10 14 3 2 ModStr3j ModStr3j 0 131 | ironmaiden_level 129 1 10 1024 0 132 | lifetap_level 130 1 10 1024 0 133 | thorns_percent 131 12 1024 0 134 | bonearmor 132 1 32 1024 0 135 | bonearmormax 133 1 32 1024 0 136 | item_freeze 134 1 5 1 666 12 1024 5 0 5 0 1 domeleedamage 14 domissiledamage 14 78 12 2 ModStr3l ModStr3l 0 137 | item_openwounds 135 1 7 1 23 10 1024 7 0 7 0 1 domeleedamage 15 domissiledamage 15 83 2 1 ModStr3m ModStr3m 0 138 | item_crushingblow 136 1 7 1 98 40 1024 7 0 7 0 1 domeleedamage 16 domissiledamage 16 87 2 1 ModStr5c ModStr5c 0 139 | item_kickdamage 137 1 7 77 51 1024 7 0 7 0 121 1 1 ModStr5e ModStr5e 0 140 | item_manaafterkill 138 1 7 1 17 102 1024 7 0 7 0 kill 17 16 1 1 ModStr5f ModStr5f 0 141 | item_healafterdemonkill 139 1 7 1 18 102 1024 7 0 7 0 kill 18 15 1 1 ModStr6c ModStr6c 0 142 | item_extrablood 140 1 7 15 10 1024 7 0 7 0 1 0 143 | item_deadlystrike 141 1 7 31 25 1024 7 0 7 0 1 85 2 1 ModStr5q ModStr5q 0 144 | item_absorbfire_percent 142 1 7 5486 102 1024 7 0 7 0 23 2 2 ModStr5g ModStr5g 0 145 | item_absorbfire 143 1 7 1739 204 1024 7 0 7 0 27 1 1 ModStr5h ModStr5h 0 146 | item_absorblight_percent 144 1 7 5486 102 1024 7 0 7 0 24 2 2 ModStr5i ModStr5i 0 147 | item_absorblight 145 1 7 1739 204 1024 7 0 7 0 29 1 1 ModStr5j ModStr5j 0 148 | item_absorbmagic_percent 146 1 7 5486 102 1024 7 0 7 0 26 2 2 ModStr5k ModStr5k 0 149 | item_absorbmagic 147 1 7 1739 204 1024 7 0 7 0 33 1 1 ModStr5l ModStr5l 0 150 | item_absorbcold_percent 148 1 7 5486 102 1024 7 0 7 0 25 2 2 ModStr5m ModStr5m 0 151 | item_absorbcold 149 1 7 1739 204 1024 7 0 7 0 31 1 1 ModStr5n ModStr5n 0 152 | item_slow 150 1 7 1 101 40 1024 7 0 7 0 1 domeleedamage 19 domissiledamage 19 77 2 2 ModStr5r ModStr5r 0 153 | item_aura 151 1 1 1024 7 0 5 0 9 159 16 0 ModitemAura ModitemAura 0 154 | item_indesctructible 152 1 1 1024 7 0 1 160 3 0 ModStre9s ModStre9s 0 155 | item_cannotbefrozen 153 1 2 15011 2048 1024 1 1 20 3 0 ModStr5z ModStr5z 0 156 | item_staminadrainpct 154 1 7 102 20 1024 7 20 7 20 1 49 2 1 ModStr6e ModStr6e 0 157 | item_reanimate 155 7 10 1 1024 7 0 7 0 10 1 kill 31 17 23 1 Moditemreanimas Moditemreanimas 0 158 | item_pierce 156 1 7 1924 2048 1024 7 0 7 0 1 132 3 0 ModStr6g ModStr6g 0 159 | item_magicarrow 157 1 7 511 1024 1024 7 0 7 0 131 3 0 ModStr6h ModStr6h 0 160 | item_explosivearrow 158 1 7 492 1536 1024 7 0 7 0 133 3 0 ModStr6i ModStr6i 0 161 | item_throw_mindamage 159 1 6 76 128 1024 6 0 6 0 1 0 162 | item_throw_maxdamage 160 1 7 88 128 1024 7 0 7 0 1 0 163 | skill_handofathena 161 1 12 1024 0 164 | skill_staminapercent 162 1 13 1024 1 maxstamina 0 165 | skill_passive_staminapercent 163 1 12 1024 1 maxstamina 0 166 | skill_concentration 164 1 12 1024 0 167 | skill_enchant 165 1 12 1024 0 168 | skill_pierce 166 1 12 1024 0 169 | skill_conviction 167 1 12 1024 0 170 | skill_chillingarmor 168 1 12 1024 0 171 | skill_frenzy 169 1 12 1024 0 172 | skill_decrepify 170 1 12 1024 0 173 | skill_armor_percent 171 1 16 1024 0 174 | alignment 172 1 2 1024 0 175 | target0 173 32 1024 0 176 | target1 174 32 1024 0 177 | goldlost 175 24 1024 0 178 | conversion_level 176 8 1024 0 179 | conversion_maxhp 177 16 1024 0 180 | unit_dooverlay 178 16 1024 0 181 | attack_vs_montype 179 9 10 19 14 1024 3 0 9 10 1 108 22 1 ModitemAttratvsM ModitemAttratvsM 0 182 | damage_vs_montype 180 9 10 27 17 1024 3 0 9 10 1 106 22 1 Moditemdamvsm Moditemdamvsm 0 183 | fade 181 1 7 1024 14 0 3 0 184 | armor_override_percent 182 1 1 8 1024 14 0 0 185 | unused183 183 1024 14 0 0 186 | unused184 184 1024 14 0 0 187 | unused185 185 1024 14 0 0 188 | unused186 186 1024 14 0 0 189 | unused187 187 1024 14 0 0 190 | item_addskill_tab 188 1 3 6 1 11042 768 1024 10 0 3 0 16 151 14 StrSklTabItem1 StrSklTabItem1 0 191 | unused189 189 1024 10 0 0 192 | unused190 190 1024 10 0 0 193 | unused191 191 1024 10 0 0 194 | unused192 192 1024 10 0 0 195 | unused193 193 1024 10 0 0 196 | item_numsockets 194 1 4 38 170 1024 4 0 4 0 1 0 197 | item_skillonattack 195 1 7 16 1 2 190 256 1024 21 0 7 0 16 1 domeleeattack 20 domissileattack 20 160 15 ItemExpansiveChancX ItemExpansiveChancX 0 198 | item_skillonkill 196 1 7 16 1 2 85 19 1024 21 0 7 0 16 1 kill 20 160 15 ModitemskonKill ModitemskonKill 0 199 | item_skillondeath 197 1 7 16 1 2 11 9 1024 21 0 7 0 16 killed 30 160 15 Moditemskondeath Moditemskondeath 0 200 | item_skillonhit 198 1 7 16 1 2 190 256 1024 21 0 7 0 16 1 domeleedamage 20 domissiledamage 20 160 15 ItemExpansiveChanc1 ItemExpansiveChanc1 0 201 | item_skillonlevelup 199 1 7 16 1 2 7 6 1024 21 0 7 0 16 levelup 30 160 15 ModitemskonLevel ModitemskonLevel 0 202 | unused200 200 1024 21 0 0 203 | item_skillongethit 201 1 7 16 1 2 190 256 1024 21 0 7 0 16 damagedinmelee 21 damagedbymissile 21 160 15 ItemExpansiveChanc2 ItemExpansiveChanc2 0 204 | unused202 202 1024 21 0 0 205 | unused203 203 1024 21 0 0 206 | item_charged_skill 204 1 30 1 3 401 256 1024 30 0 16 0 16 1 24 ModStre10d ModStre10d 0 207 | unused204 205 1 30 3 401 256 1024 30 0 0 208 | unused205 206 1 30 3 401 256 1024 30 0 0 209 | unused206 207 1 30 3 401 256 1024 30 0 0 210 | unused207 208 1 30 3 401 256 1024 30 0 0 211 | unused208 209 1 30 3 401 256 1024 30 0 0 212 | unused209 210 1 30 3 401 256 1024 30 0 0 213 | unused210 211 1 30 3 401 256 1024 30 0 0 214 | unused211 212 1 30 3 401 256 1024 30 0 0 215 | unused212 213 1 30 3 401 256 1024 30 0 0 216 | item_armor_perlevel 214 1 6 43 42 1024 6 0 6 0 4 3 level armorclass 72 6 1 ModStr1i ModStr1i increaseswithplaylevelX 0 217 | item_armorpercent_perlevel 215 1 6 87 100 1024 6 0 6 0 5 3 level armorclass 73 8 1 Modstr2v Modstr2v increaseswithplaylevelX 0 218 | item_hp_perlevel 216 1 6 92 64 1024 8 6 0 6 0 2 3 level maxhp 57 6 1 ModStr1u ModStr1u increaseswithplaylevelX 0 219 | item_mana_perlevel 217 1 6 90 128 1024 8 6 0 6 0 2 3 level maxmana 53 6 1 ModStr1e ModStr1e increaseswithplaylevelX 0 220 | item_maxdamage_perlevel 218 1 6 54 204 1024 6 0 6 0 4 3 level maxdamage secondary_maxdamage item_throw_maxdamage 1 125 6 1 ModStr1f ModStr1f increaseswithplaylevelX 0 221 | item_maxdamage_percent_perlevel 219 1 6 86 100 1024 6 0 6 0 5 3 level maxdamage secondary_maxdamage item_throw_maxdamage 1 128 8 1 ModStr2j ModStr2j increaseswithplaylevelX 0 222 | item_strength_perlevel 220 1 6 132 128 1024 6 0 6 0 2 3 level strength 66 6 1 ModStr1a ModStr1a increaseswithplaylevelX 0 223 | item_dexterity_perlevel 221 1 6 132 128 1024 6 0 6 0 2 3 level dexterity 64 6 1 ModStr1b ModStr1b increaseswithplaylevelX 0 224 | item_energy_perlevel 222 1 6 105 128 1024 6 0 6 0 2 3 level energy 60 6 1 ModStr1d ModStr1d increaseswithplaylevelX 0 225 | item_vitality_perlevel 223 1 6 105 128 1024 6 0 6 0 2 3 level vitality 62 6 1 ModStr1c ModStr1c increaseswithplaylevelX 0 226 | item_tohit_perlevel 224 1 6 53 20 1024 6 0 6 0 2 1 level tohit 1 114 6 1 ModStr1h ModStr1h increaseswithplaylevelX 0 227 | item_tohitpercent_perlevel 225 1 6 10 256 1024 6 0 6 0 2 1 level item_tohit_percent 1 116 7 1 ModStr4c ModStr4c increaseswithplaylevelX 0 228 | item_cold_damagemax_perlevel 226 1 6 1058 340 1024 6 0 6 0 2 3 level coldmaxdam 1 94 6 1 ModStr1s ModStr1s increaseswithplaylevelX 0 229 | item_fire_damagemax_perlevel 227 1 6 49 128 1024 6 0 6 0 2 3 level firemaxdam 1 100 6 1 ModStr1o ModStr1o increaseswithplaylevelX 0 230 | item_ltng_damagemax_perlevel 228 1 6 49 128 1024 6 0 6 0 2 3 level lightmaxdam 1 97 6 1 ModStr1q ModStr1q increaseswithplaylevelX 0 231 | item_pois_damagemax_perlevel 229 1 6 49 128 1024 6 0 6 0 2 3 level poisonmaxdam 1 90 6 1 ModStr4h ModStr4h increaseswithplaylevelX 0 232 | item_resist_cold_perlevel 230 1 6 101 128 1024 6 0 6 0 2 3 level coldresist 39 7 2 ModStr1k ModStr1k increaseswithplaylevelX 0 233 | item_resist_fire_perlevel 231 1 6 101 128 1024 6 0 6 0 2 3 level fireresist 35 7 2 ModStr1j ModStr1j increaseswithplaylevelX 0 234 | item_resist_ltng_perlevel 232 1 6 101 128 1024 6 0 6 0 2 3 level lightresist 37 7 2 ModStr1l ModStr1l increaseswithplaylevelX 0 235 | item_resist_pois_perlevel 233 1 6 101 128 1024 6 0 6 0 2 3 level poisonresist 33 7 2 ModStr1n ModStr1n increaseswithplaylevelX 0 236 | item_absorb_cold_perlevel 234 1 6 207 340 1024 6 0 6 0 2 3 level item_absorbcold 32 6 1 ModStre9p ModStre9p increaseswithplaylevelX 0 237 | item_absorb_fire_perlevel 235 1 6 207 340 1024 6 0 6 0 2 3 level item_absorbfire 28 6 1 ModStre9o ModStre9o increaseswithplaylevelX 0 238 | item_absorb_ltng_perlevel 236 1 6 207 340 1024 6 0 6 0 2 3 level item_absorblight 30 6 1 ModStre9q ModStre9q increaseswithplaylevelX 0 239 | item_absorb_pois_perlevel 237 1 6 207 340 1024 6 0 6 0 2 3 level item_absorbmagic 0 240 | item_thorns_perlevel 238 1 6 55 256 1024 6 0 5 0 2 3 level item_attackertakesdamage 12 9 2 ModStr1v ModStr1v increaseswithplaylevelX 0 241 | item_find_gold_perlevel 239 1 6 42 256 1024 6 0 6 0 2 3 level item_goldbonus 9 7 1 ModStr1w ModStr1w increaseswithplaylevelX 0 242 | item_find_magic_perlevel 240 1 6 814 1024 1024 6 0 6 0 2 3 level item_magicbonus 7 7 1 ModStr1x ModStr1x increaseswithplaylevelX 0 243 | item_regenstamina_perlevel 241 1 6 79 256 1024 6 0 6 0 2 3 level staminarecoverybonus 47 8 2 ModStr3v ModStr3v increaseswithplaylevelX 0 244 | item_stamina_perlevel 242 1 6 104 64 1024 6 0 6 0 2 3 level maxstamina 50 6 1 ModStr5d ModStr5d increaseswithplaylevelX 0 245 | item_damage_demon_perlevel 243 1 6 56 10 1024 6 0 6 0 2 3 level item_demondamage_percent 1 111 8 1 ModStr4e ModStr4e increaseswithplaylevelX 0 246 | item_damage_undead_perlevel 244 1 6 91 10 1024 6 0 6 0 2 3 level item_undeaddamage_percent 1 107 8 1 ModStr4f ModStr4f increaseswithplaylevelX 0 247 | item_tohit_demon_perlevel 245 1 6 55 10 1024 6 0 6 0 2 1 level item_demon_tohit 1 109 6 1 ModStr4j ModStr4j increaseswithplaylevelX 0 248 | item_tohit_undead_perlevel 246 1 6 12 10 1024 6 0 6 0 2 1 level item_undead_tohit 1 105 6 1 ModStr4k ModStr4k increaseswithplaylevelX 0 249 | item_crushingblow_perlevel 247 1 6 213 1024 1024 6 0 6 0 2 3 level item_crushingblow 1 86 7 1 ModStr5c ModStr5c increaseswithplaylevelX 0 250 | item_openwounds_perlevel 248 1 6 181 128 1024 6 0 6 0 2 3 level item_openwounds 1 82 7 1 ModStr3m ModStr3m increaseswithplaylevelX 0 251 | item_kick_damage_perlevel 249 1 6 104 128 1024 6 0 6 0 2 3 level item_kickdamage 1 120 6 1 ModStr5e ModStr5e increaseswithplaylevelX 0 252 | item_deadlystrike_perlevel 250 1 6 118 512 1024 6 0 6 0 2 3 level item_deadlystrike 1 84 7 1 ModStr5q ModStr5q increaseswithplaylevelX 0 253 | item_find_gems_perlevel 251 1 1024 0 254 | item_replenish_durability 252 1 5 106 256 1024 5 0 6 0 1 11 0 ModStre9t ModStre9t 0 255 | item_replenish_quantity 253 1 5 106 256 1024 5 0 6 0 2 3 0 ModStre9v ModStre9v 0 256 | item_extra_stack 254 1 99 10 1024 8 0 8 0 4 3 0 ModStre9i ModStre9i 0 257 | item_find_item 255 1 1024 0 258 | item_slash_damage 256 1 1024 1 0 259 | item_slash_damage_percent 257 1 1024 1 0 260 | item_crush_damage 258 1 1024 1 0 261 | item_crush_damage_percent 259 1 1024 1 0 262 | item_thrust_damage 260 1 1024 1 0 263 | item_thrust_damage_percent 261 1 1024 1 0 264 | item_absorb_slash 262 1 1024 0 265 | item_absorb_crush 263 1 1024 0 266 | item_absorb_thrust 264 1 1024 0 267 | item_absorb_slash_percent 265 1 1024 0 268 | item_absorb_crush_percent 266 1 1024 0 269 | item_absorb_thrust_percent 267 1 1024 0 270 | item_armor_bytime 268 1 22 4 0 1024 22 0 22 0 6 armorclass 180 17 1 ModStr1i ModStr1i 0 271 | item_armorpercent_bytime 269 1 22 4 0 1024 22 0 22 0 7 armorclass 180 18 1 Modstr2v Modstr2v 0 272 | item_hp_bytime 270 1 22 4 0 1024 22 0 22 0 6 maxhp 180 17 1 ModStr1u ModStr1u 0 273 | item_mana_bytime 271 1 22 4 0 1024 22 0 22 0 6 maxmana 180 17 1 ModStr1e ModStr1e 0 274 | item_maxdamage_bytime 272 1 22 4 0 1024 22 0 22 0 6 maxdamage secondary_maxdamage item_throw_maxdamage 1 180 17 1 ModStr1f ModStr1f 0 275 | item_maxdamage_percent_bytime 273 1 22 4 0 1024 22 0 22 0 7 maxdamage secondary_mindamage item_throw_mindamage 1 180 18 1 ModStr2j ModStr2j 0 276 | item_strength_bytime 274 1 22 4 0 1024 22 0 22 0 6 strength 180 17 1 ModStr1a ModStr1a 0 277 | item_dexterity_bytime 275 1 22 4 0 1024 22 0 22 0 6 dexterity 180 17 1 ModStr1b ModStr1b 0 278 | item_energy_bytime 276 1 22 4 0 1024 22 0 22 0 6 energy 180 17 1 ModStr1d ModStr1d 0 279 | item_vitality_bytime 277 1 22 4 0 1024 22 0 22 0 6 vitality 180 17 1 ModStr1c ModStr1c 0 280 | item_tohit_bytime 278 1 22 4 0 1024 22 0 22 0 6 tohit 1 180 17 1 ModStr1h ModStr1h 0 281 | item_tohitpercent_bytime 279 1 22 4 0 1024 22 0 22 0 6 item_tohit_percent 1 180 18 1 ModStr4c ModStr4c 0 282 | item_cold_damagemax_bytime 280 1 22 4 0 1024 22 0 22 0 6 coldmaxdam 1 180 17 1 ModStr1s ModStr1s 0 283 | item_fire_damagemax_bytime 281 1 22 4 0 1024 22 0 22 0 6 firemaxdam 1 180 17 1 ModStr1o ModStr1o 0 284 | item_ltng_damagemax_bytime 282 1 22 4 0 1024 22 0 22 0 6 lightmaxdam 1 180 17 1 ModStr1q ModStr1q 0 285 | item_pois_damagemax_bytime 283 1 22 4 0 1024 22 0 22 0 6 poisonmaxdam 1 180 17 1 ModStr4h ModStr4h 0 286 | item_resist_cold_bytime 284 1 22 4 0 1024 22 0 22 0 6 coldresist 180 18 2 ModStr1k ModStr1k 0 287 | item_resist_fire_bytime 285 1 22 4 0 1024 22 0 22 0 6 fireresist 180 18 2 ModStr1j ModStr1j 0 288 | item_resist_ltng_bytime 286 1 22 4 0 1024 22 0 22 0 6 lightresist 180 18 2 ModStr1l ModStr1l 0 289 | item_resist_pois_bytime 287 1 22 4 0 1024 22 0 22 0 6 poisonresist 180 18 2 ModStr1n ModStr1n 0 290 | item_absorb_cold_bytime 288 1 22 4 0 1024 22 0 22 0 6 item_absorbcold 180 18 1 ModStre9p ModStre9p 0 291 | item_absorb_fire_bytime 289 1 22 4 0 1024 22 0 22 0 6 item_absorbfire 180 18 1 ModStre9o ModStre9o 0 292 | item_absorb_ltng_bytime 290 1 22 4 0 1024 22 0 22 0 6 item_absorblight 180 18 1 ModStre9q ModStre9q 0 293 | item_absorb_pois_bytime 291 1 22 4 0 1024 22 0 22 0 6 item_absorbmagic 0 294 | item_find_gold_bytime 292 1 22 4 0 1024 22 0 22 0 6 item_goldbonus 180 18 2 ModStr1w ModStr1w 0 295 | item_find_magic_bytime 293 1 22 4 0 1024 22 0 22 0 6 item_magicbonus 180 18 1 ModStr1x ModStr1x 0 296 | item_regenstamina_bytime 294 1 22 4 0 1024 22 0 22 0 6 staminarecoverybonus 180 18 2 ModStr3v ModStr3v 0 297 | item_stamina_bytime 295 1 22 4 0 1024 22 0 22 0 6 maxstamina 180 17 1 ModStr5d ModStr5d 0 298 | item_damage_demon_bytime 296 1 22 4 0 1024 22 0 22 0 6 item_demondamage_percent 1 180 18 1 ModStr4e ModStr4e 0 299 | item_damage_undead_bytime 297 1 22 4 0 1024 22 0 22 0 6 item_undeaddamage_percent 1 180 18 1 ModStr4f ModStr4f 0 300 | item_tohit_demon_bytime 298 1 22 4 0 1024 22 0 22 0 6 item_demon_tohit 1 180 17 1 ModStr4j ModStr4j 0 301 | item_tohit_undead_bytime 299 1 22 4 0 1024 22 0 22 0 6 item_undead_tohit 1 180 17 1 ModStr4k ModStr4k 0 302 | item_crushingblow_bytime 300 1 22 4 0 1024 22 0 22 0 6 item_crushingblow 1 180 18 1 ModStr5c ModStr5c 0 303 | item_openwounds_bytime 301 1 22 4 0 1024 22 0 22 0 6 item_openwounds 1 180 18 1 ModStr3m ModStr3m 0 304 | item_kick_damage_bytime 302 1 22 4 0 1024 22 0 22 0 6 item_kickdamage 1 180 17 1 ModStr5e ModStr5e 0 305 | item_deadlystrike_bytime 303 1 22 4 0 1024 22 0 22 0 6 item_deadlystrike 1 180 18 1 ModStr5q ModStr5q 0 306 | item_find_gems_bytime 304 1 4 0 1024 0 307 | item_pierce_cold 305 1 9 1432 513 1024 8 50 88 20 1 Moditemenrescoldsk Moditemenrescoldsk 0 308 | item_pierce_fire 306 1 9 1240 497 1024 8 50 88 20 1 Moditemenresfiresk Moditemenresfiresk 0 309 | item_pierce_ltng 307 1 9 1187 481 1024 8 50 88 20 1 Moditemenresltngsk Moditemenresltngsk 0 310 | item_pierce_pois 308 1 9 1322 506 1024 8 50 88 20 1 Moditemenrespoissk Moditemenrespoissk 0 311 | item_damage_vs_monster 309 1 1024 1 0 312 | item_damage_percent_vs_monster 310 1 1024 1 0 313 | item_tohit_vs_monster 311 1 1024 1 0 314 | item_tohit_percent_vs_monster 312 1 1024 1 0 315 | item_ac_vs_monster 313 1 1024 0 316 | item_ac_percent_vs_monster 314 1 1024 0 317 | firelength 315 1 16 1024 0 318 | burningmin 316 1 16 1024 0 319 | burningmax 317 1 16 1024 0 320 | progressive_damage 318 1 3 1024 0 321 | progressive_steal 319 1 3 1024 0 322 | progressive_other 320 1 3 1024 0 323 | progressive_fire 321 1 3 1024 0 324 | progressive_cold 322 1 3 1024 0 325 | progressive_lightning 323 1 3 1024 0 326 | item_extra_charges 324 1 6 1024 6 0 6 0 1 1 0 327 | progressive_tohit 325 1 16 1024 0 328 | poison_count 326 1 5 1024 1 0 329 | damage_framerate 327 1 8 1024 0 330 | pierce_idx 328 1 6 1024 0 331 | passive_fire_mastery 329 1 12 1117 415 1024 8 0 9 50 88 4 1 ModitemdamFiresk ModitemdamFiresk 0 332 | passive_ltng_mastery 330 1 12 1054 408 1024 8 0 9 50 88 4 1 ModitemdamLtngsk ModitemdamLtngsk 0 333 | passive_cold_mastery 331 1 12 1295 379 1024 8 0 9 50 88 4 1 ModitemdamColdsk ModitemdamColdsk 0 334 | passive_pois_mastery 332 1 12 978 394 1024 8 0 9 50 88 4 1 ModitemdamPoissk ModitemdamPoissk 0 335 | passive_fire_pierce 333 1 9 2578 1024 8 0 8 0 88 20 1 Moditemenresfiresk Moditemenresfiresk 0 336 | passive_ltng_pierce 334 1 9 2493 1024 8 0 8 0 88 20 1 Moditemenresltngsk Moditemenresltngsk 0 337 | passive_cold_pierce 335 1 9 1984 1024 8 0 8 0 88 20 1 Moditemenrescoldsk Moditemenrescoldsk 0 338 | passive_pois_pierce 336 1 9 2345 1024 8 0 8 0 88 20 1 Moditemenrespoissk Moditemenrespoissk 0 339 | passive_critical_strike 337 1 9 1024 8 0 8 0 0 340 | passive_dodge 338 1 9 1024 7 0 7 0 0 341 | passive_avoid 339 1 9 1024 7 0 7 0 0 342 | passive_evade 340 1 9 1024 7 0 7 0 0 343 | passive_warmth 341 1 9 1024 8 0 8 0 0 344 | passive_mastery_melee_th 342 1 11 8 1024 8 0 8 0 0 345 | passive_mastery_melee_dmg 343 1 11 8 1024 8 0 8 0 0 346 | passive_mastery_melee_crit 344 1 9 8 1024 8 0 8 0 0 347 | passive_mastery_throw_th 345 1 11 8 1024 8 0 8 0 0 348 | passive_mastery_throw_dmg 346 1 11 8 1024 8 0 8 0 0 349 | passive_mastery_throw_crit 347 1 9 8 1024 8 0 8 0 0 350 | passive_weaponblock 348 1 9 8 1024 8 0 8 0 0 351 | passive_summon_resist 349 1 9 1024 8 0 8 0 0 352 | modifierlist_skill 350 9 1024 0 353 | modifierlist_level 351 8 1024 0 354 | last_sent_hp_pct 352 1 8 1024 0 355 | source_unit_type 353 5 1024 0 356 | source_unit_id 354 32 1024 0 357 | shortparam1 355 16 1024 0 358 | questitemdifficulty 356 1024 2 0 0 359 | passive_mag_mastery 357 1 12 1211 431 1024 8 0 9 50 0 360 | passive_mag_pierce 358 1 9 2812 1024 8 0 8 0 0 361 | --------------------------------------------------------------------------------