diff --git a/BossMod/BossModReborn.csproj b/BossMod/BossModReborn.csproj index 8198e3ed4c..89091da793 100644 --- a/BossMod/BossModReborn.csproj +++ b/BossMod/BossModReborn.csproj @@ -103,4 +103,8 @@ PreserveNewest + + + + diff --git a/BossMod/Debug/DebugObstacles.cs b/BossMod/Debug/DebugObstacles.cs index 2144e26f8a..825121565f 100644 --- a/BossMod/Debug/DebugObstacles.cs +++ b/BossMod/Debug/DebugObstacles.cs @@ -51,7 +51,8 @@ protected override void DrawSidebar() ImGui.SameLine(); if (ImGui.Button("Reload")) { - CheckpointNoClone(new(owner.Obstacles.RootPath + e.Filename)); + using var stream = File.OpenRead(owner.Obstacles.RootPath + e.Filename); + CheckpointNoClone(new(stream)); } ImGui.SetNextItemWidth(100); @@ -162,6 +163,7 @@ public void Draw() private void DrawEntries(List entries) { Action? modifications = null; + using var disableScope = ImRaii.Disabled(!Obstacles.CanEditDatabase()); for (int i = 0; i < entries.Count; ++i) { using var id = ImRaii.PushId(i); @@ -174,9 +176,8 @@ private void DrawEntries(List entries) if (ImGui.Button("Move down")) modifications += () => (entries[index], entries[index + 1]) = (entries[index + 1], entries[index]); ImGui.SameLine(); - using (ImRaii.Disabled(!Obstacles.CanEditDatabase())) - if (ImGui.Button("Delete")) - modifications += () => DeleteMap(entries, index); + if (ImGui.Button("Delete")) + modifications += () => DeleteMap(entries, index); ImGui.SameLine(); if (ImGui.Button("Edit")) OpenEditor(entries[index]); @@ -230,7 +231,8 @@ private string GenerateMapName() private void OpenEditor(ObstacleMapDatabase.Entry entry) { - var editor = new Editor(this, new(Obstacles.RootPath + entry.Filename), entry); + using var stream = File.OpenRead(Obstacles.RootPath + entry.Filename); + var editor = new Editor(this, new(stream), entry); _ = new UISimpleWindow($"Obstacle map {entry.Filename}", editor.Draw, true, new(1000, 1000)); } } diff --git a/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMeganereis.cs b/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMeganereis.cs index 438674f584..e9bde39a52 100644 --- a/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMeganereis.cs +++ b/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMeganereis.cs @@ -45,7 +45,24 @@ class Ceras(BossModule module) : Components.SingleTargetCast(module, ActionID.Ma class WaveOfTurmoil(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.WaveOfTurmoil), 20, stopAtWall: true) { - public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false; + private readonly Hydrobomb _aoe = module.FindComponent()!; + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => _aoe?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var forbidden = new List>(); + var source = Sources(slot, actor).FirstOrDefault(); + if (source != default) + { + foreach (var c in _aoe.ActiveAOEs(slot, actor)) + { + forbidden.Add(ShapeDistance.Cone(Arena.Center, 20, Angle.FromDirection(c.Origin - Module.Center), 30.Degrees())); + } + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Min(f => f(p)), source.Activation); + } + } } class Hydrobomb(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Hydrobomb), 10); @@ -70,8 +87,8 @@ public GymnasiouMeganereisStates(BossModule module) : base(module) { TrivialPhase() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousTyphon.cs b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousTyphon.cs index 437f941466..b01bab4e4c 100644 --- a/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousTyphon.cs +++ b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousTyphon.cs @@ -161,7 +161,7 @@ public DaenOseTheAvariciousTyphonStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808, SortOrder = 1)] public class DaenOseTheAvariciousTyphon(WorldState ws, Actor primary) : THTemplate(ws, primary) { private static readonly uint[] bonusAdds = [(uint)OID.SecretEgg, (uint)OID.SecretGarlic, (uint)OID.SecretOnion, (uint)OID.SecretTomato, diff --git a/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousUltros.cs b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousUltros.cs new file mode 100644 index 0000000000..661d22873b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/DaenOseTheAvariciousUltros.cs @@ -0,0 +1,136 @@ +namespace BossMod.Shadowbringers.TreasureHunt.ShiftingOubliettesOfLyheGhiah.DaenOseTheAvariciousUltros; + +public enum OID : uint +{ + Boss = 0x3030, // R0.75-5.1 + StylishTentacle = 0x3031, // R7.2 + SecretQueen = 0x3021, // R0.84, icon 5, needs to be killed in order from 1 to 5 for maximum rewards + SecretGarlic = 0x301F, // R0.84, icon 3, needs to be killed in order from 1 to 5 for maximum rewards + SecretTomato = 0x3020, // R0.84, icon 4, needs to be killed in order from 1 to 5 for maximum rewards + SecretOnion = 0x301D, // R0.84, icon 1, needs to be killed in order from 1 to 5 for maximum rewards + SecretEgg = 0x301E, // R0.84, icon 2, needs to be killed in order from 1 to 5 for maximum rewards + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 872, // Boss/SecretTomato/SecretQueen->player, no cast, single-target + Change = 21741, // Boss->self, 6.0s cast, single-target, boss morphs into Ultros + + TentacleVisual = 21753, // Boss->self, no cast, single-target + Tentacle = 21754, // StylishTentacle->self, 3.0s cast, range 8 circle + Megavolt = 21752, // Boss->self, 3.5s cast, range 11 circle + Wallop = 21755, // StylishTentacle->self, 5.0s cast, range 20 width 10 rect + ThunderIII = 21743, // Boss->player, 4.0s cast, single-target, tankbuster + + WaveOfTurmoilVisual = 21748, // Boss->self, 5.0s cast, single-target + WaveOfTurmoil = 21749, // Helper->self, 5.0s cast, range 40 circle, knockback 20, away from source + + SoakingSplatter = 21750, // Helper->location, 6.5s cast, range 10 circle + AquaBreath = 21751, // Boss->self, 3.0s cast, range 13 90-degree cone + + FallingWaterVisual = 21746, // Boss->self, 5.0s cast, single-target + FallingWater = 21747, // Helper->player, 5.0s cast, range 8 circle, spread + + WaterspoutVisual = 21744, // Boss->self, 3.0s cast, single-target + Waterspout = 21745, // Helper->location, 3.0s cast, range 4 circle + + Pollen = 6452, // SecretQueen->self, 3.5s cast, range 6+R circle + TearyTwirl = 6448, // SecretOnion->self, 3.5s cast, range 6+R circle + HeirloomScream = 6451, // SecretTomato->self, 3.5s cast, range 6+R circle + PluckAndPrune = 6449, // SecretEgg->self, 3.5s cast, range 6+R circle + PungentPirouette = 6450, // SecretGarlic->self, 3.5s cast, range 6+R circle + Telega = 9630 // Mandragoras->self, no cast, single-target, bonus adds disappear +} + +class AquaBreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AquaBreath), new AOEShapeCone(13, 45.Degrees())); +class Tentacle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Tentacle), new AOEShapeCircle(8)); +class Wallop(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Wallop), new AOEShapeRect(20, 5)); +class Megavolt(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Megavolt), new AOEShapeCircle(11)); +class Waterspout(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Waterspout), 4); +class SoakingSplatter(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SoakingSplatter), 10); +class FallingWater(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FallingWater), 8); +class ThunderIII(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.ThunderIII)); + +class WaveOfTurmoil(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.WaveOfTurmoil), 20, stopAtWall: true) +{ + private readonly SoakingSplatter _aoe = module.FindComponent()!; + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => _aoe?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var forbidden = new List>(); + var source = Sources(slot, actor).FirstOrDefault(); + if (source != default) + { + foreach (var c in _aoe.ActiveAOEs(slot, actor)) + { + forbidden.Add(ShapeDistance.Cone(Arena.Center, 20, Angle.FromDirection(c.Origin - Module.Center), 30.Degrees())); + } + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Min(f => f(p)), source.Activation); + } + } +} + +abstract class Mandragoras(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCircle(6.84f)); +class PluckAndPrune(BossModule module) : Mandragoras(module, AID.PluckAndPrune); +class TearyTwirl(BossModule module) : Mandragoras(module, AID.TearyTwirl); +class HeirloomScream(BossModule module) : Mandragoras(module, AID.HeirloomScream); +class PungentPirouette(BossModule module) : Mandragoras(module, AID.PungentPirouette); +class Pollen(BossModule module) : Mandragoras(module, AID.Pollen); + +class DaenOseTheAvariciousUltrosStates : StateMachineBuilder +{ + public DaenOseTheAvariciousUltrosStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(DaenOseTheAvariciousUltros.All).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 745, NameID = 9808, SortOrder = 2)] +public class DaenOseTheAvariciousUltros(WorldState ws, Actor primary) : THTemplate(ws, primary) +{ + private static readonly uint[] bonusAdds = [(uint)OID.SecretEgg, (uint)OID.SecretGarlic, (uint)OID.SecretOnion, (uint)OID.SecretTomato, + (uint)OID.SecretQueen]; + public static readonly uint[] All = [(uint)OID.Boss, .. bonusAdds]; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor); + Arena.Actors(Enemies(bonusAdds), Colors.Vulnerable); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = (OID)e.Actor.OID switch + { + OID.SecretOnion => 5, + OID.SecretEgg => 4, + OID.SecretGarlic => 3, + OID.SecretTomato => 2, + OID.SecretQueen => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Pathfinding/ObstacleMapDatabase.cs b/BossMod/Pathfinding/ObstacleMapDatabase.cs index fb23e82c52..a2d5fab3ce 100644 --- a/BossMod/Pathfinding/ObstacleMapDatabase.cs +++ b/BossMod/Pathfinding/ObstacleMapDatabase.cs @@ -19,35 +19,28 @@ public sealed record class Entry(Vector3 MinBounds, Vector3 MaxBounds, WPos Orig public readonly Dictionary> Entries = []; - public void Load(string listPath) + public void Load(Stream stream) { Entries.Clear(); - try + using var json = Serialization.ReadJson(stream); + foreach (var jentries in json.RootElement.EnumerateObject()) { - using var json = Serialization.ReadJson(listPath); - foreach (var jentries in json.RootElement.EnumerateObject()) + var sep = jentries.Name.IndexOf('.', StringComparison.Ordinal); + var zone = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[..sep]) : uint.Parse(jentries.Name); + var cfc = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[(sep + 1)..]) : 0; + var entries = Entries[(zone << 16) | cfc] = []; + foreach (var jentry in jentries.Value.EnumerateArray()) { - var sep = jentries.Name.IndexOf('.', StringComparison.Ordinal); - var zone = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[..sep]) : uint.Parse(jentries.Name); - var cfc = sep >= 0 ? uint.Parse(jentries.Name.AsSpan()[(sep + 1)..]) : 0; - var entries = Entries[(zone << 16) | cfc] = []; - foreach (var jentry in jentries.Value.EnumerateArray()) - { - entries.Add(new( - ReadVec3(jentry, nameof(Entry.MinBounds)), - ReadVec3(jentry, nameof(Entry.MaxBounds)), - ReadWPos(jentry, nameof(Entry.Origin)), - jentry.GetProperty(nameof(Entry.ViewWidth)).GetInt32(), - jentry.GetProperty(nameof(Entry.ViewHeight)).GetInt32(), - jentry.GetProperty(nameof(Entry.Filename)).GetString() ?? "" - )); - } + entries.Add(new( + ReadVec3(jentry, nameof(Entry.MinBounds)), + ReadVec3(jentry, nameof(Entry.MaxBounds)), + ReadWPos(jentry, nameof(Entry.Origin)), + jentry.GetProperty(nameof(Entry.ViewWidth)).GetInt32(), + jentry.GetProperty(nameof(Entry.ViewHeight)).GetInt32(), + jentry.GetProperty(nameof(Entry.Filename)).GetString() ?? "" + )); } } - catch (Exception ex) - { - Service.Log($"Failed to load obstacle map database '{listPath}': {ex}"); - } } public void Save(string listPath) diff --git a/BossMod/Pathfinding/ObstacleMapManager.cs b/BossMod/Pathfinding/ObstacleMapManager.cs index 9e77a27f3b..7b9662fc49 100644 --- a/BossMod/Pathfinding/ObstacleMapManager.cs +++ b/BossMod/Pathfinding/ObstacleMapManager.cs @@ -1,4 +1,7 @@ -namespace BossMod.Pathfinding; +using System.IO; +using System.Reflection; + +namespace BossMod.Pathfinding; [ConfigDisplay(Name = "Obstacle map development", Order = 8)] public sealed class ObstacleMapConfig : ConfigNode @@ -19,6 +22,8 @@ public sealed class ObstacleMapManager : IDisposable private readonly EventSubscriptions _subscriptions; private readonly List<(ObstacleMapDatabase.Entry entry, Bitmap data)> _entries = []; + public bool LoadFromSource => _config.LoadFromSource; + public ObstacleMapManager(WorldState ws) { World = ws; @@ -41,10 +46,20 @@ public void Dispose() public void ReloadDatabase() { - var dbPath = _config.LoadFromSource ? _config.SourcePath : ""; // TODO: load from near assembly instead - Service.Log($"Loading obstacle database from '{dbPath}'"); - Database.Load(dbPath); - RootPath = dbPath[..(dbPath.LastIndexOfAny(['\\', '/']) + 1)]; + Service.Log($"Loading obstacle database from {(_config.LoadFromSource ? _config.SourcePath : "")}"); + RootPath = _config.LoadFromSource ? _config.SourcePath[..(_config.SourcePath.LastIndexOfAny(['\\', '/']) + 1)] : ""; + + try + { + using var dbStream = _config.LoadFromSource ? File.OpenRead(_config.SourcePath) : GetEmbeddedResource("maplist.json"); + Database.Load(dbStream); + } + catch (Exception ex) + { + Service.Log($"Failed to load obstacle database: {ex}"); + Database.Entries.Clear(); + } + LoadMaps(World.CurrentZone, World.CurrentCFCID); } @@ -63,17 +78,19 @@ private void LoadMaps(ushort zoneId, ushort cfcId) { foreach (var e in entries) { - var filename = RootPath + e.Filename; try { - var bitmap = new Bitmap(filename); + using var eStream = _config.LoadFromSource ? File.OpenRead(RootPath + e.Filename) : GetEmbeddedResource(e.Filename); + var bitmap = new Bitmap(eStream); _entries.Add((e, bitmap)); } catch (Exception ex) { - Service.Log($"Failed to load map {filename} for {zoneId}.{cfcId}: {ex}"); + Service.Log($"Failed to load map {e.Filename} from {(_config.LoadFromSource ? RootPath : "")} for {zoneId}.{cfcId}: {ex}"); } } } } + + private Stream GetEmbeddedResource(string name) => Assembly.GetExecutingAssembly().GetManifestResourceStream($"BossMod.Pathfinding.ObstacleMaps.{name}") ?? throw new InvalidDataException($"Missing embedded resource {name}"); } diff --git a/BossMod/Util/Bitmap.cs b/BossMod/Util/Bitmap.cs index 20d78f31e8..6a530236c4 100644 --- a/BossMod/Util/Bitmap.cs +++ b/BossMod/Util/Bitmap.cs @@ -150,29 +150,28 @@ public Bitmap(int width, int height, Color color0, Color color1, int resolution Pixels = new byte[height * BytesPerRow]; } - public Bitmap(string filename) + public Bitmap(Stream stream) { - using var fstream = File.OpenRead(filename); - using var reader = new BinaryReader(fstream); - var fileHeader = fstream.ReadStruct(); + using var reader = new BinaryReader(stream); + var fileHeader = stream.ReadStruct(); if (fileHeader.Type != Magic) - throw new ArgumentException($"File '{filename}' is not a bitmap: magic is {fileHeader.Type:X4}"); + throw new ArgumentException($"Not a bitmap: magic is {fileHeader.Type:X4}"); - var header = fstream.ReadStruct(); + var header = stream.ReadStruct(); if (header.SizeInBytes != Marshal.SizeOf()) - throw new ArgumentException($"Bitmap '{filename}' has unsupported header size {header.SizeInBytes}"); + throw new ArgumentException($"Bitmap has unsupported header size {header.SizeInBytes}"); if (header.Width <= 0) - throw new ArgumentException($"Bitmap '{filename}' has non-positive width {header.Width}"); + throw new ArgumentException($"Bitmap has non-positive width {header.Width}"); if (header.Height >= 0) - throw new ArgumentException($"Bitmap '{filename}' is not top-down (height={header.Height})"); + throw new ArgumentException($"Bitmap is not top-down (height={header.Height})"); if (header.BitCount != 1) - throw new ArgumentException($"Bitmap '{filename}' is not 1bpp (bitcount={header.BitCount})"); + throw new ArgumentException($"Bitmap is not 1bpp (bitcount={header.BitCount})"); if (header.Compression != 0) - throw new ArgumentException($"Bitmap '{filename}' has unsupported compression method {header.Compression:X8}"); + throw new ArgumentException($"Bitmap has unsupported compression method {header.Compression:X8}"); if (header.XPixelsPerMeter != header.YPixelsPerMeter || header.XPixelsPerMeter <= 0) - throw new ArgumentException($"Bitmap '{filename}' has inconsistent or non-positive resolution {header.XPixelsPerMeter}x{header.YPixelsPerMeter}"); + throw new ArgumentException($"Bitmap has inconsistent or non-positive resolution {header.XPixelsPerMeter}x{header.YPixelsPerMeter}"); if (header.ColorUsedCount is not 0 or 2) - throw new ArgumentException($"Bitmap '{filename}' has wrong palette size {header.ColorUsedCount}"); + throw new ArgumentException($"Bitmap has wrong palette size {header.ColorUsedCount}"); Width = header.Width; Height = -header.Height; diff --git a/BossMod/Util/Serialization.cs b/BossMod/Util/Serialization.cs index 97e48dc8d9..01c72b2d32 100644 --- a/BossMod/Util/Serialization.cs +++ b/BossMod/Util/Serialization.cs @@ -26,9 +26,10 @@ public class JsonTypeConverter : JsonConverter public static JsonDocument ReadJson(string path) { using var fstream = File.OpenRead(path); - return JsonDocument.Parse(fstream, new JsonDocumentOptions() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); + return ReadJson(fstream); } + public static JsonDocument ReadJson(Stream stream) => JsonDocument.Parse(stream, new JsonDocumentOptions() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); public static Utf8JsonWriter WriteJson(Stream fstream, bool indented = true) => new(fstream, new JsonWriterOptions() { Indented = indented }); public static unsafe T ReadStruct(this Stream stream) where T : unmanaged diff --git a/TODO b/TODO index bef64be35f..5c5907301d 100644 --- a/TODO +++ b/TODO @@ -44,8 +44,6 @@ general: - zone modules: module info ui - refactor ipc/dtr - questbattles -- collisions for pathfinding --- embedded mode boss modules: - timers diff --git a/UIDev/BitmapEditorTest.cs b/UIDev/BitmapEditorTest.cs index da4601fc6f..1c6e0dcf94 100644 --- a/UIDev/BitmapEditorTest.cs +++ b/UIDev/BitmapEditorTest.cs @@ -1,5 +1,6 @@ using BossMod; using ImGuiNET; +using System.IO; namespace UIDev; @@ -43,7 +44,8 @@ protected override void DrawSidebar() if (ImGui.Button("load")) { - CheckpointNoClone(new("D:\\test.bmp")); + using var stream = File.OpenRead("D:\\test.bmp"); + CheckpointNoClone(new(stream)); } }