mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
More Actor stuff.
This commit is contained in:
parent
17a8e06c1d
commit
0444c28187
4 changed files with 302 additions and 105 deletions
|
|
@ -8,102 +8,15 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public class IndividualCollections
|
||||
{
|
||||
private readonly ActorManager _manager;
|
||||
private readonly List< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > _assignments = new();
|
||||
private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new();
|
||||
|
||||
public IReadOnlyList< (string DisplayName, ModCollection Collection, IReadOnlyList< ActorIdentifier > Identifiers) > Assignments
|
||||
=> _assignments;
|
||||
|
||||
public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals
|
||||
=> _individuals;
|
||||
|
||||
public IndividualCollections( ActorManager manager )
|
||||
=> _manager = manager;
|
||||
|
||||
public bool CanAdd( ActorIdentifier identifier )
|
||||
=> identifier.IsValid && !Individuals.ContainsKey( identifier );
|
||||
|
||||
public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers )
|
||||
{
|
||||
identifiers = Array.Empty< ActorIdentifier >();
|
||||
|
||||
switch( type )
|
||||
{
|
||||
case IdentifierType.Player:
|
||||
{
|
||||
if( !ByteString.FromString( name, out var playerName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var identifier = _manager.CreatePlayer( playerName, homeWorld );
|
||||
if( !CanAdd( identifier ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
identifiers = new[] { identifier };
|
||||
return true;
|
||||
}
|
||||
//case IdentifierType.Owned:
|
||||
//{
|
||||
// if( !ByteString.FromString( name, out var ownerName ) )
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray();
|
||||
// return
|
||||
// identifier = _manager.CreateIndividual( type, byteName, homeWorld, kind, dataId );
|
||||
// return CanAdd( identifier );
|
||||
//}
|
||||
//case IdentifierType.Npc:
|
||||
//{
|
||||
// identifier = _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, dataId );
|
||||
// return CanAdd( identifier );
|
||||
//}
|
||||
default:
|
||||
identifiers = Array.Empty< ActorIdentifier >();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection )
|
||||
=> Add( displayName, identifier, collection, Array.Empty< uint >() );
|
||||
|
||||
public bool Add( string displayName, ActorIdentifier identifier, ModCollection collection, IEnumerable< uint > additionalIds )
|
||||
{
|
||||
if( Individuals.ContainsKey( identifier ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//var identifiers = additionalIds
|
||||
// .Select( id => CanAdd( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, id, out var value ) ? value : ActorIdentifier.Invalid )
|
||||
// .Prepend( identifier )
|
||||
// .ToArray();
|
||||
//if( identifiers.Any( i => !i.IsValid || i.DataId == identifier.DataId ) )
|
||||
//{
|
||||
// return false;
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ModCollection
|
||||
{
|
||||
public sealed partial class Manager
|
||||
{
|
||||
public const int Version = 1;
|
||||
|
||||
// Is invoked after the collections actually changed.
|
||||
public event CollectionChangeDelegate CollectionChanged;
|
||||
|
||||
|
|
@ -406,6 +319,35 @@ public partial class ModCollection
|
|||
jObject.WriteTo( j );
|
||||
}
|
||||
|
||||
// Migrate individual collections to Identifiers for 0.6.0.
|
||||
private bool MigrateIndividualCollections(JObject jObject, out IndividualCollections collections)
|
||||
{
|
||||
var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0;
|
||||
collections = new IndividualCollections( Penumbra.Actors );
|
||||
if( version > 0 )
|
||||
return false;
|
||||
|
||||
// Load character collections. If a player name comes up multiple times, the last one is applied.
|
||||
var characters = jObject[nameof( Characters )]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
||||
var dict = new Dictionary< string, ModCollection >( characters.Count );
|
||||
foreach( var (player, collectionName) in characters )
|
||||
{
|
||||
var idx = GetIndexForCollectionName( collectionName );
|
||||
if( idx < 0 )
|
||||
{
|
||||
Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." );
|
||||
dict.Add( player, Empty );
|
||||
}
|
||||
else
|
||||
{
|
||||
dict.Add( player, this[idx] );
|
||||
}
|
||||
}
|
||||
|
||||
collections.Migrate0To1( dict );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void SaveActiveCollections()
|
||||
{
|
||||
|
|
|
|||
225
Penumbra/Collections/IndividualCollections.cs
Normal file
225
Penumbra/Collections/IndividualCollections.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class IndividualCollections
|
||||
{
|
||||
public const int Version = 1;
|
||||
|
||||
internal void Migrate0To1( Dictionary< string, ModCollection > old )
|
||||
{
|
||||
foreach( var (name, collection) in old )
|
||||
{
|
||||
if( ActorManager.VerifyPlayerName( name ) )
|
||||
{
|
||||
var identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue );
|
||||
if( Add( name, new[] { identifier }, collection ) )
|
||||
{
|
||||
var shortName = string.Join( " ", name.Split().Select( n => $"{n[0]}." ) );
|
||||
Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) >
|
||||
{
|
||||
private readonly ActorManager _manager;
|
||||
private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new();
|
||||
private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new();
|
||||
|
||||
public IReadOnlyDictionary< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments
|
||||
=> _assignments;
|
||||
|
||||
public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals
|
||||
=> _individuals;
|
||||
|
||||
public IndividualCollections( ActorManager manager )
|
||||
=> _manager = manager;
|
||||
|
||||
public bool CanAdd( params ActorIdentifier[] identifiers )
|
||||
=> identifiers.Length > 0 && identifiers.All( i => i.IsValid && !Individuals.ContainsKey( i ) );
|
||||
|
||||
public bool CanAdd( IdentifierType type, string name, ushort homeWorld, ObjectKind kind, IEnumerable< uint > dataIds, out ActorIdentifier[] identifiers )
|
||||
{
|
||||
identifiers = Array.Empty< ActorIdentifier >();
|
||||
|
||||
switch( type )
|
||||
{
|
||||
case IdentifierType.Player:
|
||||
if( !ByteString.FromString( name, out var playerName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var identifier = _manager.CreatePlayer( playerName, homeWorld );
|
||||
identifiers = new[] { identifier };
|
||||
break;
|
||||
case IdentifierType.Owned:
|
||||
if( !ByteString.FromString( name, out var ownerName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray();
|
||||
break;
|
||||
case IdentifierType.Npc:
|
||||
identifiers = dataIds.Select( id => _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray();
|
||||
break;
|
||||
default:
|
||||
identifiers = Array.Empty< ActorIdentifier >();
|
||||
break;
|
||||
}
|
||||
|
||||
return CanAdd( identifiers );
|
||||
}
|
||||
|
||||
public ActorIdentifier[] GetGroup( ActorIdentifier identifier )
|
||||
{
|
||||
if( !identifier.IsValid )
|
||||
{
|
||||
return Array.Empty< ActorIdentifier >();
|
||||
}
|
||||
|
||||
static ActorIdentifier[] CreateNpcs( ActorManager manager, ActorIdentifier identifier )
|
||||
{
|
||||
var name = manager.ToName( identifier.Kind, identifier.DataId );
|
||||
var table = identifier.Kind switch
|
||||
{
|
||||
ObjectKind.BattleNpc => manager.BNpcs,
|
||||
ObjectKind.EventNpc => manager.ENpcs,
|
||||
ObjectKind.Companion => manager.Companions,
|
||||
ObjectKind.MountType => manager.Mounts,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
return table.Where( kvp => kvp.Value == name )
|
||||
.Select( kvp => manager.CreateIndividual( identifier.Type, identifier.PlayerName, identifier.HomeWorld, identifier.Kind, kvp.Key ) ).ToArray();
|
||||
}
|
||||
|
||||
return identifier.Type switch
|
||||
{
|
||||
IdentifierType.Player => new[] { identifier.CreatePermanent() },
|
||||
IdentifierType.Special => new[] { identifier },
|
||||
IdentifierType.Owned => CreateNpcs( _manager, identifier.CreatePermanent() ),
|
||||
IdentifierType.Npc => CreateNpcs( _manager, identifier ),
|
||||
_ => Array.Empty< ActorIdentifier >(),
|
||||
};
|
||||
}
|
||||
|
||||
public bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection )
|
||||
{
|
||||
if( !CanAdd( identifiers ) || _assignments.ContainsKey( displayName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_assignments.Add( displayName, ( identifiers, collection ) );
|
||||
foreach( var identifier in identifiers )
|
||||
{
|
||||
_individuals.Add( identifier, collection );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ChangeCollection( string displayName, ModCollection newCollection )
|
||||
{
|
||||
var displayIndex = _assignments.IndexOfKey( displayName );
|
||||
return ChangeCollection( displayIndex, newCollection );
|
||||
}
|
||||
|
||||
public bool ChangeCollection( int displayIndex, ModCollection newCollection )
|
||||
{
|
||||
if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments.Values[ displayIndex ].Collection == newCollection )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_assignments.Values[ displayIndex ] = _assignments.Values[ displayIndex ] with { Collection = newCollection };
|
||||
foreach( var identifier in _assignments.Values[ displayIndex ].Identifiers )
|
||||
{
|
||||
_individuals[ identifier ] = newCollection;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Delete( string displayName )
|
||||
{
|
||||
var displayIndex = _assignments.IndexOfKey( displayName );
|
||||
return Delete( displayIndex );
|
||||
}
|
||||
|
||||
public bool Delete( int displayIndex )
|
||||
{
|
||||
if( displayIndex < 0 || displayIndex >= _assignments.Count )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (identifiers, _) = _assignments.Values[ displayIndex ];
|
||||
_assignments.RemoveAt( displayIndex );
|
||||
foreach( var identifier in identifiers )
|
||||
{
|
||||
_individuals.Remove( identifier );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator()
|
||||
=> _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> _assignments.Count;
|
||||
|
||||
public (string DisplayName, ModCollection Collection) this[ int index ]
|
||||
=> ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection );
|
||||
|
||||
public bool TryGetCollection( ActorIdentifier identifier, out ModCollection? collection )
|
||||
{
|
||||
collection = null;
|
||||
if( !identifier.IsValid )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( _individuals.TryGetValue( identifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( identifier.Type is not (IdentifierType.Player or IdentifierType.Owned) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
identifier = _manager.CreateIndividual( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId );
|
||||
if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection )
|
||||
=> TryGetCollection( _manager.FromObject( gameObject ), out collection );
|
||||
|
||||
public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection )
|
||||
=> TryGetCollection( _manager.FromObject( gameObject ), out collection );
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Utility;
|
||||
using EmbedIO;
|
||||
using EmbedIO.WebApi;
|
||||
using ImGuiNET;
|
||||
|
|
@ -34,6 +35,7 @@ namespace Penumbra;
|
|||
|
||||
public class Penumbra : IDalamudPlugin
|
||||
{
|
||||
public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json";
|
||||
public string Name
|
||||
=> "Penumbra";
|
||||
|
||||
|
|
@ -46,6 +48,7 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
public static bool DevPenumbraExists;
|
||||
public static bool IsNotInstalledPenumbra;
|
||||
public static bool IsValidSourceRepo;
|
||||
|
||||
public static Logger Log { get; private set; } = null!;
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
|
|
@ -84,11 +87,12 @@ public class Penumbra : IDalamudPlugin
|
|||
{
|
||||
Dalamud.Initialize( pluginInterface );
|
||||
Log = new Logger();
|
||||
DevPenumbraExists = CheckDevPluginPenumbra();
|
||||
IsNotInstalledPenumbra = CheckIsNotInstalled();
|
||||
IsValidSourceRepo = CheckSourceRepo();
|
||||
Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData );
|
||||
GamePathParser = GameData.GameData.GetGamePathParser();
|
||||
StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData );
|
||||
DevPenumbraExists = CheckDevPluginPenumbra();
|
||||
IsNotInstalledPenumbra = CheckIsNotInstalled();
|
||||
|
||||
Framework = new FrameworkManager();
|
||||
CharacterUtility = new CharacterUtility();
|
||||
|
|
@ -153,9 +157,9 @@ public class Penumbra : IDalamudPlugin
|
|||
}
|
||||
else
|
||||
{
|
||||
Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded." );
|
||||
Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." );
|
||||
}
|
||||
|
||||
|
||||
Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
||||
|
||||
OtterTex.NativeDll.Initialize( Dalamud.PluginInterface.AssemblyLocation.DirectoryName );
|
||||
|
|
@ -561,7 +565,7 @@ public class Penumbra : IDalamudPlugin
|
|||
#endif
|
||||
}
|
||||
|
||||
// Check if the loaded version of penumbra itself is in devPlugins.
|
||||
// Check if the loaded version of Penumbra itself is in devPlugins.
|
||||
private static bool CheckIsNotInstalled()
|
||||
{
|
||||
#if !DEBUG
|
||||
|
|
@ -572,6 +576,22 @@ public class Penumbra : IDalamudPlugin
|
|||
return !ret;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Check if the loaded version of Penumbra is installed from a valid source repo.
|
||||
private static bool CheckSourceRepo()
|
||||
{
|
||||
#if !DEBUG
|
||||
return Dalamud.PluginInterface.SourceRepository.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
null => false,
|
||||
Repository => true,
|
||||
"https://raw.githubusercontent.com/xivdev/Penumbra/test/repo.json" => true,
|
||||
_ => false,
|
||||
};
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,14 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
DrawProblemWindow( $"There were {Penumbra.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n"
|
||||
+ "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n"
|
||||
+ "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n"
|
||||
+ "Please use the Launcher's Repair Game Files function to repair your client installation." );
|
||||
+ "Please use the Launcher's Repair Game Files function to repair your client installation.", true );
|
||||
}
|
||||
else if( !Penumbra.IsValidSourceRepo )
|
||||
{
|
||||
DrawProblemWindow(
|
||||
$"You are loading a release version of Penumbra from the repository \"{Dalamud.PluginInterface.SourceRepository}\" instead of the official repository.\n"
|
||||
+ $"Please use the official repository at {Penumbra.Repository}.\n\n"
|
||||
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false );
|
||||
}
|
||||
else if( Penumbra.IsNotInstalledPenumbra )
|
||||
{
|
||||
|
|
@ -67,7 +74,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
$"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n"
|
||||
+ "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n"
|
||||
+ "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n"
|
||||
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." );
|
||||
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false );
|
||||
}
|
||||
else if( Penumbra.DevPenumbraExists )
|
||||
{
|
||||
|
|
@ -75,7 +82,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
$"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", "
|
||||
+ "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n"
|
||||
+ "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n"
|
||||
+ "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode." );
|
||||
+ "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -96,12 +103,12 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static void DrawProblemWindow( string text )
|
||||
private static void DrawProblemWindow( string text, bool withExceptions )
|
||||
{
|
||||
using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder );
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
ImGui.TextWrapped( text );
|
||||
ImGuiUtil.TextWrapped( text );
|
||||
color.Pop();
|
||||
|
||||
ImGui.NewLine();
|
||||
|
|
@ -112,14 +119,17 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
|
||||
ImGui.TextUnformatted( "Exceptions" );
|
||||
ImGui.Separator();
|
||||
using var box = ImRaii.ListBox( "##Exceptions", new Vector2(-1, -1) );
|
||||
foreach( var exception in Penumbra.ImcExceptions )
|
||||
if( withExceptions )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( exception.ToString() );
|
||||
ImGui.TextUnformatted( "Exceptions" );
|
||||
ImGui.Separator();
|
||||
ImGui.NewLine();
|
||||
using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) );
|
||||
foreach( var exception in Penumbra.ImcExceptions )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( exception.ToString() );
|
||||
ImGui.Separator();
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue