Skip to content

Commit

Permalink
Add DSCS Battle UI Shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
Tupelov committed Nov 5, 2021
1 parent 17c6bcf commit 747fc1b
Show file tree
Hide file tree
Showing 15 changed files with 650 additions and 15 deletions.
32 changes: 32 additions & 0 deletions PC/DSCS/BattleUIShortcuts/BattleUIShortcuts.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputType>WinExe</OutputType>
</PropertyGroup>

<ItemGroup>
<None Remove="ModConfig.json" />
<None Remove="Preview.png" />
<None Remove="Publish.ps1" />
</ItemGroup>

<ItemGroup>
<Content Include="ModConfig.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Preview.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Reloaded.Memory" Version="3.0.1" />
<PackageReference Include="Reloaded.Memory.Sigscan" Version="1.2.1" />
<PackageReference Include="Reloaded.Mod.Interfaces" Version="1.6.0" />
<PackageReference Include="Reloaded.SharedLib.Hooks" Version="1.4.0" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions PC/DSCS/BattleUIShortcuts/Configuration/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using BattleUIShortcuts.Configuration.Implementation;
using System.ComponentModel;

namespace BattleUIShortcuts.Configuration
{
public class Config : Configurable<Config>
{
/*
User Properties:
- Please put all of your configurable properties here.
- Tip: Consider using the various available attributes https://stackoverflow.com/a/15051390/11106111
By default, configuration saves as "Config.json" in mod folder.
Need more config files/classes? See Configuration.cs
*/


[DisplayName("String")]
[Description("This is a string.")]
public string String { get; set; } = "Default Name";

[DisplayName("Int")]
[Description("This is an int.")]
public int Integer { get; set; } = 42;

[DisplayName("Bool")]
[Description("This is a bool.")]
public bool Boolean { get; set; } = true;

[DisplayName("Float")]
[Description("This is a floating point number.")]
public float Float { get; set; } = 6.987654F;

[DisplayName("Enum")]
[Description("This is an enumerable.")]
public SampleEnum Reloaded { get; set; } = SampleEnum.ILoveIt;

public enum SampleEnum
{
NoOpinion,
Sucks,
IsMediocre,
IsOk,
IsCool,
ILoveIt
}
}
}
145 changes: 145 additions & 0 deletions PC/DSCS/BattleUIShortcuts/Configuration/Implementation/Configurable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Reloaded.Mod.Interfaces;
using System;
using System.ComponentModel;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace BattleUIShortcuts.Configuration.Implementation
{
public class Configurable<TParentType> : IUpdatableConfigurable where TParentType : Configurable<TParentType>, new()
{
// Default Serialization Options
public static JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions()
{
Converters = { new JsonStringEnumConverter() },
WriteIndented = true
};

/* Events */

/// <summary>
/// Automatically executed when the external configuration file is updated.
/// Passes a new instance of the configuration as parameter.
/// Inside your event handler, replace the variable storing the configuration with the new one.
/// </summary>
[Browsable(false)]
public event Action<IUpdatableConfigurable> ConfigurationUpdated;

/* Class Properties */

/// <summary>
/// Full path to the configuration file.
/// </summary>
[JsonIgnore]
[Browsable(false)]
public string FilePath { get; private set; }

/// <summary>
/// The name of the configuration file.
/// </summary>
[JsonIgnore]
[Browsable(false)]
public string ConfigName { get; private set; }

/// <summary>
/// Receives events on whenever the file is actively changed or updated.
/// </summary>
[JsonIgnore]
[Browsable(false)]
private FileSystemWatcher ConfigWatcher { get; set; }

/* Construction */
public Configurable() { }

private void Initialize(string filePath, string configName)
{
// Initializes an instance after construction by e.g. a serializer.
FilePath = filePath;
ConfigName = configName;

MakeConfigWatcher();
Save = OnSave;
}

/* Cleanup */
public void DisposeEvents()
{
// Halts the FilesystemWatcher and all events associated with this instance.
ConfigWatcher?.Dispose();
ConfigurationUpdated = null;
}

/* Load/Save support. */

/// <summary>
/// Saves the configuration to the hard disk.
/// </summary>
[JsonIgnore]
[Browsable(false)]
public Action Save { get; private set; }

/// <summary>
/// Safety lock for when changed event gets raised twice on file save.
/// </summary>
[Browsable(false)]
private static object _readLock = new object();

/// <summary>
/// Loads a specified configuration from the hard disk, or creates a default if it does not exist.
/// </summary>
/// <param name="filePath">The full file path of the config.</param>
/// <param name="configName">The name of the configuration.</param>
public static TParentType FromFile(string filePath, string configName) => ReadFrom(filePath, configName);

/* Event */

/// <summary>
/// Creates a <see cref="FileSystemWatcher"/> that will automatically raise an
/// <see cref="OnConfigurationUpdated"/> event when the config file is changed.
/// </summary>
/// <returns></returns>
private void MakeConfigWatcher()
{
ConfigWatcher = new FileSystemWatcher(Path.GetDirectoryName(FilePath), Path.GetFileName(FilePath));
ConfigWatcher.Changed += (sender, e) => OnConfigurationUpdated();
ConfigWatcher.EnableRaisingEvents = true;
}

/// <summary>
/// Reloads the configuration from the hard disk and raises the updated event.
/// </summary>
private void OnConfigurationUpdated()
{
lock (_readLock)
{
// Load and copy events.
// Note: External program might still be writing to file while this is being executed, so we need to keep retrying.
var newConfig = Utilities.TryGetValue(() => ReadFrom(this.FilePath, this.ConfigName), 250, 2);
newConfig.ConfigurationUpdated = ConfigurationUpdated;

// Disable events for this instance.
DisposeEvents();

// Call subscribers through the new config.
newConfig.ConfigurationUpdated?.Invoke(newConfig);
}
}

private void OnSave()
{
var parent = (TParentType)this;
File.WriteAllText(FilePath, JsonSerializer.Serialize(parent, SerializerOptions));
}

/* Utility */
private static TParentType ReadFrom(string filePath, string configName)
{
var result = File.Exists(filePath)
? JsonSerializer.Deserialize<TParentType>(File.ReadAllBytes(filePath), SerializerOptions)
: new TParentType();
result.Initialize(filePath, configName);
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Reloaded.Mod.Interfaces;
using System.IO;

namespace BattleUIShortcuts.Configuration.Implementation
{
public class Configurator : IConfigurator
{
/* For latest documentation:
- See the interface! (Go To Definition) or if not available
- Google the Source Code!
*/

/// <summary>
/// Full path to the mod folder.
/// </summary>
public string ModFolder { get; private set; }

/// <summary>
/// Returns a list of configurations.
/// </summary>
public IUpdatableConfigurable[] Configurations => _configurations ?? MakeConfigurations();
private IUpdatableConfigurable[] _configurations;

private IUpdatableConfigurable[] MakeConfigurations()
{
_configurations = new IUpdatableConfigurable[]
{
// Add more configurations here if needed.
Configurable<Config>.FromFile(Path.Combine(ModFolder, "Config.json"), "Default Config")
};

// Add self-updating to configurations.
for (int x = 0; x < Configurations.Length; x++)
{
var xCopy = x;
Configurations[x].ConfigurationUpdated += configurable =>
{
Configurations[xCopy] = configurable;
};
}

return _configurations;
}

public Configurator() { }
public Configurator(string modDirectory) : this()
{
ModFolder = modDirectory;
}

/* Configurator */

/// <summary>
/// Gets an individual user configuration.
/// </summary>
public TType GetConfiguration<TType>(int index) => (TType)Configurations[index];

/* IConfigurator. */

/// <summary>
/// Sets the mod directory for the Configurator.
/// </summary>
public void SetModDirectory(string modDirectory) => ModFolder = modDirectory;

/// <summary>
/// Returns a list of user configurations.
/// </summary>
public IConfigurable[] GetConfigurations() => Configurations;

/// <summary>
/// Allows for custom launcher/configurator implementation.
/// If you have your own configuration program/code, run that code here and return true, else return false.
/// </summary>
public bool TryRunCustomConfiguration() => false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Diagnostics;
using System.Threading;

namespace BattleUIShortcuts.Configuration.Implementation
{
public class Utilities
{
/// <param name="getValue">Function that retrieves the value.</param>
/// <param name="timeout">The timeout in milliseconds.</param>
/// <param name="sleepTime">Amount of sleep per iteration/attempt.</param>
/// <param name="token">Token that allows for cancellation of the task.</param>
/// <exception cref="Exception">Timeout expired.</exception>
public static T TryGetValue<T>(Func<T> getValue, int timeout, int sleepTime, CancellationToken token = default)
{
Stopwatch watch = new Stopwatch();
watch.Start();
bool valueSet = false;
T value = default;

while (watch.ElapsedMilliseconds < timeout)
{
if (token.IsCancellationRequested)
return value;

try
{
value = getValue();
valueSet = true;
break;
}
catch (Exception) { /* Ignored */ }

Thread.Sleep(sleepTime);
}

if (valueSet == false)
throw new Exception($"Timeout limit {timeout} exceeded.");

return value;
}
}
}
Loading

0 comments on commit 747fc1b

Please sign in to comment.