Initial commit
This commit is contained in:
commit
3f1075e67f
|
@ -0,0 +1,9 @@
|
||||||
|
reference*/
|
||||||
|
TemplateMod/
|
||||||
|
OxygenNotIncluded*
|
||||||
|
Libs
|
||||||
|
Logs
|
||||||
|
Backups/
|
||||||
|
.vscode/
|
||||||
|
IlSpy
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,3 @@
|
||||||
|
obj/
|
||||||
|
bin/
|
||||||
|
dist/
|
|
@ -0,0 +1,63 @@
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Database;
|
||||||
|
using PeterHan.PLib.Options;
|
||||||
|
|
||||||
|
namespace PriorityUX {
|
||||||
|
public class Mod : KMod.UserMod2 {
|
||||||
|
// public static readonly Type[] ToolTypes = {
|
||||||
|
// typeof(AttackTool),
|
||||||
|
// typeof(CaptureTool),
|
||||||
|
// typeof(ClearTool),
|
||||||
|
// typeof(DeconstructTool),
|
||||||
|
// typeof(DigTool),
|
||||||
|
// typeof(EmptyPipeTool),
|
||||||
|
// typeof(HarvestTool),
|
||||||
|
// typeof(MopTool),
|
||||||
|
// typeof(PrioritizeTool),
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
public override void OnLoad(Harmony harmony) {
|
||||||
|
Debug.Log("UX Mod Load");
|
||||||
|
Harmony.DEBUG = true;
|
||||||
|
|
||||||
|
base.OnLoad(harmony);
|
||||||
|
|
||||||
|
Debug.Log("UX Mod PostLoad base");
|
||||||
|
|
||||||
|
PUtil.InitLibrary();
|
||||||
|
LocString.CreateLocStringKeys(typeof(Strings.PRIORITYUX));
|
||||||
|
new PLocalization().Register();
|
||||||
|
new POptions().RegisterOptions(this, typeof(Options));
|
||||||
|
|
||||||
|
// MethodInfo patchMethod = typeof(GenericToolPatches).GetMethod(nameof(GenericToolPatches.OnActivateTranspiler));
|
||||||
|
// foreach (var toolType in ToolTypes) {
|
||||||
|
// MethodInfo method = toolType.GetMethod("OnActivateTool", BindingFlags.NonPublic);
|
||||||
|
// harmony.Patch(method, postfix: new HarmonyMethod(patchMethod));
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// static class DefaultPriorityFixes {
|
||||||
|
// [HarmonyPatch(typeof(Capturable), nameof(Capturable.MarkForCapture))]
|
||||||
|
// [HarmonyPrefix]
|
||||||
|
// static bool Capturable_MarkForCapture(Capturable __instance, bool mark) {
|
||||||
|
// Debug.Log("[PriorityUX] >> Capturable_MarkForCapture");
|
||||||
|
// PrioritySetting priority = new PrioritySetting(PriorityScreen.PriorityClass.basic, 5);
|
||||||
|
// __instance.MarkForCapture(mark, priority);
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static readonly FieldInfo masterPrioritySetting = typeof(Prioritizable).GetField("masterPrioritySetting", BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
// [HarmonyPatch(typeof(Prioritizable), "OnSpawn")]
|
||||||
|
// [HarmonyPrefix]
|
||||||
|
// static void OnSpawn(object __instance) {
|
||||||
|
// if (Options.Instance.buildingsInheritPriority) {
|
||||||
|
// Debug.Log("[PriorityUX] >> Set Prioritizable.OnSpawn");
|
||||||
|
// masterPrioritySetting.SetValue(__instance, Utils.getLastPriority());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PeterHan.PLib.Options;
|
||||||
|
|
||||||
|
namespace PriorityUX {
|
||||||
|
[ModInfo("tdrz.nl")]
|
||||||
|
class Options : SingletonOptions<Options> {
|
||||||
|
[Option]
|
||||||
|
[JsonProperty]
|
||||||
|
public bool resetPriority { get; set; } = false;
|
||||||
|
|
||||||
|
[Option]
|
||||||
|
[Limit(1.0, 9.0)]
|
||||||
|
[JsonProperty]
|
||||||
|
public int resetPriorityLevel { get; set; } = 3;
|
||||||
|
|
||||||
|
[Option]
|
||||||
|
[JsonProperty]
|
||||||
|
public bool enableKeysForAllTools { get; set; } = true;
|
||||||
|
|
||||||
|
[Option]
|
||||||
|
[JsonProperty]
|
||||||
|
public bool enableKeysForBuilding { get; set; } = true;
|
||||||
|
|
||||||
|
[Option]
|
||||||
|
[JsonProperty]
|
||||||
|
public bool deconstructInheritPriority { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PeterHan.PLib</Title>
|
||||||
|
<AssemblyTitle>PLib</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<!--<PackageVersion>4.10.0-beta</PackageVersion>-->
|
||||||
|
<Authors>Peter Han (https://github.com/peterhaneve)</Authors>
|
||||||
|
<PackageProjectUrl>https://github.com/peterhaneve/ONIMods</PackageProjectUrl>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
|
<PackageTags>Game, OxygenNotIncluded, Oxygen, PLib, Library</PackageTags>
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<Copyright>Copyright 2022 Peter Han</Copyright>
|
||||||
|
<Description>PLib - Peter Han's library used for creating mods for Oxygen Not Included, a simulation game by Klei Entertainment.
|
||||||
|
|
||||||
|
Contains methods aimed at improving cross-mod compatibility, in-game user interfaces, and game-wide functions such as Actions and Lighting. An easy-to-use Mod Options menu is also included.</Description>
|
||||||
|
<PackageReleaseNotes>
|
||||||
|
Update PLib Lighting for compatibility with Hot Shots.
|
||||||
|
|
||||||
|
Significantly improve the PLib Options DynamicOption attribute. Custom option handlers can now
|
||||||
|
have constructors with similar parameters to those used in the provided option handlers. The
|
||||||
|
resulting option can also have a title and tooltip assigned with a parallel OptionAttribute.
|
||||||
|
Provide a LogFloatOptionsEntry to allow logarithmic float input, which can now be instantiated
|
||||||
|
with the new dynamic option improvements using DynamicOption.
|
||||||
|
</PackageReleaseNotes>
|
||||||
|
<RootNamespace>PeterHan.PLib</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||||
|
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLib.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="icon.png" Pack="True" PackagePath="" />
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibActions/PLibActions.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibAVC/PLibAVC.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibBuildings/PLibBuildings.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibDatabase/PLibDatabase.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibLighting/PLibLighting.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibOptions/PLibOptions.csproj" PrivateAssets="all" />
|
||||||
|
<ProjectReference Include="../PLibUI/PLibUI.csproj" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
Binary file not shown.
|
@ -0,0 +1,239 @@
|
||||||
|
# PLib
|
||||||
|
|
||||||
|
PLib is Peter's library for making mods. All mods in this repository depend on PLib via [ILMerge](https://github.com/dotnet/ILMerge).
|
||||||
|
|
||||||
|
PLib 4.0 is now modular, allowing mods to only include and merge the components that they use, along with the much reduced PLib Core library.
|
||||||
|
PLib 4.0 is not backwards compatible with PLib 2.0 and 3.0, but both can be run side by side (if that was possible).
|
||||||
|
PLib 4.0 and up supports [Harmony 2.0 for the new game versions merging the vanilla game and Spaced Out! DLC](https://forums.kleientertainment.com/forums/topic/130712-oni-is-upgrading-to-harmony-20/), but PLib 3.0 and lower do not.
|
||||||
|
|
||||||
|
## Obtaining
|
||||||
|
|
||||||
|
### Source
|
||||||
|
|
||||||
|
PLib can be checked out and compiled from source (tested on Visual Studio Community 2019).
|
||||||
|
If Oxygen Not Included is installed in a different location, make a copy of `Directory.Build.Props.default` named `Directory.Build.Props.user` and update the game folder paths appropriately.
|
||||||
|
|
||||||
|
### Binary
|
||||||
|
|
||||||
|
DLL releases for major versions are available in the [releases](https://github.com/peterhaneve/ONIMods/releases) page.
|
||||||
|
**Only the complete library is available from NuGet, which is sufficient for most users.**
|
||||||
|
The lightweight PLib Options library (which includes only the options portion of PLib, and does not perform any forwarding) is also available in the releases section.
|
||||||
|
|
||||||
|
### NuGet
|
||||||
|
|
||||||
|
PLib is available as a [NuGet package](https://www.nuget.org/packages/PLib/).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
PLib must be included with mods that depend on it.
|
||||||
|
The best method to do this is to use ILMerge or ILRepack and add the PLib project or DLL as a reference in the mod project.
|
||||||
|
ILMerge is available as a [NuGet package](https://www.nuget.org/packages/ilmerge) and is best used as a post-build command.
|
||||||
|
Suggested command:
|
||||||
|
```powershell
|
||||||
|
"$(ILMergeConsolePath)" /ndebug /out:$(TargetName)Merged.dll $(TargetName).dll PLib.dll /targetplatform:v4,C:\Windows\Microsoft.NET\Framework64\v4.0.30319
|
||||||
|
```
|
||||||
|
|
||||||
|
This helps ensure that each mod uses the version of PLib that it was built against, reducing the risk of breakage due to PLib changes.
|
||||||
|
Note that if using ILMerge, all dependencies of your mod *and PLib* must be added as references to the project.
|
||||||
|
Avoid merging Unity assemblies with the compiled DLL, and turn off *Copy Local* on all other library DLLs such as `0Harmony` and `Assembly-CSharp`.
|
||||||
|
PLib can also be packaged separately as a DLL with an individual mod, but on Mac and Linux this may lead to version issues.
|
||||||
|
|
||||||
|
Some parts of PLib need to be patched only once, or rely on having the latest version.
|
||||||
|
To handle this problem, PLib uses *forwarded components*, which only loads the components and patches that are in use, using the latest version of each component available across all installed mods.
|
||||||
|
Only one instance of each registered forwarded component is instantiated, even if multiple mods have the same version, although which one is used in case of a tie is unspecified.
|
||||||
|
Custom forwarded components in mod code can be implemented by subclassing `PeterHan.PLib.Core.PForwardedComponent`.
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
Initialize PLib by calling `PUtil.InitLibrary(bool)` in `OnLoad`.
|
||||||
|
PLib *must* be initialized before using most of PLib functionality, but instantiating most PLib components will now also initialize PLib if necessary.
|
||||||
|
|
||||||
|
It will emit the mod's `AssemblyFileVersion` to the log if the `bool` parameter is true, which can aids with debugging.
|
||||||
|
Using the `AssemblyVersion` instead is discouraged, because changing `AssemblyVersion` breaks any explicit references to the assembly by name.
|
||||||
|
(Ever wonder why .NET 3.5 still uses the .NET 2.0 version string?)
|
||||||
|
|
||||||
|
## Core
|
||||||
|
|
||||||
|
The `PLib.Core` component is required by all other PLib components.
|
||||||
|
It contains only a minimal patch manager and general utilities that do not require any patches at runtime, such as Detours, reflection utilities, and basic game helpers.
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
|
||||||
|
The `PLib.UI` component is used to create custom user interfaces, in the same style as the base game.
|
||||||
|
It requires `PLib.Core`.
|
||||||
|
For more details on the classes in this component, see the XML documentation.
|
||||||
|
|
||||||
|
### Side Screens
|
||||||
|
|
||||||
|
Add a side screen class in a postfix patch on `DetailsScreen.OnPrefabInit` using `PUIUtils.AddSideScreenContent<T>()`.
|
||||||
|
The type parameter should be a custom class which extends `SideScreenContent`.
|
||||||
|
The optional argument can be used to set an existing UI `GameObject` as the UI to be displayed, either created using PLib UI or custom creation.
|
||||||
|
|
||||||
|
If the argument is `null`, create the UI in the `OnPrefabInit` of the side screen content class, and use `AddTo(gameObject, 0)` on the root PPanel to add it to the side screen content.
|
||||||
|
A reference to the UI `GameObject` created by `AddTo` should also be stored in the `ContentContainer` property of the side screen content class.
|
||||||
|
Note that `SetTarget` is called on the very first object selected *before* `OnPrefabInit` runs for the first time.
|
||||||
|
Make sure that this case is handled in code, and that `OnPrefabInit` refreshes the UI after it is built to match the current target object.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
The `PLib.Options` component provides utilities for reading and writing config files, as well as editing configs in-game via the mod menu.
|
||||||
|
It requires `PLib.UI` and `PLib.Core`.
|
||||||
|
|
||||||
|
#### Reading/writing config files
|
||||||
|
|
||||||
|
To read, use `PLib.Options.POptions.ReadSettings<T>()` where T is the type that the config file will be deserialized to.
|
||||||
|
In PLib 4.0, the type will be associated with the assembly that defines that type, not the calling assembly like PLib 2.0 and 3.0.
|
||||||
|
By default, PLib will place the config file in the mod assembly directory, named `config.json`, and will give each archived version its own configuration.
|
||||||
|
|
||||||
|
The `ConfigFile` attribute can be used to modify the name of the configuration file and enable auto-indenting to improve human readability.
|
||||||
|
If the Use
|
||||||
|
|
||||||
|
To write, use `PLib.Options.POptions.WriteSettings<T>(T settings)`, where again T is the settings type.
|
||||||
|
|
||||||
|
#### Registering for the config screen
|
||||||
|
|
||||||
|
PLib.Options adds configuration menus to the Mods screen for mods that are registered.
|
||||||
|
Register a mod by using `POptions.RegisterOptions(UserMod2, Type settingsType)` in `OnLoad`.
|
||||||
|
Creating a new `POptions` instance is required to use this method, but only one `POptions` instance should be created per mod.
|
||||||
|
|
||||||
|
The argument should be the type of the class the mod uses for its options, and must be JSON serializable.
|
||||||
|
`Newtonsoft.Json` is bundled with the game and can be referenced.
|
||||||
|
|
||||||
|
The class used for mod options can also contain a `ModInfo([string url=""], [string image=""])` annotation to display additional mod information.
|
||||||
|
**Note that the title from PLib 2.0 and 3.0 is no longer part of this attribute**, as this functionality has been moved to the Klei `mod.yaml` file.
|
||||||
|
The URL can be used to specify a custom website for the mod's home page; if left empty, it defaults to the Steam Workshop page for the mod.
|
||||||
|
The image, if specified, will attempt to load a preview image (best size is 192x192) with that name from the mod's data folder and display it in the settings dialog.
|
||||||
|
|
||||||
|
Each option must be a property, not a member, and should be annotated with `Option(string displaytext, [string tooltip=""])` to be visible in the mod config menu.
|
||||||
|
Currently supported types are: `int`, `int?`, `float`, `float?`, `string`, `bool`, `Color`, `Color32`, and `Enum`.
|
||||||
|
If a property is a read-only `System.Action`, a button will be created that will execute the returned action if clicked.
|
||||||
|
If a property is of type `LocText`, no matter what it returns, the text in `displaytext` will be displayed as a full-width label with no input field.
|
||||||
|
If a property is of a user-defined type, PLib will check the public properties of that type -- if any of them have `Option` attributes, the property will be rendered as its own category with each of the inner options grouped inside.
|
||||||
|
If a valid localization string key name is used for `displaytext` (such as `STRINGS.YOURMOD.OPTIONS.YOUROPTION`), the localized value of that string from the strings database is used as the display text.
|
||||||
|
|
||||||
|
To support types not in the predefined list, the `[DynamicOption(Type)]` attribute can be added to specify the type of an `IOptionsEntry` handler class that can display the specified type.
|
||||||
|
|
||||||
|
#### Categories
|
||||||
|
|
||||||
|
The optional third parameter of `Option` allows setting a custom category for the option to group related options together.
|
||||||
|
The category name is displayed as the title for the section.
|
||||||
|
If a valid localization string key name is used for the category (such as `STRINGS.YOURMOD.OPTIONS.YOURCATEGORY`), the localized value of that string from the strings database is used as the title.
|
||||||
|
All options inside a nested custom options class are placed under a category matching the title of the declaring `Option` property.
|
||||||
|
|
||||||
|
#### Range limits
|
||||||
|
|
||||||
|
`int`, `int?`, `float` and `float?` options can have validation in the form of a range limit.
|
||||||
|
Annotate the property with `PLib.Options.Limit(double min, double max)`.
|
||||||
|
If `PLib.Options.Limit` is used on a `string` field, the `max` will be used as the maximum string length for the option value.
|
||||||
|
Note that users can still enter values outside of the range manually in the configuration file.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PeterHan.PLib.Options;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
[ModInfo("https://www.github.com/peterhaneve/ONIMods")]
|
||||||
|
public class TestModSettings
|
||||||
|
{
|
||||||
|
[Option("Wattage", "How many watts you can use before exploding.")]
|
||||||
|
[Limit(1, 50000)]
|
||||||
|
[JsonProperty]
|
||||||
|
public float Watts { get; set; }
|
||||||
|
|
||||||
|
public TestModSettings()
|
||||||
|
{
|
||||||
|
Watts = 10000f; // defaults to 10000, e.g. if the config doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Options;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public sealed class ModLoad : KMod.UserMod2
|
||||||
|
{
|
||||||
|
public static void OnLoad()
|
||||||
|
{
|
||||||
|
PUtil.InitLibrary(false);
|
||||||
|
new POptions().RegisterOptions(typeof(TestModSettings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how it looks in the mod menu:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
The `PLib.Actions` component is used to register actions to be executed on user input.
|
||||||
|
It requires `PLib.Core`.
|
||||||
|
Actions created by this component can be rebound in the game options.
|
||||||
|
|
||||||
|
Register actions by using `PActionManager.CreateAction(string, LocString, PKeyBinding)` in `OnLoad`.
|
||||||
|
Creating a new `PActionManager` instance is required to use this method, but only one `PActionManager` instance should be created per mod.
|
||||||
|
|
||||||
|
The identifier should be unique to the action used and should include the mod name to avoid conflicts with other mods.
|
||||||
|
If multiple mods register the same action identifier, only the first will receive a valid `PAction`.
|
||||||
|
The returned `PAction` object has a `GetKAction` method which can be used to retrieve an `Action` that works in standard Klei functions.
|
||||||
|
The `PKeyBinding` is used to specify the default key binding.
|
||||||
|
|
||||||
|
Note that the game can change the values in the `Action` enum. Instead of using the built-in `Action.NumActions` to denote "no action", consider using `PAction.MaxAction` instead which will use the correct value at runtime if necessary.
|
||||||
|
|
||||||
|
## Lighting
|
||||||
|
|
||||||
|
The `PLib.Lighting` component allows `Light2D` objects to emit light in a custom shape and intensity falloff.
|
||||||
|
It requires `PLib.Core`.
|
||||||
|
|
||||||
|
Register lighting types by using `PLightManager.Register(string, CastLight)` in `OnLoad`.
|
||||||
|
Creating a new `PLightManager` instance is required to use this method, but only one `PLightManager` instance should be created per mod.
|
||||||
|
|
||||||
|
The identifier should be unique to the light pattern that will be registered.
|
||||||
|
If multiple mods register the same lighting type identifier, all will receive a valid `ILightShape` object but only the first mod to register that shape will be used to render it.
|
||||||
|
The returned `ILightShape` object has a `GetKLightShape` method which can be used to retrieve a `LightShape` that works in standard `Light2D` functions.
|
||||||
|
The `CastLight` specifies a callback in the mod that can handle drawing the light. It needs the signature
|
||||||
|
```c
|
||||||
|
void CastLight(LightingArgs args);
|
||||||
|
```
|
||||||
|
The mod will receive an object encapsulating the lighting arguments, including the source of the light with the `Light2D` component and the starting cell.
|
||||||
|
See the `LightingArgs` class for more details.
|
||||||
|
|
||||||
|
## Buildings
|
||||||
|
|
||||||
|
The `PLib.Buildings` component abstracts several details of adding new buildings.
|
||||||
|
It requires `PLib.Core`.
|
||||||
|
|
||||||
|
Register a new building by using `PBuildingManager.Register(PBuilding)` in `OnLoad`.
|
||||||
|
Creating a new `PBuildingManager` instance is required to use this method, but only one `PBuildingManager` instance should be created per mod.
|
||||||
|
|
||||||
|
The `PBuilding` instance should be created only once, in `OnLoad`.
|
||||||
|
The building name, description, and effect can all be specified directly, or left empty to use the default localization string keys for each string.
|
||||||
|
The tech tree location and build menu location can also be specified without needing other patches.
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The `PLib.Database` component deals with non-building operations involving the game database, translation, and codex files.
|
||||||
|
It requires `PLib.Core`.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
Register a mod for translation by using `PLocalization.Register()` in `OnLoad`.
|
||||||
|
Creating a new `PLocalization` instance is required to use this method, but only one `PLocalization` instance should be created per mod.
|
||||||
|
|
||||||
|
All classes in the mod assembly with `public static` `LocString` fields will be eligible for translation.
|
||||||
|
Translation files need to be placed in the `translations` folder in the mod directory, named as the target language code (*zh-CN* for example) and ending with the `.po` extension.
|
||||||
|
Note that the translation only occurs after all mods load, so avoid referencing the `LocString` fields during class initialization or `OnLoad` as they may not yet be localized at that time.
|
||||||
|
|
||||||
|
### Codex Entries
|
||||||
|
|
||||||
|
Register a mod for codex loading by using `PCodexManager.RegisterCreatures()` and/or `PCodexManager.RegisterPlants()`.
|
||||||
|
Creating a new `PCodexManager` instance is required to use this method, but only one `PCodexManager` instance should be created per mod.
|
||||||
|
|
||||||
|
The codex files will be loaded using the same structure as the base game: a `codex` folder must exist in the mod directory, with `Creatures` and `Plants` subfolders containing the codex data.
|
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by classes which can check the current mod version and detect if it is out
|
||||||
|
/// of date.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModVersionChecker {
|
||||||
|
/// <summary>
|
||||||
|
/// The event to subscribe for when the check completes.
|
||||||
|
/// </summary>
|
||||||
|
event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the mod and reports if it is out of date. The mod's current version as
|
||||||
|
/// reported by its mod_info.yaml file is available on the packagedModInfo member.
|
||||||
|
///
|
||||||
|
/// This method might not be run on the foreground thread. Do not create new behaviors
|
||||||
|
/// or components without a coroutine to an existing GameObject.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mod">The mod whose version is being checked.</param>
|
||||||
|
/// <returns>true if the version check has started, or false if it could not be
|
||||||
|
/// started, which will trigger the next version checker in line.</returns>
|
||||||
|
bool CheckVersion(KMod.Mod mod);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using KMod;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the mod version using a URL to a JSON file. The file at this URL must resolve
|
||||||
|
/// to a JSON file which can deserialize to the JsonURLVersionChecker.ModVersions class.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class JsonURLVersionChecker : IModVersionChecker {
|
||||||
|
/// <summary>
|
||||||
|
/// The timeout in seconds for the web request before declaring the check as failed.
|
||||||
|
/// </summary>
|
||||||
|
public const int REQUEST_TIMEOUT = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL to query for checking the mod version.
|
||||||
|
/// </summary>
|
||||||
|
public string JsonVersionURL { get; }
|
||||||
|
|
||||||
|
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
|
||||||
|
|
||||||
|
public JsonURLVersionChecker(string url) {
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
throw new ArgumentNullException(nameof(url));
|
||||||
|
JsonVersionURL = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckVersion(Mod mod) {
|
||||||
|
if (mod == null)
|
||||||
|
throw new ArgumentNullException(nameof(mod));
|
||||||
|
var request = UnityWebRequest.Get(JsonVersionURL);
|
||||||
|
request.SetRequestHeader("Content-Type", "application/json");
|
||||||
|
request.SetRequestHeader("User-Agent", "PLib AVC");
|
||||||
|
request.timeout = REQUEST_TIMEOUT;
|
||||||
|
var operation = request.SendWebRequest();
|
||||||
|
operation.completed += (_) => OnRequestFinished(request, mod);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a web request completes, triggers the handler for the next updater.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The JSON web request data.</param>
|
||||||
|
/// <param name="mod">The mod that needs to be checked.</param>
|
||||||
|
private void OnRequestFinished(UnityWebRequest request, Mod mod) {
|
||||||
|
ModVersionCheckResults result = null;
|
||||||
|
if (request.result == UnityWebRequest.Result.Success) {
|
||||||
|
// Parse the text
|
||||||
|
ModVersions versions;
|
||||||
|
using (var reader = new StreamReader(new MemoryStream(request.
|
||||||
|
downloadHandler.data))) {
|
||||||
|
versions = new JsonSerializer() {
|
||||||
|
MaxDepth = 4, DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||||
|
}.Deserialize<ModVersions>(new JsonTextReader(reader));
|
||||||
|
}
|
||||||
|
if (versions != null)
|
||||||
|
result = ParseModVersion(mod, versions);
|
||||||
|
}
|
||||||
|
request.Dispose();
|
||||||
|
OnVersionCheckCompleted?.Invoke(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the JSON file and looks up the version for the specified mod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mod">The mod's static ID.</param>
|
||||||
|
/// <param name="versions">The data from the web JSON file.</param>
|
||||||
|
/// <returns>The results of the update, or null if the mod could not be found in the
|
||||||
|
/// JSON.</returns>
|
||||||
|
private ModVersionCheckResults ParseModVersion(Mod mod, ModVersions versions) {
|
||||||
|
ModVersionCheckResults result = null;
|
||||||
|
string id = mod.staticID;
|
||||||
|
if (versions.mods != null)
|
||||||
|
foreach (var modVersion in versions.mods)
|
||||||
|
if (modVersion != null && modVersion.staticID == id) {
|
||||||
|
string newVersion = modVersion.version?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(newVersion))
|
||||||
|
result = new ModVersionCheckResults(id, true);
|
||||||
|
else
|
||||||
|
result = new ModVersionCheckResults(id, newVersion !=
|
||||||
|
PVersionCheck.GetCurrentVersion(mod), newVersion);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serialization type for JSONURLVersionChecker. Allows multiple mods to query
|
||||||
|
/// the same URL.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public sealed class ModVersions {
|
||||||
|
[JsonProperty]
|
||||||
|
public List<ModVersion> mods;
|
||||||
|
|
||||||
|
public ModVersions() {
|
||||||
|
mods = new List<ModVersion>(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the current version of each mod.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ModVersion {
|
||||||
|
/// <summary>
|
||||||
|
/// The mod's static ID, as reported by its mod.yaml. If a mod does not specify its
|
||||||
|
/// static ID, it gets the default ID mod.label.id + "_" + mod.label.
|
||||||
|
/// distribution_platform.
|
||||||
|
/// </summary>
|
||||||
|
public string staticID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod's current version.
|
||||||
|
/// </summary>
|
||||||
|
public string version { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "{0}: version={1}".F(staticID, version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// The results of checking the mod version.
|
||||||
|
/// </summary>
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public sealed class ModVersionCheckResults {
|
||||||
|
/// <summary>
|
||||||
|
/// true if the mod is up to date, or false if it is out of date.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty]
|
||||||
|
public bool IsUpToDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod whose version was queried. The current mod version is available on this
|
||||||
|
/// mod through its packagedModInfo.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty]
|
||||||
|
public string ModChecked { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new version of this mod. If it is not available, it can be null, even if
|
||||||
|
/// IsUpdated is false. Not relevant if IsUpToDate reports true.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty]
|
||||||
|
public string NewVersion { get; set; }
|
||||||
|
|
||||||
|
public ModVersionCheckResults() : this("", false) { }
|
||||||
|
|
||||||
|
public ModVersionCheckResults(string id, bool updated, string newVersion = null) {
|
||||||
|
IsUpToDate = updated;
|
||||||
|
ModChecked = id;
|
||||||
|
NewVersion = newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is ModVersionCheckResults other && other.ModChecked == ModChecked &&
|
||||||
|
IsUpToDate == other.IsUpToDate && NewVersion == other.NewVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return ModChecked.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "ModVersionCheckResults[{0},updated={1},newVersion={2}]".F(
|
||||||
|
ModChecked, IsUpToDate, NewVersion ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Automatic Version Check</Title>
|
||||||
|
<AssemblyTitle>PLib.AVC</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.AVC</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibAVC.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||||
|
<ProjectReference Include="../PLibUI/PLibUI.csproj" />
|
||||||
|
<Reference Include="UnityEngine.UnityWebRequestModule" HintPath="$(GameFolderActive)/UnityEngine.UnityWebRequestModule.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,340 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Implements a basic automatic version check, using either Steam or an external website.
|
||||||
|
///
|
||||||
|
/// The version of the current mod is taken from the mod version attribute of the provided
|
||||||
|
/// mod.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PVersionCheck : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type used when a background version check completes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The results of the check. If null, the check has failed,
|
||||||
|
/// and the next version should be tried.</param>
|
||||||
|
public delegate void OnVersionCheckComplete(ModVersionCheckResults result);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PVersionCheck Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reported version of the specified assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to check.</param>
|
||||||
|
/// <returns>The assembly's file version, or if that is unset, its assembly version.</returns>
|
||||||
|
private static string GetCurrentVersion(Assembly assembly) {
|
||||||
|
string version = null;
|
||||||
|
if (assembly != null) {
|
||||||
|
version = assembly.GetFileVersion();
|
||||||
|
if (string.IsNullOrEmpty(version))
|
||||||
|
version = assembly.GetName()?.Version?.ToString();
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current version of the mod. If the version is specified in mod_info.yaml,
|
||||||
|
/// that version is reported. Otherwise, the assembly file version (and failing that,
|
||||||
|
/// the assembly version) of the assembly defining the mod's first UserMod2 instance
|
||||||
|
/// is reported.
|
||||||
|
///
|
||||||
|
/// This method will only work after mods have loaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mod">The mod to check.</param>
|
||||||
|
/// <returns>The current version of that mod.</returns>
|
||||||
|
public static string GetCurrentVersion(KMod.Mod mod) {
|
||||||
|
if (mod == null)
|
||||||
|
throw new ArgumentNullException(nameof(mod));
|
||||||
|
string version = mod.packagedModInfo?.version;
|
||||||
|
if (string.IsNullOrEmpty(version)) {
|
||||||
|
// Does it have UM2 instances?
|
||||||
|
var instances = mod.loaded_mod_data?.userMod2Instances;
|
||||||
|
var dlls = mod.loaded_mod_data?.dlls;
|
||||||
|
if (instances != null)
|
||||||
|
// Use first UserMod2
|
||||||
|
foreach (var um2 in instances) {
|
||||||
|
version = GetCurrentVersion(um2.Key);
|
||||||
|
if (!string.IsNullOrEmpty(version))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (dlls != null && dlls.Count > 0)
|
||||||
|
// Use first DLL
|
||||||
|
foreach (var assembly in dlls) {
|
||||||
|
version = GetCurrentVersion(assembly);
|
||||||
|
if (!string.IsNullOrEmpty(version))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// All methods of determining the version have failed
|
||||||
|
version = "";
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MainMenu_OnSpawn_Postfix() {
|
||||||
|
Instance?.RunVersionCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ModsScreen_BuildDisplay_Postfix(System.Collections.IEnumerable
|
||||||
|
___displayedMods) {
|
||||||
|
// Must cast the type because ModsScreen.DisplayedMod is private
|
||||||
|
if (Instance != null && ___displayedMods != null)
|
||||||
|
foreach (var modEntry in ___displayedMods)
|
||||||
|
Instance.AddWarningIfOutdated(modEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mods whose version will be checked.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IDictionary<string, VersionCheckMethods> checkVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The location where the outcome of mod version checking will be stored.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, ModVersionCheckResults> results;
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
public PVersionCheck() {
|
||||||
|
checkVersions = new Dictionary<string, VersionCheckMethods>(8);
|
||||||
|
results = new ConcurrentDictionary<string, ModVersionCheckResults>(2, 16);
|
||||||
|
InstanceData = results.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a warning to the mods screen if a mod is outdated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modEntry">The mod entry to modify.</param>
|
||||||
|
private void AddWarningIfOutdated(object modEntry) {
|
||||||
|
int index = -1;
|
||||||
|
var type = modEntry.GetType();
|
||||||
|
var indexVal = type.GetFieldSafe("mod_index", false)?.GetValue(modEntry);
|
||||||
|
if (indexVal is int intVal)
|
||||||
|
index = intVal;
|
||||||
|
var rowInstance = type.GetFieldSafe("rect_transform", false)?.GetValue(
|
||||||
|
modEntry) as RectTransform;
|
||||||
|
var mods = Global.Instance.modManager?.mods;
|
||||||
|
string id;
|
||||||
|
if (rowInstance != null && mods != null && index >= 0 && index < mods.Count &&
|
||||||
|
!string.IsNullOrEmpty(id = mods[index]?.staticID) && rowInstance.
|
||||||
|
TryGetComponent(out HierarchyReferences hr) && results.TryGetValue(id,
|
||||||
|
out ModVersionCheckResults data) && data != null)
|
||||||
|
// Version text is thankfully known, even if other mods have added buttons
|
||||||
|
AddWarningIfOutdated(data, hr.GetReference<LocText>("Version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a warning to a mod version label if it is outdated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The updated mod version.</param>
|
||||||
|
/// <param name="versionText">The current mod version label.</param>
|
||||||
|
private void AddWarningIfOutdated(ModVersionCheckResults data, LocText versionText) {
|
||||||
|
GameObject go;
|
||||||
|
if (versionText != null && (go = versionText.gameObject) != null && !data.
|
||||||
|
IsUpToDate) {
|
||||||
|
string text = versionText.text;
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
text = PLibStrings.OUTDATED_WARNING;
|
||||||
|
else
|
||||||
|
text = text + " " + PLibStrings.OUTDATED_WARNING;
|
||||||
|
versionText.text = text;
|
||||||
|
go.AddOrGet<ToolTip>().toolTip = string.Format(PLibStrings.OUTDATED_TOOLTIP,
|
||||||
|
data.NewVersion ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
plibInstance.Patch(typeof(MainMenu), "OnSpawn", postfix: PatchMethod(nameof(
|
||||||
|
MainMenu_OnSpawn_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(ModsScreen), "BuildDisplay", postfix: PatchMethod(
|
||||||
|
nameof(ModsScreen_BuildDisplay_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object args) {
|
||||||
|
if (operation == 0 && args is System.Action runNext) {
|
||||||
|
VersionCheckTask first = null, previous = null;
|
||||||
|
results.Clear();
|
||||||
|
foreach (var pair in checkVersions) {
|
||||||
|
string staticID = pair.Key;
|
||||||
|
var mod = pair.Value;
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Checking version for mod {0}".F(staticID));
|
||||||
|
#endif
|
||||||
|
foreach (var checker in mod.Methods) {
|
||||||
|
var node = new VersionCheckTask(mod.ModToCheck, checker, results) {
|
||||||
|
Next = runNext
|
||||||
|
};
|
||||||
|
if (previous != null)
|
||||||
|
previous.Next = runNext;
|
||||||
|
if (first == null)
|
||||||
|
first = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first?.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the specified mod for automatic version checking. Mods will be registered
|
||||||
|
/// using their static ID, so to avoid the default ID from being used instead, set this
|
||||||
|
/// attribute in mod.yaml.
|
||||||
|
///
|
||||||
|
/// The same mod can be registered multiple times with different methods to check the
|
||||||
|
/// mod versions. The methods will be attempted in order from first registered to last.
|
||||||
|
/// However, the same mod must not be registered multiple times in different instances
|
||||||
|
/// of PVersionCheck.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mod">The mod instance to check.</param>
|
||||||
|
/// <param name="checker">The method to use for checking the mod version.</param>
|
||||||
|
public void Register(KMod.UserMod2 mod, IModVersionChecker checker) {
|
||||||
|
var kmod = mod?.mod;
|
||||||
|
if (kmod == null)
|
||||||
|
throw new ArgumentNullException(nameof(mod));
|
||||||
|
if (checker == null)
|
||||||
|
throw new ArgumentNullException(nameof(checker));
|
||||||
|
RegisterForForwarding();
|
||||||
|
string staticID = kmod.staticID;
|
||||||
|
if (!checkVersions.TryGetValue(staticID, out VersionCheckMethods checkers))
|
||||||
|
checkVersions.Add(staticID, checkers = new VersionCheckMethods(kmod));
|
||||||
|
checkers.Methods.Add(checker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports the results of the version check.
|
||||||
|
/// </summary>
|
||||||
|
private void ReportResults() {
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
results.Clear();
|
||||||
|
if (allMods != null)
|
||||||
|
// Consolidate them through JSON roundtrip into results dictionary
|
||||||
|
foreach (var mod in allMods) {
|
||||||
|
var modResults = mod.GetInstanceDataSerialized<ICollection<
|
||||||
|
ModVersionCheckResults>>();
|
||||||
|
if (modResults != null)
|
||||||
|
foreach (var result in modResults)
|
||||||
|
results.TryAdd(result.ModChecked, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the automatic version check for all mods.
|
||||||
|
/// </summary>
|
||||||
|
internal void RunVersionCheck() {
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
// See if Mod Updater triggered master disable
|
||||||
|
if (!PRegistry.GetData<bool>("PLib.VersionCheck.ModUpdaterActive") &&
|
||||||
|
allMods != null)
|
||||||
|
new AllVersionCheckTask(allMods, this).Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks each mod's version one at a time to avoid saturating the network with
|
||||||
|
/// generally nonessential traffic (in the case of yaml/json checkers).
|
||||||
|
/// </summary>
|
||||||
|
private sealed class AllVersionCheckTask {
|
||||||
|
/// <summary>
|
||||||
|
/// A list of actions that will check each version in turn.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IList<PForwardedComponent> checkAllVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current location in the list.
|
||||||
|
/// </summary>
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles version check result reporting when complete.
|
||||||
|
/// </summary>
|
||||||
|
private readonly PVersionCheck parent;
|
||||||
|
|
||||||
|
internal AllVersionCheckTask(IEnumerable<PForwardedComponent> allMods,
|
||||||
|
PVersionCheck parent) {
|
||||||
|
if (allMods == null)
|
||||||
|
throw new ArgumentNullException(nameof(allMods));
|
||||||
|
checkAllVersions = new List<PForwardedComponent>(allMods);
|
||||||
|
index = 0;
|
||||||
|
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs all checks and fires the callback when complete.
|
||||||
|
/// </summary>
|
||||||
|
internal void Run() {
|
||||||
|
int n = checkAllVersions.Count;
|
||||||
|
if (index >= n)
|
||||||
|
parent.ReportResults();
|
||||||
|
while (index < n) {
|
||||||
|
var doCheck = checkAllVersions[index++];
|
||||||
|
if (doCheck != null) {
|
||||||
|
doCheck.Process(0, new System.Action(Run));
|
||||||
|
break;
|
||||||
|
} else if (index >= n)
|
||||||
|
parent.ReportResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "AllVersionCheckTask for {0:D} mods".F(checkAllVersions.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A placeholder class which stores all methods used to check a single mod.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class VersionCheckMethods {
|
||||||
|
/// <summary>
|
||||||
|
/// The methods which will be used to check.
|
||||||
|
/// </summary>
|
||||||
|
internal IList<IModVersionChecker> Methods { get; }
|
||||||
|
|
||||||
|
// <summary>
|
||||||
|
/// The mod whose version will be checked.
|
||||||
|
/// </summary>
|
||||||
|
internal KMod.Mod ModToCheck { get; }
|
||||||
|
|
||||||
|
internal VersionCheckMethods(KMod.Mod mod) {
|
||||||
|
Methods = new List<IModVersionChecker>(8);
|
||||||
|
ModToCheck = mod ?? throw new ArgumentNullException(nameof(mod));
|
||||||
|
PUtil.LogDebug("Registered mod ID {0} for automatic version checking".F(
|
||||||
|
ModToCheck.staticID));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return ModToCheck.staticID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using KMod;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Checks Steam to see if mods are out of date.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SteamVersionChecker : IModVersionChecker {
|
||||||
|
/// <summary>
|
||||||
|
/// A reference to the PublishedFileId_t type, or null if running on the EGS/WeGame
|
||||||
|
/// version.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Type PUBLISHED_FILE_ID = PPatchTools.GetTypeSafe(
|
||||||
|
"Steamworks.PublishedFileId_t");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A reference to the SteamUGC type, or null if running on the EGS/WeGame version.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Type STEAM_UGC = PPatchTools.GetTypeSafe(
|
||||||
|
"Steamworks.SteamUGC");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A reference to the game's version of SteamUGCService, or null if running on the
|
||||||
|
/// EGS/WeGame version.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Type STEAM_UGC_SERVICE = PPatchTools.GetTypeSafe(
|
||||||
|
nameof(SteamUGCService), "Assembly-CSharp");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detours requires knowing the types at compile time, which might not be available,
|
||||||
|
/// and these methods are only called once at startup.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly MethodInfo FIND_MOD = STEAM_UGC_SERVICE?.GetMethodSafe(
|
||||||
|
nameof(SteamUGCService.FindMod), false, PUBLISHED_FILE_ID);
|
||||||
|
|
||||||
|
private static readonly MethodInfo GET_ITEM_INSTALL_INFO = STEAM_UGC?.GetMethodSafe(
|
||||||
|
"GetItemInstallInfo", true, PUBLISHED_FILE_ID, typeof(ulong).
|
||||||
|
MakeByRefType(), typeof(string).MakeByRefType(), typeof(uint), typeof(uint).
|
||||||
|
MakeByRefType());
|
||||||
|
|
||||||
|
private static readonly ConstructorInfo NEW_PUBLISHED_FILE_ID = PUBLISHED_FILE_ID?.
|
||||||
|
GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Instance, null,
|
||||||
|
new Type[] { typeof(ulong) }, null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of minutes allowed before a mod is considered out of date.
|
||||||
|
/// </summary>
|
||||||
|
public const double UPDATE_JITTER = 10.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The epoch time for Steam time stamps.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly System.DateTime UNIX_EPOCH = new System.DateTime(1970, 1, 1,
|
||||||
|
0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last modified date of a mod's local files. The time is returned in UTC.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The mod ID to check.</param>
|
||||||
|
/// <returns>The date and time of its last modification.</returns>
|
||||||
|
private static System.DateTime GetLocalLastModified(ulong id) {
|
||||||
|
var result = System.DateTime.UtcNow;
|
||||||
|
// Create a published file object, leave it boxed
|
||||||
|
if (GET_ITEM_INSTALL_INFO != null) {
|
||||||
|
// 260 = MAX_PATH
|
||||||
|
var methodArgs = new object[] {
|
||||||
|
NEW_PUBLISHED_FILE_ID.Invoke(new object[] { id }), 0UL, "", 260U, 0U
|
||||||
|
};
|
||||||
|
if (GET_ITEM_INSTALL_INFO.Invoke(null, methodArgs) is bool success &&
|
||||||
|
success && methodArgs.Length == 5 && methodArgs[4] is uint timestamp &&
|
||||||
|
timestamp > 0U)
|
||||||
|
result = UnixEpochToDateTime(timestamp);
|
||||||
|
else
|
||||||
|
PUtil.LogDebug("Unable to determine last modified date for: " + id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a time from Steam (seconds since Unix epoch) to a C# DateTime.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeSeconds">The timestamp since the epoch.</param>
|
||||||
|
/// <returns>The UTC date and time that it represents.</returns>
|
||||||
|
public static System.DateTime UnixEpochToDateTime(ulong timeSeconds) {
|
||||||
|
return UNIX_EPOCH.AddSeconds(timeSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
|
||||||
|
|
||||||
|
public bool CheckVersion(Mod mod) {
|
||||||
|
// Epic editions of the game do not even have SteamUGCService
|
||||||
|
return FIND_MOD != null && NEW_PUBLISHED_FILE_ID != null && DoCheckVersion(mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the mod on Steam and reports if it is out of date. This helper method
|
||||||
|
/// avoids a type load error if a non-Steam version of the game is used to load this
|
||||||
|
/// mod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mod">The mod whose version is being checked.</param>
|
||||||
|
/// <returns>true if the version check has started, or false if it could not be
|
||||||
|
/// started.</returns>
|
||||||
|
private bool DoCheckVersion(Mod mod) {
|
||||||
|
bool check = false;
|
||||||
|
if (mod.label.distribution_platform == Label.DistributionPlatform.Steam && ulong.
|
||||||
|
TryParse(mod.label.id, out ulong id)) {
|
||||||
|
// Jump into a coroutine and wait for it to be initialized
|
||||||
|
Global.Instance.StartCoroutine(WaitForSteamInit(id, mod));
|
||||||
|
check = true;
|
||||||
|
} else
|
||||||
|
PUtil.LogWarning("SteamVersionChecker cannot check version for non-Steam mod {0}".
|
||||||
|
F(mod.staticID));
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// To avoid blowing the stack, waits for Steam to initialize in a coroutine.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The Steam file ID of the mod.</param>
|
||||||
|
/// <param name="mod">The mod to check for updates.</param>
|
||||||
|
private System.Collections.IEnumerator WaitForSteamInit(ulong id, Mod mod) {
|
||||||
|
var boxedID = NEW_PUBLISHED_FILE_ID.Invoke(new object[] { id });
|
||||||
|
ModVersionCheckResults results = null;
|
||||||
|
int timeout = 0;
|
||||||
|
do {
|
||||||
|
yield return null;
|
||||||
|
var inst = SteamUGCService.Instance;
|
||||||
|
// Mod takes time to be populated in the list
|
||||||
|
if (inst != null && FIND_MOD.Invoke(inst, new object[] { boxedID }) is
|
||||||
|
SteamUGCService.Mod steamMod) {
|
||||||
|
ulong ticks = steamMod.lastUpdateTime;
|
||||||
|
var steamUpdate = (ticks == 0U) ? System.DateTime.MinValue :
|
||||||
|
UnixEpochToDateTime(ticks);
|
||||||
|
bool updated = steamUpdate <= GetLocalLastModified(id).AddMinutes(
|
||||||
|
UPDATE_JITTER);
|
||||||
|
results = new ModVersionCheckResults(mod.staticID,
|
||||||
|
updated, updated ? null : steamUpdate.ToString("f"));
|
||||||
|
}
|
||||||
|
// 2 seconds at 60 FPS
|
||||||
|
} while (results == null && ++timeout < 120);
|
||||||
|
if (results == null)
|
||||||
|
PUtil.LogWarning("Unable to check version for mod {0} (SteamUGCService timeout)".
|
||||||
|
F(mod.label.title));
|
||||||
|
OnVersionCheckCompleted?.Invoke(results);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a "task" to check a particular mod for updates.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class VersionCheckTask {
|
||||||
|
/// <summary>
|
||||||
|
/// The method which will be used to check.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IModVersionChecker method;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod whose version will be checked.
|
||||||
|
/// </summary>
|
||||||
|
private readonly KMod.Mod mod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next task to run when the check completes, or null to not run any task.
|
||||||
|
/// </summary>
|
||||||
|
internal System.Action Next { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The location where the outcome of mod version checking will be stored.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, ModVersionCheckResults> results;
|
||||||
|
|
||||||
|
internal VersionCheckTask(KMod.Mod mod, IModVersionChecker method,
|
||||||
|
ConcurrentDictionary<string, ModVersionCheckResults> results) {
|
||||||
|
this.mod = mod ?? throw new ArgumentNullException(nameof(mod));
|
||||||
|
this.method = method ?? throw new ArgumentNullException(nameof(method));
|
||||||
|
this.results = results ?? throw new ArgumentNullException(nameof(results));
|
||||||
|
Next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the result of the mod version check, and runs the next checker in
|
||||||
|
/// line, from this mod or a different one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The results from the version check.</param>
|
||||||
|
private void OnComplete(ModVersionCheckResults result) {
|
||||||
|
method.OnVersionCheckCompleted -= OnComplete;
|
||||||
|
if (result != null) {
|
||||||
|
results.TryAdd(result.ModChecked, result);
|
||||||
|
if (!result.IsUpToDate)
|
||||||
|
PUtil.LogWarning("Mod {0} is out of date! New version: {1}".F(result.
|
||||||
|
ModChecked, result.NewVersion ?? "unknown"));
|
||||||
|
else {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Mod {0} is up to date".F(result.ModChecked));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
RunNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the version check, and registers a callback to run the next one if
|
||||||
|
/// it is not null.
|
||||||
|
/// </summary>
|
||||||
|
internal void Run() {
|
||||||
|
if (results.ContainsKey(mod.staticID))
|
||||||
|
RunNext();
|
||||||
|
else {
|
||||||
|
bool run = false;
|
||||||
|
method.OnVersionCheckCompleted += OnComplete;
|
||||||
|
// Version check errors should not crash the game
|
||||||
|
try {
|
||||||
|
run = method.CheckVersion(mod);
|
||||||
|
} catch (Exception e) {
|
||||||
|
PUtil.LogWarning("Unable to check version for mod " + mod.label.title + ":");
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
run = false;
|
||||||
|
}
|
||||||
|
if (!run) {
|
||||||
|
method.OnVersionCheckCompleted -= OnComplete;
|
||||||
|
RunNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the next version check.
|
||||||
|
/// </summary>
|
||||||
|
private void RunNext() {
|
||||||
|
Next?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using Klei;
|
||||||
|
using KMod;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.AVC {
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the mod version using a URL to a YAML file. The file at this URL must resolve
|
||||||
|
/// to a YAML file of the same format as the mod_info.yaml class.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class YamlURLVersionChecker : IModVersionChecker {
|
||||||
|
/// <summary>
|
||||||
|
/// The URL to query for checking the mod version.
|
||||||
|
/// </summary>
|
||||||
|
public string YamlVersionURL { get; }
|
||||||
|
|
||||||
|
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
|
||||||
|
|
||||||
|
public YamlURLVersionChecker(string url) {
|
||||||
|
if (string.IsNullOrEmpty(url))
|
||||||
|
throw new ArgumentNullException(nameof(url));
|
||||||
|
YamlVersionURL = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckVersion(Mod mod) {
|
||||||
|
if (mod == null)
|
||||||
|
throw new ArgumentNullException(nameof(mod));
|
||||||
|
var request = UnityWebRequest.Get(YamlVersionURL);
|
||||||
|
request.SetRequestHeader("Content-Type", "application/x-yaml");
|
||||||
|
request.SetRequestHeader("User-Agent", "PLib AVC");
|
||||||
|
request.timeout = JsonURLVersionChecker.REQUEST_TIMEOUT;
|
||||||
|
var operation = request.SendWebRequest();
|
||||||
|
operation.completed += (_) => OnRequestFinished(request, mod);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When a web request completes, triggers the handler for the next updater.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The YAML web request data.</param>
|
||||||
|
/// <param name="mod">The mod that needs to be checked.</param>
|
||||||
|
private void OnRequestFinished(UnityWebRequest request, Mod mod) {
|
||||||
|
ModVersionCheckResults result = null;
|
||||||
|
if (request.result == UnityWebRequest.Result.Success) {
|
||||||
|
// Parse the text
|
||||||
|
var modInfo = YamlIO.Parse<Mod.PackagedModInfo>(request.downloadHandler.
|
||||||
|
text, default);
|
||||||
|
string newVersion = modInfo?.version;
|
||||||
|
if (modInfo != null && !string.IsNullOrEmpty(newVersion)) {
|
||||||
|
string curVersion = PVersionCheck.GetCurrentVersion(mod);
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Current version: {0} New YAML version: {1}".F(
|
||||||
|
curVersion, newVersion));
|
||||||
|
#endif
|
||||||
|
result = new ModVersionCheckResults(mod.staticID, newVersion !=
|
||||||
|
curVersion, newVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.Dispose();
|
||||||
|
OnVersionCheckCompleted?.Invoke(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Actions {
|
||||||
|
/// <summary>
|
||||||
|
/// An Action managed by PLib. Actions have key bindings assigned to them.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PAction {
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum action value (typically used to mean "no action") used in the currently
|
||||||
|
/// running instance of the game.
|
||||||
|
///
|
||||||
|
/// Since Action is compiled to a const int when a mod is built, any changes to the
|
||||||
|
/// Action enum will break direct references to Action.NumActions. Use this property
|
||||||
|
/// instead to always use the intended "no action" value.
|
||||||
|
/// </summary>
|
||||||
|
public static Action MaxAction { get; }
|
||||||
|
|
||||||
|
static PAction() {
|
||||||
|
if (!Enum.TryParse(nameof(Action.NumActions), out Action limit))
|
||||||
|
limit = Action.NumActions;
|
||||||
|
MaxAction = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default key binding for this action. Not necessarily the current key binding.
|
||||||
|
/// </summary>
|
||||||
|
internal PKeyBinding DefaultBinding { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action's non-localized identifier. Something like YOURMOD.CATEGORY.ACTIONNAME.
|
||||||
|
/// </summary>
|
||||||
|
public string Identifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action's ID. This ID is assigned internally upon register and used for PLib
|
||||||
|
/// indexing. Even if you somehow obtain it in your mod, it is not to be used!
|
||||||
|
/// </summary>
|
||||||
|
private readonly int id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action's title.
|
||||||
|
/// </summary>
|
||||||
|
public LocString Title { get; }
|
||||||
|
|
||||||
|
internal PAction(int id, string identifier, LocString title, PKeyBinding binding) {
|
||||||
|
if (id <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(id));
|
||||||
|
DefaultBinding = binding;
|
||||||
|
Identifier = identifier;
|
||||||
|
this.id = id;
|
||||||
|
Title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return (obj is PAction other) && other.id == id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the Klei action for this PAction.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The Klei action for use in game functions.</returns>
|
||||||
|
public Action GetKAction() {
|
||||||
|
return (Action)((int)MaxAction + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PAction[" + Identifier + "]: " + Title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,357 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Detours;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Actions {
|
||||||
|
/// <summary>
|
||||||
|
/// Manages PAction functionality which must be single instance.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PActionManager : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// Prototypes the required parameters for new BindingEntry() since they changed in
|
||||||
|
/// U39-489490.
|
||||||
|
/// </summary>
|
||||||
|
private delegate BindingEntry NewEntry(string group, GamepadButton button,
|
||||||
|
KKeyCode key_code, Modifier modifier, Action action);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The category used for all PLib keys.
|
||||||
|
/// </summary>
|
||||||
|
public const string CATEGORY = "PLib";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new BindingEntry.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly NewEntry NEW_BINDING_ENTRY = typeof(BindingEntry).
|
||||||
|
DetourConstructor<NewEntry>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PActionManager Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assigns the key bindings to each Action when they are needed.
|
||||||
|
/// </summary>
|
||||||
|
private static void AssignKeyBindings() {
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(typeof(PActionManager).FullName);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var mod in allMods)
|
||||||
|
mod.Process(0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CKeyDef_Postfix(KInputController.KeyDef __instance) {
|
||||||
|
if (Instance != null)
|
||||||
|
__instance.mActionFlags = ExtendFlags(__instance.mActionFlags,
|
||||||
|
Instance.GetMaxAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extends the action flags array to the new maximum length.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldActionFlags">The old flags array.</param>
|
||||||
|
/// <param name="newMax">The minimum length.</param>
|
||||||
|
/// <returns>The new action flags array.</returns>
|
||||||
|
internal static bool[] ExtendFlags(bool[] oldActionFlags, int newMax) {
|
||||||
|
int n = oldActionFlags.Length;
|
||||||
|
bool[] newActionFlags;
|
||||||
|
if (n < newMax) {
|
||||||
|
newActionFlags = new bool[newMax];
|
||||||
|
Array.Copy(oldActionFlags, newActionFlags, n);
|
||||||
|
} else
|
||||||
|
newActionFlags = oldActionFlags;
|
||||||
|
return newActionFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if an action is already bound to a key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentBindings">The current key bindings.</param>
|
||||||
|
/// <param name="action">The action to look up.</param>
|
||||||
|
/// <returns>true if the action already has a binding assigned, or false otherwise.</returns>
|
||||||
|
private static bool FindKeyBinding(IEnumerable<BindingEntry> currentBindings,
|
||||||
|
Action action) {
|
||||||
|
bool inBindings = false;
|
||||||
|
foreach (var entry in currentBindings)
|
||||||
|
if (entry.mAction == action) {
|
||||||
|
LogKeyBind("Action {0} already exists; assigned to KeyCode {1}".F(action,
|
||||||
|
entry.mKeyCode));
|
||||||
|
inBindings = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return inBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a Klei key binding title.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category">The category of the key binding.</param>
|
||||||
|
/// <param name="item">The key binding to retrieve.</param>
|
||||||
|
/// <returns>The Strings entry describing this key binding.</returns>
|
||||||
|
private static string GetBindingTitle(string category, string item) {
|
||||||
|
return "STRINGS.INPUT_BINDINGS." + category.ToUpperInvariant() + "." + item.
|
||||||
|
ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a "localized" (if PLib is localized) description of additional key codes
|
||||||
|
/// from the KKeyCode enumeration, to avoid warning spam on popular keybinds like
|
||||||
|
/// arrow keys, delete, home, and so forth.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">The key code.</param>
|
||||||
|
/// <returns>A description of that key code, or null if no localization is found.</returns>
|
||||||
|
internal static string GetExtraKeycodeLocalized(KKeyCode code) {
|
||||||
|
string localCode = null;
|
||||||
|
switch (code) {
|
||||||
|
case KKeyCode.Home:
|
||||||
|
localCode = PLibStrings.KEY_HOME;
|
||||||
|
break;
|
||||||
|
case KKeyCode.End:
|
||||||
|
localCode = PLibStrings.KEY_END;
|
||||||
|
break;
|
||||||
|
case KKeyCode.Delete:
|
||||||
|
localCode = PLibStrings.KEY_DELETE;
|
||||||
|
break;
|
||||||
|
case KKeyCode.PageDown:
|
||||||
|
localCode = PLibStrings.KEY_PAGEDOWN;
|
||||||
|
break;
|
||||||
|
case KKeyCode.PageUp:
|
||||||
|
localCode = PLibStrings.KEY_PAGEUP;
|
||||||
|
break;
|
||||||
|
case KKeyCode.LeftArrow:
|
||||||
|
localCode = PLibStrings.KEY_ARROWLEFT;
|
||||||
|
break;
|
||||||
|
case KKeyCode.UpArrow:
|
||||||
|
localCode = PLibStrings.KEY_ARROWUP;
|
||||||
|
break;
|
||||||
|
case KKeyCode.RightArrow:
|
||||||
|
localCode = PLibStrings.KEY_ARROWRIGHT;
|
||||||
|
break;
|
||||||
|
case KKeyCode.DownArrow:
|
||||||
|
localCode = PLibStrings.KEY_ARROWDOWN;
|
||||||
|
break;
|
||||||
|
case KKeyCode.Pause:
|
||||||
|
localCode = PLibStrings.KEY_PAUSE;
|
||||||
|
break;
|
||||||
|
case KKeyCode.SysReq:
|
||||||
|
localCode = PLibStrings.KEY_SYSRQ;
|
||||||
|
break;
|
||||||
|
case KKeyCode.Print:
|
||||||
|
localCode = PLibStrings.KEY_PRTSCREEN;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return localCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetKeycodeLocalized_Prefix(KKeyCode key_code, ref string __result) {
|
||||||
|
string newResult = GetExtraKeycodeLocalized(key_code);
|
||||||
|
if (newResult != null)
|
||||||
|
__result = newResult;
|
||||||
|
return newResult == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void IsActive_Prefix(ref bool[] ___mActionState) {
|
||||||
|
if (Instance != null)
|
||||||
|
___mActionState = ExtendFlags(___mActionState, Instance.GetMaxAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message encountered by the PLib key binding system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message.</param>
|
||||||
|
internal static void LogKeyBind(string message) {
|
||||||
|
Debug.LogFormat("[PKeyBinding] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a warning encountered by the PLib key binding system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The warning message.</param>
|
||||||
|
internal static void LogKeyBindWarning(string message) {
|
||||||
|
Debug.LogWarningFormat("[PKeyBinding] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void QueueButtonEvent_Prefix(ref bool[] ___mActionState,
|
||||||
|
KInputController.KeyDef key_def) {
|
||||||
|
if (KInputManager.isFocused && Instance != null) {
|
||||||
|
int max = Instance.GetMaxAction();
|
||||||
|
key_def.mActionFlags = ExtendFlags(key_def.mActionFlags, max);
|
||||||
|
___mActionState = ExtendFlags(___mActionState, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetDefaultKeyBindings_Postfix() {
|
||||||
|
Instance?.UpdateMaxAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queued key binds which are resolved on load.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IList<PAction> actions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum action index of any custom action registered across all mods.
|
||||||
|
/// </summary>
|
||||||
|
private int maxAction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new action manager used to create and assign custom actions. Due to the
|
||||||
|
/// timing of when the user's key bindings are loaded, all actions must be added in
|
||||||
|
/// OnLoad().
|
||||||
|
/// </summary>
|
||||||
|
public PActionManager() {
|
||||||
|
actions = new List<PAction>(8);
|
||||||
|
maxAction = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Bootstrap(Harmony plibInstance) {
|
||||||
|
// GameInputMapping.LoadBindings occurs after mods load but before the rest of the
|
||||||
|
// PLib components are initialized
|
||||||
|
plibInstance.Patch(typeof(GameInputMapping), nameof(GameInputMapping.
|
||||||
|
LoadBindings), prefix: PatchMethod(nameof(AssignKeyBindings)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a PAction with the action manager.
|
||||||
|
///
|
||||||
|
/// This call should occur after PUtil.InitLibrary() during the mod OnLoad(). If called
|
||||||
|
/// earlier, it may fail with InvalidOperationException, and if called later, the
|
||||||
|
/// user's custom key bind (if applicable) will be discarded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">The identifier for this action.</param>
|
||||||
|
/// <param name="title">The action's title. If null, the default value from
|
||||||
|
/// STRINGS.INPUT_BINDINGS.PLIB.identifier will be used instead.</param>
|
||||||
|
/// <param name="binding">The default key binding for this action. If null, no key will
|
||||||
|
/// be bound by default, but the user can set a key bind.</param>
|
||||||
|
/// <returns>The action thus registered.</returns>
|
||||||
|
public PAction CreateAction(string identifier, LocString title,
|
||||||
|
PKeyBinding binding = null) {
|
||||||
|
PAction action;
|
||||||
|
RegisterForForwarding();
|
||||||
|
int curIndex = GetSharedData(1);
|
||||||
|
action = new PAction(curIndex, identifier, title, binding);
|
||||||
|
SetSharedData(curIndex + 1);
|
||||||
|
actions.Add(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the maximum length of the Action enum, including custom actions. If no
|
||||||
|
/// actions are defined, returns NumActions - 1 since NumActions is reserved in the
|
||||||
|
/// base game.
|
||||||
|
///
|
||||||
|
/// This value will not be accurate until all mods have loaded and key binds
|
||||||
|
/// registered (AfterLayerableLoad or later such as BeforeDbInit).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The maximum length required to represent all Actions.</returns>
|
||||||
|
public int GetMaxAction() {
|
||||||
|
int nActions = (int)PAction.MaxAction;
|
||||||
|
return (maxAction > 0) ? (maxAction + nActions) : nActions - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
// GameInputMapping
|
||||||
|
plibInstance.Patch(typeof(GameInputMapping), nameof(GameInputMapping.
|
||||||
|
SetDefaultKeyBindings), postfix: PatchMethod(nameof(
|
||||||
|
SetDefaultKeyBindings_Postfix)));
|
||||||
|
|
||||||
|
// GameUtil
|
||||||
|
plibInstance.Patch(typeof(GameUtil), nameof(GameUtil.GetKeycodeLocalized),
|
||||||
|
prefix: PatchMethod(nameof(GetKeycodeLocalized_Prefix)));
|
||||||
|
|
||||||
|
// KInputController
|
||||||
|
plibInstance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] {
|
||||||
|
typeof(KKeyCode), typeof(Modifier)
|
||||||
|
}, postfix: PatchMethod(nameof(CKeyDef_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(KInputController), nameof(KInputController.IsActive),
|
||||||
|
prefix: PatchMethod(nameof(IsActive_Prefix)));
|
||||||
|
plibInstance.Patch(typeof(KInputController), nameof(KInputController.
|
||||||
|
QueueButtonEvent), prefix: PatchMethod(nameof(QueueButtonEvent_Prefix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostInitialize(Harmony plibInstance) {
|
||||||
|
// Needs to occur after localization
|
||||||
|
Strings.Add(GetBindingTitle(CATEGORY, "NAME"), PLibStrings.KEY_CATEGORY_TITLE);
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var mod in allMods)
|
||||||
|
mod.Process(1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object _) {
|
||||||
|
int n = actions.Count;
|
||||||
|
if (n > 0) {
|
||||||
|
if (operation == 0)
|
||||||
|
RegisterKeyBindings();
|
||||||
|
else if (operation == 1) {
|
||||||
|
#if DEBUG
|
||||||
|
LogKeyBind("Localizing titles for {0}".F(Assembly.GetExecutingAssembly().
|
||||||
|
GetNameSafe() ?? "?"));
|
||||||
|
#endif
|
||||||
|
foreach (var action in actions)
|
||||||
|
Strings.Add(GetBindingTitle(CATEGORY, action.GetKAction().ToString()),
|
||||||
|
action.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterKeyBindings() {
|
||||||
|
int n = actions.Count;
|
||||||
|
LogKeyBind("Registering {0:D} key bind(s) for mod {1}".F(n, Assembly.
|
||||||
|
GetExecutingAssembly().GetNameSafe() ?? "?"));
|
||||||
|
var currentBindings = new List<BindingEntry>(GameInputMapping.DefaultBindings);
|
||||||
|
foreach (var action in actions) {
|
||||||
|
var kAction = action.GetKAction();
|
||||||
|
var binding = action.DefaultBinding;
|
||||||
|
if (!FindKeyBinding(currentBindings, kAction)) {
|
||||||
|
if (binding == null)
|
||||||
|
binding = new PKeyBinding();
|
||||||
|
// This constructor changes often enough to be worth detouring
|
||||||
|
currentBindings.Add(NEW_BINDING_ENTRY.Invoke(CATEGORY, binding.
|
||||||
|
GamePadButton, binding.Key, binding.Modifiers, kAction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameInputMapping.SetDefaultKeyBindings(currentBindings.ToArray());
|
||||||
|
UpdateMaxAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the maximum action for this instance.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateMaxAction() {
|
||||||
|
maxAction = GetSharedData(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Actions {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single key binding to an action.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PKeyBinding {
|
||||||
|
/// <summary>
|
||||||
|
/// The gamepad button to bind.
|
||||||
|
/// </summary>
|
||||||
|
public GamepadButton GamePadButton { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The key code.
|
||||||
|
/// </summary>
|
||||||
|
public KKeyCode Key { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The modifier code.
|
||||||
|
/// </summary>
|
||||||
|
public Modifier Modifiers { get; set; }
|
||||||
|
|
||||||
|
public PKeyBinding(KKeyCode keyCode = KKeyCode.None, Modifier modifiers = Modifier.
|
||||||
|
None, GamepadButton gamePadButton = GamepadButton.NumButtons) {
|
||||||
|
GamePadButton = gamePadButton;
|
||||||
|
Key = keyCode;
|
||||||
|
Modifiers = modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PKeyBinding(PKeyBinding other) : this() {
|
||||||
|
if (other != null) {
|
||||||
|
GamePadButton = other.GamePadButton;
|
||||||
|
Key = other.Key;
|
||||||
|
Modifiers = other.Modifiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is PKeyBinding other && other.Key == Key && other.Modifiers ==
|
||||||
|
Modifiers && other.GamePadButton == GamePadButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
// Was auto generated by VS, please do not kill me
|
||||||
|
int hashCode = 1488379021 + GamePadButton.GetHashCode();
|
||||||
|
hashCode = hashCode * -1521134295 + Key.GetHashCode();
|
||||||
|
hashCode = hashCode * -1521134295 + Modifiers.GetHashCode();
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return Modifiers + " " + Key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Actions</Title>
|
||||||
|
<AssemblyTitle>PLib.Actions</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.Actions</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibActions.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Actions {
|
||||||
|
/// <summary>
|
||||||
|
/// A tool mode used in custom tool menus. Shown in the options in the bottom right.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PToolMode {
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up tool options in the tool parameter menu.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="menu">The menu to configure.</param>
|
||||||
|
/// <param name="options">The available modes.</param>
|
||||||
|
/// <returns>A dictionary which is updated in real time to contain the actual state of each mode.</returns>
|
||||||
|
public static IDictionary<string, ToolParameterMenu.ToggleState> PopulateMenu(
|
||||||
|
ToolParameterMenu menu, ICollection<PToolMode> options) {
|
||||||
|
if (options == null)
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
var kOpt = new Dictionary<string, ToolParameterMenu.ToggleState>(options.Count);
|
||||||
|
// Add to Klei format, yes it loses the order but it means less of a mess
|
||||||
|
foreach (var option in options) {
|
||||||
|
string key = option.Key;
|
||||||
|
if (!string.IsNullOrEmpty(option.Title))
|
||||||
|
Strings.Add("STRINGS.UI.TOOLS.FILTERLAYERS." + key, option.Title);
|
||||||
|
kOpt.Add(key, option.State);
|
||||||
|
}
|
||||||
|
menu.PopulateMenu(kOpt);
|
||||||
|
return kOpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a tool with the game. It still must be added to a tool collection to be
|
||||||
|
/// visible.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The tool type to register.</typeparam>
|
||||||
|
/// <param name="controller">The player controller which will be its parent; consider
|
||||||
|
/// using in a postfix on PlayerController.OnPrefabInit.</param>
|
||||||
|
public static void RegisterTool<T>(PlayerController controller) where T : InterfaceTool
|
||||||
|
{
|
||||||
|
if (controller == null)
|
||||||
|
throw new ArgumentNullException(nameof(controller));
|
||||||
|
// Create list so that new tool can be appended at the end
|
||||||
|
var interfaceTools = ListPool<InterfaceTool, PlayerController>.Allocate();
|
||||||
|
interfaceTools.AddRange(controller.tools);
|
||||||
|
var newTool = new UnityEngine.GameObject(typeof(T).Name);
|
||||||
|
var tool = newTool.AddComponent<T>();
|
||||||
|
// Reparent tool to the player controller, then enable/disable to load it
|
||||||
|
newTool.transform.SetParent(controller.gameObject.transform);
|
||||||
|
newTool.gameObject.SetActive(true);
|
||||||
|
newTool.gameObject.SetActive(false);
|
||||||
|
interfaceTools.Add(tool);
|
||||||
|
controller.tools = interfaceTools.ToArray();
|
||||||
|
interfaceTools.Recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unique key used to identify this mode.
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current state of this tool mode.
|
||||||
|
/// </summary>
|
||||||
|
public ToolParameterMenu.ToggleState State { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title displayed on-screen for this mode.
|
||||||
|
/// </summary>
|
||||||
|
public LocString Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new tool mode entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key which identifies this tool mode.</param>
|
||||||
|
/// <param name="title">The title to be displayed. If null, the title will be taken
|
||||||
|
/// from the default location in STRINGS.UI.TOOLS.FILTERLAYERS.</param>
|
||||||
|
/// <param name="state">The initial state, default Off.</param>
|
||||||
|
public PToolMode(string key, LocString title, ToolParameterMenu.ToggleState state =
|
||||||
|
ToolParameterMenu.ToggleState.Off) {
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
Key = key;
|
||||||
|
State = state;
|
||||||
|
Title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is PToolMode other && other.Key == Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Key.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "{0} ({1})".F(Key, Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// An ingredient to be used in a building.
|
||||||
|
/// </summary>
|
||||||
|
public class BuildIngredient {
|
||||||
|
/// <summary>
|
||||||
|
/// The material tag name.
|
||||||
|
/// </summary>
|
||||||
|
public string Material { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The quantity required in kg.
|
||||||
|
/// </summary>
|
||||||
|
public float Quantity { get; }
|
||||||
|
|
||||||
|
public BuildIngredient(string name, float quantity) {
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
if (quantity.IsNaNOrInfinity() || quantity <= 0.0f)
|
||||||
|
throw new ArgumentException(nameof(quantity));
|
||||||
|
Material = name;
|
||||||
|
Quantity = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildIngredient(string[] material, int tier) : this(material[0], tier) { }
|
||||||
|
|
||||||
|
public BuildIngredient(string material, int tier) {
|
||||||
|
// Intended for use from the MATERIALS and BUILDINGS presets
|
||||||
|
if (string.IsNullOrEmpty(material))
|
||||||
|
throw new ArgumentNullException(nameof(material));
|
||||||
|
Material = material;
|
||||||
|
float qty;
|
||||||
|
switch (tier) {
|
||||||
|
case -1:
|
||||||
|
// Tier -1: 5
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER_TINY[0];
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
// Tier 0: 25
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER0[0];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// Tier 1: 50
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER1[0];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// Tier 2: 100
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER2[0];
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// Tier 3: 200
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER3[0];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// Tier 4: 400
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER4[0];
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
// Tier 5: 800
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER5[0];
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
// Tier 6: 1200
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER6[0];
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
// Tier 7: 2000
|
||||||
|
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER7[0];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("tier must be between -1 and 7 inclusive");
|
||||||
|
}
|
||||||
|
Quantity = qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is BuildIngredient other && other.Material == Material && other.
|
||||||
|
Quantity == Quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Material.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "Material[Tag={0},Quantity={1:F0}]".F(Material, Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// A visualizer that colors cells with an overlay when a building is selected or being
|
||||||
|
/// previewed.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ColoredRangeVisualizer : KMonoBehaviour {
|
||||||
|
/// <summary>
|
||||||
|
/// The anim name to use when visualizing.
|
||||||
|
/// </summary>
|
||||||
|
private const string ANIM_NAME = "transferarmgrid_kanim";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The animations to play when the visualization is created.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashedString[] PRE_ANIMS = new HashedString[] {
|
||||||
|
"grid_pre",
|
||||||
|
"grid_loop"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The animation to play when the visualization is destroyed.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashedString POST_ANIM = "grid_pst";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The layer on which to display the visualizer.
|
||||||
|
/// </summary>
|
||||||
|
public Grid.SceneLayer Layer { get; set; }
|
||||||
|
|
||||||
|
// These components are automatically populated by KMonoBehaviour
|
||||||
|
#pragma warning disable IDE0044 // Add readonly modifier
|
||||||
|
#pragma warning disable CS0649
|
||||||
|
[MyCmpGet]
|
||||||
|
protected BuildingPreview preview;
|
||||||
|
|
||||||
|
[MyCmpGet]
|
||||||
|
protected Rotatable rotatable;
|
||||||
|
#pragma warning restore CS0649
|
||||||
|
#pragma warning restore IDE0044 // Add readonly modifier
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cells where animations are being displayed.
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<VisCellData> cells;
|
||||||
|
|
||||||
|
protected ColoredRangeVisualizer() {
|
||||||
|
cells = new HashSet<VisCellData>();
|
||||||
|
Layer = Grid.SceneLayer.FXFront;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or updates the visualizers as necessary.
|
||||||
|
/// </summary>
|
||||||
|
private void CreateVisualizers() {
|
||||||
|
var visCells = HashSetPool<VisCellData, ColoredRangeVisualizer>.Allocate();
|
||||||
|
var newCells = ListPool<VisCellData, ColoredRangeVisualizer>.Allocate();
|
||||||
|
try {
|
||||||
|
if (gameObject != null)
|
||||||
|
VisualizeCells(visCells);
|
||||||
|
// Destroy cells that are not used in the new one
|
||||||
|
foreach (var cell in cells)
|
||||||
|
if (visCells.Remove(cell))
|
||||||
|
newCells.Add(cell);
|
||||||
|
else
|
||||||
|
cell.Destroy();
|
||||||
|
// Newcomers get their controller created and added to the list
|
||||||
|
foreach (var newCell in visCells) {
|
||||||
|
newCell.CreateController(Layer);
|
||||||
|
newCells.Add(newCell);
|
||||||
|
}
|
||||||
|
// Copy back to global
|
||||||
|
cells.Clear();
|
||||||
|
foreach (var cell in newCells)
|
||||||
|
cells.Add(cell);
|
||||||
|
} finally {
|
||||||
|
visCells.Recycle();
|
||||||
|
newCells.Recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when cells are changed in the building radius.
|
||||||
|
/// </summary>
|
||||||
|
private void OnCellChange() {
|
||||||
|
CreateVisualizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnCleanUp() {
|
||||||
|
Unsubscribe((int)GameHashes.SelectObject);
|
||||||
|
if (preview != null) {
|
||||||
|
Singleton<CellChangeMonitor>.Instance.UnregisterCellChangedHandler(transform,
|
||||||
|
OnCellChange);
|
||||||
|
if (rotatable != null)
|
||||||
|
Unsubscribe((int)GameHashes.Rotated);
|
||||||
|
}
|
||||||
|
RemoveVisualizers();
|
||||||
|
base.OnCleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the object is rotated.
|
||||||
|
/// </summary>
|
||||||
|
private void OnRotated(object _) {
|
||||||
|
CreateVisualizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the object is selected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">true if selected, or false if deselected.</param>
|
||||||
|
private void OnSelect(object data) {
|
||||||
|
if (data is bool selected) {
|
||||||
|
var position = transform.position;
|
||||||
|
// Play the appropriate sound and update the visualizers
|
||||||
|
if (selected) {
|
||||||
|
PGameUtils.PlaySound("RadialGrid_form", position);
|
||||||
|
CreateVisualizers();
|
||||||
|
} else {
|
||||||
|
PGameUtils.PlaySound("RadialGrid_disappear", position);
|
||||||
|
RemoveVisualizers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSpawn() {
|
||||||
|
base.OnSpawn();
|
||||||
|
Subscribe((int)GameHashes.SelectObject, OnSelect);
|
||||||
|
if (preview != null) {
|
||||||
|
// Previews can be moved
|
||||||
|
Singleton<CellChangeMonitor>.Instance.RegisterCellChangedHandler(transform,
|
||||||
|
OnCellChange, nameof(ColoredRangeVisualizer) + ".OnSpawn");
|
||||||
|
if (rotatable != null)
|
||||||
|
Subscribe((int)GameHashes.Rotated, OnRotated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all of the visualizers.
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveVisualizers() {
|
||||||
|
foreach (var cell in cells)
|
||||||
|
cell.Destroy();
|
||||||
|
cells.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the offset cell from the specified starting point, including the
|
||||||
|
/// rotation of this object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseCell">The starting cell.</param>
|
||||||
|
/// <param name="offset">The offset if the building had its default rotation.</param>
|
||||||
|
/// <returns>The computed destination cell.</returns>
|
||||||
|
protected int RotateOffsetCell(int baseCell, CellOffset offset) {
|
||||||
|
if (rotatable != null)
|
||||||
|
offset = rotatable.GetRotatedCellOffset(offset);
|
||||||
|
return Grid.OffsetCell(baseCell, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when cell visualizations need to be updated. Visualized cells should be
|
||||||
|
/// added to the collection supplied as an argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newCells">The cells which should be visualized.</param>
|
||||||
|
protected abstract void VisualizeCells(ICollection<VisCellData> newCells);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the data about a particular cell, including its anim controller and tint
|
||||||
|
/// color.
|
||||||
|
/// </summary>
|
||||||
|
protected sealed class VisCellData : IComparable<VisCellData> {
|
||||||
|
/// <summary>
|
||||||
|
/// The target cell.
|
||||||
|
/// </summary>
|
||||||
|
public int Cell { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The anim controller for this cell.
|
||||||
|
/// </summary>
|
||||||
|
public KBatchedAnimController Controller { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tint used for this cell.
|
||||||
|
/// </summary>
|
||||||
|
public Color Tint { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a visualized cell.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cell">The cell to visualize.</param>
|
||||||
|
public VisCellData(int cell) : this(cell, Color.white) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a visualized cell.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cell">The cell to visualize.</param>
|
||||||
|
/// <param name="tint">The color to tint it.</param>
|
||||||
|
public VisCellData(int cell, Color tint) {
|
||||||
|
Cell = cell;
|
||||||
|
Controller = null;
|
||||||
|
Tint = tint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(VisCellData other) {
|
||||||
|
if (other == null)
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
return Cell.CompareTo(other.Cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the anim controller for this cell.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sceneLayer">The layer on which to display the animation.</param>
|
||||||
|
public void CreateController(Grid.SceneLayer sceneLayer) {
|
||||||
|
Controller = FXHelpers.CreateEffect(ANIM_NAME, Grid.CellToPosCCC(Cell,
|
||||||
|
sceneLayer), null, false, sceneLayer, true);
|
||||||
|
Controller.destroyOnAnimComplete = false;
|
||||||
|
Controller.visibilityType = KAnimControllerBase.VisibilityType.Always;
|
||||||
|
Controller.gameObject.SetActive(true);
|
||||||
|
Controller.Play(PRE_ANIMS, KAnim.PlayMode.Loop);
|
||||||
|
Controller.TintColour = Tint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys the anim controller for this cell.
|
||||||
|
/// </summary>
|
||||||
|
public void Destroy() {
|
||||||
|
if (Controller != null) {
|
||||||
|
Controller.destroyOnAnimComplete = true;
|
||||||
|
Controller.Play(POST_ANIM, KAnim.PlayMode.Once, 1f, 0f);
|
||||||
|
Controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is VisCellData other && other.Cell == Cell && Tint.Equals(other.
|
||||||
|
Tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "CellData[cell={0:D},color={1}]".F(Cell, Tint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pipe connection to a building.
|
||||||
|
/// </summary>
|
||||||
|
public class ConduitConnection {
|
||||||
|
/// <summary>
|
||||||
|
/// The conduit location.
|
||||||
|
/// </summary>
|
||||||
|
public CellOffset Location { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The conduit type.
|
||||||
|
/// </summary>
|
||||||
|
public ConduitType Type { get; }
|
||||||
|
|
||||||
|
public ConduitConnection(ConduitType type, CellOffset location) {
|
||||||
|
Location = location;
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("Connection[Type={0},Location={1}]", Type, Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// Utility methods for creating new buildings.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PBuilding {
|
||||||
|
/// <summary>
|
||||||
|
/// The default building category.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashedString DEFAULT_CATEGORY = new HashedString("Base");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes the building always operational.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="go">The game object to configure.</param>
|
||||||
|
private static void ApplyAlwaysOperational(GameObject go) {
|
||||||
|
// Remove default components that could make a building non-operational
|
||||||
|
if (go.TryGetComponent(out BuildingEnabledButton enabled))
|
||||||
|
Object.DestroyImmediate(enabled);
|
||||||
|
if (go.TryGetComponent(out Operational op))
|
||||||
|
Object.DestroyImmediate(op);
|
||||||
|
if (go.TryGetComponent(out LogicPorts lp))
|
||||||
|
Object.DestroyImmediate(lp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logic port, in a method compatible with both the new and old Automation
|
||||||
|
/// updates. The port will have the default strings which fit well with the
|
||||||
|
/// LogicOperationalController.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A logic port compatible with both editions.</returns>
|
||||||
|
public static LogicPorts.Port CompatLogicPort(LogicPortSpriteType type,
|
||||||
|
CellOffset offset) {
|
||||||
|
return new LogicPorts.Port(LogicOperationalController.PORT_ID, offset,
|
||||||
|
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL,
|
||||||
|
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL_ACTIVE,
|
||||||
|
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL_INACTIVE, false, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the building to the plan menu.
|
||||||
|
/// </summary>
|
||||||
|
public void AddPlan() {
|
||||||
|
if (!addedPlan && Category.IsValid) {
|
||||||
|
bool add = false;
|
||||||
|
foreach (var menu in TUNING.BUILDINGS.PLANORDER)
|
||||||
|
if (menu.category == Category) {
|
||||||
|
AddPlanToCategory(menu);
|
||||||
|
add = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!add)
|
||||||
|
PUtil.LogWarning("Unable to find build menu: " + Category);
|
||||||
|
addedPlan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a building to a specific plan menu.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="menu">The menu to which to add the building.</param>
|
||||||
|
private void AddPlanToCategory(PlanScreen.PlanInfo menu) {
|
||||||
|
// Found category
|
||||||
|
var data = menu.buildingAndSubcategoryData;
|
||||||
|
if (data != null) {
|
||||||
|
string addID = AddAfter;
|
||||||
|
bool add = false;
|
||||||
|
if (addID != null) {
|
||||||
|
// Optionally choose the position
|
||||||
|
int n = data.Count;
|
||||||
|
for (int i = 0; i < n - 1 && !add; i++)
|
||||||
|
if (data[i].Key == addID) {
|
||||||
|
data.Insert(i + 1, new KeyValuePair<string, string>(ID,
|
||||||
|
SubCategory));
|
||||||
|
add = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!add)
|
||||||
|
data.Add(new KeyValuePair<string, string>(ID, SubCategory));
|
||||||
|
} else
|
||||||
|
PUtil.LogWarning("Build menu " + Category + " has invalid entries!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the building strings to the strings list.
|
||||||
|
/// </summary>
|
||||||
|
public void AddStrings() {
|
||||||
|
if (!addedStrings) {
|
||||||
|
string prefix = "STRINGS.BUILDINGS.PREFABS." + ID.ToUpperInvariant() + ".";
|
||||||
|
string nameStr = prefix + "NAME";
|
||||||
|
if (Strings.TryGet(nameStr, out StringEntry localized))
|
||||||
|
Name = localized.String;
|
||||||
|
else
|
||||||
|
Strings.Add(nameStr, Name);
|
||||||
|
// Allow null values to be defined in LocString class adds / etc
|
||||||
|
if (Description != null)
|
||||||
|
Strings.Add(prefix + "DESC", Description);
|
||||||
|
if (EffectText != null)
|
||||||
|
Strings.Add(prefix + "EFFECT", EffectText);
|
||||||
|
addedStrings = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the building tech to the tech tree.
|
||||||
|
/// </summary>
|
||||||
|
public void AddTech() {
|
||||||
|
if (!addedTech && Tech != null) {
|
||||||
|
var technology = Db.Get().Techs?.TryGet(Tech);
|
||||||
|
if (technology != null)
|
||||||
|
technology.unlockedItemIDs?.Add(ID);
|
||||||
|
addedTech = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Splits up logic input/output ports and configures the game object with them.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="go">The game object to configure.</param>
|
||||||
|
private void SplitLogicPorts(GameObject go) {
|
||||||
|
int n = LogicIO.Count;
|
||||||
|
var inputs = new List<LogicPorts.Port>(n);
|
||||||
|
var outputs = new List<LogicPorts.Port>(n);
|
||||||
|
foreach (var port in LogicIO)
|
||||||
|
if (port.spriteType == LogicPortSpriteType.Output)
|
||||||
|
outputs.Add(port);
|
||||||
|
else
|
||||||
|
inputs.Add(port);
|
||||||
|
// This works in both the old and new versions
|
||||||
|
var ports = go.AddOrGet<LogicPorts>();
|
||||||
|
if (inputs.Count > 0)
|
||||||
|
ports.inputPortInfo = inputs.ToArray();
|
||||||
|
if (outputs.Count > 0)
|
||||||
|
ports.outputPortInfo = outputs.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,479 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// A class used for creating new buildings. Abstracts many of the details to allow them
|
||||||
|
/// to be used across different game versions.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PBuilding {
|
||||||
|
/// <summary>
|
||||||
|
/// The building ID which should precede this building ID in the plan menu.
|
||||||
|
/// </summary>
|
||||||
|
public string AddAfter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the building is always operational.
|
||||||
|
/// </summary>
|
||||||
|
public bool AlwaysOperational { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building's animation.
|
||||||
|
/// </summary>
|
||||||
|
public string Animation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The audio sounds used when placing/completing the building.
|
||||||
|
/// </summary>
|
||||||
|
public string AudioCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The audio volume used when placing/completing the building.
|
||||||
|
/// </summary>
|
||||||
|
public string AudioSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this building can break down.
|
||||||
|
/// </summary>
|
||||||
|
public bool Breaks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The build menu category.
|
||||||
|
/// </summary>
|
||||||
|
public HashedString Category { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The construction time in seconds on x1 speed.
|
||||||
|
/// </summary>
|
||||||
|
public float ConstructionTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The decor of this building.
|
||||||
|
/// </summary>
|
||||||
|
public EffectorValues Decor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building description.
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text describing the building's effect.
|
||||||
|
/// </summary>
|
||||||
|
public string EffectText { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this building can entomb.
|
||||||
|
/// </summary>
|
||||||
|
public bool Entombs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The heat generation from the exhaust in kDTU/s.
|
||||||
|
/// </summary>
|
||||||
|
public float ExhaustHeatGeneration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this building can flood.
|
||||||
|
/// </summary>
|
||||||
|
public bool Floods { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default priority of this building, with null to not add a priority.
|
||||||
|
/// </summary>
|
||||||
|
public int? DefaultPriority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The self-heating when active in kDTU/s.
|
||||||
|
/// </summary>
|
||||||
|
public float HeatGeneration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building height.
|
||||||
|
/// </summary>
|
||||||
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building HP until it breaks down.
|
||||||
|
/// </summary>
|
||||||
|
public int HP { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ingredients required for construction.
|
||||||
|
/// </summary>
|
||||||
|
public IList<BuildIngredient> Ingredients { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building ID.
|
||||||
|
/// </summary>
|
||||||
|
public string ID { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this building is an industrial machine.
|
||||||
|
/// </summary>
|
||||||
|
public bool IndustrialMachine { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The input conduits.
|
||||||
|
/// </summary>
|
||||||
|
public IList<ConduitConnection> InputConduits { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this building is (or can be) a solid tile.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSolidTile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logic ports.
|
||||||
|
/// </summary>
|
||||||
|
public IList<LogicPorts.Port> LogicIO { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The noise of this building (not used by Klei).
|
||||||
|
/// </summary>
|
||||||
|
public EffectorValues Noise { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The layer for this building.
|
||||||
|
/// </summary>
|
||||||
|
public ObjectLayer ObjectLayer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The output conduits.
|
||||||
|
/// </summary>
|
||||||
|
public IList<ConduitConnection> OutputConduits { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If null, the building does not overheat; otherwise, it overheats at this
|
||||||
|
/// temperature in K.
|
||||||
|
/// </summary>
|
||||||
|
public float? OverheatTemperature { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The location where this building may be built.
|
||||||
|
/// </summary>
|
||||||
|
public BuildLocationRule Placement { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If null, the building has no power input; otherwise, it uses this much power.
|
||||||
|
/// </summary>
|
||||||
|
public PowerRequirement PowerInput { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If null, the building has no power output; otherwise, it provides this much power.
|
||||||
|
/// </summary>
|
||||||
|
public PowerRequirement PowerOutput { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The directions this building can face.
|
||||||
|
/// </summary>
|
||||||
|
public PermittedRotations RotateMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scene layer for this building.
|
||||||
|
/// </summary>
|
||||||
|
public Grid.SceneLayer SceneLayer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The subcategory for this building.
|
||||||
|
///
|
||||||
|
/// The base game currently defines the following:
|
||||||
|
/// Base:
|
||||||
|
/// ladders, tiles, printing pods, doors, storage, tubes, default
|
||||||
|
/// Oxygen:
|
||||||
|
/// producers, scrubbers
|
||||||
|
/// Power:
|
||||||
|
/// generators, wires, batteries, transformers, switches
|
||||||
|
/// Food:
|
||||||
|
/// cooking, farming, ranching
|
||||||
|
/// Plumbing:
|
||||||
|
/// bathroom, pipes, pumps, valves, sensors
|
||||||
|
/// HVAC:
|
||||||
|
/// pipes, pumps, valves, sensors
|
||||||
|
/// Refining:
|
||||||
|
/// materials, oil, advanced
|
||||||
|
/// Medical:
|
||||||
|
/// cleaning, hospital, wellness
|
||||||
|
/// Furniture:
|
||||||
|
/// bed, lights, dining, recreation, pots, sculpture, electronic decor, moulding,
|
||||||
|
/// canvas, dispaly, monument, signs
|
||||||
|
/// Equipment:
|
||||||
|
/// research, exploration, work stations, suits general, oxygen masks, atmo suits,
|
||||||
|
/// jet suits, lead suits
|
||||||
|
/// Utilities:
|
||||||
|
/// temperature, other utilities, special
|
||||||
|
/// Automation:
|
||||||
|
/// wires, sensors, logic gates, utilities
|
||||||
|
/// Solid Transport:
|
||||||
|
/// conduit, valves, utilities
|
||||||
|
/// Rocketry:
|
||||||
|
/// telescopes, launch pad, railguns, engines, fuel and oxidizer, cargo, utility,
|
||||||
|
/// command, fittings
|
||||||
|
/// Radiation:
|
||||||
|
/// HEP, uranium, radiation
|
||||||
|
/// </summary>
|
||||||
|
public string SubCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The technology name required to unlock the building.
|
||||||
|
/// </summary>
|
||||||
|
public string Tech { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The view mode used when placing this building.
|
||||||
|
/// </summary>
|
||||||
|
public HashedString ViewMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The building width.
|
||||||
|
/// </summary>
|
||||||
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the building was added to the plan menu.
|
||||||
|
/// </summary>
|
||||||
|
private bool addedPlan;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the strings were added.
|
||||||
|
/// </summary>
|
||||||
|
private bool addedStrings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the technology wes added.
|
||||||
|
/// </summary>
|
||||||
|
private bool addedTech;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new building. All buildings thus created must be registered using
|
||||||
|
/// PBuilding.Register and have an appropriate IBuildingConfig class.
|
||||||
|
///
|
||||||
|
/// Building should be created in OnLoad or a post-load patch (not in static
|
||||||
|
/// initializers) to give the localization framework time to patch the LocString
|
||||||
|
/// containing the building name and description.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The building ID.</param>
|
||||||
|
/// <param name="name">The building name.</param>
|
||||||
|
public PBuilding(string id, string name) {
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
AddAfter = null;
|
||||||
|
AlwaysOperational = false;
|
||||||
|
Animation = "";
|
||||||
|
AudioCategory = "Metal";
|
||||||
|
AudioSize = "medium";
|
||||||
|
Breaks = true;
|
||||||
|
Category = DEFAULT_CATEGORY;
|
||||||
|
ConstructionTime = 10.0f;
|
||||||
|
Decor = TUNING.BUILDINGS.DECOR.NONE;
|
||||||
|
DefaultPriority = null;
|
||||||
|
Description = "Default Building Description";
|
||||||
|
EffectText = "Default Building Effect";
|
||||||
|
Entombs = true;
|
||||||
|
ExhaustHeatGeneration = 0.0f;
|
||||||
|
Floods = true;
|
||||||
|
HeatGeneration = 0.0f;
|
||||||
|
Height = 1;
|
||||||
|
Ingredients = new List<BuildIngredient>(4);
|
||||||
|
IndustrialMachine = false;
|
||||||
|
InputConduits = new List<ConduitConnection>(4);
|
||||||
|
HP = 100;
|
||||||
|
ID = id;
|
||||||
|
LogicIO = new List<LogicPorts.Port>(4);
|
||||||
|
Name = name;
|
||||||
|
Noise = TUNING.NOISE_POLLUTION.NONE;
|
||||||
|
ObjectLayer = PGameUtils.GetObjectLayer(nameof(ObjectLayer.Building), ObjectLayer.
|
||||||
|
Building);
|
||||||
|
OutputConduits = new List<ConduitConnection>(4);
|
||||||
|
OverheatTemperature = null;
|
||||||
|
Placement = BuildLocationRule.OnFloor;
|
||||||
|
PowerInput = null;
|
||||||
|
PowerOutput = null;
|
||||||
|
RotateMode = PermittedRotations.Unrotatable;
|
||||||
|
SceneLayer = Grid.SceneLayer.Building;
|
||||||
|
// Hard coded strings in base game, no const to reference
|
||||||
|
SubCategory = "default";
|
||||||
|
Tech = null;
|
||||||
|
ViewMode = OverlayModes.None.ID;
|
||||||
|
Width = 1;
|
||||||
|
|
||||||
|
addedPlan = false;
|
||||||
|
addedStrings = false;
|
||||||
|
addedTech = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the building def from this class.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The Klei building def.</returns>
|
||||||
|
public BuildingDef CreateDef() {
|
||||||
|
// The number of fields in BuildingDef makes it somewhat impractical to detour
|
||||||
|
if (Width < 1)
|
||||||
|
throw new InvalidOperationException("Building width: " + Width);
|
||||||
|
if (Height < 1)
|
||||||
|
throw new InvalidOperationException("Building height: " + Height);
|
||||||
|
if (HP < 1)
|
||||||
|
throw new InvalidOperationException("Building HP: " + HP);
|
||||||
|
if (ConstructionTime.IsNaNOrInfinity())
|
||||||
|
throw new InvalidOperationException("Construction time: " + ConstructionTime);
|
||||||
|
// Build an ingredients list
|
||||||
|
int n = Ingredients.Count;
|
||||||
|
if (n < 1)
|
||||||
|
throw new InvalidOperationException("No ingredients for build");
|
||||||
|
float[] quantity = new float[n];
|
||||||
|
string[] tag = new string[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
var ingredient = Ingredients[i];
|
||||||
|
if (ingredient == null)
|
||||||
|
throw new ArgumentNullException(nameof(ingredient));
|
||||||
|
quantity[i] = ingredient.Quantity;
|
||||||
|
tag[i] = ingredient.Material;
|
||||||
|
}
|
||||||
|
// Melting point is not currently used
|
||||||
|
var def = BuildingTemplates.CreateBuildingDef(ID, Width, Height, Animation, HP,
|
||||||
|
Math.Max(0.1f, ConstructionTime), quantity, tag, 2400.0f, Placement, Decor,
|
||||||
|
Noise);
|
||||||
|
// Solid tile?
|
||||||
|
if (IsSolidTile) {
|
||||||
|
//def.isSolidTile = true;
|
||||||
|
def.BaseTimeUntilRepair = -1.0f;
|
||||||
|
def.UseStructureTemperature = false;
|
||||||
|
BuildingTemplates.CreateFoundationTileDef(def);
|
||||||
|
}
|
||||||
|
def.AudioCategory = AudioCategory;
|
||||||
|
def.AudioSize = AudioSize;
|
||||||
|
if (OverheatTemperature != null) {
|
||||||
|
def.Overheatable = true;
|
||||||
|
def.OverheatTemperature = OverheatTemperature ?? 348.15f;
|
||||||
|
} else
|
||||||
|
def.Overheatable = false;
|
||||||
|
// Plug in
|
||||||
|
if (PowerInput != null) {
|
||||||
|
def.RequiresPowerInput = true;
|
||||||
|
def.EnergyConsumptionWhenActive = PowerInput.MaxWattage;
|
||||||
|
def.PowerInputOffset = PowerInput.PlugLocation;
|
||||||
|
}
|
||||||
|
// Plug out
|
||||||
|
if (PowerOutput != null) {
|
||||||
|
def.RequiresPowerOutput = true;
|
||||||
|
def.GeneratorWattageRating = PowerOutput.MaxWattage;
|
||||||
|
def.PowerOutputOffset = PowerOutput.PlugLocation;
|
||||||
|
}
|
||||||
|
def.Breakable = Breaks;
|
||||||
|
def.PermittedRotations = RotateMode;
|
||||||
|
def.ExhaustKilowattsWhenActive = ExhaustHeatGeneration;
|
||||||
|
def.SelfHeatKilowattsWhenActive = HeatGeneration;
|
||||||
|
def.Floodable = Floods;
|
||||||
|
def.Entombable = Entombs;
|
||||||
|
def.ObjectLayer = ObjectLayer;
|
||||||
|
def.SceneLayer = SceneLayer;
|
||||||
|
def.ViewMode = ViewMode;
|
||||||
|
// Conduits (multiple per building are hard but will be added someday...)
|
||||||
|
if (InputConduits.Count > 1)
|
||||||
|
throw new InvalidOperationException("Only supports one input conduit");
|
||||||
|
foreach (var conduit in InputConduits) {
|
||||||
|
def.UtilityInputOffset = conduit.Location;
|
||||||
|
def.InputConduitType = conduit.Type;
|
||||||
|
}
|
||||||
|
if (OutputConduits.Count > 1)
|
||||||
|
throw new InvalidOperationException("Only supports one output conduit");
|
||||||
|
foreach (var conduit in OutputConduits) {
|
||||||
|
def.UtilityOutputOffset = conduit.Location;
|
||||||
|
def.OutputConduitType = conduit.Type;
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the building template of this building. Should be called in
|
||||||
|
/// ConfigureBuildingTemplate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="go">The game object to configure.</param>
|
||||||
|
public void ConfigureBuildingTemplate(GameObject go) {
|
||||||
|
if (AlwaysOperational)
|
||||||
|
ApplyAlwaysOperational(go);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the logic ports of this building. Must be used <b>after</b> the
|
||||||
|
/// PBuilding.DoPostConfigureComplete method if logic ports are required.
|
||||||
|
///
|
||||||
|
/// Should be called in DoPostConfigureComplete, DoPostConfigurePreview, and
|
||||||
|
/// DoPostConfigureUnderConstruction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="go">The game object to configure.</param>
|
||||||
|
public void CreateLogicPorts(GameObject go) {
|
||||||
|
SplitLogicPorts(go);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the post-configure complete steps that this building object can do.
|
||||||
|
/// Not exhaustive! Other components must likely be added.
|
||||||
|
///
|
||||||
|
/// This method does NOT add the logic ports. Use CreateLogicPorts to do so,
|
||||||
|
/// <b>after</b> this method has been invoked.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="go">The game object to configure.</param>
|
||||||
|
public void DoPostConfigureComplete(GameObject go) {
|
||||||
|
if (InputConduits.Count == 1) {
|
||||||
|
var conduitConsumer = go.AddOrGet<ConduitConsumer>();
|
||||||
|
foreach (var conduit in InputConduits) {
|
||||||
|
conduitConsumer.alwaysConsume = true;
|
||||||
|
conduitConsumer.conduitType = conduit.Type;
|
||||||
|
conduitConsumer.wrongElementResult = ConduitConsumer.WrongElementResult.
|
||||||
|
Store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (OutputConduits.Count == 1) {
|
||||||
|
var conduitDispenser = go.AddOrGet<ConduitDispenser>();
|
||||||
|
foreach (var conduit in OutputConduits) {
|
||||||
|
conduitDispenser.alwaysDispense = true;
|
||||||
|
conduitDispenser.conduitType = conduit.Type;
|
||||||
|
conduitDispenser.elementFilter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (IndustrialMachine && go.TryGetComponent(out KPrefabID id))
|
||||||
|
id.AddTag(RoomConstraints.ConstraintTags.IndustrialMachinery, false);
|
||||||
|
if (PowerInput != null)
|
||||||
|
go.AddOrGet<EnergyConsumer>();
|
||||||
|
if (PowerOutput != null)
|
||||||
|
go.AddOrGet<EnergyGenerator>();
|
||||||
|
// Set a default priority
|
||||||
|
if (DefaultPriority != null && go.TryGetComponent(out Prioritizable pr)) {
|
||||||
|
Prioritizable.AddRef(go);
|
||||||
|
pr.SetMasterPriority(new PrioritySetting(PriorityScreen.PriorityClass.basic,
|
||||||
|
DefaultPriority ?? 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PBuilding[ID={0}]".F(ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.PatchManager;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// Manages PLib buildings to break down PBuilding into a more reasonable sized class.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PBuildingManager : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PBuildingManager Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immediately adds an <i>existing</i> building ID to an existing technology ID in the
|
||||||
|
/// tech tree.
|
||||||
|
///
|
||||||
|
/// Do <b>not</b> use this method on buildings registered through PBuilding as they
|
||||||
|
/// are added automatically.
|
||||||
|
///
|
||||||
|
/// This method must be used in a Db.Initialize postfix patch or RunAt.AfterDbInit
|
||||||
|
/// PPatchManager method/patch.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tech">The technology tree node ID.</param>
|
||||||
|
/// <param name="id">The building ID to add to that node.</param>
|
||||||
|
public static void AddExistingBuildingToTech(string tech, string id) {
|
||||||
|
if (string.IsNullOrEmpty(tech))
|
||||||
|
throw new ArgumentNullException(nameof(tech));
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
var technology = Db.Get().Techs?.TryGet(id);
|
||||||
|
if (technology != null)
|
||||||
|
technology.unlockedItemIDs?.Add(tech);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateBuildingDef_Postfix(BuildingDef __result, string anim,
|
||||||
|
string id) {
|
||||||
|
var animFiles = __result?.AnimFiles;
|
||||||
|
if (animFiles != null && animFiles.Length > 0 && animFiles[0] == null)
|
||||||
|
Debug.LogWarningFormat("(when looking for KAnim named {0} on building {1})",
|
||||||
|
anim, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateEquipmentDef_Postfix(EquipmentDef __result, string Anim,
|
||||||
|
string Id) {
|
||||||
|
var anim = __result?.Anim;
|
||||||
|
if (anim == null)
|
||||||
|
Debug.LogWarningFormat("(when looking for KAnim named {0} on equipment {1})",
|
||||||
|
Anim, Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message encountered by the PLib building system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The debug message.</param>
|
||||||
|
internal static void LogBuildingDebug(string message) {
|
||||||
|
Debug.LogFormat("[PLibBuildings] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LoadGeneratedBuildings_Prefix() {
|
||||||
|
Instance?.AddAllStrings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The buildings which need to be registered.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ICollection<PBuilding> buildings;
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a building manager to register PLib buildings.
|
||||||
|
/// </summary>
|
||||||
|
public PBuildingManager() {
|
||||||
|
buildings = new List<PBuilding>(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the strings for every registered building in all mods to the database.
|
||||||
|
/// </summary>
|
||||||
|
private void AddAllStrings() {
|
||||||
|
InvokeAllProcess(0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the strings for each registered building in this mod to the database.
|
||||||
|
/// </summary>
|
||||||
|
private void AddStrings() {
|
||||||
|
int n = buildings.Count;
|
||||||
|
if (n > 0) {
|
||||||
|
LogBuildingDebug("Register strings for {0:D} building(s) from {1}".F(n,
|
||||||
|
Assembly.GetExecutingAssembly().GetNameSafe() ?? "?"));
|
||||||
|
foreach (var building in buildings)
|
||||||
|
if (building != null) {
|
||||||
|
building.AddStrings();
|
||||||
|
building.AddPlan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the techs for every registered building in all mods to the database.
|
||||||
|
/// </summary>
|
||||||
|
private void AddAllTechs() {
|
||||||
|
InvokeAllProcess(1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the techs for each registered building in this mod to the database.
|
||||||
|
/// </summary>
|
||||||
|
private void AddTechs() {
|
||||||
|
int n = buildings.Count;
|
||||||
|
if (n > 0) {
|
||||||
|
LogBuildingDebug("Register techs for {0:D} building(s) from {1}".F(n,
|
||||||
|
Assembly.GetExecutingAssembly().GetNameSafe() ?? "?"));
|
||||||
|
foreach (var building in buildings)
|
||||||
|
if (building != null) {
|
||||||
|
building.AddTech();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a building to properly display its name, description, and tech tree
|
||||||
|
/// entry. PLib must be initialized using InitLibrary before using this method. Each
|
||||||
|
/// building should only be registered once, either in OnLoad or a post-load patch.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="building">The building to register.</param>
|
||||||
|
public void Register(PBuilding building) {
|
||||||
|
if (building == null)
|
||||||
|
throw new ArgumentNullException(nameof(building));
|
||||||
|
RegisterForForwarding();
|
||||||
|
// Must use object as the building table type
|
||||||
|
buildings.Add(building);
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Registered building: {0}".F(building.ID));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
// Non essential, do not crash on fail
|
||||||
|
try {
|
||||||
|
plibInstance.Patch(typeof(BuildingTemplates), nameof(BuildingTemplates.
|
||||||
|
CreateBuildingDef), postfix: PatchMethod(nameof(
|
||||||
|
CreateBuildingDef_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(EquipmentTemplates), nameof(EquipmentTemplates.
|
||||||
|
CreateEquipmentDef), postfix: PatchMethod(nameof(
|
||||||
|
CreateEquipmentDef_Postfix)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
plibInstance.Patch(typeof(GeneratedBuildings), nameof(GeneratedBuildings.
|
||||||
|
LoadGeneratedBuildings), prefix: PatchMethod(nameof(
|
||||||
|
LoadGeneratedBuildings_Prefix)));
|
||||||
|
|
||||||
|
// Avoid another Harmony patch by using PatchManager
|
||||||
|
var pm = new PPatchManager(plibInstance);
|
||||||
|
pm.RegisterPatch(RunAt.AfterDbInit, new BuildingTechRegistration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object _) {
|
||||||
|
if (operation == 0)
|
||||||
|
AddStrings();
|
||||||
|
else if (operation == 1)
|
||||||
|
AddTechs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A Patch Manager patch which registers all PBuilding technologies.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class BuildingTechRegistration : IPatchMethodInstance {
|
||||||
|
public void Run(Harmony instance) {
|
||||||
|
Instance?.AddAllTechs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Buildings</Title>
|
||||||
|
<AssemblyTitle>PLib.Buildings</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.Buildings</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibBuildings.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Buildings {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores related information about a building's power requirements.
|
||||||
|
/// </summary>
|
||||||
|
public class PowerRequirement {
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum building wattage.
|
||||||
|
/// </summary>
|
||||||
|
public float MaxWattage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The location of the plug related to the foundation tile.
|
||||||
|
/// </summary>
|
||||||
|
public CellOffset PlugLocation { get; }
|
||||||
|
|
||||||
|
public PowerRequirement(float wattage, CellOffset plugLocation) {
|
||||||
|
if (wattage.IsNaNOrInfinity() || wattage < 0.0f)
|
||||||
|
throw new ArgumentException("wattage");
|
||||||
|
MaxWattage = wattage;
|
||||||
|
PlugLocation = plugLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "Power[Watts={0:F0},Location={1}]".F(MaxWattage, PlugLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// An exception thrown when constructing a detour.
|
||||||
|
/// </summary>
|
||||||
|
public class DetourException : ArgumentException {
|
||||||
|
public DetourException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores delegates used to read and write fields or properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||||
|
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||||
|
internal sealed class DetouredField<P, T> : IDetouredField<P, T> {
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to get the field/property value.
|
||||||
|
/// </summary>
|
||||||
|
public Func<P, T> Get { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The field name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to set the field/property value. Null if the field is const or readonly.
|
||||||
|
/// </summary>
|
||||||
|
public Action<P, T> Set { get; }
|
||||||
|
|
||||||
|
internal DetouredField(string name, Func<P, T> get, Action<P, T> set) {
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Get = get;
|
||||||
|
Set = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("DetouredField[name={0}]", Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a detoured method, only performing the expensive reflection when the detour is
|
||||||
|
/// first used.
|
||||||
|
///
|
||||||
|
/// This class is not thread safe.
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DetouredMethod<D> where D : Delegate {
|
||||||
|
/// <summary>
|
||||||
|
/// Emulates the ability of Delegate.Invoke to actually call the method.
|
||||||
|
/// </summary>
|
||||||
|
public D Invoke {
|
||||||
|
get {
|
||||||
|
Initialize();
|
||||||
|
return delg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate method which will be called.
|
||||||
|
/// </summary>
|
||||||
|
private D delg;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target type.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Type type;
|
||||||
|
|
||||||
|
internal DetouredMethod(Type type, string name) {
|
||||||
|
this.type = type ?? throw new ArgumentNullException(nameof(type));
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
delg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the getter and setter functions immediately if necessary.
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize() {
|
||||||
|
if (delg == null)
|
||||||
|
delg = PDetours.Detour<D>(type, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("LazyDetouredMethod[type={1},name={0}]", Name, type.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// An interface that describes a detoured field, which stores delegates used to read and
|
||||||
|
/// write fields or properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||||
|
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||||
|
public interface IDetouredField<P, T> {
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to get the field/property value.
|
||||||
|
/// </summary>
|
||||||
|
Func<P, T> Get { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The field name.
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to set the field/property value.
|
||||||
|
/// </summary>
|
||||||
|
Action<P, T> Set { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores delegates used to read and write fields or properties. This version is lazy and
|
||||||
|
/// only calculates the destination when it is first used.
|
||||||
|
///
|
||||||
|
/// This class is not thread safe.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||||
|
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||||
|
internal sealed class LazyDetouredField<P, T> : IDetouredField<P, T> {
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to get the field/property value.
|
||||||
|
/// </summary>
|
||||||
|
public Func<P, T> Get {
|
||||||
|
get {
|
||||||
|
Initialize();
|
||||||
|
return getter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The field name.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke to set the field/property value.
|
||||||
|
/// </summary>
|
||||||
|
public Action<P, T> Set {
|
||||||
|
get {
|
||||||
|
Initialize();
|
||||||
|
return setter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The function to get the field value.
|
||||||
|
/// </summary>
|
||||||
|
private Func<P, T> getter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The function to set the field value.
|
||||||
|
/// </summary>
|
||||||
|
private Action<P, T> setter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target type.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Type type;
|
||||||
|
|
||||||
|
internal LazyDetouredField(Type type, string name) {
|
||||||
|
this.type = type ?? throw new ArgumentNullException(nameof(type));
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
getter = null;
|
||||||
|
setter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the getter and setter functions immediately if necessary.
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize() {
|
||||||
|
if (getter == null && setter == null) {
|
||||||
|
var dt = PDetours.DetourField<P, T>(Name);
|
||||||
|
getter = dt.Get;
|
||||||
|
setter = dt.Set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("LazyDetouredField[type={1},name={0}]", Name, type.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,600 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Detours {
|
||||||
|
/// <summary>
|
||||||
|
/// Efficiently detours around many changes in the game by creating detour methods and
|
||||||
|
/// accessors which are resilient against many types of source compatible but binary
|
||||||
|
/// incompatible changes.
|
||||||
|
/// </summary>
|
||||||
|
public static class PDetours {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// method with the same name as the delegate type. The dynamic method will
|
||||||
|
/// automatically adapt if optional parameters are added, filling in their default
|
||||||
|
/// values.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="type">The target type.</param>
|
||||||
|
/// <returns>The detour that will call the method with the name of the delegate type.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||||
|
public static D Detour<D>(this Type type) where D : Delegate {
|
||||||
|
return Detour<D>(type, typeof(D).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// method with the specified name. The dynamic method will automatically adapt if
|
||||||
|
/// optional parameters are added, filling in their default values.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="type">The target type.</param>
|
||||||
|
/// <param name="name">The method name.</param>
|
||||||
|
/// <returns>The detour that will call that method.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||||
|
public static D Detour<D>(this Type type, string name) where D : Delegate {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
var methods = type.GetMethods(PPatchTools.BASE_FLAGS | BindingFlags.Static |
|
||||||
|
BindingFlags.Instance);
|
||||||
|
// Determine delegate return type
|
||||||
|
var expected = DelegateInfo.Create(typeof(D));
|
||||||
|
MethodInfo bestMatch = null;
|
||||||
|
int bestParamCount = int.MaxValue;
|
||||||
|
foreach (var method in methods)
|
||||||
|
if (method.Name == name) {
|
||||||
|
try {
|
||||||
|
var result = ValidateDelegate(expected, method, method.ReturnType);
|
||||||
|
int n = result.Length;
|
||||||
|
// Choose overload with fewest parameters to substitute
|
||||||
|
if (n < bestParamCount) {
|
||||||
|
bestParamCount = n;
|
||||||
|
bestMatch = method;
|
||||||
|
}
|
||||||
|
} catch (DetourException) {
|
||||||
|
// Keep looking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestMatch == null)
|
||||||
|
throw new DetourException("No match found for {1}.{0}".F(name, type.FullName));
|
||||||
|
return Detour<D>(bestMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// constructor. The dynamic method will automatically adapt if optional parameters
|
||||||
|
/// are added, filling in their default values.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="type">The target type.</param>
|
||||||
|
/// <returns>The detour that will call that type's constructor.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||||
|
public static D DetourConstructor<D>(this Type type) where D : Delegate {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
var constructors = type.GetConstructors(PPatchTools.BASE_FLAGS | BindingFlags.
|
||||||
|
Instance);
|
||||||
|
// Determine delegate return type
|
||||||
|
var expected = DelegateInfo.Create(typeof(D));
|
||||||
|
ConstructorInfo bestMatch = null;
|
||||||
|
int bestParamCount = int.MaxValue;
|
||||||
|
foreach (var constructor in constructors)
|
||||||
|
try {
|
||||||
|
var result = ValidateDelegate(expected, constructor, type);
|
||||||
|
int n = result.Length;
|
||||||
|
// Choose overload with fewest parameters to substitute
|
||||||
|
if (n < bestParamCount) {
|
||||||
|
bestParamCount = n;
|
||||||
|
bestMatch = constructor;
|
||||||
|
}
|
||||||
|
} catch (DetourException) {
|
||||||
|
// Keep looking
|
||||||
|
}
|
||||||
|
if (bestMatch == null)
|
||||||
|
throw new DetourException("No match found for {0} constructor".F(type.
|
||||||
|
FullName));
|
||||||
|
return Detour<D>(bestMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// method with the specified name. The dynamic method will automatically adapt if
|
||||||
|
/// optional parameters are added, filling in their default values.
|
||||||
|
///
|
||||||
|
/// This overload creates a lazy detour that only performs the expensive reflection
|
||||||
|
/// when it is first used.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="type">The target type.</param>
|
||||||
|
/// <param name="name">The method name.</param>
|
||||||
|
/// <returns>The detour that will call that method.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||||
|
public static DetouredMethod<D> DetourLazy<D>(this Type type, string name)
|
||||||
|
where D : Delegate {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
return new DetouredMethod<D>(type, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// method with the specified name. The dynamic method will automatically adapt if
|
||||||
|
/// optional parameters are added, filling in their default values.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="target">The target method to be called.</param>
|
||||||
|
/// <returns>The detour that will call that method.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||||
|
public static D Detour<D>(this MethodInfo target) where D : Delegate {
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
if (target.ContainsGenericParameters)
|
||||||
|
throw new ArgumentException("Generic types must have all parameters defined");
|
||||||
|
var expected = DelegateInfo.Create(typeof(D));
|
||||||
|
var parentType = target.DeclaringType;
|
||||||
|
var expectedParamTypes = expected.parameterTypes;
|
||||||
|
var actualParams = ValidateDelegate(expected, target, target.ReturnType);
|
||||||
|
int offset = target.IsStatic ? 0 : 1;
|
||||||
|
if (parentType == null)
|
||||||
|
throw new ArgumentException("Method is not declared by an actual type");
|
||||||
|
// Method will be "declared" in the type of the target, as we are detouring around
|
||||||
|
// a method of that type
|
||||||
|
var caller = new DynamicMethod(target.Name + "_Detour", expected.returnType,
|
||||||
|
expectedParamTypes, parentType, true);
|
||||||
|
var generator = caller.GetILGenerator();
|
||||||
|
LoadParameters(generator, actualParams, expectedParamTypes, offset);
|
||||||
|
if (parentType.IsValueType || target.IsStatic)
|
||||||
|
generator.Emit(OpCodes.Call, target);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Callvirt, target);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
FinishDynamicMethod(caller, actualParams, expectedParamTypes, offset);
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Created delegate {0} for method {1}.{2} with parameters [{3}]".
|
||||||
|
F(caller.Name, parentType.FullName, target.Name, actualParams.Join(",")));
|
||||||
|
#endif
|
||||||
|
return caller.CreateDelegate(typeof(D)) as D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||||
|
/// constructor. The dynamic method will automatically adapt if optional parameters
|
||||||
|
/// are added, filling in their default values.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||||
|
/// <param name="target">The target constructor to be called.</param>
|
||||||
|
/// <returns>The detour that will call that constructor.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||||
|
public static D Detour<D>(this ConstructorInfo target) where D : Delegate {
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
if (target.ContainsGenericParameters)
|
||||||
|
throw new ArgumentException("Generic types must have all parameters defined");
|
||||||
|
if (target.IsStatic)
|
||||||
|
throw new ArgumentException("Static constructors cannot be called manually");
|
||||||
|
var expected = DelegateInfo.Create(typeof(D));
|
||||||
|
var parentType = target.DeclaringType;
|
||||||
|
var expectedParamTypes = expected.parameterTypes;
|
||||||
|
var actualParams = ValidateDelegate(expected, target, parentType);
|
||||||
|
if (parentType == null)
|
||||||
|
throw new ArgumentException("Method is not declared by an actual type");
|
||||||
|
// Method will be "declared" in the type of the target, as we are detouring around
|
||||||
|
// a constructor of that type
|
||||||
|
var caller = new DynamicMethod("Constructor_Detour", expected.returnType,
|
||||||
|
expectedParamTypes, parentType, true);
|
||||||
|
var generator = caller.GetILGenerator();
|
||||||
|
LoadParameters(generator, actualParams, expectedParamTypes, 0);
|
||||||
|
generator.Emit(OpCodes.Newobj, target);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
FinishDynamicMethod(caller, actualParams, expectedParamTypes, 0);
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Created delegate {0} for constructor {1} with parameters [{2}]".
|
||||||
|
F(caller.Name, parentType.FullName, actualParams.Join(",")));
|
||||||
|
#endif
|
||||||
|
return caller.CreateDelegate(typeof(D)) as D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates dynamic detour methods to wrap a base game field or property with the
|
||||||
|
/// specified name. The detour will still work even if the field is converted to a
|
||||||
|
/// source compatible property and vice versa.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||||
|
/// <typeparam name="T">The type of the field or property element.</typeparam>
|
||||||
|
/// <param name="name">The name of the field or property to be accessed.</param>
|
||||||
|
/// <returns>A detour element that wraps the field or property with common getter and
|
||||||
|
/// setter delegates which will work on both types.</returns>
|
||||||
|
public static IDetouredField<P, T> DetourField<P, T>(string name) {
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
var pt = typeof(P);
|
||||||
|
var field = pt.GetField(name, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||||
|
Static | BindingFlags.Instance);
|
||||||
|
IDetouredField<P, T> d;
|
||||||
|
if (field == null) {
|
||||||
|
try {
|
||||||
|
var property = pt.GetProperty(name, PPatchTools.BASE_FLAGS |
|
||||||
|
BindingFlags.Static | BindingFlags.Instance);
|
||||||
|
if (property == null)
|
||||||
|
throw new DetourException("Unable to find {0} on type {1}".
|
||||||
|
F(name, typeof(P).FullName));
|
||||||
|
d = DetourProperty<P, T>(property);
|
||||||
|
} catch (AmbiguousMatchException) {
|
||||||
|
throw new DetourException("Unable to find {0} on type {1}".
|
||||||
|
F(name, typeof(P).FullName));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pt.IsValueType || (pt.IsByRef && (pt.GetElementType()?.IsValueType ??
|
||||||
|
false)))
|
||||||
|
throw new ArgumentException("For accessing struct fields, use DetourStructField");
|
||||||
|
d = DetourField<P, T>(field);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates dynamic detour methods to wrap a base game field or property with the
|
||||||
|
/// specified name. The detour will still work even if the field is converted to a
|
||||||
|
/// source compatible property and vice versa.
|
||||||
|
///
|
||||||
|
/// This overload creates a lazy detour that only performs the expensive reflection
|
||||||
|
/// when it is first used.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||||
|
/// <typeparam name="T">The type of the field or property element.</typeparam>
|
||||||
|
/// <param name="name">The name of the field or property to be accessed.</param>
|
||||||
|
/// <returns>A detour element that wraps the field or property with common getter and
|
||||||
|
/// setter delegates which will work on both types.</returns>
|
||||||
|
public static IDetouredField<P, T> DetourFieldLazy<P, T>(string name) {
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
return new LazyDetouredField<P, T>(typeof(P), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates dynamic detour methods to wrap a base game field with the specified name.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||||
|
/// <typeparam name="T">The type of the field element.</typeparam>
|
||||||
|
/// <param name="target">The field which will be accessed.</param>
|
||||||
|
/// <returns>A detour element that wraps the field with a common interface matching
|
||||||
|
/// that of a detoured property.</returns>
|
||||||
|
private static IDetouredField<P, T> DetourField<P, T>(FieldInfo target) {
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
var parentType = target.DeclaringType;
|
||||||
|
string name = target.Name;
|
||||||
|
if (parentType != typeof(P))
|
||||||
|
throw new ArgumentException("Parent type does not match delegate to be created");
|
||||||
|
var getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||||
|
typeof(P)
|
||||||
|
}, true);
|
||||||
|
var generator = getter.GetILGenerator();
|
||||||
|
// Getter will load the first argument and use ldfld/ldsfld
|
||||||
|
if (target.IsStatic)
|
||||||
|
generator.Emit(OpCodes.Ldsfld, target);
|
||||||
|
else {
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Ldfld, target);
|
||||||
|
}
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
DynamicMethod setter;
|
||||||
|
if (target.IsInitOnly)
|
||||||
|
// Handle readonly fields
|
||||||
|
setter = null;
|
||||||
|
else {
|
||||||
|
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||||
|
typeof(P), typeof(T)
|
||||||
|
}, true);
|
||||||
|
generator = setter.GetILGenerator();
|
||||||
|
// Setter will load both arguments and use stfld/stsfld (argument 1 is ignored
|
||||||
|
// for static fields)
|
||||||
|
if (target.IsStatic) {
|
||||||
|
generator.Emit(OpCodes.Ldarg_1);
|
||||||
|
generator.Emit(OpCodes.Stsfld, target);
|
||||||
|
} else {
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Ldarg_1);
|
||||||
|
generator.Emit(OpCodes.Stfld, target);
|
||||||
|
}
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Created delegate for field {0}.{1} with type {2}".
|
||||||
|
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||||
|
#endif
|
||||||
|
return new DetouredField<P, T>(name, getter.CreateDelegate(typeof(Func<P, T>)) as
|
||||||
|
Func<P, T>, setter?.CreateDelegate(typeof(Action<P, T>)) as Action<P, T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates dynamic detour methods to wrap a base game property with the specified name.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||||
|
/// <typeparam name="T">The type of the property element.</typeparam>
|
||||||
|
/// <param name="target">The property which will be accessed.</param>
|
||||||
|
/// <returns>A detour element that wraps the property with a common interface matching
|
||||||
|
/// that of a detoured field.</returns>
|
||||||
|
/// <exception cref="DetourException">If the property has indexers.</exception>
|
||||||
|
private static IDetouredField<P, T> DetourProperty<P, T>(PropertyInfo target) {
|
||||||
|
if (target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
var parentType = target.DeclaringType;
|
||||||
|
string name = target.Name;
|
||||||
|
if (parentType != typeof(P))
|
||||||
|
throw new ArgumentException("Parent type does not match delegate to be created");
|
||||||
|
var indexes = target.GetIndexParameters();
|
||||||
|
if (indexes != null && indexes.Length > 0)
|
||||||
|
throw new DetourException("Cannot detour on properties with index arguments");
|
||||||
|
DynamicMethod getter, setter;
|
||||||
|
var getMethod = target.GetGetMethod(true);
|
||||||
|
if (target.CanRead && getMethod != null) {
|
||||||
|
getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||||
|
typeof(P)
|
||||||
|
}, true);
|
||||||
|
var generator = getter.GetILGenerator();
|
||||||
|
// Getter will load the first argument and call the property getter
|
||||||
|
if (!getMethod.IsStatic)
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Call, getMethod);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
} else
|
||||||
|
getter = null;
|
||||||
|
var setMethod = target.GetSetMethod(true);
|
||||||
|
if (target.CanWrite && setMethod != null) {
|
||||||
|
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||||
|
typeof(P), typeof(T)
|
||||||
|
}, true);
|
||||||
|
var generator = setter.GetILGenerator();
|
||||||
|
// Setter will load both arguments and call property setter (argument 1 is
|
||||||
|
// ignored for static properties)
|
||||||
|
if (!setMethod.IsStatic)
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Ldarg_1);
|
||||||
|
generator.Emit(OpCodes.Call, setMethod);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
} else
|
||||||
|
setter = null;
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Created delegate for property {0}.{1} with type {2}".
|
||||||
|
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||||
|
#endif
|
||||||
|
return new DetouredField<P, T>(name, getter?.CreateDelegate(typeof(Func<P, T>)) as
|
||||||
|
Func<P, T>, setter?.CreateDelegate(typeof(Action<P, T>)) as Action<P, T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates dynamic detour methods to wrap a base game struct field with the specified
|
||||||
|
/// name. For static struct fields, use the regular DetourField.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the field element.</typeparam>
|
||||||
|
/// <param name="parentType">The struct type which will be accessed.</param>
|
||||||
|
/// <param name="name">The name of the struct field to be accessed.</param>
|
||||||
|
/// <returns>A detour element that wraps the field with a common interface matching
|
||||||
|
/// that of a detoured property.</returns>
|
||||||
|
public static IDetouredField<object, T> DetourStructField<T>(this Type parentType,
|
||||||
|
string name) {
|
||||||
|
if (parentType == null)
|
||||||
|
throw new ArgumentNullException(nameof(parentType));
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
var target = parentType.GetField(name, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||||
|
Instance);
|
||||||
|
if (target == null)
|
||||||
|
throw new DetourException("Unable to find {0} on type {1}".F(name, parentType.
|
||||||
|
FullName));
|
||||||
|
var getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||||
|
typeof(object)
|
||||||
|
}, true);
|
||||||
|
var generator = getter.GetILGenerator();
|
||||||
|
// Getter will load the first argument, unbox. and use ldfld
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Unbox, parentType);
|
||||||
|
generator.Emit(OpCodes.Ldfld, target);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
DynamicMethod setter;
|
||||||
|
if (target.IsInitOnly)
|
||||||
|
// Handle readonly fields
|
||||||
|
setter = null;
|
||||||
|
else {
|
||||||
|
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||||
|
typeof(object), typeof(T)
|
||||||
|
}, true);
|
||||||
|
generator = setter.GetILGenerator();
|
||||||
|
// Setter will load both arguments, unbox and use stfld
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
generator.Emit(OpCodes.Unbox, parentType);
|
||||||
|
generator.Emit(OpCodes.Ldarg_1);
|
||||||
|
generator.Emit(OpCodes.Stfld, target);
|
||||||
|
generator.Emit(OpCodes.Ret);
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Created delegate for struct field {0}.{1} with type {2}".
|
||||||
|
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||||
|
#endif
|
||||||
|
return new DetouredField<object, T>(name, getter.CreateDelegate(typeof(
|
||||||
|
Func<object, T>)) as Func<object, T>, setter?.CreateDelegate(typeof(
|
||||||
|
Action<object, T>)) as Action<object, T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the required method parameters for the dynamic detour method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="caller">The method where the parameters will be defined.</param>
|
||||||
|
/// <param name="actualParams">The actual parameters required.</param>
|
||||||
|
/// <param name="expectedParams">The parameters provided.</param>
|
||||||
|
/// <param name="offset">The offset to start loading (0 = static, 1 = instance).</param>
|
||||||
|
private static void FinishDynamicMethod(DynamicMethod caller,
|
||||||
|
ParameterInfo[] actualParams, Type[] expectedParams, int offset) {
|
||||||
|
int n = expectedParams.Length;
|
||||||
|
if (offset > 0)
|
||||||
|
caller.DefineParameter(1, ParameterAttributes.None, "this");
|
||||||
|
for (int i = offset; i < n; i++) {
|
||||||
|
var oldParam = actualParams[i - offset];
|
||||||
|
caller.DefineParameter(i + 1, oldParam.Attributes, oldParam.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates instructions to load arguments or default values onto the stack in a
|
||||||
|
/// detour method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="generator">The method where the calls will be added.</param>
|
||||||
|
/// <param name="actualParams">The actual parameters required.</param>
|
||||||
|
/// <param name="expectedParams">The parameters provided.</param>
|
||||||
|
/// <param name="offset">The offset to start loading (0 = static, 1 = instance).</param>
|
||||||
|
private static void LoadParameters(ILGenerator generator, ParameterInfo[] actualParams,
|
||||||
|
Type[] expectedParams, int offset) {
|
||||||
|
// Load the known method arguments onto the stack
|
||||||
|
int n = expectedParams.Length, need = actualParams.Length + offset;
|
||||||
|
if (n > 0)
|
||||||
|
generator.Emit(OpCodes.Ldarg_0);
|
||||||
|
if (n > 1)
|
||||||
|
generator.Emit(OpCodes.Ldarg_1);
|
||||||
|
if (n > 2)
|
||||||
|
generator.Emit(OpCodes.Ldarg_2);
|
||||||
|
if (n > 3)
|
||||||
|
generator.Emit(OpCodes.Ldarg_3);
|
||||||
|
for (int i = 4; i < n; i++)
|
||||||
|
generator.Emit(OpCodes.Ldarg_S, i);
|
||||||
|
// Load the rest as defaults
|
||||||
|
for (int i = n; i < need; i++) {
|
||||||
|
var param = actualParams[i - offset];
|
||||||
|
PTranspilerTools.GenerateDefaultLoad(generator, param.ParameterType, param.
|
||||||
|
DefaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the delegate signature provided in dst can be dynamically mapped to
|
||||||
|
/// the method provided by src, with the possible addition of optional parameters set
|
||||||
|
/// to their default values.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expected">The method return type and parameter types expected.</param>
|
||||||
|
/// <param name="actual">The method to be called.</param>
|
||||||
|
/// <param name="actualReturn">The type of the method or constructor's return value.</param>
|
||||||
|
/// <returns>The parameters used in the call to the actual method.</returns>
|
||||||
|
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||||
|
private static ParameterInfo[] ValidateDelegate(DelegateInfo expected,
|
||||||
|
MethodBase actual, Type actualReturn) {
|
||||||
|
var parameterTypes = expected.parameterTypes;
|
||||||
|
var returnType = expected.returnType;
|
||||||
|
// Validate return types
|
||||||
|
if (!returnType.IsAssignableFrom(actualReturn))
|
||||||
|
throw new DetourException("Return type {0} cannot be converted to type {1}".
|
||||||
|
F(actualReturn.FullName, returnType.FullName));
|
||||||
|
// Do not allow methods declared in not yet closed generic types
|
||||||
|
var baseType = actual.DeclaringType;
|
||||||
|
if (baseType == null)
|
||||||
|
throw new ArgumentException("Method is not declared by an actual type");
|
||||||
|
if (baseType.ContainsGenericParameters)
|
||||||
|
throw new DetourException(("Method parent type {0} must have all " +
|
||||||
|
"generic parameters defined").F(baseType.FullName));
|
||||||
|
// Validate parameter types
|
||||||
|
string actualName = baseType.FullName + "." + actual.Name;
|
||||||
|
var actualParams = actual.GetParameters();
|
||||||
|
int n = actualParams.Length, check = parameterTypes.Length;
|
||||||
|
Type[] actualParamTypes, currentTypes = new Type[n];
|
||||||
|
bool noThisPointer = actual.IsStatic || actual.IsConstructor;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
currentTypes[i] = actualParams[i].ParameterType;
|
||||||
|
if (noThisPointer)
|
||||||
|
actualParamTypes = currentTypes;
|
||||||
|
else {
|
||||||
|
actualParamTypes = PTranspilerTools.PushDeclaringType(currentTypes, baseType);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (check > n)
|
||||||
|
throw new DetourException(("Method {0} has only {1:D} parameters, but " +
|
||||||
|
"{2:D} were supplied").F(actual.ToString(), n, check));
|
||||||
|
// Check up to the number we have
|
||||||
|
for (int i = 0; i < check; i++) {
|
||||||
|
Type have = actualParamTypes[i], want = parameterTypes[i];
|
||||||
|
if (!have.IsAssignableFrom(want))
|
||||||
|
throw new DetourException(("Argument {0:D} for method {3} cannot be " +
|
||||||
|
"converted from {1} to {2}").F(i, have.FullName, want.FullName,
|
||||||
|
actualName));
|
||||||
|
}
|
||||||
|
// Any remaining parameters must be optional
|
||||||
|
int offset = noThisPointer ? 0 : 1;
|
||||||
|
for (int i = check; i < n; i++) {
|
||||||
|
var cParam = actualParams[i - offset];
|
||||||
|
if (!cParam.IsOptional)
|
||||||
|
throw new DetourException(("New argument {0:D} for method {1} ({2}) " +
|
||||||
|
"is not optional").F(i, actualName, cParam.ParameterType.FullName));
|
||||||
|
}
|
||||||
|
return actualParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores information about a delegate.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class DelegateInfo {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates delegate information on the specified delegate type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delegateType">The delegate type to wrap.</param>
|
||||||
|
/// <returns>Information about that delegate's return and parameter types.</returns>
|
||||||
|
public static DelegateInfo Create(Type delegateType) {
|
||||||
|
if (delegateType == null)
|
||||||
|
throw new ArgumentNullException(nameof(delegateType));
|
||||||
|
var expected = delegateType.GetMethodSafe("Invoke", false, PPatchTools.
|
||||||
|
AnyArguments);
|
||||||
|
if (expected == null)
|
||||||
|
throw new ArgumentException("Invalid delegate type: " + delegateType);
|
||||||
|
return new DelegateInfo(delegateType, expected.GetParameterTypes(),
|
||||||
|
expected.ReturnType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate's type.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Type delegateType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate's parameter types.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Type[] parameterTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate's return types.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Type returnType;
|
||||||
|
|
||||||
|
private DelegateInfo(Type delegateType, Type[] parameterTypes, Type returnType) {
|
||||||
|
this.delegateType = delegateType;
|
||||||
|
this.parameterTypes = parameterTypes;
|
||||||
|
this.returnType = returnType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "DelegateInfo[delegate={0},return={1},parameters={2}]".F(delegateType,
|
||||||
|
returnType, parameterTypes.Join());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,285 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods to make life easier!
|
||||||
|
/// </summary>
|
||||||
|
public static class ExtensionMethods {
|
||||||
|
/// <summary>
|
||||||
|
/// Shorthand for string.Format() which can be invoked directly on the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The format template message.</param>
|
||||||
|
/// <param name="args">The substitutions to be included.</param>
|
||||||
|
/// <returns>The formatted string.</returns>
|
||||||
|
public static string F(this string message, params object[] args) {
|
||||||
|
return string.Format(message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a component, but returns null if the GameObject is disposed.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The component type to retrieve.</typeparam>
|
||||||
|
/// <param name="obj">The GameObject that hosts the component.</param>
|
||||||
|
/// <returns>The requested component, or null if it does not exist</returns>
|
||||||
|
public static T GetComponentSafe<T>(this GameObject obj) where T : Component {
|
||||||
|
#pragma warning disable IDE0031 // Use null propagation
|
||||||
|
// == operator is overloaded on GameObject to be equal to null if destroyed
|
||||||
|
return (obj == null) ? null : obj.GetComponent<T>();
|
||||||
|
#pragma warning restore IDE0031 // Use null propagation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the assembly name of an assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to query.</param>
|
||||||
|
/// <returns>The assembly name, or null if assembly is null.</returns>
|
||||||
|
public static string GetNameSafe(this Assembly assembly) {
|
||||||
|
return assembly?.GetName()?.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the file version of the specified assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to query</param>
|
||||||
|
/// <returns>The AssemblyFileVersion of that assembly, or null if it could not be determined.</returns>
|
||||||
|
public static string GetFileVersion(this Assembly assembly) {
|
||||||
|
// Mod version
|
||||||
|
var fileVersions = assembly.GetCustomAttributes(typeof(
|
||||||
|
AssemblyFileVersionAttribute), true);
|
||||||
|
string modVersion = null;
|
||||||
|
if (fileVersions != null && fileVersions.Length > 0) {
|
||||||
|
// Retrieves the "File Version" attribute
|
||||||
|
var assemblyFileVersion = (AssemblyFileVersionAttribute)fileVersions[0];
|
||||||
|
if (assemblyFileVersion != null)
|
||||||
|
modVersion = assemblyFileVersion.Version;
|
||||||
|
}
|
||||||
|
return modVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Coerces a floating point number into the specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The original number.</param>
|
||||||
|
/// <param name="min">The minimum value (inclusive).</param>
|
||||||
|
/// <param name="max">The maximum value (inclusive).</param>
|
||||||
|
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||||
|
public static double InRange(this double value, double min, double max) {
|
||||||
|
double result = value;
|
||||||
|
if (result < min) result = min;
|
||||||
|
if (result > max) result = max;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Coerces a floating point number into the specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The original number.</param>
|
||||||
|
/// <param name="min">The minimum value (inclusive).</param>
|
||||||
|
/// <param name="max">The maximum value (inclusive).</param>
|
||||||
|
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||||
|
public static float InRange(this float value, float min, float max) {
|
||||||
|
float result = value;
|
||||||
|
if (result < min) result = min;
|
||||||
|
if (result > max) result = max;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Coerces an integer into the specified range.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The original number.</param>
|
||||||
|
/// <param name="min">The minimum value (inclusive).</param>
|
||||||
|
/// <param name="max">The maximum value (inclusive).</param>
|
||||||
|
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||||
|
public static int InRange(this int value, int min, int max) {
|
||||||
|
int result = value;
|
||||||
|
if (result < min) result = min;
|
||||||
|
if (result > max) result = max;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if an object is falling.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The object to check.</param>
|
||||||
|
/// <returns>true if it is falling, or false otherwise.</returns>
|
||||||
|
public static bool IsFalling(this GameObject obj) {
|
||||||
|
int cell = Grid.PosToCell(obj);
|
||||||
|
return obj.TryGetComponent(out Navigator navigator) && !navigator.IsMoving() &&
|
||||||
|
Grid.IsValidCell(cell) && Grid.IsValidCell(Grid.CellBelow(cell)) &&
|
||||||
|
!navigator.NavGrid.NavTable.IsValid(cell, navigator.CurrentNavType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if a floating point value is NaN or infinite.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to check.</param>
|
||||||
|
/// <returns>true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise.</returns>
|
||||||
|
public static bool IsNaNOrInfinity(this double value) {
|
||||||
|
return double.IsNaN(value) || double.IsInfinity(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if a floating point value is NaN or infinite.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to check.</param>
|
||||||
|
/// <returns>true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise.</returns>
|
||||||
|
public static bool IsNaNOrInfinity(this float value) {
|
||||||
|
return float.IsNaN(value) || float.IsInfinity(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if a building is usable.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="building">The building component to check.</param>
|
||||||
|
/// <returns>true if it is usable (enabled, not broken, not overheated), or false otherwise.</returns>
|
||||||
|
public static bool IsUsable(this GameObject building) {
|
||||||
|
return building.TryGetComponent(out Operational op) && op.IsFunctional;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a string joining the members of an enumerable.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The values to join.</param>
|
||||||
|
/// <param name="delimiter">The delimiter to use between values.</param>
|
||||||
|
/// <returns>A string consisting of each value in order, with the delimiter in between.</returns>
|
||||||
|
public static string Join(this System.Collections.IEnumerable values,
|
||||||
|
string delimiter = ",") {
|
||||||
|
var ret = new StringBuilder(128);
|
||||||
|
bool first = true;
|
||||||
|
// Append all, but skip comma if the first time
|
||||||
|
foreach (var value in values) {
|
||||||
|
if (!first)
|
||||||
|
ret.Append(delimiter);
|
||||||
|
ret.Append(value);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
return ret.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a method manually.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance.</param>
|
||||||
|
/// <param name="type">The class to modify.</param>
|
||||||
|
/// <param name="methodName">The method to patch.</param>
|
||||||
|
/// <param name="prefix">The prefix to apply, or null if none.</param>
|
||||||
|
/// <param name="postfix">The postfix to apply, or null if none.</param>
|
||||||
|
public static void Patch(this Harmony instance, Type type, string methodName,
|
||||||
|
HarmonyMethod prefix = null, HarmonyMethod postfix = null) {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (string.IsNullOrEmpty(methodName))
|
||||||
|
throw new ArgumentNullException(nameof(methodName));
|
||||||
|
// Fetch the method
|
||||||
|
try {
|
||||||
|
var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||||
|
Static | BindingFlags.Instance);
|
||||||
|
if (method != null)
|
||||||
|
instance.Patch(method, prefix, postfix);
|
||||||
|
else
|
||||||
|
PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName,
|
||||||
|
type.FullName));
|
||||||
|
} catch (AmbiguousMatchException e) {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogWarning("When patching candidate method {0}.{1}:".F(type.FullName,
|
||||||
|
methodName));
|
||||||
|
#endif
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a constructor manually.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance.</param>
|
||||||
|
/// <param name="type">The class to modify.</param>
|
||||||
|
/// <param name="arguments">The constructor's argument types.</param>
|
||||||
|
/// <param name="prefix">The prefix to apply, or null if none.</param>
|
||||||
|
/// <param name="postfix">The postfix to apply, or null if none.</param>
|
||||||
|
public static void PatchConstructor(this Harmony instance, Type type,
|
||||||
|
Type[] arguments, HarmonyMethod prefix = null, HarmonyMethod postfix = null) {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
// Fetch the constructor
|
||||||
|
try {
|
||||||
|
var cons = type.GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Static |
|
||||||
|
BindingFlags.Instance, null, arguments, null);
|
||||||
|
if (cons != null)
|
||||||
|
instance.Patch(cons, prefix, postfix);
|
||||||
|
else
|
||||||
|
PUtil.LogWarning("Unable to find constructor on type {0}".F(type.
|
||||||
|
FullName));
|
||||||
|
} catch (ArgumentException e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a method manually with a transpiler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance.</param>
|
||||||
|
/// <param name="type">The class to modify.</param>
|
||||||
|
/// <param name="methodName">The method to patch.</param>
|
||||||
|
/// <param name="transpiler">The transpiler to apply.</param>
|
||||||
|
public static void PatchTranspile(this Harmony instance, Type type,
|
||||||
|
string methodName, HarmonyMethod transpiler) {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (string.IsNullOrEmpty(methodName))
|
||||||
|
throw new ArgumentNullException(nameof(methodName));
|
||||||
|
// Fetch the method
|
||||||
|
try {
|
||||||
|
var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS |
|
||||||
|
BindingFlags.Static | BindingFlags.Instance);
|
||||||
|
if (method != null)
|
||||||
|
instance.Patch(method, null, null, transpiler);
|
||||||
|
else
|
||||||
|
PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName,
|
||||||
|
type.FullName));
|
||||||
|
} catch (AmbiguousMatchException e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
PUtil.LogWarning("Unable to transpile method {0}: {1}".F(methodName,
|
||||||
|
e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a game object's parent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="child">The game object to modify.</param>
|
||||||
|
/// <param name="parent">The new parent object.</param>
|
||||||
|
/// <returns>The game object, for call chaining.</returns>
|
||||||
|
public static GameObject SetParent(this GameObject child, GameObject parent) {
|
||||||
|
if (child == null)
|
||||||
|
throw new ArgumentNullException(nameof(child));
|
||||||
|
#pragma warning disable IDE0031 // Use null propagation
|
||||||
|
child.transform.SetParent((parent == null) ? null : parent.transform, false);
|
||||||
|
#pragma warning restore IDE0031 // Use null propagation
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// An interface used for both local and remote PLib registry instances.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPLibRegistry {
|
||||||
|
/// <summary>
|
||||||
|
/// Data shared between mods in key value pairs.
|
||||||
|
/// </summary>
|
||||||
|
IDictionary<string, object> ModData { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a candidate version of a forwarded component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance of the component to add.</param>
|
||||||
|
void AddCandidateVersion(PForwardedComponent instance);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the latest version of a forwarded component of PLib (or another mod).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID to look up.</param>
|
||||||
|
/// <returns>The latest version of that component, or a forwarded proxy of the
|
||||||
|
/// component if functionality is provided by another mod.</returns>
|
||||||
|
PForwardedComponent GetLatestVersion(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the shared data for a particular component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID that holds the data.</param>
|
||||||
|
/// <returns>The shared data for components with that ID, or null if no component by
|
||||||
|
/// that name was found, or if the data is unset.</returns>
|
||||||
|
object GetSharedData(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all registered forwarded components for the given ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID to look up.</param>
|
||||||
|
/// <returns>All registered components with that ID, with forwarded proxies for any
|
||||||
|
/// whose functionality is provided by another mod.</returns>
|
||||||
|
IEnumerable<PForwardedComponent> GetAllComponents(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the shared data for a particular component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID that holds the data.</param>
|
||||||
|
/// <param name="data">The new shared data value.</param>
|
||||||
|
void SetSharedData(string id, object data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by classes which want to use the utility user menu refresh to save some
|
||||||
|
/// boilerplate code.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRefreshUserMenu {
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the user button menu in the info panel is refreshed. Since the
|
||||||
|
/// arguments are always null, no parameter is passed.
|
||||||
|
/// </summary>
|
||||||
|
void OnRefreshUserMenu();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// A library component that is forwarded across multiple assemblies, to allow only the
|
||||||
|
/// latest version available on the system to run. Provides methods to marshal some
|
||||||
|
/// objects across the assembly boundaries.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class PForwardedComponent : IComparable<PForwardedComponent> {
|
||||||
|
/// <summary>
|
||||||
|
/// The default maximum serialization depth for marshaling data.
|
||||||
|
/// </summary>
|
||||||
|
public const int MAX_DEPTH = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data stored in this object. It can be retrieved, with optional round trip
|
||||||
|
/// serialization, by the instantiated version of this component.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual object InstanceData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID used by PLib for this component.
|
||||||
|
///
|
||||||
|
/// This method is non-virtual for a reason, as the ID is sometimes only available
|
||||||
|
/// on methods of type object, so GetType().FullName is used directly there.
|
||||||
|
/// </summary>
|
||||||
|
public string ID {
|
||||||
|
get {
|
||||||
|
return GetType().FullName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The JSON serialization settings to be used if the Data is marshaled across
|
||||||
|
/// assembly boundaries.
|
||||||
|
/// </summary>
|
||||||
|
protected JsonSerializer SerializationSettings { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the version of the component provided by this assembly.
|
||||||
|
/// </summary>
|
||||||
|
public abstract Version Version { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this object has been registered.
|
||||||
|
/// </summary>
|
||||||
|
private volatile bool registered;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes access to avoid race conditions when registering this component.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object candidateLock;
|
||||||
|
|
||||||
|
protected PForwardedComponent() {
|
||||||
|
candidateLock = new object();
|
||||||
|
InstanceData = null;
|
||||||
|
registered = false;
|
||||||
|
SerializationSettings = new JsonSerializer() {
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
|
||||||
|
Culture = System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
MaxDepth = MAX_DEPTH
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called only on the first instance of a particular component to be registered.
|
||||||
|
/// For some particular components that need very early patches, this call might be
|
||||||
|
/// required to initialize state before the rest of the forwarded components are
|
||||||
|
/// initialized. However, this call might occur on a version that is not the latest of
|
||||||
|
/// this component in the system, or on an instance that will not be instantiated or
|
||||||
|
/// initialized by the other callbacks.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||||
|
public virtual void Bootstrap(Harmony plibInstance) { }
|
||||||
|
|
||||||
|
public int CompareTo(PForwardedComponent other) {
|
||||||
|
return Version.CompareTo(other.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes this component. Only called on the version that is selected as the
|
||||||
|
/// latest.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||||
|
/// <returns>The initialized instance.</returns>
|
||||||
|
internal virtual object DoInitialize(Harmony plibInstance) {
|
||||||
|
Initialize(plibInstance);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data from this component as a specific type. Only works if the type is
|
||||||
|
/// shared across all mods (in some shared assembly's memory space) such as types in
|
||||||
|
/// System or the base game.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The data type to retrieve.</typeparam>
|
||||||
|
/// <param name="defValue">The default value if the instance data is unset.</param>
|
||||||
|
/// <returns>The data, or defValue if the instance data has not been set.</returns>
|
||||||
|
public T GetInstanceData<T>(T defValue = default) {
|
||||||
|
if (!(InstanceData is T outData))
|
||||||
|
outData = defValue;
|
||||||
|
return outData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data from this component, serialized to the specified type. The data is
|
||||||
|
/// retrieved from the base component, serialized with JSON, and reconstituted as type
|
||||||
|
/// T in the memory space of the caller.
|
||||||
|
///
|
||||||
|
/// The target type must exist and be a [JsonObject] in both this assembly and the
|
||||||
|
/// target component's assembly.
|
||||||
|
///
|
||||||
|
/// This method is somewhat slow and memory intensive, and should be used sparingly.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The data type to retrieve and into which to convert.</typeparam>
|
||||||
|
/// <param name="defValue">The default value if the instance data is unset.</param>
|
||||||
|
/// <returns>The data, or defValue if the instance data has not been set or cannot be serialized.</returns>
|
||||||
|
public T GetInstanceDataSerialized<T>(T defValue = default) {
|
||||||
|
var remoteData = InstanceData;
|
||||||
|
T result = defValue;
|
||||||
|
using (var buffer = new MemoryStream(1024)) {
|
||||||
|
try {
|
||||||
|
var writer = new StreamWriter(buffer, Encoding.UTF8);
|
||||||
|
SerializationSettings.Serialize(writer, remoteData);
|
||||||
|
writer.Flush();
|
||||||
|
buffer.Position = 0L;
|
||||||
|
var reader = new StreamReader(buffer, Encoding.UTF8);
|
||||||
|
if (SerializationSettings.Deserialize(reader, typeof(T)) is T decoded)
|
||||||
|
result = decoded;
|
||||||
|
} catch (JsonException e) {
|
||||||
|
PUtil.LogError("Unable to serialize instance data for component " + ID +
|
||||||
|
":");
|
||||||
|
PUtil.LogException(e);
|
||||||
|
result = defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the shared data between components with this ID as a specific type. Only works
|
||||||
|
/// if the type is shared across all mods (in some shared assembly's memory space) such
|
||||||
|
/// as types in System or the base game.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The data type to retrieve.</typeparam>
|
||||||
|
/// <param name="defValue">The default value if the shared data is unset.</param>
|
||||||
|
/// <returns>The data, or defValue if the shared data has not been set.</returns>
|
||||||
|
public T GetSharedData<T>(T defValue = default) {
|
||||||
|
if (!(PRegistry.Instance.GetSharedData(ID) is T outData))
|
||||||
|
outData = defValue;
|
||||||
|
return outData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the shared data between components with this ID, serialized to the specified
|
||||||
|
/// type. The shared data is retrieved, serialized with JSON, and reconstituted as type
|
||||||
|
/// T in the memory space of the caller.
|
||||||
|
///
|
||||||
|
/// The target type must exist and be a [JsonObject] in both this assembly and the
|
||||||
|
/// target component's assembly.
|
||||||
|
///
|
||||||
|
/// This method is somewhat slow and memory intensive, and should be used sparingly.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The data type to retrieve and into which to convert.</typeparam>
|
||||||
|
/// <param name="defValue">The default value if the shared data is unset.</param>
|
||||||
|
/// <returns>The data, or defValue if the shared data has not been set or cannot be serialized.</returns>
|
||||||
|
public T GetSharedDataSerialized<T>(T defValue = default) {
|
||||||
|
var remoteData = PRegistry.Instance.GetSharedData(ID);
|
||||||
|
T result = defValue;
|
||||||
|
using (var buffer = new MemoryStream(1024)) {
|
||||||
|
try {
|
||||||
|
SerializationSettings.Serialize(new StreamWriter(buffer, Encoding.UTF8),
|
||||||
|
remoteData);
|
||||||
|
buffer.Position = 0L;
|
||||||
|
if (SerializationSettings.Deserialize(new StreamReader(buffer,
|
||||||
|
Encoding.UTF8), typeof(T)) is T decoded)
|
||||||
|
result = decoded;
|
||||||
|
} catch (JsonException e) {
|
||||||
|
PUtil.LogError("Unable to serialize shared data for component " + ID +
|
||||||
|
":");
|
||||||
|
PUtil.LogException(e);
|
||||||
|
result = defValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the assembly which provides this component.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The assembly which owns this component.</returns>
|
||||||
|
public virtual Assembly GetOwningAssembly() {
|
||||||
|
return GetType().Assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes this component. Only called on the version that is selected as the
|
||||||
|
/// latest. Component initialization order is undefined, so anything relying on another
|
||||||
|
/// component cannot be used until PostInitialize.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||||
|
public abstract void Initialize(Harmony plibInstance);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the Process method on all registered components of this type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operation">The operation to pass to Process.</param>
|
||||||
|
/// <param name="args">The arguments to pass to Process.</param>
|
||||||
|
protected void InvokeAllProcess(uint operation, object args) {
|
||||||
|
var allComponents = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
if (allComponents != null)
|
||||||
|
foreach (var component in allComponents)
|
||||||
|
component.Process(operation, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a HarmonyMethod instance for manual patching using a method from this class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The method name.</param>
|
||||||
|
/// <returns>A reference to that method as a HarmonyMethod for patching.</returns>
|
||||||
|
public HarmonyMethod PatchMethod(string name) {
|
||||||
|
return new HarmonyMethod(GetType(), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes this component. Only called on the version that is selected as the
|
||||||
|
/// latest. Other components have been initialized when this method is called.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||||
|
public virtual void PostInitialize(Harmony plibInstance) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on demand by the initialized instance to run processing in all other
|
||||||
|
/// instances.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="operation">The operation to perform. The meaning of this parameter
|
||||||
|
/// varies by component.</param>
|
||||||
|
/// <param name="args">The arguments for processing.</param>
|
||||||
|
public virtual void Process(uint operation, object args) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers this component into the list of versions available for forwarding. This
|
||||||
|
/// method is thread safe. If this component instance is already registered, it will
|
||||||
|
/// not be registered again.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true if the component was registered, or false if it was already registered.</returns>
|
||||||
|
protected bool RegisterForForwarding() {
|
||||||
|
bool result = false;
|
||||||
|
lock (candidateLock) {
|
||||||
|
if (!registered) {
|
||||||
|
PUtil.InitLibrary(false);
|
||||||
|
PRegistry.Instance.AddCandidateVersion(this);
|
||||||
|
registered = result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the shared data between components with this ID. Only works if the type is
|
||||||
|
/// shared across all mods (in some shared assembly's memory space) such as types in
|
||||||
|
/// System or the base game.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The new value for the shared data.</param>
|
||||||
|
public void SetSharedData(object value) {
|
||||||
|
PRegistry.Instance.SetSharedData(ID, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Utility and helper functions to perform common game-related (not UI) tasks.
|
||||||
|
/// </summary>
|
||||||
|
public static class PGameUtils {
|
||||||
|
/// <summary>
|
||||||
|
/// Centers and selects an entity.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">The entity to center and focus.</param>
|
||||||
|
public static void CenterAndSelect(KMonoBehaviour entity) {
|
||||||
|
if (entity != null && entity.TryGetComponent(out KSelectable select))
|
||||||
|
SelectTool.Instance.SelectAndFocus(entity.transform.position, select, Vector3.
|
||||||
|
zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the sounds from one animation to another animation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dstAnim">The destination anim file name.</param>
|
||||||
|
/// <param name="srcAnim">The source anim file name.</param>
|
||||||
|
public static void CopySoundsToAnim(string dstAnim, string srcAnim) {
|
||||||
|
if (string.IsNullOrEmpty(dstAnim))
|
||||||
|
throw new ArgumentNullException(nameof(dstAnim));
|
||||||
|
if (string.IsNullOrEmpty(srcAnim))
|
||||||
|
throw new ArgumentNullException(nameof(srcAnim));
|
||||||
|
var anim = Assets.GetAnim(dstAnim);
|
||||||
|
if (anim != null) {
|
||||||
|
var audioSheet = GameAudioSheets.Get();
|
||||||
|
var animData = anim.GetData();
|
||||||
|
// For each anim in the kanim, look for existing sound events under the old
|
||||||
|
// anim's file name
|
||||||
|
for (int i = 0; i < animData.animCount; i++) {
|
||||||
|
string animName = animData.GetAnim(i)?.name ?? "";
|
||||||
|
var events = audioSheet.GetEvents(srcAnim + "." + animName);
|
||||||
|
if (events != null) {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Adding {0:D} audio event(s) to anim {1}.{2}".F(events.
|
||||||
|
Count, dstAnim, animName));
|
||||||
|
#endif
|
||||||
|
audioSheet.events[dstAnim + "." + animName] = events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
PUtil.LogWarning("Destination animation \"{0}\" not found!".F(dstAnim));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a popup message at the specified cell location on the Move layer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The image to display, likely from PopFXManager.Instance.</param>
|
||||||
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="cell">The cell location to create the message.</param>
|
||||||
|
public static void CreatePopup(Sprite image, string text, int cell) {
|
||||||
|
CreatePopup(image, text, Grid.CellToPosCBC(cell, Grid.SceneLayer.Move));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a popup message at the specified location.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The image to display, likely from PopFXManager.Instance.</param>
|
||||||
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="position">The position to create the message.</param>
|
||||||
|
public static void CreatePopup(Sprite image, string text, Vector3 position) {
|
||||||
|
PopFXManager.Instance.SpawnFX(image, text, null, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a default user menu handler for a class implementing IRefreshUserMenu.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The class to handle events.</typeparam>
|
||||||
|
/// <returns>A handler which can be used to Subscribe for RefreshUserMenu events.</returns>
|
||||||
|
public static EventSystem.IntraObjectHandler<T> CreateUserMenuHandler<T>()
|
||||||
|
where T : Component, IRefreshUserMenu {
|
||||||
|
return new Action<T, object>((T target, object ignore) => {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("OnRefreshUserMenu<{0}> on {1}".F(typeof(T).Name, target));
|
||||||
|
#endif
|
||||||
|
target.OnRefreshUserMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves an object layer by its name, resolving the value at runtime to handle
|
||||||
|
/// differences in the layer enum. This method is slower than a direct lookup -
|
||||||
|
/// consider caching the result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the layer (use nameof()!)</param>
|
||||||
|
/// <param name="defValue">The default value (use the value at compile time)</param>
|
||||||
|
/// <returns>The value to use for this object layer.</returns>
|
||||||
|
public static ObjectLayer GetObjectLayer(string name, ObjectLayer defValue) {
|
||||||
|
if (!Enum.TryParse(name, out ObjectLayer value))
|
||||||
|
value = defValue;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Highlights an entity. Use Color.black to unhighlight it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">The entity to highlight.</param>
|
||||||
|
/// <param name="highlightColor">The color to highlight it.</param>
|
||||||
|
public static void HighlightEntity(Component entity, Color highlightColor) {
|
||||||
|
if (entity != null && entity.TryGetComponent(out KAnimControllerBase kbac))
|
||||||
|
kbac.HighlightColour = highlightColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plays a sound effect.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The sound effect name to play.</param>
|
||||||
|
/// <param name="position">The position where the sound is generated.</param>
|
||||||
|
public static void PlaySound(string name, Vector3 position) {
|
||||||
|
SoundEvent.PlayOneShot(GlobalAssets.GetSound(name), position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the current list of mods.
|
||||||
|
/// </summary>
|
||||||
|
public static void SaveMods() {
|
||||||
|
Global.Instance.modManager.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Core</Title>
|
||||||
|
<AssemblyTitle>PLib.Core</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.Core</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||||
|
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibCore.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Utils\**" />
|
||||||
|
<EmbeddedResource Remove="Utils\**" />
|
||||||
|
<None Remove="Utils\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// A small component which applies core patches used by PLib.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PLibCorePatches : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Localizes all mods to the current locale.
|
||||||
|
/// </summary>
|
||||||
|
private static void Initialize_Postfix() {
|
||||||
|
var locale = Localization.GetLocale();
|
||||||
|
if (locale != null) {
|
||||||
|
int n = 0;
|
||||||
|
string locCode = locale.Code;
|
||||||
|
if (string.IsNullOrEmpty(locCode))
|
||||||
|
locCode = Localization.GetCurrentLanguageCode();
|
||||||
|
var libLocal = PRegistry.Instance.GetAllComponents(typeof(PLibCorePatches).
|
||||||
|
FullName);
|
||||||
|
if (libLocal != null)
|
||||||
|
foreach (var component in libLocal)
|
||||||
|
component.Process(0, locale);
|
||||||
|
var modLocal = PRegistry.Instance.GetAllComponents(
|
||||||
|
"PeterHan.PLib.Database.PLocalization");
|
||||||
|
if (modLocal != null)
|
||||||
|
foreach (var component in modLocal) {
|
||||||
|
component.Process(0, locale);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (n > 0)
|
||||||
|
PRegistry.LogPatchDebug("Localized {0:D} mod(s) to locale {1}".F(
|
||||||
|
n, locCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<CodeInstruction> LoadPreviewImage_Transpile(
|
||||||
|
IEnumerable<CodeInstruction> body) {
|
||||||
|
var target = typeof(Debug).GetMethodSafe(nameof(Debug.LogFormat), true,
|
||||||
|
typeof(string), typeof(object[]));
|
||||||
|
return (target == null) ? body : PPatchTools.RemoveMethodCall(body, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
UI.TextMeshProPatcher.Patch();
|
||||||
|
var ugc = PPatchTools.GetTypeSafe("SteamUGCService", "Assembly-CSharp");
|
||||||
|
if (ugc != null)
|
||||||
|
try {
|
||||||
|
plibInstance.PatchTranspile(ugc, "LoadPreviewImage", PatchMethod(nameof(
|
||||||
|
LoadPreviewImage_Transpile)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
plibInstance.Patch(typeof(Localization), nameof(Localization.Initialize),
|
||||||
|
postfix: PatchMethod(nameof(Initialize_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object _) {
|
||||||
|
var locale = Localization.GetLocale();
|
||||||
|
if (locale != null && operation == 0)
|
||||||
|
PLibLocalization.LocalizeItself(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers this instance of the PLib core patches.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The registry instance to use (since PRegistry.Instance
|
||||||
|
/// is not yet fully initialized).</param>
|
||||||
|
internal void Register(IPLibRegistry instance) {
|
||||||
|
if (instance == null)
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
instance.AddCandidateVersion(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Handles localization of PLib for mods by automatically loading po files stored as
|
||||||
|
/// EmbeddedResources in PLibCore.dll and ILMerged with the mod assembly.
|
||||||
|
/// </summary>
|
||||||
|
public static class PLibLocalization {
|
||||||
|
/// <summary>
|
||||||
|
/// The file extension used for localization files.
|
||||||
|
/// </summary>
|
||||||
|
public const string TRANSLATIONS_EXT = ".po";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Prefix of LogicalName of EmbeddedResources that stores the content of po files.
|
||||||
|
/// Must match the specified value in the Directory.Build.targets file.
|
||||||
|
/// </summary>
|
||||||
|
private const string TRANSLATIONS_RES_PATH = "PeterHan.PLib.Core.PLibStrings.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Localizes the PLib strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="locale">The locale to use.</param>
|
||||||
|
internal static void LocalizeItself(Localization.Locale locale) {
|
||||||
|
if (locale == null)
|
||||||
|
throw new ArgumentNullException(nameof(locale));
|
||||||
|
Localization.RegisterForTranslation(typeof(PLibStrings));
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
string locCode = locale.Code;
|
||||||
|
if (string.IsNullOrEmpty(locCode))
|
||||||
|
locCode = Localization.GetCurrentLanguageCode();
|
||||||
|
try {
|
||||||
|
using (var stream = assembly.GetManifestResourceStream(
|
||||||
|
TRANSLATIONS_RES_PATH + locCode + TRANSLATIONS_EXT)) {
|
||||||
|
if (stream != null) {
|
||||||
|
// File.ReadAllLines does not work on streams unfortunately
|
||||||
|
var lines = new List<string>(128);
|
||||||
|
using (var reader = new StreamReader(stream, Encoding.UTF8)) {
|
||||||
|
string line;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
lines.Add(line);
|
||||||
|
}
|
||||||
|
Localization.OverloadStrings(Localization.ExtractTranslatedStrings(
|
||||||
|
lines.ToArray()));
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("Localizing PLib Core to locale {0}".F(locCode));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
PUtil.LogWarning("Failed to load {0} localization for PLib Core:".F(locCode));
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Strings used in PLib.
|
||||||
|
/// </summary>
|
||||||
|
public static class PLibStrings {
|
||||||
|
/// <summary>
|
||||||
|
/// The button used to manually edit the mod configuration.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString BUTTON_MANUAL = "MANUAL CONFIG";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The button used to reset the configuration to its default value.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString BUTTON_RESET = "RESET TO DEFAULT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The text shown on the Done button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString BUTTON_OK = STRINGS.UI.FRONTEND.OPTIONS_SCREEN.BACK;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The text shown on the Options button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString BUTTON_OPTIONS = STRINGS.UI.FRONTEND.MAINMENU.OPTIONS;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The dialog title used for options, where {0} is substituted with the mod friendly name.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString DIALOG_TITLE = "Options for {0}";
|
||||||
|
|
||||||
|
// Utility key names
|
||||||
|
public static LocString KEY_HOME = "Home";
|
||||||
|
public static LocString KEY_END = "End";
|
||||||
|
public static LocString KEY_DELETE = "Delete";
|
||||||
|
public static LocString KEY_PAGEUP = "Page Up";
|
||||||
|
public static LocString KEY_PAGEDOWN = "Page Down";
|
||||||
|
public static LocString KEY_SYSRQ = "SysRq";
|
||||||
|
public static LocString KEY_PRTSCREEN = "Print Screen";
|
||||||
|
public static LocString KEY_PAUSE = "Pause";
|
||||||
|
|
||||||
|
// Arrow key names
|
||||||
|
public static LocString KEY_ARROWLEFT = "Left Arrow";
|
||||||
|
public static LocString KEY_ARROWUP = "Up Arrow";
|
||||||
|
public static LocString KEY_ARROWRIGHT = "Right Arrow";
|
||||||
|
public static LocString KEY_ARROWDOWN = "Down Arrow";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title used for the PLib key bind category.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString KEY_CATEGORY_TITLE = "Mods";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The abbreviation text shown on the Blue field.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString LABEL_B = "B";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The abbreviation text shown on the Green field.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString LABEL_G = "G";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The abbreviation text shown on the Red field.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString LABEL_R = "R";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod version in Mod Options if retrieved from the default AssemblyVersion, where
|
||||||
|
/// {0} is substituted with the version text.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString MOD_ASSEMBLY_VERSION = "Assembly Version: {0}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The button text which goes to the mod's home page when clicked.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString MOD_HOMEPAGE = "Mod Homepage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod version in Mod Options if specified via AssemblyFileVersion, where {0} is
|
||||||
|
/// substituted with the version text.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString MOD_VERSION = "Mod Version: {0}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cancel button in the restart dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString RESTART_CANCEL = STRINGS.UI.FRONTEND.MOD_DIALOGS.RESTART.CANCEL;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The OK button in the restart dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString RESTART_OK = STRINGS.UI.FRONTEND.MOD_DIALOGS.RESTART.OK;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The details tooltip when AVC detects a mod to be outdated.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString OUTDATED_TOOLTIP = "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displayed when AVC detects a mod to be outdated.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString OUTDATED_WARNING = "<b><style=\"logic_off\">Outdated!</style></b>";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message prompting the user to restart.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString RESTART_REQUIRED = "Oxygen Not Included must be restarted " +
|
||||||
|
"for these options to take effect.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the BLUE field in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_BLUE = "Blue";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the CANCEL button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_CANCEL = "Discard changes.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the GREEN field in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_GREEN = "Green";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the Mod Homepage button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_HOMEPAGE = "Visit the mod's website.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the Hue slider in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_HUE = "Hue";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the MANUAL CONFIG button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_MANUAL = "Opens the folder containing the full mod configuration.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip for cycling to the next item.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_NEXT = "Next";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the OK button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_OK = "Save these options. Some mods may require " +
|
||||||
|
"a restart for the options to take effect.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip for cycling to the previous item.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_PREVIOUS = "Previous";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the RED field in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_RED = "Red";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the RESET TO DEFAULT button.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_RESET = "Resets the mod configuration to default values.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the Saturation slider in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_SATURATION = "Saturation";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip for each category visibility toggle.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_TOGGLE = "Show or hide this options category";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip on the Value slider in color pickers.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_VALUE = "Value";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The tooltip for the mod version.
|
||||||
|
/// </summary>
|
||||||
|
public static LocString TOOLTIP_VERSION = "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated.";
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the user facing API to the PLib Registry.
|
||||||
|
/// </summary>
|
||||||
|
public static class PRegistry {
|
||||||
|
/// <summary>
|
||||||
|
/// The singleton instance of this class.
|
||||||
|
/// </summary>
|
||||||
|
public static IPLibRegistry Instance {
|
||||||
|
get {
|
||||||
|
lock (instanceLock) {
|
||||||
|
if (instance == null)
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A pointer to the active PLib registry.
|
||||||
|
/// </summary>
|
||||||
|
private static IPLibRegistry instance = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that PLib can only be initialized by one thread at a time.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly object instanceLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a value from the single-instance share.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the desired data.</typeparam>
|
||||||
|
/// <param name="key">The string key to retrieve. <i>Suggested key format: YourMod.
|
||||||
|
/// Category.KeyName</i></param>
|
||||||
|
/// <returns>The data associated with that key.</returns>
|
||||||
|
public static T GetData<T>(string key) {
|
||||||
|
T value = default;
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
var registry = Instance.ModData;
|
||||||
|
if (registry != null && registry.TryGetValue(key, out object sval) && sval is
|
||||||
|
T newVal)
|
||||||
|
value = newVal;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the patch bootstrapper, creating a PRegistry if not yet present.
|
||||||
|
/// </summary>
|
||||||
|
private static void Init() {
|
||||||
|
const string INTENDED_NAME = "PRegistryComponent";
|
||||||
|
var obj = Global.Instance?.gameObject;
|
||||||
|
if (obj != null) {
|
||||||
|
var plr = obj.GetComponent(INTENDED_NAME);
|
||||||
|
if (plr == null) {
|
||||||
|
var localReg = obj.AddComponent<PRegistryComponent>();
|
||||||
|
// If PLib is ILMerged more than once, PRegistry gets added with a weird
|
||||||
|
// type name including a GUID which does not match GetComponent.Name!
|
||||||
|
string typeName = localReg.GetType().Name;
|
||||||
|
if (typeName != INTENDED_NAME)
|
||||||
|
LogPatchWarning(INTENDED_NAME + " has the type name " + typeName +
|
||||||
|
"; this may be the result of ILMerging PLib more than once!");
|
||||||
|
#if DEBUG
|
||||||
|
LogPatchDebug("Creating PLib Registry from " + System.Reflection.Assembly.
|
||||||
|
GetExecutingAssembly()?.FullName ?? "?");
|
||||||
|
#endif
|
||||||
|
// Patch in the bootstrap method
|
||||||
|
localReg.ApplyBootstrapper();
|
||||||
|
instance = localReg;
|
||||||
|
} else {
|
||||||
|
instance = new PRemoteRegistry(plr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#if DEBUG
|
||||||
|
LogPatchWarning("Attempted to initialize PLib Registry before Global created!");
|
||||||
|
#endif
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
if (instance != null)
|
||||||
|
new PLibCorePatches().Register(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a debug message while patching in PLib patches.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The debug message.</param>
|
||||||
|
internal static void LogPatchDebug(string message) {
|
||||||
|
Debug.LogFormat("[PLibPatches] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a warning encountered while patching in PLib patches.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The warning message.</param>
|
||||||
|
internal static void LogPatchWarning(string message) {
|
||||||
|
Debug.LogWarningFormat("[PLibPatches] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves a value into the single-instance share.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The string key to set. <i>Suggested key format: YourMod.
|
||||||
|
/// Category.KeyName</i></param>
|
||||||
|
/// <param name="value">The data to be associated with that key.</param>
|
||||||
|
public static void PutData(string key, object value) {
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
var registry = Instance.ModData;
|
||||||
|
if (registry != null) {
|
||||||
|
if (registry.ContainsKey(key))
|
||||||
|
registry[key] = value;
|
||||||
|
else
|
||||||
|
registry.Add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// A custom component added to manage shared data between mods, especially instances of
|
||||||
|
/// PForwardedComponent used by both PLib and other mods.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PRegistryComponent : MonoBehaviour, IPLibRegistry {
|
||||||
|
/// <summary>
|
||||||
|
/// The Harmony instance name used when patching via PLib.
|
||||||
|
/// </summary>
|
||||||
|
internal const string PLIB_HARMONY = "PeterHan.PLib";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A pointer to the active PLib registry.
|
||||||
|
/// </summary>
|
||||||
|
private static PRegistryComponent instance = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// true if the forwarded components have been instantiated, or false otherwise.
|
||||||
|
/// </summary>
|
||||||
|
private static bool instantiated = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the latest version of all forwarded components.
|
||||||
|
/// </summary>
|
||||||
|
private static void ApplyLatest() {
|
||||||
|
bool apply = false;
|
||||||
|
if (instance != null)
|
||||||
|
lock (instance) {
|
||||||
|
if (!instantiated)
|
||||||
|
apply = instantiated = true;
|
||||||
|
}
|
||||||
|
if (apply)
|
||||||
|
instance.Instantiate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores shared mod data which needs single instance existence. Available to all
|
||||||
|
/// PLib consumers through PLib API.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> ModData { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Harmony instance used by PLib patching.
|
||||||
|
/// </summary>
|
||||||
|
public Harmony PLibInstance { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The candidate components with versions, from multiple assemblies.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, PVersionList> forwardedComponents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The components actually instantiated (latest version of each).
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, object> instantiatedComponents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The latest versions of each component.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<string, PForwardedComponent> latestComponents;
|
||||||
|
|
||||||
|
internal PRegistryComponent() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = this;
|
||||||
|
else {
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchWarning("Multiple PLocalRegistry created!");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
ModData = new ConcurrentDictionary<string, object>(2, 64);
|
||||||
|
forwardedComponents = new ConcurrentDictionary<string, PVersionList>(2, 32);
|
||||||
|
instantiatedComponents = new ConcurrentDictionary<string, object>(2, 32);
|
||||||
|
latestComponents = new ConcurrentDictionary<string, PForwardedComponent>(2, 32);
|
||||||
|
PLibInstance = new Harmony(PLIB_HARMONY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCandidateVersion(PForwardedComponent instance) {
|
||||||
|
if (instance == null)
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
AddCandidateVersion(instance.ID, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a remote or local forwarded component by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The real ID of the component.</param>
|
||||||
|
/// <param name="instance">The candidate instance to add.</param>
|
||||||
|
private void AddCandidateVersion(string id, PForwardedComponent instance) {
|
||||||
|
var versions = forwardedComponents.GetOrAdd(id, (_) => new PVersionList());
|
||||||
|
if (versions == null)
|
||||||
|
PRegistry.LogPatchWarning("Missing version info for component type " + id);
|
||||||
|
else {
|
||||||
|
var list = versions.Components;
|
||||||
|
bool first = list.Count < 1;
|
||||||
|
list.Add(instance);
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchDebug("Candidate version of {0} from {1}".F(id, instance.
|
||||||
|
GetOwningAssembly()));
|
||||||
|
#endif
|
||||||
|
if (first)
|
||||||
|
instance.Bootstrap(PLibInstance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a bootstrapper patch which will complete forwarded component initialization
|
||||||
|
/// before mods are post-loaded.
|
||||||
|
/// </summary>
|
||||||
|
internal void ApplyBootstrapper() {
|
||||||
|
try {
|
||||||
|
PLibInstance.Patch(typeof(KMod.Mod), nameof(KMod.Mod.PostLoad), prefix:
|
||||||
|
new HarmonyMethod(typeof(PRegistryComponent), nameof(ApplyLatest)));
|
||||||
|
} catch (AmbiguousMatchException e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
} catch (ArgumentException e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
} catch (TypeLoadException e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called from other mods to add a candidate version of a particular component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The component to be added.</param>
|
||||||
|
internal void DoAddCandidateVersion(object instance) {
|
||||||
|
AddCandidateVersion(instance.GetType().FullName, new PRemoteComponent(instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called from other mods to get a list of all components with the given ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID to retrieve.</param>
|
||||||
|
/// <returns>The instantiated instance of that component, or null if no component by
|
||||||
|
/// that name was found or ever registered.</returns>
|
||||||
|
internal System.Collections.ICollection DoGetAllComponents(string id) {
|
||||||
|
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
|
||||||
|
all = null;
|
||||||
|
return all?.Components;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called from other mods to get the instantiated version of a particular component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The component ID to retrieve.</param>
|
||||||
|
/// <returns>The instantiated instance of that component, or null if no component by
|
||||||
|
/// that name was found or successfully instantiated.</returns>
|
||||||
|
internal object DoGetLatestVersion(string id) {
|
||||||
|
if (!instantiatedComponents.TryGetValue(id, out object component))
|
||||||
|
component = null;
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<PForwardedComponent> GetAllComponents(string id) {
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
|
||||||
|
all = null;
|
||||||
|
return all?.Components;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PForwardedComponent GetLatestVersion(string id) {
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
if (!latestComponents.TryGetValue(id, out PForwardedComponent remoteComponent)) {
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchWarning("Unable to find a component matching: " + id);
|
||||||
|
#endif
|
||||||
|
remoteComponent = null;
|
||||||
|
}
|
||||||
|
return remoteComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetSharedData(string id) {
|
||||||
|
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
|
||||||
|
all = null;
|
||||||
|
return all?.SharedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Goes through the forwarded components, and picks the latest version of each to
|
||||||
|
/// instantiate.
|
||||||
|
/// </summary>
|
||||||
|
public void Instantiate() {
|
||||||
|
foreach (var pair in forwardedComponents) {
|
||||||
|
// Sort value by version
|
||||||
|
var versions = pair.Value.Components;
|
||||||
|
int n = versions.Count;
|
||||||
|
if (n > 0) {
|
||||||
|
string id = pair.Key;
|
||||||
|
versions.Sort();
|
||||||
|
var component = versions[n - 1];
|
||||||
|
latestComponents.GetOrAdd(id, component);
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchDebug("Instantiating component {0} using version {1} from assembly {2}".F(
|
||||||
|
id, component.Version, component.GetOwningAssembly().FullName));
|
||||||
|
#endif
|
||||||
|
try {
|
||||||
|
instantiatedComponents.GetOrAdd(id, component?.DoInitialize(
|
||||||
|
PLibInstance));
|
||||||
|
} catch (Exception e) {
|
||||||
|
PRegistry.LogPatchWarning("Error when instantiating component " + id +
|
||||||
|
":");
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Post initialize for component compatibility
|
||||||
|
foreach (var pair in latestComponents)
|
||||||
|
try {
|
||||||
|
pair.Value.PostInitialize(PLibInstance);
|
||||||
|
} catch (Exception e) {
|
||||||
|
PRegistry.LogPatchWarning("Error when instantiating component " +
|
||||||
|
pair.Key + ":");
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSharedData(string id, object data) {
|
||||||
|
if (forwardedComponents.TryGetValue(id, out PVersionList all))
|
||||||
|
all.SharedData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return forwardedComponents.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Contains tools for dealing with state machines.
|
||||||
|
/// </summary>
|
||||||
|
public static class PStateMachines {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and initializes a new state. This method should be used in a postfix patch
|
||||||
|
/// on InitializeStates if new states are to be added.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The state machine type.</typeparam>
|
||||||
|
/// <typeparam name="I">The state machine Instance type.</typeparam>
|
||||||
|
/// <param name="sm">The base state machine.</param>
|
||||||
|
/// <param name="name">The state name.</param>
|
||||||
|
/// <returns>The new state.</returns>
|
||||||
|
public static GameStateMachine<T, I>.State CreateState<T, I>(
|
||||||
|
this GameStateMachine<T, I> sm, string name)
|
||||||
|
where T : GameStateMachine<T, I, IStateMachineTarget, object> where I :
|
||||||
|
GameStateMachine<T, I, IStateMachineTarget, object>.GameInstance {
|
||||||
|
var state = new GameStateMachine<T, I>.State();
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
name = "State";
|
||||||
|
if (sm == null)
|
||||||
|
throw new ArgumentNullException(nameof(sm));
|
||||||
|
state.defaultState = sm.GetDefaultState();
|
||||||
|
// Process any sub parameters
|
||||||
|
sm.CreateStates(state);
|
||||||
|
sm.BindState(sm.root, state, name);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and initializes a new state. This method should be used in a postfix patch
|
||||||
|
/// on InitializeStates if new states are to be added.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The state machine type.</typeparam>
|
||||||
|
/// <typeparam name="I">The state machine Instance type.</typeparam>
|
||||||
|
/// <typeparam name="M">The state machine Target type.</typeparam>
|
||||||
|
/// <param name="sm">The base state machine.</param>
|
||||||
|
/// <param name="name">The state name.</param>
|
||||||
|
/// <returns>The new state.</returns>
|
||||||
|
public static GameStateMachine<T, I, M>.State CreateState<T, I, M>(
|
||||||
|
this GameStateMachine<T, I, M> sm, string name) where M : IStateMachineTarget
|
||||||
|
where T : GameStateMachine<T, I, M, object> where I :
|
||||||
|
GameStateMachine<T, I, M, object>.GameInstance {
|
||||||
|
var state = new GameStateMachine<T, I, M>.State();
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
name = "State";
|
||||||
|
if (sm == null)
|
||||||
|
throw new ArgumentNullException(nameof(sm));
|
||||||
|
state.defaultState = sm.GetDefaultState();
|
||||||
|
// Process any sub parameters
|
||||||
|
sm.CreateStates(state);
|
||||||
|
sm.BindState(sm.root, state, name);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the existing Enter actions on a state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The state to modify.</param>
|
||||||
|
public static void ClearEnterActions(this StateMachine.BaseState state) {
|
||||||
|
if (state != null)
|
||||||
|
state.enterActions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the existing Exit actions on a state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The state to modify.</param>
|
||||||
|
public static void ClearExitActions(this StateMachine.BaseState state) {
|
||||||
|
if (state != null)
|
||||||
|
state.exitActions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the existing Transition actions on a state. Parameter transitions are not
|
||||||
|
/// affected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The state to modify.</param>
|
||||||
|
public static void ClearTransitions(this StateMachine.BaseState state) {
|
||||||
|
if (state != null)
|
||||||
|
state.transitions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,359 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// A utility class with transpiler tools.
|
||||||
|
/// </summary>
|
||||||
|
internal static class PTranspilerTools {
|
||||||
|
/// <summary>
|
||||||
|
/// The opcodes that branch control conditionally.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ISet<OpCode> BRANCH_CODES;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opcodes to load an integer onto the stack.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly OpCode[] LOAD_INT = {
|
||||||
|
OpCodes.Ldc_I4_M1, OpCodes.Ldc_I4_0, OpCodes.Ldc_I4_1, OpCodes.Ldc_I4_2,
|
||||||
|
OpCodes.Ldc_I4_3, OpCodes.Ldc_I4_4, OpCodes.Ldc_I4_5, OpCodes.Ldc_I4_6,
|
||||||
|
OpCodes.Ldc_I4_7, OpCodes.Ldc_I4_8
|
||||||
|
};
|
||||||
|
|
||||||
|
static PTranspilerTools() {
|
||||||
|
// OpCode has a GetHashCode method!
|
||||||
|
BRANCH_CODES = new HashSet<OpCode> {
|
||||||
|
OpCodes.Beq,
|
||||||
|
OpCodes.Beq_S,
|
||||||
|
OpCodes.Bge,
|
||||||
|
OpCodes.Bge_S,
|
||||||
|
OpCodes.Bge_Un,
|
||||||
|
OpCodes.Bge_Un_S,
|
||||||
|
OpCodes.Bgt,
|
||||||
|
OpCodes.Bgt_S,
|
||||||
|
OpCodes.Bgt_Un,
|
||||||
|
OpCodes.Bgt_Un_S,
|
||||||
|
OpCodes.Ble,
|
||||||
|
OpCodes.Ble_S,
|
||||||
|
OpCodes.Ble_Un,
|
||||||
|
OpCodes.Ble_Un_S,
|
||||||
|
OpCodes.Blt,
|
||||||
|
OpCodes.Blt_S,
|
||||||
|
OpCodes.Blt_Un,
|
||||||
|
OpCodes.Blt_Un_S,
|
||||||
|
OpCodes.Bne_Un,
|
||||||
|
OpCodes.Bne_Un_S,
|
||||||
|
OpCodes.Brfalse,
|
||||||
|
OpCodes.Brfalse_S,
|
||||||
|
OpCodes.Brtrue,
|
||||||
|
OpCodes.Brtrue_S,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares the method parameters and throws ArgumentException if they do not match.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="victim">The victim method.</param>
|
||||||
|
/// <param name="paramTypes">The method's parameter types.</param>
|
||||||
|
/// <param name="newMethod">The replacement method.</param>
|
||||||
|
internal static void CompareMethodParams(MethodInfo victim, Type[] paramTypes,
|
||||||
|
MethodInfo newMethod) {
|
||||||
|
var newTypes = GetParameterTypes(newMethod);
|
||||||
|
if (!newMethod.IsStatic)
|
||||||
|
newTypes = PushDeclaringType(newTypes, newMethod.DeclaringType);
|
||||||
|
if (!victim.IsStatic)
|
||||||
|
paramTypes = PushDeclaringType(paramTypes, victim.DeclaringType);
|
||||||
|
int n = paramTypes.Length;
|
||||||
|
// Argument count check
|
||||||
|
if (newTypes.Length != n)
|
||||||
|
throw new ArgumentException(("New method {0} ({1:D} arguments) does not " +
|
||||||
|
"match method {2} ({3:D} arguments)").F(newMethod.Name, newTypes.Length,
|
||||||
|
victim.Name, n));
|
||||||
|
// Argument type check
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
if (!newTypes[i].IsAssignableFrom(paramTypes[i]))
|
||||||
|
throw new ArgumentException(("Argument {0:D}: New method type {1} does " +
|
||||||
|
"not match old method type {2}").F(i, paramTypes[i].FullName,
|
||||||
|
newTypes[i].FullName));
|
||||||
|
if (!victim.ReturnType.IsAssignableFrom(newMethod.ReturnType))
|
||||||
|
throw new ArgumentException(("New method {0} (returns {1}) does not match " +
|
||||||
|
"method {2} (returns {3})").F(newMethod.Name, newMethod.
|
||||||
|
ReturnType, victim.Name, victim.ReturnType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes the specified value onto the evaluation stack. This method does not work on
|
||||||
|
/// compound value types or by-ref types, as those need a local variable. If the value
|
||||||
|
/// is DBNull.Value, then default(value) will be used instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
|
||||||
|
/// <param name="type">The type of the value to generate.</param>
|
||||||
|
/// <param name="value">The value to load.</param>
|
||||||
|
/// <returns>true if instructions were pushed (all basic types and reference types),
|
||||||
|
/// or false otherwise (by ref type or compound value type).</returns>
|
||||||
|
private static bool GenerateBasicLoad(ILGenerator generator, Type type, object value) {
|
||||||
|
bool ok = !type.IsByRef;
|
||||||
|
if (ok) {
|
||||||
|
if (type == typeof(int)) {
|
||||||
|
// int
|
||||||
|
if (value is int iVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, iVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(char)) {
|
||||||
|
// char
|
||||||
|
if (value is char cVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, cVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(short)) {
|
||||||
|
// short
|
||||||
|
if (value is short sVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, sVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(uint)) {
|
||||||
|
// uint
|
||||||
|
if (value is uint uiVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, (int)uiVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(ushort)) {
|
||||||
|
// ushort
|
||||||
|
if (value is ushort usVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, usVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(byte)) {
|
||||||
|
// byte (unsigned)
|
||||||
|
if (value is byte bVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_S, bVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(sbyte)) {
|
||||||
|
// byte (signed)
|
||||||
|
if (value is sbyte sbVal)
|
||||||
|
generator.Emit(OpCodes.Ldc_I4, sbVal);
|
||||||
|
else
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
} else if (type == typeof(bool))
|
||||||
|
// bool
|
||||||
|
generator.Emit((value is bool kVal && kVal) ? OpCodes.Ldc_I4_1 : OpCodes.
|
||||||
|
Ldc_I4_0);
|
||||||
|
else if (type == typeof(long))
|
||||||
|
// long
|
||||||
|
generator.Emit(OpCodes.Ldc_I8, (value is long lVal) ? lVal : 0L);
|
||||||
|
else if (type == typeof(ulong))
|
||||||
|
// ulong
|
||||||
|
generator.Emit(OpCodes.Ldc_I8, (value is ulong ulVal) ? (long)ulVal : 0L);
|
||||||
|
else if (type == typeof(float))
|
||||||
|
// float
|
||||||
|
generator.Emit(OpCodes.Ldc_R4, (value is float fVal) ? fVal : 0.0f);
|
||||||
|
else if (type == typeof(double))
|
||||||
|
// double
|
||||||
|
generator.Emit(OpCodes.Ldc_R8, (value is double dVal) ? dVal : 0.0);
|
||||||
|
else if (type == typeof(string))
|
||||||
|
// string
|
||||||
|
generator.Emit(OpCodes.Ldstr, (value is string sVal) ? sVal : "");
|
||||||
|
else if (type.IsPointer)
|
||||||
|
// All pointers
|
||||||
|
generator.Emit(OpCodes.Ldc_I4_0);
|
||||||
|
else if (!type.IsValueType)
|
||||||
|
// All reference types (including Nullable)
|
||||||
|
generator.Emit(OpCodes.Ldnull);
|
||||||
|
else
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a local if necessary, and generates initialization code for the default
|
||||||
|
/// value of the specified type. The resulting value ends up on the stack in a form
|
||||||
|
/// that it would be used for the method argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
|
||||||
|
/// <param name="type">The type to load and initialize.</param>
|
||||||
|
/// <param name="defaultValue">The default value to load.</param>
|
||||||
|
internal static void GenerateDefaultLoad(ILGenerator generator, Type type,
|
||||||
|
object defaultValue) {
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
if (!GenerateBasicLoad(generator, type, defaultValue)) {
|
||||||
|
// This method will fail if there are more than 255 local variables, oh no!
|
||||||
|
if (type.IsByRef) {
|
||||||
|
var baseType = type.GetElementType();
|
||||||
|
var localVariable = generator.DeclareLocal(baseType);
|
||||||
|
int index = localVariable.LocalIndex;
|
||||||
|
if (GenerateBasicLoad(generator, baseType, defaultValue))
|
||||||
|
// Reference type or basic type
|
||||||
|
generator.Emit(OpCodes.Stloc_S, index);
|
||||||
|
else {
|
||||||
|
// Value types not handled by it, ref vars cannot have a default value
|
||||||
|
generator.Emit(OpCodes.Ldloca_S, index);
|
||||||
|
generator.Emit(OpCodes.Initobj, type);
|
||||||
|
}
|
||||||
|
generator.Emit(OpCodes.Ldloca_S, index);
|
||||||
|
} else {
|
||||||
|
var localVariable = generator.DeclareLocal(type);
|
||||||
|
int index = localVariable.LocalIndex;
|
||||||
|
// Is a value type, those cannot have default values other than default()
|
||||||
|
// as it must be constant
|
||||||
|
generator.Emit(OpCodes.Ldloca_S, index);
|
||||||
|
generator.Emit(OpCodes.Initobj, type);
|
||||||
|
generator.Emit(OpCodes.Ldloc_S, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the method's parameter types.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The method to query.</param>
|
||||||
|
/// <returns>The type of each parameter of the method.</returns>
|
||||||
|
internal static Type[] GetParameterTypes(this MethodInfo method) {
|
||||||
|
if (method == null)
|
||||||
|
throw new ArgumentNullException(nameof(method));
|
||||||
|
var pm = method.GetParameters();
|
||||||
|
int n = pm.Length;
|
||||||
|
var types = new Type[n];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
types[i] = pm[i].ParameterType;
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if an instruction opcode is a branch instruction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="opcode">The opcode to check.</param>
|
||||||
|
/// <returns>true if it is a branch, or false otherwise.</returns>
|
||||||
|
internal static bool IsConditionalBranchInstruction(OpCode opcode) {
|
||||||
|
return BRANCH_CODES.Contains(opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a logger to all unhandled exceptions.
|
||||||
|
///
|
||||||
|
/// Not for production use.
|
||||||
|
/// </summary>
|
||||||
|
internal static void LogAllExceptions() {
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += OnThrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a logger to all failed assertions. The assertions will still fail, but a stack
|
||||||
|
/// trace will be printed for each failed assertion.
|
||||||
|
///
|
||||||
|
/// Not for production use.
|
||||||
|
/// </summary>
|
||||||
|
internal static void LogAllFailedAsserts() {
|
||||||
|
var inst = new Harmony("PeterHan.PLib.LogFailedAsserts");
|
||||||
|
MethodBase assert;
|
||||||
|
var handler = new HarmonyMethod(typeof(PTranspilerTools), nameof(OnAssertFailed));
|
||||||
|
try {
|
||||||
|
// Assert(bool)
|
||||||
|
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool));
|
||||||
|
if (assert != null)
|
||||||
|
inst.Patch(assert, handler);
|
||||||
|
// Assert(bool, object)
|
||||||
|
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
|
||||||
|
object));
|
||||||
|
if (assert != null)
|
||||||
|
inst.Patch(assert, handler);
|
||||||
|
// Assert(bool, object, UnityEngine.Object)
|
||||||
|
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
|
||||||
|
object), typeof(UnityEngine.Object));
|
||||||
|
if (assert != null)
|
||||||
|
inst.Patch(assert, handler);
|
||||||
|
} catch (Exception e) {
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifies a load instruction to load the specified constant, using short forms if
|
||||||
|
/// possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instruction">The instruction to modify.</param>
|
||||||
|
/// <param name="newValue">The new i4 constant to load.</param>
|
||||||
|
internal static void ModifyLoadI4(CodeInstruction instruction, int newValue) {
|
||||||
|
if (newValue >= -1 && newValue <= 8) {
|
||||||
|
// Short form: constant
|
||||||
|
instruction.opcode = LOAD_INT[newValue + 1];
|
||||||
|
instruction.operand = null;
|
||||||
|
} else if (newValue >= sbyte.MinValue && newValue <= sbyte.MaxValue) {
|
||||||
|
// Short form: -128 to 127 -- looks like Harmony has issues with emitting
|
||||||
|
// the operand as a Byte
|
||||||
|
instruction.opcode = OpCodes.Ldc_I4_S;
|
||||||
|
instruction.operand = newValue;
|
||||||
|
} else {
|
||||||
|
// Long form
|
||||||
|
instruction.opcode = OpCodes.Ldc_I4;
|
||||||
|
instruction.operand = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a failed assertion that is about to occur.
|
||||||
|
/// </summary>
|
||||||
|
internal static void OnAssertFailed(bool condition) {
|
||||||
|
if (!condition) {
|
||||||
|
Debug.LogError("Assert is about to fail:");
|
||||||
|
Debug.LogError(new System.Diagnostics.StackTrace().ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional handler for all unhandled exceptions.
|
||||||
|
/// </summary>
|
||||||
|
internal static void OnThrown(object sender, UnhandledExceptionEventArgs e) {
|
||||||
|
if (!e.IsTerminating) {
|
||||||
|
Debug.LogError("Unhandled exception on Thread " + System.Threading.Thread.
|
||||||
|
CurrentThread.Name);
|
||||||
|
if (e.ExceptionObject is Exception ex)
|
||||||
|
Debug.LogException(ex);
|
||||||
|
else
|
||||||
|
Debug.LogError(e.ExceptionObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts the declaring instance type to the front of the specified array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="types">The parameter types.</param>
|
||||||
|
/// <param name="declaringType">The type which declared this method.</param>
|
||||||
|
/// <returns>The types with declaringType inserted at the beginning.</returns>
|
||||||
|
internal static Type[] PushDeclaringType(Type[] types, Type declaringType) {
|
||||||
|
int n = types.Length;
|
||||||
|
// Allow special case of passing "this" as first static arg
|
||||||
|
var newParamTypes = new Type[n + 1];
|
||||||
|
if (declaringType.IsValueType)
|
||||||
|
declaringType = declaringType.MakeByRefType();
|
||||||
|
newParamTypes[0] = declaringType;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
newParamTypes[i + 1] = types[i];
|
||||||
|
return newParamTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Static utility functions used across mods.
|
||||||
|
/// </summary>
|
||||||
|
public static class PUtil {
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the current changelist version of the game. LU-371502 has a version of
|
||||||
|
/// 371502u.
|
||||||
|
///
|
||||||
|
/// If the version cannot be determined, returns 0.
|
||||||
|
/// </summary>
|
||||||
|
public static uint GameVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether PLib has been initialized.
|
||||||
|
/// </summary>
|
||||||
|
private static volatile bool initialized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes attempts to initialize PLib.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly object initializeLock;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The characters which are not allowed in file names.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashSet<char> INVALID_FILE_CHARS;
|
||||||
|
|
||||||
|
static PUtil() {
|
||||||
|
initialized = false;
|
||||||
|
initializeLock = new object();
|
||||||
|
INVALID_FILE_CHARS = new HashSet<char>(Path.GetInvalidFileNameChars());
|
||||||
|
GameVersion = GetGameVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a mapping of assembly names to Mod instances. Only works after all mods
|
||||||
|
/// have been loaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A mapping from assemblies to the Mod instance that owns them.</returns>
|
||||||
|
public static IDictionary<Assembly, KMod.Mod> CreateAssemblyToModTable() {
|
||||||
|
var allMods = Global.Instance?.modManager?.mods;
|
||||||
|
var result = new Dictionary<Assembly, KMod.Mod>(32);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var mod in allMods) {
|
||||||
|
var dlls = mod?.loaded_mod_data?.dlls;
|
||||||
|
if (dlls != null)
|
||||||
|
foreach (var assembly in dlls)
|
||||||
|
result[assembly] = mod;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the distance between two points.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x1">The first X coordinate.</param>
|
||||||
|
/// <param name="y1">The first Y coordinate.</param>
|
||||||
|
/// <param name="x2">The second X coordinate.</param>
|
||||||
|
/// <param name="y2">The second Y coordinate.</param>
|
||||||
|
/// <returns>The non-taxicab (straight line) distance between the points.</returns>
|
||||||
|
public static float Distance(float x1, float y1, float x2, float y2) {
|
||||||
|
float dx = x2 - x1, dy = y2 - y1;
|
||||||
|
return Mathf.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the distance between two points.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x1">The first X coordinate.</param>
|
||||||
|
/// <param name="y1">The first Y coordinate.</param>
|
||||||
|
/// <param name="x2">The second X coordinate.</param>
|
||||||
|
/// <param name="y2">The second Y coordinate.</param>
|
||||||
|
/// <returns>The non-taxicab (straight line) distance between the points.</returns>
|
||||||
|
public static double Distance(double x1, double y1, double x2, double y2) {
|
||||||
|
double dx = x2 - x1, dy = y2 - y1;
|
||||||
|
return Math.Sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the current game version from the Klei code.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The change list version of the game, or 0 if it cannot be determined.</returns>
|
||||||
|
private static uint GetGameVersion() {
|
||||||
|
/*
|
||||||
|
* KleiVersion.ChangeList is a const which is substituted at compile time; if
|
||||||
|
* accessed directly, PLib would have a version "baked in" and would never
|
||||||
|
* update depending on the game version in use.
|
||||||
|
*/
|
||||||
|
var field = PPatchTools.GetFieldSafe(typeof(KleiVersion), nameof(KleiVersion.
|
||||||
|
ChangeList), true);
|
||||||
|
uint ver = 0U;
|
||||||
|
if (field != null && field.GetValue(null) is uint newVer)
|
||||||
|
ver = newVer;
|
||||||
|
return ver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the mod directory for the specified assembly. If an archived version is
|
||||||
|
/// running, the path to that version is reported.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modDLL">The assembly used for a mod.</param>
|
||||||
|
/// <returns>The directory where the mod is currently executing.</returns>
|
||||||
|
public static string GetModPath(Assembly modDLL) {
|
||||||
|
if (modDLL == null)
|
||||||
|
throw new ArgumentNullException(nameof(modDLL));
|
||||||
|
string dir = null;
|
||||||
|
try {
|
||||||
|
dir = Directory.GetParent(modDLL.Location)?.FullName;
|
||||||
|
} catch (NotSupportedException e) {
|
||||||
|
// Guess from the Klei strings
|
||||||
|
LogExcWarn(e);
|
||||||
|
} catch (System.Security.SecurityException e) {
|
||||||
|
// Guess from the Klei strings
|
||||||
|
LogExcWarn(e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Guess from the Klei strings
|
||||||
|
LogExcWarn(e);
|
||||||
|
}
|
||||||
|
if (dir == null)
|
||||||
|
dir = Path.Combine(KMod.Manager.GetDirectory(), modDLL.GetName()?.Name ?? "");
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes PLib. While most components are initialized dynamically if used, some
|
||||||
|
/// key infrastructure must be initialized first.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logVersion">If true, the mod name and version is emitted to the log.</param>
|
||||||
|
public static void InitLibrary(bool logVersion = true) {
|
||||||
|
var assembly = Assembly.GetCallingAssembly();
|
||||||
|
lock (initializeLock) {
|
||||||
|
if (!initialized) {
|
||||||
|
initialized = true;
|
||||||
|
if (assembly != null && logVersion)
|
||||||
|
Debug.LogFormat("[PLib] Mod {0} initialized, version {1}",
|
||||||
|
assembly.GetNameSafe(), assembly.GetFileVersion() ?? "Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the file is a valid file name. If the argument contains path
|
||||||
|
/// separator characters, this method returns false, since that is not a valid file
|
||||||
|
/// name.
|
||||||
|
///
|
||||||
|
/// Null and empty file names are not valid file names.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file">The file name to check.</param>
|
||||||
|
/// <returns>true if the name could be used to name a file, or false otherwise.</returns>
|
||||||
|
public static bool IsValidFileName(string file) {
|
||||||
|
bool valid = (file != null);
|
||||||
|
if (valid) {
|
||||||
|
// Cannot contain characters in INVALID_FILE_CHARS
|
||||||
|
int len = file.Length;
|
||||||
|
for (int i = 0; i < len && valid; i++)
|
||||||
|
if (INVALID_FILE_CHARS.Contains(file[i]))
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message to the debug log.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
public static void LogDebug(object message) {
|
||||||
|
Debug.LogFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().GetNameSafe(),
|
||||||
|
message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs an error message to the debug log.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
public static void LogError(object message) {
|
||||||
|
// Cannot make a utility property or method for Assembly.GetCalling... because
|
||||||
|
// its caller would then be the assembly PLib is in, not the assembly which
|
||||||
|
// invoked LogXXX
|
||||||
|
Debug.LogErrorFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().
|
||||||
|
GetNameSafe() ?? "?", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs an exception message to the debug log.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="thrown">The exception to log.</param>
|
||||||
|
public static void LogException(Exception thrown) {
|
||||||
|
Debug.LogErrorFormat("[PLib/{0}] {1} {2} {3}", Assembly.GetCallingAssembly().
|
||||||
|
GetNameSafe() ?? "?", thrown.GetType(), thrown.Message, thrown.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs an exception message to the debug log at WARNING level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="thrown">The exception to log.</param>
|
||||||
|
public static void LogExcWarn(Exception thrown) {
|
||||||
|
Debug.LogWarningFormat("[PLib/{0}] {1} {2} {3}", Assembly.GetCallingAssembly().
|
||||||
|
GetNameSafe() ?? "?", thrown.GetType(), thrown.Message, thrown.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a warning message to the debug log.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to log.</param>
|
||||||
|
public static void LogWarning(object message) {
|
||||||
|
Debug.LogWarningFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().
|
||||||
|
GetNameSafe() ?? "?", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Measures how long the specified code takes to run. The result is logged to the
|
||||||
|
/// debug log in microseconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">The code to execute.</param>
|
||||||
|
/// <param name="header">The name used in the log to describe this code.</param>
|
||||||
|
public static void Time(System.Action code, string header = "Code") {
|
||||||
|
if (code == null)
|
||||||
|
throw new ArgumentNullException(nameof(code));
|
||||||
|
var watch = new System.Diagnostics.Stopwatch();
|
||||||
|
watch.Start();
|
||||||
|
code.Invoke();
|
||||||
|
watch.Stop();
|
||||||
|
LogDebug("{1} took {0:D} us".F(watch.ElapsedTicks * 1000000L / System.Diagnostics.
|
||||||
|
Stopwatch.Frequency, header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib {
|
||||||
|
/// <summary>
|
||||||
|
/// Used to pass the PLib version in the ILMerged assembly since the PLib version will
|
||||||
|
/// not be included in the file version.
|
||||||
|
/// </summary>
|
||||||
|
public static class PVersion {
|
||||||
|
/// <summary>
|
||||||
|
/// The PLib version.
|
||||||
|
/// </summary>
|
||||||
|
public const string VERSION = "4.11.0.0";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a list of forwarded component versions and their shared data.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PVersionList {
|
||||||
|
/// <summary>
|
||||||
|
/// The list of registered components.
|
||||||
|
/// </summary>
|
||||||
|
public List<PForwardedComponent> Components { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data shared between all components.
|
||||||
|
/// </summary>
|
||||||
|
public object SharedData { get; set; }
|
||||||
|
|
||||||
|
public PVersionList() {
|
||||||
|
Components = new List<PForwardedComponent>(32);
|
||||||
|
SharedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return Components.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// The commmon parent of [PLibPatch] and [PLibMethod].
|
||||||
|
/// </summary>
|
||||||
|
internal interface IPLibAnnotation {
|
||||||
|
/// <summary>
|
||||||
|
/// When this method is run.
|
||||||
|
/// </summary>
|
||||||
|
uint Runtime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new patch method instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The method that was attributed.</param>
|
||||||
|
/// <returns>An instance that can execute this patch.</returns>
|
||||||
|
IPatchMethodInstance CreateInstance(MethodInfo method);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Refers to a single instance of the annotation, with its annotated method.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPatchMethodInstance {
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the patch or method if the conditions are met. This method should check its
|
||||||
|
/// preconditions before executing the target.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance to use.</param>
|
||||||
|
void Run(Harmony instance);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a method that will be run by PLib at a specific time to reduce the number
|
||||||
|
/// of patches required and allow conditional integration with other mods.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||||
|
public sealed class PLibMethodAttribute : Attribute, IPLibAnnotation {
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the specified assembly to be loaded for this method to run. If RequireType
|
||||||
|
/// is null or empty, no particular types need to be defined in the assembly. The
|
||||||
|
/// assembly name is required, but the version is optional (strong named assemblies
|
||||||
|
/// can never load in ONI, since neither Unity nor Klei types are strong named...)
|
||||||
|
/// </summary>
|
||||||
|
public string RequireAssembly { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the specified type full name (not assembly qualified name) to exist for
|
||||||
|
/// this method to run. If RequireAssembly is null or empty, a type in any assembly
|
||||||
|
/// will satisfy the requirement.
|
||||||
|
/// </summary>
|
||||||
|
public string RequireType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this method is run.
|
||||||
|
/// </summary>
|
||||||
|
public uint Runtime { get; }
|
||||||
|
|
||||||
|
public PLibMethodAttribute(uint runtime) {
|
||||||
|
Runtime = runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new patch method instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The method that was attributed.</param>
|
||||||
|
/// <returns>An instance that can execute this patch.</returns>
|
||||||
|
public IPatchMethodInstance CreateInstance(MethodInfo method) {
|
||||||
|
return new PLibMethodInstance(this, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PLibMethod[RunAt={0}]".F(RunAt.ToString(Runtime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refers to a single instance of the annotation, with its annotated method.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PLibMethodInstance : IPatchMethodInstance {
|
||||||
|
/// <summary>
|
||||||
|
/// The attribute describing the method.
|
||||||
|
/// </summary>
|
||||||
|
public PLibMethodAttribute Descriptor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method to run.
|
||||||
|
/// </summary>
|
||||||
|
public MethodInfo Method { get; }
|
||||||
|
|
||||||
|
public PLibMethodInstance(PLibMethodAttribute attribute, MethodInfo method) {
|
||||||
|
Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
|
||||||
|
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the method, passing the required parameters if any.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance to use if the method wants to
|
||||||
|
/// perform a patch.</param>
|
||||||
|
public void Run(Harmony instance) {
|
||||||
|
if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
|
||||||
|
RequireType, out Type requiredType)) {
|
||||||
|
// Only runs once, no meaningful savings with a delegate
|
||||||
|
var paramTypes = Method.GetParameterTypes();
|
||||||
|
int len = paramTypes.Length;
|
||||||
|
if (len <= 0)
|
||||||
|
// No parameters, static method only
|
||||||
|
Method.Invoke(null, null);
|
||||||
|
else if (paramTypes[0] == typeof(Harmony)) {
|
||||||
|
if (len == 1)
|
||||||
|
// Harmony instance parameter
|
||||||
|
Method.Invoke(null, new object[] { instance });
|
||||||
|
else if (len == 2 && paramTypes[1] == typeof(Type))
|
||||||
|
// Type parameter
|
||||||
|
Method.Invoke(null, new object[] { instance, requiredType });
|
||||||
|
} else
|
||||||
|
PUtil.LogWarning("Invalid signature for PLibMethod - must have (), " +
|
||||||
|
"(HarmonyInstance), or (HarmonyInstance, Type)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,312 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a method that will be patched by PLib at a specific time to allow
|
||||||
|
/// conditional integration with other mods.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||||
|
public sealed class PLibPatchAttribute : Attribute, IPLibAnnotation {
|
||||||
|
/// <summary>
|
||||||
|
/// The required argument types. If null, any matching method name is patched, or an
|
||||||
|
/// exception thrown if more than one matches.
|
||||||
|
/// </summary>
|
||||||
|
public Type[] ArgumentTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If this flag is set, the patch will emit only at DEBUG level if the target method
|
||||||
|
/// is not found or matches ambiguously.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreOnFail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the method to patch.
|
||||||
|
/// </summary>
|
||||||
|
public string MethodName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of patch to apply through Harmony.
|
||||||
|
/// </summary>
|
||||||
|
public HarmonyPatchType PatchType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the specified assembly to be loaded for this method to run. If RequireType
|
||||||
|
/// is null or empty, no particular types need to be defined in the assembly. The
|
||||||
|
/// assembly name is required, but the version is optional (strong named assemblies
|
||||||
|
/// can never load in ONI, since neither Unity nor Klei types are strong named...)
|
||||||
|
/// </summary>
|
||||||
|
public string RequireAssembly { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the specified type full name (not assembly qualified name) to exist for
|
||||||
|
/// this method to run. If RequireAssembly is null or empty, a type in any assembly
|
||||||
|
/// will satisfy the requirement.
|
||||||
|
/// </summary>
|
||||||
|
public string RequireType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this method is run.
|
||||||
|
/// </summary>
|
||||||
|
public uint Runtime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type to patch. If null, the patcher will try to use the required type from the
|
||||||
|
/// RequireType parameter.
|
||||||
|
/// </summary>
|
||||||
|
public Type TargetType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a concrete type and method.
|
||||||
|
///
|
||||||
|
/// Passing null as the method name will attempt to patch a constructor. Only one
|
||||||
|
/// declared constructor may be present, or the call will fail at patch time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">When to apply the patch.</param>
|
||||||
|
/// <param name="target">The type to patch.</param>
|
||||||
|
/// <param name="method">The method name to patch.</param>
|
||||||
|
public PLibPatchAttribute(uint runtime, Type target, string method) {
|
||||||
|
ArgumentTypes = null;
|
||||||
|
IgnoreOnFail = false;
|
||||||
|
MethodName = method;
|
||||||
|
PatchType = HarmonyPatchType.All;
|
||||||
|
Runtime = runtime;
|
||||||
|
TargetType = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a concrete type and overloaded method.
|
||||||
|
///
|
||||||
|
/// Passing null as the method name will attempt to patch a constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">When to apply the patch.</param>
|
||||||
|
/// <param name="target">The type to patch.</param>
|
||||||
|
/// <param name="method">The method name to patch.</param>
|
||||||
|
/// <param name="argTypes">The types of the overload to patch.</param>
|
||||||
|
public PLibPatchAttribute(uint runtime, Type target, string method,
|
||||||
|
params Type[] argTypes) {
|
||||||
|
ArgumentTypes = argTypes;
|
||||||
|
IgnoreOnFail = false;
|
||||||
|
MethodName = method;
|
||||||
|
PatchType = HarmonyPatchType.All;
|
||||||
|
Runtime = runtime;
|
||||||
|
TargetType = target ?? throw new ArgumentNullException(nameof(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches a method only if a specified type is available. Use optional parameters to
|
||||||
|
/// specify the type to patch using RequireType / RequireAssembly.
|
||||||
|
///
|
||||||
|
/// Passing null as the method name will attempt to patch a constructor. Only one
|
||||||
|
/// declared constructor may be present, or the call will fail at patch time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">When to apply the patch.</param>
|
||||||
|
/// <param name="method">The method name to patch.</param>
|
||||||
|
public PLibPatchAttribute(uint runtime, string method) {
|
||||||
|
ArgumentTypes = null;
|
||||||
|
IgnoreOnFail = false;
|
||||||
|
MethodName = method;
|
||||||
|
PatchType = HarmonyPatchType.All;
|
||||||
|
Runtime = runtime;
|
||||||
|
TargetType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches an overloaded method only if a specified type is available. Use optional
|
||||||
|
/// parameters to specify the type to patch using RequireType / RequireAssembly.
|
||||||
|
///
|
||||||
|
/// Passing null as the method name will attempt to patch a constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">When to apply the patch.</param>
|
||||||
|
/// <param name="method">The method name to patch.</param>
|
||||||
|
/// <param name="argTypes">The types of the overload to patch.</param>
|
||||||
|
public PLibPatchAttribute(uint runtime, string method, params Type[] argTypes) {
|
||||||
|
ArgumentTypes = argTypes;
|
||||||
|
IgnoreOnFail = false;
|
||||||
|
MethodName = method;
|
||||||
|
PatchType = HarmonyPatchType.All;
|
||||||
|
Runtime = runtime;
|
||||||
|
TargetType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new patch method instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">The method that was attributed.</param>
|
||||||
|
/// <returns>An instance that can execute this patch.</returns>
|
||||||
|
public IPatchMethodInstance CreateInstance(MethodInfo method) {
|
||||||
|
return new PLibPatchInstance(this, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PLibPatch[RunAt={0},PatchType={1},MethodName={2}]".F(RunAt.ToString(
|
||||||
|
Runtime), PatchType, MethodName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refers to a single instance of the annotation, with its annotated method.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PLibPatchInstance : IPatchMethodInstance {
|
||||||
|
/// <summary>
|
||||||
|
/// The attribute describing the method.
|
||||||
|
/// </summary>
|
||||||
|
public PLibPatchAttribute Descriptor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method to run.
|
||||||
|
/// </summary>
|
||||||
|
public MethodInfo Method { get; }
|
||||||
|
|
||||||
|
public PLibPatchInstance(PLibPatchAttribute attribute, MethodInfo method) {
|
||||||
|
Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
|
||||||
|
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the patch type to perform.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The type of Harmony patch to use for this method.</returns>
|
||||||
|
private HarmonyPatchType GetPatchType() {
|
||||||
|
var patchType = Descriptor.PatchType;
|
||||||
|
if (patchType == HarmonyPatchType.All) {
|
||||||
|
// Auto-determine the patch type based on name, if possible
|
||||||
|
string patchName = Method.Name;
|
||||||
|
foreach (var value in Enum.GetValues(typeof(HarmonyPatchType)))
|
||||||
|
if (value is HarmonyPatchType eval && eval != patchType && patchName.
|
||||||
|
EndsWith(eval.ToString(), StringComparison.Ordinal)) {
|
||||||
|
patchType = eval;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified instance constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetType">The type to be constructed.</param>
|
||||||
|
/// <returns>The target constructor.</returns>
|
||||||
|
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||||
|
/// and multiple declared constructors exist.</exception>
|
||||||
|
private MethodBase GetTargetConstructor(Type targetType, Type[] argumentTypes) {
|
||||||
|
MethodBase constructor;
|
||||||
|
if (argumentTypes == null) {
|
||||||
|
var cons = targetType.GetConstructors(PPatchManager.FLAGS | BindingFlags.
|
||||||
|
Instance);
|
||||||
|
if (cons == null || cons.Length != 1)
|
||||||
|
throw new InvalidOperationException("No constructor for {0} found".F(
|
||||||
|
targetType.FullName));
|
||||||
|
constructor = cons[0];
|
||||||
|
} else
|
||||||
|
constructor = targetType.GetConstructor(PPatchManager.FLAGS | BindingFlags.
|
||||||
|
Instance, null, argumentTypes, null);
|
||||||
|
return constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the target method to patch.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requiredType">The type to use if no type was specified.</param>
|
||||||
|
/// <returns>The method to patch.</returns>
|
||||||
|
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||||
|
/// and multiple options match the method name.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">If the target method was not found.</exception>
|
||||||
|
private MethodBase GetTargetMethod(Type requiredType) {
|
||||||
|
var targetType = Descriptor.TargetType;
|
||||||
|
var argumentTypes = Descriptor.ArgumentTypes;
|
||||||
|
string name = Descriptor.MethodName;
|
||||||
|
MethodBase method;
|
||||||
|
if (targetType == null)
|
||||||
|
targetType = requiredType;
|
||||||
|
// Only allow non-inherited members, patching inherited members gets spooky on
|
||||||
|
// Mac OS and Linux
|
||||||
|
if (targetType == null)
|
||||||
|
throw new InvalidOperationException("No type specified to patch");
|
||||||
|
if (string.IsNullOrEmpty(name) || name == ".ctor")
|
||||||
|
// Constructor
|
||||||
|
method = GetTargetConstructor(targetType, argumentTypes);
|
||||||
|
else
|
||||||
|
// Method
|
||||||
|
method = (argumentTypes == null) ? targetType.GetMethod(name, PPatchManager.
|
||||||
|
FLAGS_EITHER) : targetType.GetMethod(name, PPatchManager.FLAGS_EITHER,
|
||||||
|
null, argumentTypes, null);
|
||||||
|
if (method == null)
|
||||||
|
throw new InvalidOperationException("Method {0}.{1} not found".F(targetType.
|
||||||
|
FullName, name));
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message at debug level if Ignore On Patch Fail is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="e">The exception thrown during patching.</param>
|
||||||
|
/// <returns>true to suppress the exception, or false to rethrow it.</returns>
|
||||||
|
private bool LogIgnoreOnFail(Exception e) {
|
||||||
|
bool ignore = Descriptor.IgnoreOnFail;
|
||||||
|
if (ignore)
|
||||||
|
PUtil.LogDebug("Patch for {0} not applied: {1}".F(Descriptor.
|
||||||
|
MethodName, e.Message));
|
||||||
|
return ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the patch.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The Harmony instance to use.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">If the </exception>
|
||||||
|
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||||
|
/// and multiple options match the method name.</exception>
|
||||||
|
public void Run(Harmony instance) {
|
||||||
|
if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
|
||||||
|
RequireType, out Type requiredType)) {
|
||||||
|
var dest = new HarmonyMethod(Method);
|
||||||
|
if (instance == null)
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
try {
|
||||||
|
var method = GetTargetMethod(requiredType);
|
||||||
|
switch (GetPatchType()) {
|
||||||
|
case HarmonyPatchType.Postfix:
|
||||||
|
instance.Patch(method, postfix: dest);
|
||||||
|
break;
|
||||||
|
case HarmonyPatchType.Prefix:
|
||||||
|
instance.Patch(method, prefix: dest);
|
||||||
|
break;
|
||||||
|
case HarmonyPatchType.Transpiler:
|
||||||
|
instance.Patch(method, transpiler: dest);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(HarmonyPatchType));
|
||||||
|
}
|
||||||
|
} catch (AmbiguousMatchException e) {
|
||||||
|
// Multi catch or filtering is not available in this version of C#
|
||||||
|
if (!LogIgnoreOnFail(e))
|
||||||
|
throw;
|
||||||
|
} catch (InvalidOperationException e) {
|
||||||
|
if (!LogIgnoreOnFail(e))
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
using PrivateRunList = System.Collections.Generic.ICollection<PeterHan.PLib.PatchManager.
|
||||||
|
IPatchMethodInstance>;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Manages patches that PLib will conditionally apply.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PPatchManager : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The base flags to use when matching instance or static methods.
|
||||||
|
/// </summary>
|
||||||
|
internal const BindingFlags FLAGS = PPatchTools.BASE_FLAGS | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The flags to use when matching instance and static methods.
|
||||||
|
/// </summary>
|
||||||
|
internal const BindingFlags FLAGS_EITHER = FLAGS | BindingFlags.Static | BindingFlags.
|
||||||
|
Instance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PPatchManager Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// true if the AfterModsLoad patches have been run, or false otherwise.
|
||||||
|
/// </summary>
|
||||||
|
private static volatile bool afterModsLoaded = false;
|
||||||
|
|
||||||
|
private static void Game_DestroyInstances_Postfix() {
|
||||||
|
Instance?.InvokeAllProcess(RunAt.OnEndGame, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Game_OnPrefabInit_Postfix() {
|
||||||
|
Instance?.InvokeAllProcess(RunAt.OnStartGame, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Initialize_Prefix() {
|
||||||
|
Instance?.InvokeAllProcess(RunAt.BeforeDbInit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Initialize_Postfix() {
|
||||||
|
Instance?.InvokeAllProcess(RunAt.AfterDbInit, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Instance_Postfix() {
|
||||||
|
bool load = false;
|
||||||
|
if (Instance != null)
|
||||||
|
lock (VERSION) {
|
||||||
|
if (!afterModsLoaded)
|
||||||
|
load = afterModsLoaded = true;
|
||||||
|
}
|
||||||
|
if (load)
|
||||||
|
Instance.InvokeAllProcess(RunAt.AfterLayerableLoad, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MainMenu_OnSpawn_Postfix() {
|
||||||
|
Instance?.InvokeAllProcess(RunAt.InMainMenu, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Harmony instance to use for patching.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Harmony harmony;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches and delegates to be run at specific points in the runtime. Put the kibosh
|
||||||
|
/// on patching Db.Initialize()!
|
||||||
|
/// </summary>
|
||||||
|
private readonly IDictionary<uint, PrivateRunList> patches;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if the conditions for a method running are met.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assemblyName">The assembly name that must be present, or null if none is required.</param>
|
||||||
|
/// <param name="typeName">The type full name that must be present, or null if none is required.</param>
|
||||||
|
/// <param name="requiredType">The type that was required, if typeName was not null or empty.</param>
|
||||||
|
/// <returns>true if the requirements are met, or false otherwise.</returns>
|
||||||
|
internal static bool CheckConditions(string assemblyName, string typeName,
|
||||||
|
out Type requiredType) {
|
||||||
|
bool ok = false, emptyType = string.IsNullOrEmpty(typeName);
|
||||||
|
if (string.IsNullOrEmpty(assemblyName)) {
|
||||||
|
if (emptyType) {
|
||||||
|
requiredType = null;
|
||||||
|
ok = true;
|
||||||
|
} else {
|
||||||
|
requiredType = PPatchTools.GetTypeSafe(typeName);
|
||||||
|
ok = requiredType != null;
|
||||||
|
}
|
||||||
|
} else if (emptyType) {
|
||||||
|
requiredType = null;
|
||||||
|
// Search for assembly only, by name
|
||||||
|
foreach (var candidate in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
if (candidate.GetName().Name == assemblyName) {
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requiredType = PPatchTools.GetTypeSafe(typeName, assemblyName);
|
||||||
|
ok = requiredType != null;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a patch manager to execute patches at specific times.
|
||||||
|
///
|
||||||
|
/// Create this instance in OnLoad() and use RegisterPatchClass to register a
|
||||||
|
/// patch class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="harmony">The Harmony instance to use for patching.</param>
|
||||||
|
public PPatchManager(Harmony harmony) {
|
||||||
|
if (harmony == null) {
|
||||||
|
PUtil.LogWarning("Use the Harmony instance from OnLoad to create PPatchManager");
|
||||||
|
harmony = new Harmony("PLib.PostLoad." + Assembly.GetExecutingAssembly().
|
||||||
|
GetNameSafe());
|
||||||
|
}
|
||||||
|
this.harmony = harmony;
|
||||||
|
patches = new Dictionary<uint, PrivateRunList>(8);
|
||||||
|
InstanceData = patches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schedules a patch method instance to be run.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="when">When to run the patch.</param>
|
||||||
|
/// <param name="instance">The patch method instance to run.</param>
|
||||||
|
/// <param name="harmony">The Harmony instance to use for patching.</param>
|
||||||
|
private void AddHandler(uint when, IPatchMethodInstance instance) {
|
||||||
|
if (!patches.TryGetValue(when, out PrivateRunList atTime))
|
||||||
|
patches.Add(when, atTime = new List<IPatchMethodInstance>(16));
|
||||||
|
atTime.Add(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
// Db
|
||||||
|
plibInstance.Patch(typeof(Db), nameof(Db.Initialize), prefix: PatchMethod(nameof(
|
||||||
|
Initialize_Prefix)), postfix: PatchMethod(nameof(Initialize_Postfix)));
|
||||||
|
|
||||||
|
// Game
|
||||||
|
plibInstance.Patch(typeof(Game), "DestroyInstances", postfix: PatchMethod(nameof(
|
||||||
|
Game_DestroyInstances_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(Game), "OnPrefabInit", postfix: PatchMethod(nameof(
|
||||||
|
Game_OnPrefabInit_Postfix)));
|
||||||
|
|
||||||
|
// GlobalResources
|
||||||
|
plibInstance.Patch(typeof(GlobalResources), "Instance", postfix:
|
||||||
|
PatchMethod(nameof(Instance_Postfix)));
|
||||||
|
|
||||||
|
// MainMenu
|
||||||
|
plibInstance.Patch(typeof(MainMenu), "OnSpawn", postfix: PatchMethod(
|
||||||
|
nameof(MainMenu_OnSpawn_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostInitialize(Harmony plibInstance) {
|
||||||
|
InvokeAllProcess(RunAt.AfterModsLoad, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint when, object _) {
|
||||||
|
if (patches.TryGetValue(when, out PrivateRunList atTime) && atTime != null &&
|
||||||
|
atTime.Count > 0) {
|
||||||
|
string stage = RunAt.ToString(when);
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchDebug("Executing {0:D} handler(s) from {1} for stage {2}".F(
|
||||||
|
atTime.Count, Assembly.GetExecutingAssembly().GetNameSafe() ?? "?", stage));
|
||||||
|
#endif
|
||||||
|
foreach (var patch in atTime)
|
||||||
|
try {
|
||||||
|
patch.Run(harmony);
|
||||||
|
} catch (TargetInvocationException e) {
|
||||||
|
// Use the inner exception
|
||||||
|
PUtil.LogError("Error running patches for stage " + stage + ":");
|
||||||
|
PUtil.LogException(e.GetBaseException());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Say which mod's postload crashed
|
||||||
|
PUtil.LogError("Error running patches for stage " + stage + ":");
|
||||||
|
PUtil.LogException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a single patch to be run by Patch Manager. Obviously, the patch must be
|
||||||
|
/// registered before the time that it is used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="when">The time when the method should be run.</param>
|
||||||
|
/// <param name="patch">The patch to execute.</param>
|
||||||
|
public void RegisterPatch(uint when, IPatchMethodInstance patch) {
|
||||||
|
RegisterForForwarding();
|
||||||
|
if (patch == null)
|
||||||
|
throw new ArgumentNullException(nameof(patch));
|
||||||
|
if (when == RunAt.Immediately)
|
||||||
|
// Now now now!
|
||||||
|
patch.Run(harmony);
|
||||||
|
else
|
||||||
|
AddHandler(when, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a class containing methods for [PLibPatch] and [PLibMethod] handlers.
|
||||||
|
/// All methods, public and private, of the type will be searched for annotations.
|
||||||
|
/// However, nested and derived types will not be searched, nor will inherited methods.
|
||||||
|
///
|
||||||
|
/// This method cannot be used to register a class from another mod, as the annotations
|
||||||
|
/// on those methods would have a different assembly qualified name and would thus
|
||||||
|
/// not be recognized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to register.</param>
|
||||||
|
/// <param name="harmony">The Harmony instance to use for immediate patches. Use
|
||||||
|
/// the instance provided from UserMod2.OnLoad().</param>
|
||||||
|
public void RegisterPatchClass(Type type) {
|
||||||
|
int count = 0;
|
||||||
|
if (type == null)
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
RegisterForForwarding();
|
||||||
|
foreach (var method in type.GetMethods(FLAGS | BindingFlags.Static))
|
||||||
|
foreach (var attrib in method.GetCustomAttributes(true))
|
||||||
|
if (attrib is IPLibAnnotation pm) {
|
||||||
|
var when = pm.Runtime;
|
||||||
|
var instance = pm.CreateInstance(method);
|
||||||
|
if (when == RunAt.Immediately)
|
||||||
|
// Now now now!
|
||||||
|
instance.Run(harmony);
|
||||||
|
else
|
||||||
|
AddHandler(pm.Runtime, instance);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count > 0)
|
||||||
|
PRegistry.LogPatchDebug("Registered {0:D} handler(s) for {1}".F(count,
|
||||||
|
Assembly.GetCallingAssembly().GetNameSafe() ?? "?"));
|
||||||
|
else
|
||||||
|
PRegistry.LogPatchWarning("RegisterPatchClass could not find any handlers!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.PatchManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Describes when a PLibPatch or PLibMethod should be invoked.
|
||||||
|
///
|
||||||
|
/// Due to a bug in ILRepack an enum type in PLib cannot be used as a parameter for a
|
||||||
|
/// custom attribute. ILmerge does not have this bug.
|
||||||
|
/// </summary>
|
||||||
|
public static class RunAt {
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the method/patch now.
|
||||||
|
///
|
||||||
|
/// Note that mods may load in any order and thus not all mods may be initialized at
|
||||||
|
/// this time.
|
||||||
|
/// </summary>
|
||||||
|
public const uint Immediately = 0U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs after all mods load, but before most other aspects of the game (including
|
||||||
|
/// Assets, Db, and so forth) are initialized. This will run before any other mod
|
||||||
|
/// has their UserMod2.AfterModsLoad executed. All PLib components will be initialized
|
||||||
|
/// by this point.
|
||||||
|
/// </summary>
|
||||||
|
public const uint AfterModsLoad = 1U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs immediately before Db.Initialize.
|
||||||
|
/// </summary>
|
||||||
|
public const uint BeforeDbInit = 2U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs immediately after Db.Initialize.
|
||||||
|
/// </summary>
|
||||||
|
public const uint AfterDbInit = 3U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs when the main menu has loaded.
|
||||||
|
/// </summary>
|
||||||
|
public const uint InMainMenu = 4U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs when Game.OnPrefabInit has completed.
|
||||||
|
/// </summary>
|
||||||
|
public const uint OnStartGame = 5U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs when Game.DestroyInstances is executed.
|
||||||
|
/// </summary>
|
||||||
|
public const uint OnEndGame = 6U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs after all mod data (including layerable files like world gen and codex/
|
||||||
|
/// elements) are loaded. This comes after all UserMod2.AfterModsLoad handlers execute.
|
||||||
|
/// All PLib components will be initialized by this point.
|
||||||
|
/// </summary>
|
||||||
|
public const uint AfterLayerableLoad = 7U;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The string equivalents of each constant for debugging.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string[] STRING_VALUES = new string[] {
|
||||||
|
nameof(Immediately), nameof(AfterModsLoad), nameof(BeforeDbInit),
|
||||||
|
nameof(AfterDbInit), nameof(InMainMenu), nameof(OnStartGame), nameof(OnEndGame),
|
||||||
|
nameof(AfterLayerableLoad)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a human readable representation of a run time constant.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runtime">The time when the patch should be run.</param>
|
||||||
|
public static string ToString(uint runtime) {
|
||||||
|
return (runtime < STRING_VALUES.Length) ? STRING_VALUES[runtime] : runtime.
|
||||||
|
ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// A class similar to Queue<typeparamref name="T"/> that allows efficient access to its
|
||||||
|
/// items in ascending order.
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityQueue<T> where T : IComparable<T> {
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the index of the specified item's first child. Its second child index is
|
||||||
|
/// that index plus one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The item index.</param>
|
||||||
|
/// <returns>The index of its first child.</returns>
|
||||||
|
private static int ChildIndex(int index) {
|
||||||
|
return 2 * index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the index of the specified item's parent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The item index.</param>
|
||||||
|
/// <returns>The index of its parent.</returns>
|
||||||
|
private static int ParentIndex(int index) {
|
||||||
|
return (index - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of elements in this queue.
|
||||||
|
/// </summary>
|
||||||
|
public int Count => heap.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The heap where the items are stored.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IList<T> heap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new PriorityQueue<<typeparamref name="T"/>> with the default
|
||||||
|
/// initial capacity.
|
||||||
|
/// </summary>
|
||||||
|
public PriorityQueue() : this(32) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new PriorityQueue<<typeparamref name="T"/>> with the specified
|
||||||
|
/// initial capacity.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="capacity">The initial capacity of this queue.</param>
|
||||||
|
public PriorityQueue(int capacity) {
|
||||||
|
if (capacity < 1)
|
||||||
|
throw new ArgumentException("capacity > 0");
|
||||||
|
heap = new List<T>(Math.Max(capacity, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all objects from this PriorityQueue<<typeparamref name="T"/>>.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear() {
|
||||||
|
heap.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns whether the specified key is present in this priority queue. This operation
|
||||||
|
/// is fairly slow, use with caution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to check.</param>
|
||||||
|
/// <returns>true if it exists in this priority queue, or false otherwise.</returns>
|
||||||
|
public bool Contains(T key) {
|
||||||
|
return heap.Contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes and returns the smallest object in the
|
||||||
|
/// PriorityQueue<<typeparamref name="T"/>>.
|
||||||
|
///
|
||||||
|
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The object that is removed from this PriorityQueue.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">If this queue is empty.</exception>
|
||||||
|
public T Dequeue() {
|
||||||
|
int index = 0, length = heap.Count, childIndex;
|
||||||
|
if (length == 0)
|
||||||
|
throw new InvalidOperationException("Queue is empty");
|
||||||
|
T top = heap[0];
|
||||||
|
// Put the last element as the new head
|
||||||
|
heap[0] = heap[--length];
|
||||||
|
heap.RemoveAt(length);
|
||||||
|
while ((childIndex = ChildIndex(index)) < length) {
|
||||||
|
T first = heap[index], child = heap[childIndex];
|
||||||
|
if (childIndex < length - 1) {
|
||||||
|
var rightChild = heap[childIndex + 1];
|
||||||
|
// Select the smallest child
|
||||||
|
if (child.CompareTo(rightChild) > 0) {
|
||||||
|
childIndex++;
|
||||||
|
child = rightChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first.CompareTo(child) < 0)
|
||||||
|
break;
|
||||||
|
heap[childIndex] = first;
|
||||||
|
heap[index] = child;
|
||||||
|
index = childIndex;
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an object to the PriorityQueue<<typeparamref name="T"/>>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The object to add to this PriorityQueue.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">If item is null.</exception>
|
||||||
|
public void Enqueue(T item) {
|
||||||
|
if (item == null)
|
||||||
|
throw new ArgumentNullException(nameof(item));
|
||||||
|
int index = heap.Count;
|
||||||
|
heap.Add(item);
|
||||||
|
while (index > 0) {
|
||||||
|
int parentIndex = ParentIndex(index);
|
||||||
|
T first = heap[index], parent = heap[parentIndex];
|
||||||
|
if (first.CompareTo(parent) > 0)
|
||||||
|
break;
|
||||||
|
heap[parentIndex] = first;
|
||||||
|
heap[index] = parent;
|
||||||
|
index = parentIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the smallest object in the PriorityQueue<<typeparamref name="T"/>>
|
||||||
|
/// without removing it.
|
||||||
|
///
|
||||||
|
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The smallest object in this PriorityQueue.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">If this queue is empty.</exception>
|
||||||
|
public T Peek() {
|
||||||
|
if (Count == 0)
|
||||||
|
throw new InvalidOperationException("Queue is empty");
|
||||||
|
return heap[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return heap.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A priority queue that includes a paired value.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="K">The type to use for the sorting in the PriorityQueue.</typeparam>
|
||||||
|
/// <typeparam name="V">The type to include as extra data.</typeparam>
|
||||||
|
public sealed class PriorityDictionary<K, V> : PriorityQueue<PriorityDictionary<K, V>.
|
||||||
|
PriorityQueuePair> where K : IComparable<K> {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new PriorityDictionary<<typeparamref name="K"/>,
|
||||||
|
/// <typeparamref name="V"/>> with the default initial capacity.
|
||||||
|
/// </summary>
|
||||||
|
public PriorityDictionary() : base() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new PriorityDictionary<<typeparamref name="K"/>,
|
||||||
|
/// <typeparamref name="V"/>> with the specified initial capacity.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="capacity">The initial capacity of this dictionary.</param>
|
||||||
|
public PriorityDictionary(int capacity) : base(capacity) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes and returns the smallest object in the
|
||||||
|
/// PriorityDictionary<<typeparamref name="K"/>, <typeparamref name="V"/>>.
|
||||||
|
///
|
||||||
|
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key of the object removed.</param>
|
||||||
|
/// <param name="value">The value of the object removed.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">If this dictionary is empty.</exception>
|
||||||
|
public void Dequeue(out K key, out V value) {
|
||||||
|
var pair = Dequeue();
|
||||||
|
key = pair.Key;
|
||||||
|
value = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an object to the PriorityDictionary<<typeparamref name="K"/>,
|
||||||
|
/// <typeparamref name="V"/>>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The object to add to this PriorityDictionary.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">If item is null.</exception>
|
||||||
|
public void Enqueue(K key, V value) {
|
||||||
|
Enqueue(new PriorityQueuePair(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the smallest object in the PriorityDictionary<<typeparamref name="K"/>,
|
||||||
|
/// <typeparamref name="V"/>> without removing it.
|
||||||
|
///
|
||||||
|
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key of the smallest object.</param>
|
||||||
|
/// <param name="value">The value of the smallest object.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">If this dictionary is empty.</exception>
|
||||||
|
public void Peek(out K key, out V value) {
|
||||||
|
var pair = Peek();
|
||||||
|
key = pair.Key;
|
||||||
|
value = pair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a value with the key that is used for comparison.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PriorityQueuePair : IComparable<PriorityQueuePair> {
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the key of this QueueItem.
|
||||||
|
/// </summary>
|
||||||
|
public K Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the value of this QueueItem.
|
||||||
|
/// </summary>
|
||||||
|
public V Value { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new priority queue pair.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The item key.</param>
|
||||||
|
/// <param name="value">The item value.</param>
|
||||||
|
public PriorityQueuePair(K key, V value) {
|
||||||
|
if (key == null)
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
Key = key;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(PriorityQueuePair other) {
|
||||||
|
if (other == null)
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
return Key.CompareTo(other.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is PriorityQueuePair other && other.Key.Equals(Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Key.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PriorityQueueItem[key=" + Key + ",value=" + Value + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Delegates calls to forwarded components in other assemblies.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PRemoteComponent : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype used for delegates to remote Initialize.
|
||||||
|
/// </summary>
|
||||||
|
private delegate void InitializeDelegate(Harmony instance);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype used for delegates to remote Process.
|
||||||
|
/// </summary>
|
||||||
|
private delegate void ProcessDelegate(uint operation, object args);
|
||||||
|
|
||||||
|
protected override object InstanceData {
|
||||||
|
get {
|
||||||
|
return getData?.Invoke();
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
setData?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version {
|
||||||
|
get {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the component's version of Bootstrap.
|
||||||
|
/// </summary>
|
||||||
|
private readonly InitializeDelegate doBootstrap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the component's version of Initialize.
|
||||||
|
/// </summary>
|
||||||
|
private readonly InitializeDelegate doInitialize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the component's version of PostInitialize.
|
||||||
|
/// </summary>
|
||||||
|
private readonly InitializeDelegate doPostInitialize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the component's data.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Func<object> getData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the processing method of the component.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ProcessDelegate process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the component's data.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Action<object> setData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The component's version.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Version version;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The wrapped instance from the other mod.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object wrapped;
|
||||||
|
|
||||||
|
internal PRemoteComponent(object wrapped) {
|
||||||
|
this.wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped));
|
||||||
|
if (!PPatchTools.TryGetPropertyValue(wrapped, nameof(Version), out Version
|
||||||
|
version))
|
||||||
|
throw new ArgumentException("Remote component missing Version property");
|
||||||
|
this.version = version;
|
||||||
|
// Initialize
|
||||||
|
var type = wrapped.GetType();
|
||||||
|
doInitialize = type.CreateDelegate<InitializeDelegate>(nameof(PForwardedComponent.
|
||||||
|
Initialize), wrapped, typeof(Harmony));
|
||||||
|
if (doInitialize == null)
|
||||||
|
throw new ArgumentException("Remote component missing Initialize");
|
||||||
|
// Bootstrap
|
||||||
|
doBootstrap = type.CreateDelegate<InitializeDelegate>(nameof(PForwardedComponent.
|
||||||
|
Bootstrap), wrapped, typeof(Harmony));
|
||||||
|
doPostInitialize = type.CreateDelegate<InitializeDelegate>(nameof(
|
||||||
|
PForwardedComponent.PostInitialize), wrapped, typeof(Harmony));
|
||||||
|
getData = type.CreateGetDelegate<object>(nameof(InstanceData), wrapped);
|
||||||
|
setData = type.CreateSetDelegate<object>(nameof(InstanceData), wrapped);
|
||||||
|
process = type.CreateDelegate<ProcessDelegate>(nameof(PForwardedComponent.
|
||||||
|
Process), wrapped, typeof(uint), typeof(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Bootstrap(Harmony plibInstance) {
|
||||||
|
doBootstrap?.Invoke(plibInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override object DoInitialize(Harmony plibInstance) {
|
||||||
|
doInitialize.Invoke(plibInstance);
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Assembly GetOwningAssembly() {
|
||||||
|
return wrapped.GetType().Assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
DoInitialize(plibInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PostInitialize(Harmony plibInstance) {
|
||||||
|
doPostInitialize?.Invoke(plibInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object args) {
|
||||||
|
process?.Invoke(operation, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PRemoteComponent[ID={0},TargetType={1}]".F(ID, wrapped.GetType().
|
||||||
|
AssemblyQualifiedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Core {
|
||||||
|
/// <summary>
|
||||||
|
/// Transparently provides the functionality of PRegistry, while the actual instance is
|
||||||
|
/// from another mod's bootstrapper.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PRemoteRegistry : IPLibRegistry {
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype used for delegates to remote GetAllComponents.
|
||||||
|
/// </summary>
|
||||||
|
private delegate System.Collections.ICollection GetAllComponentsDelegate(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype used for delegates to remote GetLatestVersion and GetSharedData.
|
||||||
|
/// </summary>
|
||||||
|
private delegate object GetObjectDelegate(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype used for delegates to remote SetSharedData.
|
||||||
|
/// </summary>
|
||||||
|
private delegate void SetObjectDelegate(string id, object value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the local registry's version of AddCandidateVersion.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Action<object> addCandidateVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the local registry's version of GetAllComponents.
|
||||||
|
/// </summary>
|
||||||
|
private readonly GetAllComponentsDelegate getAllComponents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the local registry's version of GetLatestVersion.
|
||||||
|
/// </summary>
|
||||||
|
private readonly GetObjectDelegate getLatestVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the local registry's version of GetSharedData.
|
||||||
|
/// </summary>
|
||||||
|
private readonly GetObjectDelegate getSharedData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Points to the local registry's version of SetSharedData.
|
||||||
|
/// </summary>
|
||||||
|
private readonly SetObjectDelegate setSharedData;
|
||||||
|
|
||||||
|
public IDictionary<string, object> ModData { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The components actually instantiated (latest version of each).
|
||||||
|
/// </summary>
|
||||||
|
private readonly IDictionary<string, PForwardedComponent> remoteComponents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a remote registry wrapping the target object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The PRegistryComponent instance to wrap.</param>
|
||||||
|
internal PRemoteRegistry(object instance) {
|
||||||
|
if (instance == null)
|
||||||
|
throw new ArgumentNullException(nameof(instance));
|
||||||
|
remoteComponents = new Dictionary<string, PForwardedComponent>(32);
|
||||||
|
if (!PPatchTools.TryGetPropertyValue(instance, nameof(ModData), out
|
||||||
|
IDictionary<string, object> modData))
|
||||||
|
throw new ArgumentException("Remote instance missing ModData");
|
||||||
|
ModData = modData;
|
||||||
|
var type = instance.GetType();
|
||||||
|
addCandidateVersion = type.CreateDelegate<Action<object>>(nameof(
|
||||||
|
PRegistryComponent.DoAddCandidateVersion), instance, typeof(object));
|
||||||
|
getAllComponents = type.CreateDelegate<GetAllComponentsDelegate>(nameof(
|
||||||
|
PRegistryComponent.DoGetAllComponents), instance, typeof(string));
|
||||||
|
getLatestVersion = type.CreateDelegate<GetObjectDelegate>(nameof(
|
||||||
|
PRegistryComponent.DoGetLatestVersion), instance, typeof(string));
|
||||||
|
if (addCandidateVersion == null || getLatestVersion == null ||
|
||||||
|
getAllComponents == null)
|
||||||
|
throw new ArgumentException("Remote instance missing candidate versions");
|
||||||
|
getSharedData = type.CreateDelegate<GetObjectDelegate>(nameof(IPLibRegistry.
|
||||||
|
GetSharedData), instance, typeof(string));
|
||||||
|
setSharedData = type.CreateDelegate<SetObjectDelegate>(nameof(IPLibRegistry.
|
||||||
|
SetSharedData), instance, typeof(string), typeof(object));
|
||||||
|
if (getSharedData == null || setSharedData == null)
|
||||||
|
throw new ArgumentException("Remote instance missing shared data");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCandidateVersion(PForwardedComponent instance) {
|
||||||
|
addCandidateVersion.Invoke(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<PForwardedComponent> GetAllComponents(string id) {
|
||||||
|
ICollection<PForwardedComponent> results = null;
|
||||||
|
var all = getAllComponents.Invoke(id);
|
||||||
|
if (all != null) {
|
||||||
|
results = new List<PForwardedComponent>(all.Count);
|
||||||
|
foreach (var component in all)
|
||||||
|
if (component is PForwardedComponent local)
|
||||||
|
results.Add(local);
|
||||||
|
else
|
||||||
|
results.Add(new PRemoteComponent(component));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PForwardedComponent GetLatestVersion(string id) {
|
||||||
|
if (!remoteComponents.TryGetValue(id, out PForwardedComponent remoteComponent)) {
|
||||||
|
// Attempt to resolve it
|
||||||
|
object instantiated = getLatestVersion.Invoke(id);
|
||||||
|
if (instantiated == null) {
|
||||||
|
#if DEBUG
|
||||||
|
PRegistry.LogPatchWarning("Unable to find a component matching: " + id);
|
||||||
|
#endif
|
||||||
|
remoteComponent = null;
|
||||||
|
} else if (instantiated is PForwardedComponent inThisMod)
|
||||||
|
// Running the current version
|
||||||
|
remoteComponent = inThisMod;
|
||||||
|
else
|
||||||
|
remoteComponent = new PRemoteComponent(instantiated);
|
||||||
|
remoteComponents.Add(id, remoteComponent);
|
||||||
|
}
|
||||||
|
return remoteComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetSharedData(string id) {
|
||||||
|
return getSharedData.Invoke(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSharedData(string id, object data) {
|
||||||
|
setSharedData.Invoke(id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.UI {
|
||||||
|
/// <summary>
|
||||||
|
/// Patches bugs in Text Mesh Pro.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TextMeshProPatcher {
|
||||||
|
/// <summary>
|
||||||
|
/// The ID to use for Harmony patches.
|
||||||
|
/// </summary>
|
||||||
|
private const string HARMONY_ID = "TextMeshProPatch";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks whether the TMP patches have been checked.
|
||||||
|
/// </summary>
|
||||||
|
private static volatile bool patchChecked = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes multiple thread access to the patch status.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly object patchLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applied to TMP_InputField to fix a bug that prevented auto layout from ever
|
||||||
|
/// working.
|
||||||
|
/// </summary>
|
||||||
|
private static bool AssignPositioningIfNeeded_Prefix(TMP_InputField __instance,
|
||||||
|
RectTransform ___caretRectTrans, TMP_Text ___m_TextComponent) {
|
||||||
|
bool cont = true;
|
||||||
|
var crt = ___caretRectTrans;
|
||||||
|
if (___m_TextComponent != null && crt != null && __instance != null &&
|
||||||
|
___m_TextComponent.isActiveAndEnabled) {
|
||||||
|
var rt = ___m_TextComponent.rectTransform;
|
||||||
|
if (crt.localPosition != rt.localPosition ||
|
||||||
|
crt.localRotation != rt.localRotation ||
|
||||||
|
crt.localScale != rt.localScale ||
|
||||||
|
crt.anchorMin != rt.anchorMin ||
|
||||||
|
crt.anchorMax != rt.anchorMax ||
|
||||||
|
crt.anchoredPosition != rt.anchoredPosition ||
|
||||||
|
crt.sizeDelta != rt.sizeDelta ||
|
||||||
|
crt.pivot != rt.pivot) {
|
||||||
|
__instance.StartCoroutine(ResizeCaret(crt, rt));
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if a patch with our class name has already been applied.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="patchList">The patch list to search.</param>
|
||||||
|
/// <returns>true if a patch with this class has already patched the method, or false otherwise.</returns>
|
||||||
|
private static bool HasOurPatch(IEnumerable<Patch> patchList) {
|
||||||
|
bool found = false;
|
||||||
|
if (patchList != null) {
|
||||||
|
foreach (var patch in patchList) {
|
||||||
|
string ownerName = patch.PatchMethod.DeclaringType.Name;
|
||||||
|
// Avoid stomping ourselves, or legacy PLibs < 3.14
|
||||||
|
if (ownerName == nameof(TextMeshProPatcher) || ownerName == "PLibPatches")
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
PUtil.LogDebug("TextMeshProPatcher found existing patch from: {0}".
|
||||||
|
F(patch.owner));
|
||||||
|
#endif
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches TMP_InputField with fixes, but only if necessary.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tmpType">The type of TMP_InputField.</param>
|
||||||
|
/// <param name="instance">The Harmony instance to use for patching.</param>
|
||||||
|
private static void InputFieldPatches(Type tmpType) {
|
||||||
|
var instance = new Harmony(HARMONY_ID);
|
||||||
|
var aip = tmpType.GetMethodSafe("AssignPositioningIfNeeded", false,
|
||||||
|
PPatchTools.AnyArguments);
|
||||||
|
if (aip != null && !HasOurPatch(Harmony.GetPatchInfo(aip)?.Prefixes))
|
||||||
|
instance.Patch(aip, prefix: new HarmonyMethod(typeof(TextMeshProPatcher),
|
||||||
|
nameof(AssignPositioningIfNeeded_Prefix)));
|
||||||
|
var oe = tmpType.GetMethodSafe("OnEnable", false, PPatchTools.AnyArguments);
|
||||||
|
if (oe != null && !HasOurPatch(Harmony.GetPatchInfo(oe)?.Postfixes))
|
||||||
|
instance.Patch(oe, postfix: new HarmonyMethod(typeof(TextMeshProPatcher),
|
||||||
|
nameof(OnEnable_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applied to TMPro.TMP_InputField to fix a clipping bug inside of Scroll Rects.
|
||||||
|
///
|
||||||
|
/// https://forum.unity.com/threads/textmeshpro-text-still-visible-when-using-nested-rectmask2d.537967/
|
||||||
|
/// </summary>
|
||||||
|
private static void OnEnable_Postfix(UnityEngine.UI.Scrollbar ___m_VerticalScrollbar,
|
||||||
|
TMP_Text ___m_TextComponent) {
|
||||||
|
var component = ___m_TextComponent;
|
||||||
|
if (component != null)
|
||||||
|
component.ignoreRectMaskCulling = ___m_VerticalScrollbar != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patches Text Mesh Pro input fields to fix a variety of bugs. Should be used before
|
||||||
|
/// any Text Mesh Pro objects are created.
|
||||||
|
/// </summary>
|
||||||
|
public static void Patch() {
|
||||||
|
lock (patchLock) {
|
||||||
|
if (!patchChecked) {
|
||||||
|
var tmpType = PPatchTools.GetTypeSafe("TMPro.TMP_InputField");
|
||||||
|
if (tmpType != null)
|
||||||
|
try {
|
||||||
|
InputFieldPatches(tmpType);
|
||||||
|
} catch (Exception) {
|
||||||
|
PUtil.LogWarning("Unable to patch TextMeshPro bug, text fields may display improperly inside scroll areas");
|
||||||
|
}
|
||||||
|
patchChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resizes the caret object to match the text. Used as an enumerator.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="caretTransform">The rectTransform of the caret.</param>
|
||||||
|
/// <param name="textTransform">The rectTransform of the text.</param>
|
||||||
|
private static System.Collections.IEnumerator ResizeCaret(
|
||||||
|
RectTransform caretTransform, RectTransform textTransform) {
|
||||||
|
yield return new WaitForEndOfFrame();
|
||||||
|
caretTransform.localPosition = textTransform.localPosition;
|
||||||
|
caretTransform.localRotation = textTransform.localRotation;
|
||||||
|
caretTransform.localScale = textTransform.localScale;
|
||||||
|
caretTransform.anchorMin = textTransform.anchorMin;
|
||||||
|
caretTransform.anchorMax = textTransform.anchorMax;
|
||||||
|
caretTransform.anchoredPosition = textTransform.anchoredPosition;
|
||||||
|
caretTransform.sizeDelta = textTransform.sizeDelta;
|
||||||
|
caretTransform.pivot = textTransform.pivot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Application: Oxygen Not IncludedPOT Version: 2.0\n"
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 2.4.1\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
"Language: fr\n"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||||
|
msgid "MANUAL CONFIG"
|
||||||
|
msgstr "CONFIGURATION MANUELLE"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||||
|
msgid "Options for {0}"
|
||||||
|
msgstr "Options pour {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||||
|
msgid "Assembly Version: {0}"
|
||||||
|
msgstr "Version d'assemblage: {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||||
|
msgid "Mod Homepage"
|
||||||
|
msgstr "Page d'accueil du mod"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||||
|
msgid "Mod Version: {0}"
|
||||||
|
msgstr "Version du mod: {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||||
|
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||||
|
msgstr ""
|
||||||
|
"Oxygen Not Included doit être relancé pour que ces options prennent effet."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||||
|
msgid "Discard changes."
|
||||||
|
msgstr "Annuler les changements."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||||
|
msgid "Visit the mod's website."
|
||||||
|
msgstr "Visiter le site web du mod."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||||
|
msgid "Opens the folder containing the full mod configuration."
|
||||||
|
msgstr "Ouvre le dossier contenant la configuration complète du mod."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||||
|
msgid "Next"
|
||||||
|
msgstr "Suivant"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||||
|
msgid ""
|
||||||
|
"Save these options. Some mods may require a restart for the options to take "
|
||||||
|
"effect."
|
||||||
|
msgstr ""
|
||||||
|
"Conservez ces options. Certains mods peuvent nécessiter un redémarrage pour "
|
||||||
|
"que les options prennent effet."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "Précédent"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||||
|
msgid "Show or hide this options category"
|
||||||
|
msgstr "Afficher ou masquer cette catégorie d'options"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||||
|
msgid ""
|
||||||
|
"The currently installed version of this mod.\n"
|
||||||
|
"\n"
|
||||||
|
"Compare this version with the mod's Release Notes to see if it is outdated."
|
||||||
|
msgstr ""
|
||||||
|
"La version actuellement installée de ce mod.\n"
|
||||||
|
"\n"
|
||||||
|
"Comparez cette version avec les notes de mise à jour du mod pour voir si "
|
||||||
|
"elle n'est pas dépassée."
|
|
@ -0,0 +1,163 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Application: Oxygen Not Included\n"
|
||||||
|
"POT Version: 2.0\n"
|
||||||
|
"Project-Id-Version:\n"
|
||||||
|
"POT-Creation-Date:\n"
|
||||||
|
"PO-Revision-Date:\n"
|
||||||
|
"Language-Team:\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Poedit 2.0.6\n"
|
||||||
|
"Last-Translator:\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||||
|
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"X-Poedit-SourceCharset: UTF-8\n"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||||
|
msgid "MANUAL CONFIG"
|
||||||
|
msgstr "РУЧНАЯ НАСТРОЙКА"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||||
|
msgid "RESET TO DEFAULT"
|
||||||
|
msgstr "СБРОС ПО УМОЛЧАНИЮ"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||||
|
msgid "Options for {0}"
|
||||||
|
msgstr "Настройки для {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||||
|
msgid "Down Arrow"
|
||||||
|
msgstr "Стрелка Вниз"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||||
|
msgid "Left Arrow"
|
||||||
|
msgstr "Стрелка Влево"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||||
|
msgid "Right Arrow"
|
||||||
|
msgstr "Стрелка Вправо"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||||
|
msgid "Up Arrow"
|
||||||
|
msgstr "Стрелка Вверх"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||||
|
msgid "Mods"
|
||||||
|
msgstr "Моды"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||||
|
msgid "Assembly Version: {0}"
|
||||||
|
msgstr "Версия сборки: {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||||
|
msgid "Mod Homepage"
|
||||||
|
msgstr "Страница мода"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||||
|
msgid "Mod Version: {0}"
|
||||||
|
msgstr "Версия мода: {0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||||
|
msgid "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods"
|
||||||
|
msgstr "Этот мод устарел!\nНовая версия: <b>{0}</b>\n\nОбновите локальные моды вручную или используйте <b>Mod Updater</b> для принудительного обновления модов Steam"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||||
|
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||||
|
msgstr "<b><style=\"logic_off\">Устарел!</style></b>"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||||
|
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||||
|
msgstr "Чтобы эти параметры вступили в силу, необходимо перезапустить Oxygen Not Included."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE"
|
||||||
|
msgid "Blue"
|
||||||
|
msgstr "Синий"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||||
|
msgid "Discard changes."
|
||||||
|
msgstr "Отменить изменения."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN"
|
||||||
|
msgid "Green"
|
||||||
|
msgstr "Зелёный"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||||
|
msgid "Visit the mod's website."
|
||||||
|
msgstr "Посетите сайт мода."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE"
|
||||||
|
msgid "Hue"
|
||||||
|
msgstr "Тон"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||||
|
msgid "Opens the folder containing the full mod configuration."
|
||||||
|
msgstr "Открыть папку, содержащую полную конфигурацию мода."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||||
|
msgid "Next"
|
||||||
|
msgstr "Следующий"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||||
|
msgid "Save these options. Some mods may require a restart for the options to take effect."
|
||||||
|
msgstr "Сохранить эти параметры. Для некоторых модов может потребоваться перезапуск, чтобы параметры вступили в силу."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "Предыдущий"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED"
|
||||||
|
msgid "Red"
|
||||||
|
msgstr "Красный"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||||
|
msgid "Resets the mod configuration to default values."
|
||||||
|
msgstr "Сбросить конфигурацию мода до значений по умолчанию."
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION"
|
||||||
|
msgid "Saturation"
|
||||||
|
msgstr "Насыщенность"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||||
|
msgid "Show or hide this options category"
|
||||||
|
msgstr "Показать или скрыть эту категорию настроек"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE"
|
||||||
|
msgid "Value"
|
||||||
|
msgstr "Яркость"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||||
|
msgid "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated."
|
||||||
|
msgstr "Текущая установленная версия этого мода.\n\nСравните эту версию с примечаниями к выпуску мода, чтобы узнать, не устарел ли он."
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Application: Oxygen Not Included"
|
||||||
|
"POT Version: 2.0"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||||
|
msgid "MANUAL CONFIG"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||||
|
msgid "RESET TO DEFAULT"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||||
|
msgid "Options for {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||||
|
msgid "Down Arrow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||||
|
msgid "Left Arrow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||||
|
msgid "Right Arrow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||||
|
msgid "Up Arrow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||||
|
msgid "Mods"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||||
|
msgid "Assembly Version: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||||
|
msgid "Mod Homepage"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||||
|
msgid "Mod Version: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||||
|
msgid "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||||
|
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||||
|
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE"
|
||||||
|
msgid "Blue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||||
|
msgid "Discard changes."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN"
|
||||||
|
msgid "Green"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||||
|
msgid "Visit the mod's website."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE"
|
||||||
|
msgid "Hue"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||||
|
msgid "Opens the folder containing the full mod configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||||
|
msgid "Next"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||||
|
msgid "Save these options. Some mods may require a restart for the options to take effect."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED"
|
||||||
|
msgid "Red"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||||
|
msgid "Resets the mod configuration to default values."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION"
|
||||||
|
msgid "Saturation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||||
|
msgid "Show or hide this options category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE"
|
||||||
|
msgid "Value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||||
|
msgid "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated."
|
||||||
|
msgstr ""
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
"Last-Translator: Ventulus\n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"Language: zh_CN\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"Application: Oxygen Not Included\n"
|
||||||
|
"X-Generator: Poedit 3.0.1\n"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||||
|
msgid "MANUAL CONFIG"
|
||||||
|
msgstr "手动配置"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_OK"
|
||||||
|
msgid "Done"
|
||||||
|
msgstr "完成"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_OPTIONS
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_OPTIONS"
|
||||||
|
msgid "OPTIONS"
|
||||||
|
msgstr "选项"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||||
|
msgid "RESET TO DEFAULT"
|
||||||
|
msgstr "重置为默认"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||||
|
msgid "Options for {0}"
|
||||||
|
msgstr "{0}的选项"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||||
|
msgid "Down Arrow"
|
||||||
|
msgstr "↓"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||||
|
msgid "Left Arrow"
|
||||||
|
msgstr "←"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||||
|
msgid "Right Arrow"
|
||||||
|
msgstr "→"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||||
|
msgid "Up Arrow"
|
||||||
|
msgstr "↑"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||||
|
msgid "Mods"
|
||||||
|
msgstr "模组"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_DELETE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_DELETE"
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Delete"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_END
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_END"
|
||||||
|
msgid "End"
|
||||||
|
msgstr "End"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_HOME
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_HOME"
|
||||||
|
msgid "Home"
|
||||||
|
msgstr "Home"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_PAGEDOWN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAGEDOWN"
|
||||||
|
msgid "Page Down"
|
||||||
|
msgstr "Page Down"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_PAGEUP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAGEUP"
|
||||||
|
msgid "Page Up"
|
||||||
|
msgstr "Page Up"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_PAUSE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAUSE"
|
||||||
|
msgid "Pause"
|
||||||
|
msgstr "Pause"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_PRTSCREEN
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PRTSCREEN"
|
||||||
|
msgid "Print Screen"
|
||||||
|
msgstr "Print Screen"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.KEY_SYSRQ
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_SYSRQ"
|
||||||
|
msgid "SysRq"
|
||||||
|
msgstr "SysRq"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||||
|
msgid "Assembly Version: {0}"
|
||||||
|
msgstr "程序集版本:{0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||||
|
msgid "Mod Homepage"
|
||||||
|
msgstr "模组主页"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||||
|
msgid "Mod Version: {0}"
|
||||||
|
msgstr "模组版本:{0}"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||||
|
msgid ""
|
||||||
|
"This mod is out of date!\n"
|
||||||
|
"New version: <b>{0}</b>\n"
|
||||||
|
"\n"
|
||||||
|
"Update local mods manually, or use <b>Mod Updater</b> to force update Steam "
|
||||||
|
"mods"
|
||||||
|
msgstr ""
|
||||||
|
"这个模组已经过时了!\n"
|
||||||
|
"新版本:<b>{0}</b>\n"
|
||||||
|
"\n"
|
||||||
|
"手动更新本地模组,或使用<b>Mod Updater</b>来强制更新Steam模组"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||||
|
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||||
|
msgstr "<b><style=\"logic_off\">过时了!</style></b>"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_CANCEL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_CANCEL"
|
||||||
|
msgid "CONTINUE"
|
||||||
|
msgstr "继续"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_OK"
|
||||||
|
msgid "RESTART"
|
||||||
|
msgstr "重启"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||||
|
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||||
|
msgstr "要使这些选项生效,必须重新启动游戏。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||||
|
msgid "Discard changes."
|
||||||
|
msgstr "放弃更改。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||||
|
msgid "Visit the mod's website."
|
||||||
|
msgstr "访问模组网站。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||||
|
msgid "Opens the folder containing the full mod configuration."
|
||||||
|
msgstr "打开包含完整模组配置的文件夹。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||||
|
msgid "Next"
|
||||||
|
msgstr "下一个"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||||
|
msgid ""
|
||||||
|
"Save these options. Some mods may require a restart for the options to take "
|
||||||
|
"effect."
|
||||||
|
msgstr "保存这些选项。某些模组可能需要重新启动才能使选项生效。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||||
|
msgid "Previous"
|
||||||
|
msgstr "上一个"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||||
|
msgid "Resets the mod configuration to default values."
|
||||||
|
msgstr "将模组配置重置为默认值。"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||||
|
msgid "Show or hide this options category"
|
||||||
|
msgstr "显示或隐藏此选项类别"
|
||||||
|
|
||||||
|
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||||
|
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||||
|
msgid ""
|
||||||
|
"The currently installed version of this mod.\n"
|
||||||
|
"\n"
|
||||||
|
"Compare this version with the mod's Release Notes to see if it is outdated."
|
||||||
|
msgstr ""
|
||||||
|
"此模组的当前安装版本。\n"
|
||||||
|
"\n"
|
||||||
|
"请将此版本与模组的发行说明进行比较,以查看它是否过时。"
|
|
@ -0,0 +1,316 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 Davis Cook
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Klei;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
|
||||||
|
using CodexDictionary = System.Collections.Generic.Dictionary<string, System.Collections.
|
||||||
|
Generic.ISet<string>>;
|
||||||
|
using WidgetMappingList = System.Collections.Generic.List<Tuple<string, System.Type>>;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Database {
|
||||||
|
/// <summary>
|
||||||
|
/// Handles codex entries for mods by automatically loading YAML entries and subentries for
|
||||||
|
/// critters and plants from the codex folder in their mod directories.
|
||||||
|
///
|
||||||
|
/// The layerable files loader in the stock game is broken, so this class is required to
|
||||||
|
/// correctly load new codex entries.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PCodexManager : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The subfolder from which critter codex entries are loaded.
|
||||||
|
/// </summary>
|
||||||
|
public const string CREATURES_DIR = "codex/Creatures";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The subfolder from which plant codex entries are loaded.
|
||||||
|
/// </summary>
|
||||||
|
public const string PLANTS_DIR = "codex/Plants";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file extension used for codex entry/subentries.
|
||||||
|
/// </summary>
|
||||||
|
public const string CODEX_FILES = "*.yaml";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The codex category under which critter entries should go.
|
||||||
|
/// </summary>
|
||||||
|
public const string CREATURES_CATEGORY = "CREATURES";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The codex category under which plant entries should go.
|
||||||
|
/// </summary>
|
||||||
|
public const string PLANTS_CATEGORY = "PLANTS";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow access to the private widget tag mappings field.
|
||||||
|
/// Detouring sadly is not possible because CodexCache is a static class and cannot be
|
||||||
|
/// a type parameter.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly FieldInfo WIDGET_TAG_MAPPINGS = typeof(CodexCache).
|
||||||
|
GetFieldSafe("widgetTagMappings", true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PCodexManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applied to CodexCache to collect dynamic codex entries from the file system.
|
||||||
|
/// </summary>
|
||||||
|
private static void CollectEntries_Postfix(string folder, List<CodexEntry> __result,
|
||||||
|
string ___baseEntryPath) {
|
||||||
|
// Check to see if we are loading from either the "Creatures" directory or
|
||||||
|
// "Plants" directory
|
||||||
|
if (Instance != null) {
|
||||||
|
string path = string.IsNullOrEmpty(folder) ? ___baseEntryPath : Path.Combine(
|
||||||
|
___baseEntryPath, folder);
|
||||||
|
bool modified = false;
|
||||||
|
if (path.EndsWith("Creatures")) {
|
||||||
|
__result.AddRange(Instance.LoadEntries(CREATURES_CATEGORY));
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (path.EndsWith("Plants")) {
|
||||||
|
__result.AddRange(Instance.LoadEntries(PLANTS_CATEGORY));
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (modified) {
|
||||||
|
foreach (var codexEntry in __result)
|
||||||
|
// Fill in a default sort string if necessary
|
||||||
|
if (string.IsNullOrEmpty(codexEntry.sortString))
|
||||||
|
codexEntry.sortString = Strings.Get(codexEntry.title);
|
||||||
|
__result.Sort((x, y) => x.sortString.CompareTo(y.sortString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applied to CodexCache to collect dynamic codex sub entries from the file system.
|
||||||
|
/// </summary>
|
||||||
|
private static void CollectSubEntries_Postfix(List<SubEntry> __result) {
|
||||||
|
if (Instance != null) {
|
||||||
|
int startSize = __result.Count;
|
||||||
|
__result.AddRange(Instance.LoadSubEntries());
|
||||||
|
if (__result.Count != startSize)
|
||||||
|
__result.Sort((x, y) => x.title.CompareTo(y.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads codex entries from the specified directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">The location where the data will be placed.</param>
|
||||||
|
/// <param name="dir">The directory to load.</param>
|
||||||
|
/// <param name="category">The category to assign to each entry thus loaded.</param>
|
||||||
|
private static void LoadFromDirectory(ICollection<CodexEntry> entries, string dir,
|
||||||
|
string category) {
|
||||||
|
string[] codexFiles = new string[0];
|
||||||
|
try {
|
||||||
|
// List codex data files in the codex directory
|
||||||
|
codexFiles = Directory.GetFiles(dir, CODEX_FILES);
|
||||||
|
} catch (UnauthorizedAccessException ex) {
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
}
|
||||||
|
var widgetTagMappings = WIDGET_TAG_MAPPINGS?.GetValue(null) as WidgetMappingList;
|
||||||
|
if (widgetTagMappings == null)
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Unable to load codex files: no tag mappings found");
|
||||||
|
foreach (string str in codexFiles)
|
||||||
|
try {
|
||||||
|
string filename = str;
|
||||||
|
var codexEntry = YamlIO.LoadFile<CodexEntry>(filename, YamlParseErrorCB,
|
||||||
|
widgetTagMappings);
|
||||||
|
if (codexEntry != null) {
|
||||||
|
codexEntry.category = category;
|
||||||
|
entries.Add(codexEntry);
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Unable to load codex files from {0}:".
|
||||||
|
F(dir));
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
} catch (InvalidDataException ex) {
|
||||||
|
PUtil.LogException(ex);
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Loaded codex entries from directory: {0}".F(dir));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads codex subentries from the specified directory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">The location where the data will be placed.</param>
|
||||||
|
/// <param name="dir">The directory to load.</param>
|
||||||
|
private static void LoadFromDirectory(ICollection<SubEntry> entries, string dir) {
|
||||||
|
string[] codexFiles = new string[0];
|
||||||
|
try {
|
||||||
|
// List codex data files in the codex directory
|
||||||
|
codexFiles = Directory.GetFiles(dir, CODEX_FILES, SearchOption.
|
||||||
|
AllDirectories);
|
||||||
|
} catch (UnauthorizedAccessException ex) {
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
}
|
||||||
|
var widgetTagMappings = WIDGET_TAG_MAPPINGS?.GetValue(null) as WidgetMappingList;
|
||||||
|
if (widgetTagMappings == null)
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Unable to load codex files: no tag mappings found");
|
||||||
|
foreach (string filename in codexFiles)
|
||||||
|
try {
|
||||||
|
var subEntry = YamlIO.LoadFile<SubEntry>(filename, YamlParseErrorCB,
|
||||||
|
widgetTagMappings);
|
||||||
|
if (entries != null)
|
||||||
|
entries.Add(subEntry);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Unable to load codex files from {0}:".
|
||||||
|
F(dir));
|
||||||
|
PUtil.LogExcWarn(ex);
|
||||||
|
} catch (InvalidDataException ex) {
|
||||||
|
PUtil.LogException(ex);
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Loaded codex sub entries from directory: {0}".
|
||||||
|
F(dir));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A callback function for the YAML parser to process errors that it throws.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The YAML parsing error</param>
|
||||||
|
internal static void YamlParseErrorCB(YamlIO.Error error, bool _) {
|
||||||
|
throw new InvalidDataException(string.Format("{0} parse error in {1}\n{2}", error.
|
||||||
|
severity, error.file.full_path, error.message), error.inner_exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The paths for creature codex entries.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ISet<string> creaturePaths;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The paths for plant codex entries.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ISet<string> plantPaths;
|
||||||
|
|
||||||
|
public PCodexManager() {
|
||||||
|
creaturePaths = new HashSet<string>();
|
||||||
|
plantPaths = new HashSet<string>();
|
||||||
|
// Data is a hacky but usable 2 item dictionary
|
||||||
|
InstanceData = new CodexDictionary(4) {
|
||||||
|
{ CREATURES_CATEGORY, creaturePaths },
|
||||||
|
{ PLANTS_CATEGORY, plantPaths }
|
||||||
|
};
|
||||||
|
PUtil.InitLibrary(false);
|
||||||
|
PRegistry.Instance.AddCandidateVersion(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
plibInstance.Patch(typeof(CodexCache), nameof(CodexCache.CollectEntries),
|
||||||
|
postfix: PatchMethod(nameof(CollectEntries_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(CodexCache), nameof(CodexCache.CollectSubEntries),
|
||||||
|
postfix: PatchMethod(nameof(CollectSubEntries_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all codex entries for all mods registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category">The codex category under which these data entries should be loaded.</param>
|
||||||
|
/// <returns>The list of entries that were loaded.</returns>
|
||||||
|
private IList<CodexEntry> LoadEntries(string category) {
|
||||||
|
var entries = new List<CodexEntry>(32);
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var mod in allMods) {
|
||||||
|
var codex = mod?.GetInstanceData<CodexDictionary>();
|
||||||
|
if (codex != null && codex.TryGetValue(category, out ISet<string> dirs))
|
||||||
|
foreach (var dir in dirs)
|
||||||
|
LoadFromDirectory(entries, dir, category);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all codex subentries for all mods registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The list of subentries that were loaded.</returns>
|
||||||
|
private IList<SubEntry> LoadSubEntries() {
|
||||||
|
var entries = new List<SubEntry>(32);
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var mod in allMods) {
|
||||||
|
var codex = mod?.GetInstanceData<CodexDictionary>();
|
||||||
|
if (codex != null)
|
||||||
|
// Lots of nested for, but required! (entryType should only have
|
||||||
|
// 2 values, and usually only one dir per mod)
|
||||||
|
foreach (var entryType in codex)
|
||||||
|
foreach (var dir in entryType.Value)
|
||||||
|
LoadFromDirectory(entries, dir);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the calling mod as having custom creature codex entries. The entries will
|
||||||
|
/// be read from the mod directory in the "codex/Creatures" subfolder. If the argument
|
||||||
|
/// is omitted, the calling assembly is registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to register as having creatures.</param>
|
||||||
|
public void RegisterCreatures(Assembly assembly = null) {
|
||||||
|
if (assembly == null)
|
||||||
|
assembly = Assembly.GetCallingAssembly();
|
||||||
|
string dir = Path.Combine(PUtil.GetModPath(assembly), CREATURES_DIR);
|
||||||
|
creaturePaths.Add(dir);
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Registered codex creatures directory: {0}".
|
||||||
|
F(dir));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the calling mod as having custom plant codex entries. The entries will
|
||||||
|
/// be read from the mod directory in the "codex/Plants" subfolder. If the argument
|
||||||
|
/// is omitted, the calling assembly is registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to register as having creatures.</param>
|
||||||
|
public void RegisterPlants(Assembly assembly = null) {
|
||||||
|
if (assembly == null)
|
||||||
|
assembly = Assembly.GetCallingAssembly();
|
||||||
|
string dir = Path.Combine(PUtil.GetModPath(assembly), PLANTS_DIR);
|
||||||
|
plantPaths.Add(dir);
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Registered codex plants directory: {0}".F(dir));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using Database;
|
||||||
|
using FMODUnity;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Detours;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Database {
|
||||||
|
/// <summary>
|
||||||
|
/// A wrapper class used to create ColonyAchievement instances.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PColonyAchievement {
|
||||||
|
/// <summary>
|
||||||
|
/// Prototypes the new ColonyAchievment constructor. This one is a monster with a
|
||||||
|
/// zillion parallel parameters used only for victory animations (Klei please!) and
|
||||||
|
/// gets changed often enough to warrant a detour.
|
||||||
|
/// </summary>
|
||||||
|
private delegate ColonyAchievement NewColonyAchievement(string Id,
|
||||||
|
string platformAchievementId, string Name, string description,
|
||||||
|
bool isVictoryCondition, List<ColonyAchievementRequirement> requirementChecklist,
|
||||||
|
string messageTitle, string messageBody, string videoDataName,
|
||||||
|
string victoryLoopVideo, Action<KMonoBehaviour> VictorySequence);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new colony achievement.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly NewColonyAchievement NEW_COLONY_ACHIEVEMENT =
|
||||||
|
typeof(ColonyAchievement).DetourConstructor<NewColonyAchievement>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The achievement description (string, not a string key!)
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The icon to use for the achievement.
|
||||||
|
/// </summary>
|
||||||
|
public string Icon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The achievement ID.
|
||||||
|
/// </summary>
|
||||||
|
public string ID { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this colony achievement is considered a victory achievement.
|
||||||
|
///
|
||||||
|
/// Victory achievements are displayed at the top, and can play a movie when they
|
||||||
|
/// are satisfied.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsVictory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The achievement display name (string, not a string key!)
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The callback triggered if this achievement is a victory achievement when it is
|
||||||
|
/// completed.
|
||||||
|
/// </summary>
|
||||||
|
public Action<KMonoBehaviour> OnVictory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The requirements for this achievement.
|
||||||
|
/// </summary>
|
||||||
|
public List<ColonyAchievementRequirement> Requirements { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This member is obsolete since the Sweet Dreams update. Use VictoryAudioSnapshoRef
|
||||||
|
/// instead.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Set victory audio snapshot directly due to Klei changes in the Sweet Dreams update")]
|
||||||
|
public string VictoryAudioSnapshot { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message body to display when this achievement triggers.
|
||||||
|
///
|
||||||
|
/// The game does not use this field by default, but it is available for victory
|
||||||
|
/// callbacks.
|
||||||
|
/// </summary>
|
||||||
|
public string VictoryMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message title to display when this achievement triggers.
|
||||||
|
///
|
||||||
|
/// The game does not use this field by default, but it is available for victory
|
||||||
|
/// callbacks.
|
||||||
|
/// </summary>
|
||||||
|
public string VictoryTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The video data file to play when this achievement triggers.
|
||||||
|
///
|
||||||
|
/// The game does not use this field by default, but it is available for victory
|
||||||
|
/// callbacks.
|
||||||
|
/// </summary>
|
||||||
|
public string VictoryVideoData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The video data file to loop behind the message when this achievement triggers.
|
||||||
|
///
|
||||||
|
/// The game does not use this field by default, but it is available for victory
|
||||||
|
/// callbacks.
|
||||||
|
/// </summary>
|
||||||
|
public string VictoryVideoLoop { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new colony achievement wrapper.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The achievement ID.</param>
|
||||||
|
public PColonyAchievement(string id) {
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
throw new ArgumentNullException(nameof(id));
|
||||||
|
Description = "";
|
||||||
|
Icon = "";
|
||||||
|
ID = id;
|
||||||
|
IsVictory = false;
|
||||||
|
Name = "";
|
||||||
|
OnVictory = null;
|
||||||
|
Requirements = null;
|
||||||
|
VictoryMessage = "";
|
||||||
|
VictoryTitle = "";
|
||||||
|
VictoryVideoData = "";
|
||||||
|
VictoryVideoLoop = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and adds the achievement to the database. As platform achievements cannot
|
||||||
|
/// be added using mods, the platform achievement ID will always be empty.
|
||||||
|
/// </summary>
|
||||||
|
public void AddAchievement() {
|
||||||
|
if (Requirements == null)
|
||||||
|
throw new ArgumentNullException("No colony achievement requirements specified");
|
||||||
|
var achieve = NEW_COLONY_ACHIEVEMENT.Invoke(ID, "", Name, Description, IsVictory,
|
||||||
|
Requirements, VictoryTitle, VictoryMessage, VictoryVideoData, VictoryVideoLoop,
|
||||||
|
OnVictory);
|
||||||
|
achieve.icon = Icon;
|
||||||
|
PDatabaseUtils.AddColonyAchievement(achieve);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is PColonyAchievement other && ID == other.ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return ID.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PColonyAchievement[ID={0},Name={1}]".F(ID, Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using Database;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Database {
|
||||||
|
/// <summary>
|
||||||
|
/// Functions which deal with entries in the game database and strings.
|
||||||
|
/// </summary>
|
||||||
|
public static class PDatabaseUtils {
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a colony achievement to the colony summary screen. Must be invoked after the
|
||||||
|
/// database is initialized (Db.Initialize() postfix recommended).
|
||||||
|
///
|
||||||
|
/// Note that achievement structures significantly changed from Vanilla to the DLC.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="achievement">The achievement to add.</param>
|
||||||
|
public static void AddColonyAchievement(ColonyAchievement achievement) {
|
||||||
|
if (achievement == null)
|
||||||
|
throw new ArgumentNullException(nameof(achievement));
|
||||||
|
Db.Get()?.ColonyAchievements?.resources?.Add(achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the name and description for a status item.
|
||||||
|
///
|
||||||
|
/// Must be used before the StatusItem is first instantiated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The status item ID.</param>
|
||||||
|
/// <param name="category">The status item category.</param>
|
||||||
|
/// <param name="name">The name to display in the UI.</param>
|
||||||
|
/// <param name="desc">The description to display in the UI.</param>
|
||||||
|
public static void AddStatusItemStrings(string id, string category, string name,
|
||||||
|
string desc) {
|
||||||
|
string uid = id.ToUpperInvariant();
|
||||||
|
string ucategory = category.ToUpperInvariant();
|
||||||
|
Strings.Add("STRINGS." + ucategory + ".STATUSITEMS." + uid + ".NAME", name);
|
||||||
|
Strings.Add("STRINGS." + ucategory + ".STATUSITEMS." + uid + ".TOOLTIP", desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message encountered by the PLib database system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The debug message.</param>
|
||||||
|
internal static void LogDatabaseDebug(string message) {
|
||||||
|
Debug.LogFormat("[PLibDatabase] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a warning encountered by the PLib database system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The warning message.</param>
|
||||||
|
internal static void LogDatabaseWarning(string message) {
|
||||||
|
Debug.LogWarningFormat("[PLibDatabase] {0}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Database</Title>
|
||||||
|
<AssemblyTitle>PLib.Database</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.Database</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibDatabase.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Database {
|
||||||
|
/// <summary>
|
||||||
|
/// Handles localization for mods by automatically loading po files from the translations
|
||||||
|
/// folder in their mod directories.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PLocalization : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// The subfolder from which translations will be loaded.
|
||||||
|
/// </summary>
|
||||||
|
public const string TRANSLATIONS_DIR = "translations";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Localizes the specified mod assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modAssembly">The assembly to localize.</param>
|
||||||
|
/// <param name="locale">The locale file name to be used.</param>
|
||||||
|
private static void Localize(Assembly modAssembly, Localization.Locale locale) {
|
||||||
|
string path = PUtil.GetModPath(modAssembly);
|
||||||
|
string locCode = locale.Code;
|
||||||
|
if (string.IsNullOrEmpty(locCode))
|
||||||
|
locCode = Localization.GetCurrentLanguageCode();
|
||||||
|
var poFile = Path.Combine(Path.Combine(path, TRANSLATIONS_DIR), locCode +
|
||||||
|
PLibLocalization.TRANSLATIONS_EXT);
|
||||||
|
try {
|
||||||
|
Localization.OverloadStrings(Localization.LoadStringsFile(poFile, false));
|
||||||
|
RewriteStrings(modAssembly);
|
||||||
|
} catch (FileNotFoundException) {
|
||||||
|
// No localization available for this locale
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("No {0} localization available for mod {1}".F(
|
||||||
|
locCode, modAssembly.GetNameSafe() ?? "?"));
|
||||||
|
#endif
|
||||||
|
} catch (DirectoryNotFoundException) {
|
||||||
|
} catch (IOException e) {
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Failed to load {0} localization for mod {1}:".
|
||||||
|
F(locCode, modAssembly.GetNameSafe() ?? "?"));
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches types in the assembly (no worries, Localization did this anyways, so they
|
||||||
|
/// all either loaded or failed to load) for fields that already had loc string keys
|
||||||
|
/// created, and fixes them if so.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to check for strings.</param>
|
||||||
|
internal static void RewriteStrings(Assembly assembly) {
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
foreach (var field in type.GetFields(PPatchTools.BASE_FLAGS | BindingFlags.
|
||||||
|
FlattenHierarchy | BindingFlags.Static)) {
|
||||||
|
// Only use fields of type LocString
|
||||||
|
if (field.FieldType == typeof(LocString) && field.GetValue(null) is
|
||||||
|
LocString ls) {
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Rewrote string {0}: {1} to {2}".F(ls.
|
||||||
|
key.String, Strings.Get(ls.key.String), ls.text));
|
||||||
|
#endif
|
||||||
|
Strings.Add(ls.key.String, ls.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The assemblies to be localized.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ICollection<Assembly> toLocalize;
|
||||||
|
|
||||||
|
public PLocalization() {
|
||||||
|
toLocalize = new List<Assembly>(4);
|
||||||
|
InstanceData = toLocalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debug dumps the translation templates for ALL registered PLib localized mods.
|
||||||
|
/// </summary>
|
||||||
|
internal void DumpAll() {
|
||||||
|
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||||
|
if (allMods != null)
|
||||||
|
foreach (var toDump in allMods) {
|
||||||
|
// Reach for those assemblies
|
||||||
|
var assemblies = toDump.GetInstanceData<ICollection<Assembly>>();
|
||||||
|
if (assemblies != null)
|
||||||
|
foreach (var modAssembly in assemblies)
|
||||||
|
ModUtil.RegisterForTranslation(modAssembly.GetTypes()[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
// PLibLocalization will invoke Process here
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Process(uint operation, object _) {
|
||||||
|
var locale = Localization.GetLocale();
|
||||||
|
if (locale != null && operation == 0)
|
||||||
|
foreach (var modAssembly in toLocalize)
|
||||||
|
Localize(modAssembly, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the specified assembly for automatic PLib localization. If the argument
|
||||||
|
/// is omitted, the calling assembly is registered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The assembly to register for PLib localization.</param>
|
||||||
|
public void Register(Assembly assembly = null) {
|
||||||
|
if (assembly == null)
|
||||||
|
assembly = Assembly.GetCallingAssembly();
|
||||||
|
var types = assembly.GetTypes();
|
||||||
|
if (types == null || types.Length == 0)
|
||||||
|
PDatabaseUtils.LogDatabaseWarning("Registered assembly " + assembly.
|
||||||
|
GetNameSafe() + " that had no types for localization!");
|
||||||
|
else {
|
||||||
|
RegisterForForwarding();
|
||||||
|
toLocalize.Add(assembly);
|
||||||
|
// This call searches all types in the assembly implicitly
|
||||||
|
Localization.RegisterForTranslation(types[0]);
|
||||||
|
#if DEBUG
|
||||||
|
PDatabaseUtils.LogDatabaseDebug("Localizing assembly {0} using base namespace {1}".
|
||||||
|
F(assembly.GetNameSafe(), types[0].Namespace));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// An interface describing local and remote instances of PLightShape.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILightShape {
|
||||||
|
/// <summary>
|
||||||
|
/// The light shape identifier.
|
||||||
|
/// </summary>
|
||||||
|
string Identifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Klei LightShape represented by this light shape, used in Light2D definitions.
|
||||||
|
/// </summary>
|
||||||
|
LightShape KleiLightShape { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The raycast mode used by this light shape. (-1) if no rays are to be emitted.
|
||||||
|
/// </summary>
|
||||||
|
LightShape RayMode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the light handler with the provided light information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The arguments passed to the user light handler.</param>
|
||||||
|
void FillLight(LightingArgs args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// Arguments which are passed to lighting callbacks to perform lighting calculations.
|
||||||
|
///
|
||||||
|
/// The range is the light radius supplied during the Light2D creation; do not light up
|
||||||
|
/// tiles outside of this radius (measured by a square around SourceCell)!
|
||||||
|
///
|
||||||
|
/// The source cell is the cell nearest to where the Light2D is currently located.
|
||||||
|
///
|
||||||
|
/// Use the IDictionary interface to store the relative brightness of cells by their cell
|
||||||
|
/// location. These values should be between 0 and 1 normally, with the maximum brightness
|
||||||
|
/// being set by the intensity parameter of the Light2D. The user is responsible for
|
||||||
|
/// ensuring that cells are valid before lighting them up.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LightingArgs : EventArgs, IDictionary<int, float> {
|
||||||
|
/// <summary>
|
||||||
|
/// The location where lighting results are stored.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<int, float> Brightness { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum range to use for cell lighting. Do not light up cells beyond this
|
||||||
|
/// range from SourceCell.
|
||||||
|
/// </summary>
|
||||||
|
public int Range { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source of the light.
|
||||||
|
/// </summary>
|
||||||
|
public GameObject Source { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The originating cell. Actual lighting can begin elsewhere, but the range limit is
|
||||||
|
/// measured from this cell.
|
||||||
|
/// </summary>
|
||||||
|
public int SourceCell { get; }
|
||||||
|
|
||||||
|
internal LightingArgs(GameObject source, int cell, int range,
|
||||||
|
IDictionary<int, float> output) {
|
||||||
|
if (source == null)
|
||||||
|
// Cannot use "throw" expression because of UnityEngine.Object.operator==
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
Brightness = output ?? throw new ArgumentNullException(nameof(output));
|
||||||
|
Range = range;
|
||||||
|
Source = source;
|
||||||
|
SourceCell = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IDictionary
|
||||||
|
|
||||||
|
public ICollection<int> Keys => Brightness.Keys;
|
||||||
|
|
||||||
|
public ICollection<float> Values => Brightness.Values;
|
||||||
|
|
||||||
|
public int Count => Brightness.Count;
|
||||||
|
|
||||||
|
public bool IsReadOnly => Brightness.IsReadOnly;
|
||||||
|
|
||||||
|
public float this[int key] { get => Brightness[key]; set => Brightness[key] = value; }
|
||||||
|
|
||||||
|
public bool ContainsKey(int key) {
|
||||||
|
return Brightness.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(int key, float value) {
|
||||||
|
Brightness.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(int key) {
|
||||||
|
return Brightness.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValue(int key, out float value) {
|
||||||
|
return Brightness.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(KeyValuePair<int, float> item) {
|
||||||
|
Brightness.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() {
|
||||||
|
Brightness.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(KeyValuePair<int, float> item) {
|
||||||
|
return Brightness.Contains(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyTo(KeyValuePair<int, float>[] array, int arrayIndex) {
|
||||||
|
Brightness.CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(KeyValuePair<int, float> item) {
|
||||||
|
return Brightness.Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<int, float>> GetEnumerator() {
|
||||||
|
return Brightness.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
return Brightness.GetEnumerator();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("LightingArgs[source={0:D},range={1:D}]", SourceCell, Range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Detours;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using IntHandle = HandleVector<int>.Handle;
|
||||||
|
using LightGridEmitter = LightGridManager.LightGridEmitter;
|
||||||
|
using TranspiledMethod = System.Collections.Generic.IEnumerable<HarmonyLib.CodeInstruction>;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all patches (many!) required by the PLib Lighting subsystem. Only applied by
|
||||||
|
/// the latest version of PLightManager.
|
||||||
|
/// </summary>
|
||||||
|
internal static class LightingPatches {
|
||||||
|
private delegate IntHandle AddToLayerDelegate(Light2D instance, Extents ext,
|
||||||
|
ScenePartitionerLayer layer);
|
||||||
|
|
||||||
|
private static readonly DetouredMethod<AddToLayerDelegate> ADD_TO_LAYER =
|
||||||
|
typeof(Light2D).DetourLazy<AddToLayerDelegate>("AddToLayer");
|
||||||
|
|
||||||
|
private static readonly IDetouredField<Light2D, int> ORIGIN = PDetours.
|
||||||
|
DetourFieldLazy<Light2D, int>("origin");
|
||||||
|
|
||||||
|
private static readonly IDetouredField<LightShapePreview, int> PREVIOUS_CELL =
|
||||||
|
PDetours.DetourFieldLazy<LightShapePreview, int>("previousCell");
|
||||||
|
|
||||||
|
private static bool ComputeExtents_Prefix(Light2D __instance, ref Extents __result) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
bool cont = true;
|
||||||
|
if (lm != null && __instance != null) {
|
||||||
|
var shape = __instance.shape;
|
||||||
|
int rad = Mathf.CeilToInt(__instance.Range), cell;
|
||||||
|
lm.AddLight(__instance.emitter, __instance.gameObject);
|
||||||
|
if (shape > LightShape.Cone && rad > 0 && Grid.IsValidCell(cell = ORIGIN.Get(
|
||||||
|
__instance))) {
|
||||||
|
Grid.CellToXY(cell, out int x, out int y);
|
||||||
|
// Better safe than sorry, check whole possible radius
|
||||||
|
__result = new Extents(x - rad, y - rad, 2 * rad, 2 * rad);
|
||||||
|
cont = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the required lighting related patches.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plibInstance">The Harmony instance to use for patching.</param>
|
||||||
|
public static void ApplyPatches(Harmony plibInstance) {
|
||||||
|
// Light2D
|
||||||
|
plibInstance.Patch(typeof(Light2D), "ComputeExtents",
|
||||||
|
prefix: PatchMethod(nameof(ComputeExtents_Prefix)));
|
||||||
|
plibInstance.Patch(typeof(Light2D), nameof(Light2D.FullRemove), postfix:
|
||||||
|
PatchMethod(nameof(Light2D_FullRemove_Postfix)));
|
||||||
|
plibInstance.Patch(typeof(Light2D), nameof(Light2D.RefreshShapeAndPosition),
|
||||||
|
postfix: PatchMethod(nameof(Light2D_RefreshShapeAndPosition_Postfix)));
|
||||||
|
|
||||||
|
// LightBuffer
|
||||||
|
try {
|
||||||
|
plibInstance.PatchTranspile(typeof(LightBuffer), "LateUpdate", PatchMethod(
|
||||||
|
nameof(LightBuffer_LateUpdate_Transpile)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Only visual, log as warning
|
||||||
|
PUtil.LogExcWarn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LightGridEmitter
|
||||||
|
plibInstance.Patch(typeof(LightGridEmitter), "ComputeLux", prefix:
|
||||||
|
PatchMethod(nameof(ComputeLux_Prefix)));
|
||||||
|
plibInstance.Patch(typeof(LightGridEmitter), nameof(LightGridEmitter.
|
||||||
|
UpdateLitCells), prefix: PatchMethod(nameof(UpdateLitCells_Prefix)));
|
||||||
|
|
||||||
|
// LightGridManager
|
||||||
|
plibInstance.Patch(typeof(LightGridManager), nameof(LightGridManager.
|
||||||
|
CreatePreview), prefix: PatchMethod(nameof(CreatePreview_Prefix)));
|
||||||
|
|
||||||
|
// LightShapePreview
|
||||||
|
plibInstance.Patch(typeof(LightShapePreview), "Update", prefix:
|
||||||
|
PatchMethod(nameof(LightShapePreview_Update_Prefix)));
|
||||||
|
|
||||||
|
// Rotatable
|
||||||
|
plibInstance.Patch(typeof(Rotatable), "OrientVisualizer", postfix:
|
||||||
|
PatchMethod(nameof(OrientVisualizer_Postfix)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ComputeLux_Prefix(LightGridEmitter __instance, int cell,
|
||||||
|
LightGridEmitter.State ___state, ref int __result) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
return lm == null || !lm.GetBrightness(__instance, cell, ___state, out __result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CreatePreview_Prefix(int origin_cell, float radius,
|
||||||
|
LightShape shape, int lux) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
return lm == null || !lm.PreviewLight(origin_cell, radius, shape, lux);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Light2D_FullRemove_Postfix(Light2D __instance) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
if (lm != null && __instance != null)
|
||||||
|
lm.DestroyLight(__instance.emitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Light2D_RefreshShapeAndPosition_Postfix(Light2D __instance,
|
||||||
|
Light2D.RefreshResult __result) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
if (lm != null && __instance != null && __result == Light2D.RefreshResult.Updated)
|
||||||
|
lm.AddLight(__instance.emitter, __instance.gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TranspiledMethod LightBuffer_LateUpdate_Transpile(
|
||||||
|
TranspiledMethod body) {
|
||||||
|
var target = typeof(Light2D).GetPropertySafe<LightShape>(nameof(Light2D.shape),
|
||||||
|
false)?.GetGetMethod(true);
|
||||||
|
return (target == null) ? body : PPatchTools.ReplaceMethodCallSafe(body, target,
|
||||||
|
typeof(PLightManager).GetMethodSafe(nameof(PLightManager.LightShapeToRayShape),
|
||||||
|
true, typeof(Light2D)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LightShapePreview_Update_Prefix(LightShapePreview __instance) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
// Pass the preview object into LightGridManager
|
||||||
|
if (lm != null && __instance != null)
|
||||||
|
lm.PreviewObject = __instance.gameObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OrientVisualizer_Postfix(Rotatable __instance) {
|
||||||
|
// Force regeneration on next Update()
|
||||||
|
if (__instance != null && __instance.TryGetComponent(out LightShapePreview
|
||||||
|
preview))
|
||||||
|
PREVIOUS_CELL.Set(preview, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a HarmonyMethod instance for manual patching using a method from this class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The method name.</param>
|
||||||
|
/// <returns>A reference to that method as a HarmonyMethod for patching.</returns>
|
||||||
|
private static HarmonyMethod PatchMethod(string name) {
|
||||||
|
return new HarmonyMethod(typeof(LightingPatches), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UpdateLitCells_Prefix(LightGridEmitter __instance,
|
||||||
|
List<int> ___litCells, LightGridEmitter.State ___state) {
|
||||||
|
var lm = PLightManager.Instance;
|
||||||
|
return lm == null || !lm.UpdateLitCells(__instance, ___state, ___litCells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.Detours;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
|
||||||
|
using Octant = DiscreteShadowCaster.Octant;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// A builder class which creates default light patterns based on octants.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OctantBuilder {
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type called to run the default DiscreteShadowCaster.ScanOctant.
|
||||||
|
/// </summary>
|
||||||
|
private delegate void ScanOctantFunc(Vector2I cellPos, int range, int depth,
|
||||||
|
Octant octant, double startSlope, double endSlope, List<int> visiblePoints);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method to call to scan octants.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ScanOctantFunc OCTANT_SCAN;
|
||||||
|
|
||||||
|
static OctantBuilder() {
|
||||||
|
// Cache the method for faster execution
|
||||||
|
OCTANT_SCAN = typeof(DiscreteShadowCaster).Detour<ScanOctantFunc>("ScanOctant");
|
||||||
|
if (OCTANT_SCAN == null)
|
||||||
|
PLightManager.LogLightingWarning("OctantBuilder cannot find default octant scanner!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The fallout to use when building the light.
|
||||||
|
/// </summary>
|
||||||
|
public float Falloff { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If false, uses the default game smoothing. If true, uses better smoothing.
|
||||||
|
/// </summary>
|
||||||
|
public bool SmoothLight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The origin cell.
|
||||||
|
/// </summary>
|
||||||
|
public int SourceCell { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The location where light cells are added.
|
||||||
|
/// </summary>
|
||||||
|
private readonly BrightnessDict destination;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new octant builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destination">The location where the lit cells will be placed.</param>
|
||||||
|
/// <param name="sourceCell">The origin cell of the light.</param>
|
||||||
|
public OctantBuilder(BrightnessDict destination, int sourceCell) {
|
||||||
|
if (!Grid.IsValidCell(sourceCell))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sourceCell));
|
||||||
|
this.destination = destination ?? throw new ArgumentNullException(nameof(
|
||||||
|
destination));
|
||||||
|
destination[sourceCell] = 1.0f;
|
||||||
|
Falloff = 0.5f;
|
||||||
|
// Use the default game's light algorithm
|
||||||
|
SmoothLight = false;
|
||||||
|
SourceCell = sourceCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an octant of light.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range">The range of the light.</param>
|
||||||
|
/// <param name="octant">The octant to scan.</param>
|
||||||
|
/// <returns>This object, for call chaining.</returns>
|
||||||
|
public OctantBuilder AddOctant(int range, Octant octant) {
|
||||||
|
var points = ListPool<int, OctantBuilder>.Allocate();
|
||||||
|
OCTANT_SCAN?.Invoke(Grid.CellToXY(SourceCell), range, 1, octant, 1.0, 0.0, points);
|
||||||
|
// Transfer to our array using:
|
||||||
|
foreach (int cell in points) {
|
||||||
|
float intensity;
|
||||||
|
if (SmoothLight)
|
||||||
|
// Better, not rounded falloff
|
||||||
|
intensity = PLightManager.GetSmoothFalloff(Falloff, cell, SourceCell);
|
||||||
|
else
|
||||||
|
// Default falloff
|
||||||
|
intensity = PLightManager.GetDefaultFalloff(Falloff, cell, SourceCell);
|
||||||
|
destination[cell] = intensity;
|
||||||
|
}
|
||||||
|
points.Recycle();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("OctantBuilder[Cell {0:D}, {1:D} lit]", SourceCell,
|
||||||
|
destination.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>PLib Lighting</Title>
|
||||||
|
<AssemblyTitle>PLib.Lighting</AssemblyTitle>
|
||||||
|
<Version>4.11.0.0</Version>
|
||||||
|
<UsesPLib>false</UsesPLib>
|
||||||
|
<RootNamespace>PeterHan.PLib.Lighting</RootNamespace>
|
||||||
|
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||||
|
<DistributeMod>false</DistributeMod>
|
||||||
|
<PLibCore>true</PLibCore>
|
||||||
|
<Platforms>Vanilla;Mergedown</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<DocumentationFile>bin\$(Platform)\Release\PLibLighting.xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
|
@ -0,0 +1,378 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using HarmonyLib;
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
|
||||||
|
using LightGridEmitter = LightGridManager.LightGridEmitter;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// Manages lighting. Instantiated only by the latest PLib version.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PLightManager : PForwardedComponent {
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by classes which want to handle lighting calls.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The parameters to use for lighting, and the location to
|
||||||
|
/// store results. See the LightingArgs class documentation for details.</param>
|
||||||
|
public delegate void CastLightDelegate(LightingArgs args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A singleton empty list instance for default values.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly List<object> EMPTY_SHAPES = new List<object>(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The version of this component. Uses the running PLib version.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, enables the smooth light falloff mode even on vanilla lights.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool ForceSmoothLight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantiated copy of this class.
|
||||||
|
/// </summary>
|
||||||
|
internal static PLightManager Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the brightness falloff as it would be in the stock game.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="falloffRate">The falloff rate to use.</param>
|
||||||
|
/// <param name="cell">The cell where falloff is being computed.</param>
|
||||||
|
/// <param name="origin">The light origin cell.</param>
|
||||||
|
/// <returns>The brightness at that location from 0 to 1.</returns>
|
||||||
|
public static float GetDefaultFalloff(float falloffRate, int cell, int origin) {
|
||||||
|
return 1.0f / Math.Max(1.0f, Mathf.RoundToInt(falloffRate * Math.Max(Grid.
|
||||||
|
GetCellDistance(origin, cell), 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the brightness falloff similar to the default falloff, but far smoother.
|
||||||
|
/// Slightly heavier on computation however.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="falloffRate">The falloff rate to use.</param>
|
||||||
|
/// <param name="cell">The cell where falloff is being computed.</param>
|
||||||
|
/// <param name="origin">The light origin cell.</param>
|
||||||
|
/// <returns>The brightness at that location from 0 to 1.</returns>
|
||||||
|
public static float GetSmoothFalloff(float falloffRate, int cell, int origin) {
|
||||||
|
Vector2I newCell = Grid.CellToXY(cell), start = Grid.CellToXY(origin);
|
||||||
|
return 1.0f / Math.Max(1.0f, falloffRate * PUtil.Distance(start.X, start.Y,
|
||||||
|
newCell.X, newCell.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raycasting shape to use for the given light.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="light">The light which is being drawn.</param>
|
||||||
|
/// <returns>The shape to use for its rays.</returns>
|
||||||
|
internal static LightShape LightShapeToRayShape(Light2D light) {
|
||||||
|
var shape = light.shape;
|
||||||
|
if (shape != LightShape.Cone && shape != LightShape.Circle)
|
||||||
|
shape = Instance.GetRayShape(shape);
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a message encountered by the PLib lighting system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The debug message.</param>
|
||||||
|
internal static void LogLightingDebug(string message) {
|
||||||
|
Debug.LogFormat("[PLibLighting] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a warning encountered by the PLib lighting system.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The warning message.</param>
|
||||||
|
internal static void LogLightingWarning(string message) {
|
||||||
|
Debug.LogWarningFormat("[PLibLighting] {0}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Version Version => VERSION;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The light brightness set by the last lighting brightness request.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ConcurrentDictionary<LightGridEmitter, CacheEntry> brightCache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last object that requested a preview. Only one preview can be requested at a
|
||||||
|
/// time, so no need for thread safety.
|
||||||
|
/// </summary>
|
||||||
|
internal GameObject PreviewObject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The lighting shapes available, all in this mod's namespace.
|
||||||
|
/// </summary>
|
||||||
|
private readonly IList<ILightShape> shapes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a lighting manager to register PLib lighting.
|
||||||
|
/// </summary>
|
||||||
|
public PLightManager() {
|
||||||
|
// Needs to be thread safe!
|
||||||
|
brightCache = new ConcurrentDictionary<LightGridEmitter, CacheEntry>(2, 128);
|
||||||
|
PreviewObject = null;
|
||||||
|
shapes = new List<ILightShape>(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a light to the lookup table.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the light.</param>
|
||||||
|
/// <param name="owner">The light's owning game object.</param>
|
||||||
|
internal void AddLight(LightGridEmitter source, GameObject owner) {
|
||||||
|
if (owner == null)
|
||||||
|
throw new ArgumentNullException(nameof(owner));
|
||||||
|
if (source == null)
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
// The default equality comparer will be used; since each Light2D is supposed
|
||||||
|
// to have exactly one LightGridEmitter, this should be fine
|
||||||
|
brightCache.TryAdd(source, new CacheEntry(owner));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Bootstrap(Harmony plibInstance) {
|
||||||
|
SetSharedData(new List<object>(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends a call to lighting update initiated by CreateLight.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the light.</param>
|
||||||
|
internal void DestroyLight(LightGridEmitter source) {
|
||||||
|
if (source != null)
|
||||||
|
brightCache.TryRemove(source, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the brightness at a given cell for the specified light source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the light.</param>
|
||||||
|
/// <param name="location">The location to check.</param>
|
||||||
|
/// <param name="state">The lighting state.</param>
|
||||||
|
/// <param name="result">The brightness there.</param>
|
||||||
|
/// <returns>true if that brightness is valid, or false otherwise.</returns>
|
||||||
|
internal bool GetBrightness(LightGridEmitter source, int location,
|
||||||
|
LightGridEmitter.State state, out int result) {
|
||||||
|
bool valid;
|
||||||
|
var shape = state.shape;
|
||||||
|
if (shape != LightShape.Cone && shape != LightShape.Circle) {
|
||||||
|
valid = brightCache.TryGetValue(source, out CacheEntry cacheEntry);
|
||||||
|
if (valid) {
|
||||||
|
valid = cacheEntry.Intensity.TryGetValue(location, out float ratio);
|
||||||
|
if (valid)
|
||||||
|
result = Mathf.RoundToInt(cacheEntry.BaseLux * ratio);
|
||||||
|
else {
|
||||||
|
#if DEBUG
|
||||||
|
LogLightingDebug("GetBrightness for invalid cell at {0:D}".F(location));
|
||||||
|
#endif
|
||||||
|
result = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#if DEBUG
|
||||||
|
LogLightingDebug("GetBrightness for invalid emitter at {0:D}".F(location));
|
||||||
|
#endif
|
||||||
|
result = 0;
|
||||||
|
}
|
||||||
|
} else if (ForceSmoothLight) {
|
||||||
|
// Use smooth light even for vanilla Cone and Circle
|
||||||
|
result = Mathf.RoundToInt(state.intensity * GetSmoothFalloff(state.falloffRate,
|
||||||
|
location, state.origin));
|
||||||
|
valid = true;
|
||||||
|
} else {
|
||||||
|
// Stock
|
||||||
|
result = 0;
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see if a light has specified one of the built-in ray options to cast
|
||||||
|
/// the little yellow rays around it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="shape">The light shape to check.</param>
|
||||||
|
/// <returns>The light shape to use for ray casting, or the original shape if it is
|
||||||
|
/// a stock shape or a light shape not known to PLib Lighting.</returns>
|
||||||
|
internal LightShape GetRayShape(LightShape shape) {
|
||||||
|
int index = shape - LightShape.Cone - 1;
|
||||||
|
ILightShape ps;
|
||||||
|
if (index >= 0 && index < shapes.Count && (ps = shapes[index]) != null) {
|
||||||
|
var newShape = ps.RayMode;
|
||||||
|
if (newShape >= LightShape.Circle)
|
||||||
|
shape = newShape;
|
||||||
|
}
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(Harmony plibInstance) {
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
shapes.Clear();
|
||||||
|
foreach (var light in GetSharedData(EMPTY_SHAPES)) {
|
||||||
|
var ls = PRemoteLightWrapper.LightToInstance(light);
|
||||||
|
shapes.Add(ls);
|
||||||
|
if (ls == null)
|
||||||
|
// Moe must clean it!
|
||||||
|
LogLightingWarning("Foreign contaminant in PLightManager!");
|
||||||
|
}
|
||||||
|
|
||||||
|
LightingPatches.ApplyPatches(plibInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the preview for a given light.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">The starting cell.</param>
|
||||||
|
/// <param name="radius">The light radius.</param>
|
||||||
|
/// <param name="shape">The light shape.</param>
|
||||||
|
/// <param name="lux">The base brightness in lux.</param>
|
||||||
|
/// <returns>true if the lighting was handled, or false otherwise.</returns>
|
||||||
|
internal bool PreviewLight(int origin, float radius, LightShape shape, int lux) {
|
||||||
|
bool handled = false;
|
||||||
|
var owner = PreviewObject;
|
||||||
|
int index = shape - LightShape.Cone - 1;
|
||||||
|
if (index >= 0 && index < shapes.Count && owner != null) {
|
||||||
|
var cells = DictionaryPool<int, float, PLightManager>.Allocate();
|
||||||
|
// Found handler!
|
||||||
|
shapes[index]?.FillLight(new LightingArgs(owner, origin,
|
||||||
|
(int)radius, cells));
|
||||||
|
foreach (var pair in cells) {
|
||||||
|
int cell = pair.Key;
|
||||||
|
if (Grid.IsValidCell(cell)) {
|
||||||
|
// Allow any fraction, not just linear falloff
|
||||||
|
int lightValue = Mathf.RoundToInt(lux * pair.Value);
|
||||||
|
LightGridManager.previewLightCells.Add(new Tuple<int, int>(cell,
|
||||||
|
lightValue));
|
||||||
|
LightGridManager.previewLux[cell] = lightValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PreviewObject = null;
|
||||||
|
handled = true;
|
||||||
|
cells.Recycle();
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a light shape handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identifier">A unique identifier for this shape. If another mod has
|
||||||
|
/// already registered that identifier, the previous mod will take precedence.</param>
|
||||||
|
/// <param name="handler">The handler for that shape.</param>
|
||||||
|
/// <param name="rayMode">The type of visual rays that are displayed from the light.</param>
|
||||||
|
/// <returns>The light shape which can be used.</returns>
|
||||||
|
public ILightShape Register(string identifier, CastLightDelegate handler,
|
||||||
|
LightShape rayMode = (LightShape)(-1)) {
|
||||||
|
if (string.IsNullOrEmpty(identifier))
|
||||||
|
throw new ArgumentNullException(nameof(identifier));
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler));
|
||||||
|
ILightShape lightShape = null;
|
||||||
|
RegisterForForwarding();
|
||||||
|
// Try to find a match for this identifier
|
||||||
|
var registered = GetSharedData(EMPTY_SHAPES);
|
||||||
|
int n = registered.Count;
|
||||||
|
foreach (var obj in registered) {
|
||||||
|
var light = PRemoteLightWrapper.LightToInstance(obj);
|
||||||
|
// Might be from another assembly so the types may or may not be compatible
|
||||||
|
if (light != null && light.Identifier == identifier) {
|
||||||
|
LogLightingDebug("Found existing light shape: " + identifier + " from " +
|
||||||
|
(obj.GetType().Assembly.GetNameSafe() ?? "?"));
|
||||||
|
lightShape = light;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lightShape == null) {
|
||||||
|
// Not currently existing
|
||||||
|
lightShape = new PLightShape(n + 1, identifier, handler, rayMode);
|
||||||
|
LogLightingDebug("Registered new light shape: " + identifier);
|
||||||
|
registered.Add(lightShape);
|
||||||
|
}
|
||||||
|
return lightShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the lit cells list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the light.</param>
|
||||||
|
/// <param name="state">The light emitter state.</param>
|
||||||
|
/// <param name="litCells">The location where lit cells will be placed.</param>
|
||||||
|
/// <returns>true if the lighting was handled, or false otherwise.</returns>
|
||||||
|
internal bool UpdateLitCells(LightGridEmitter source, LightGridEmitter.State state,
|
||||||
|
IList<int> litCells) {
|
||||||
|
bool handled = false;
|
||||||
|
int index = state.shape - LightShape.Cone - 1;
|
||||||
|
if (source == null)
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
if (index >= 0 && index < shapes.Count && litCells != null && brightCache.
|
||||||
|
TryGetValue(source, out CacheEntry entry)) {
|
||||||
|
var ps = shapes[index];
|
||||||
|
var brightness = entry.Intensity;
|
||||||
|
// Proper owner found
|
||||||
|
brightness.Clear();
|
||||||
|
entry.BaseLux = state.intensity;
|
||||||
|
ps.FillLight(new LightingArgs(entry.Owner, state.origin, (int)state.
|
||||||
|
radius, brightness));
|
||||||
|
foreach (var point in brightness)
|
||||||
|
litCells.Add(point.Key);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A cache entry in the light brightness cache.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class CacheEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The base intensity in lux.
|
||||||
|
/// </summary>
|
||||||
|
internal int BaseLux { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The relative brightness per cell.
|
||||||
|
/// </summary>
|
||||||
|
internal BrightnessDict Intensity { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The owner which initiated the lighting call.
|
||||||
|
/// </summary>
|
||||||
|
internal GameObject Owner { get; }
|
||||||
|
|
||||||
|
internal CacheEntry(GameObject owner) {
|
||||||
|
// Do not use the pool because these might last a long time and be numerous
|
||||||
|
Intensity = new Dictionary<int, float>(64);
|
||||||
|
Owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "Lighting Cache Entry for " + (Owner == null ? "" : Owner.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a light shape which can be used by mods.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PLightShape : ILightShape {
|
||||||
|
/// <summary>
|
||||||
|
/// The handler for this light shape.
|
||||||
|
/// </summary>
|
||||||
|
private readonly PLightManager.CastLightDelegate handler;
|
||||||
|
|
||||||
|
public string Identifier { get; }
|
||||||
|
|
||||||
|
public LightShape KleiLightShape {
|
||||||
|
get {
|
||||||
|
return ShapeID + LightShape.Cone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LightShape RayMode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The light shape ID.
|
||||||
|
/// </summary>
|
||||||
|
internal int ShapeID { get; }
|
||||||
|
|
||||||
|
internal PLightShape(int id, string identifier, PLightManager.CastLightDelegate handler,
|
||||||
|
LightShape rayMode) {
|
||||||
|
this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||||
|
Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier));
|
||||||
|
RayMode = rayMode;
|
||||||
|
ShapeID = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
return obj is PLightShape other && other.Identifier == Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the light handler with the provided light information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the light.</param>
|
||||||
|
/// <param name="cell">The origin cell.</param>
|
||||||
|
/// <param name="range">The range to fill.</param>
|
||||||
|
/// <param name="brightness">The location where lit points will be stored.</param>
|
||||||
|
internal void DoFillLight(GameObject source, int cell, int range,
|
||||||
|
BrightnessDict brightness) {
|
||||||
|
handler.Invoke(new LightingArgs(source, cell, range, brightness));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FillLight(LightingArgs args) {
|
||||||
|
handler.Invoke(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Identifier.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "PLightShape[ID=" + Identifier + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using BrightnessDict = System.Collections.Generic.IDictionary<int, float>;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Lighting {
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a lighting system call from another mod's namespace.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PRemoteLightWrapper : ILightShape {
|
||||||
|
// The delegate type covering calls to FillLight from other mods.
|
||||||
|
private delegate void FillLightDelegate(GameObject source, int cell, int range,
|
||||||
|
BrightnessDict brightness);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a light shape instance from another mod.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">The object to convert.</param>
|
||||||
|
/// <returns>A light shape object in this mod's namespace that delegates lighting
|
||||||
|
/// calls to the other mod if necessary.</returns>
|
||||||
|
internal static ILightShape LightToInstance(object other) {
|
||||||
|
return (other == null || other.GetType().Name != nameof(PLightShape)) ? null :
|
||||||
|
((other is ILightShape ls) ? ls : new PRemoteLightWrapper(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method to call when lighting system handling is requested.
|
||||||
|
/// </summary>
|
||||||
|
private readonly FillLightDelegate fillLight;
|
||||||
|
|
||||||
|
public string Identifier { get; }
|
||||||
|
|
||||||
|
public LightShape KleiLightShape { get; }
|
||||||
|
|
||||||
|
public LightShape RayMode { get; }
|
||||||
|
|
||||||
|
internal PRemoteLightWrapper(object other) {
|
||||||
|
if (other == null)
|
||||||
|
throw new ArgumentNullException(nameof(other));
|
||||||
|
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.KleiLightShape),
|
||||||
|
out LightShape ls))
|
||||||
|
throw new ArgumentException("Light shape is missing KleiLightShape");
|
||||||
|
KleiLightShape = ls;
|
||||||
|
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.Identifier),
|
||||||
|
out string id) || id == null)
|
||||||
|
throw new ArgumentException("Light shape is missing Identifier");
|
||||||
|
Identifier = id;
|
||||||
|
if (!PPatchTools.TryGetPropertyValue(other, nameof(ILightShape.RayMode),
|
||||||
|
out LightShape rm))
|
||||||
|
rm = (LightShape)(-1);
|
||||||
|
RayMode = rm;
|
||||||
|
var otherType = other.GetType();
|
||||||
|
fillLight = otherType.CreateDelegate<FillLightDelegate>(nameof(PLightShape.
|
||||||
|
DoFillLight), other, typeof(GameObject), typeof(int), typeof(int),
|
||||||
|
typeof(BrightnessDict));
|
||||||
|
if (fillLight == null)
|
||||||
|
throw new ArgumentException("Light shape is missing FillLight");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FillLight(LightingArgs args) {
|
||||||
|
fillLight.Invoke(args.Source, args.SourceCell, args.Range, args.Brightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry that displays a button. Not intended to be serializable to the
|
||||||
|
/// options file, instead declare a read-only property that returns a handler method as
|
||||||
|
/// an Action in the settings class, e.g:
|
||||||
|
///
|
||||||
|
/// [Option("Click Here!", "Button tool tip")]
|
||||||
|
/// public System.Action<object> MyButton => Handler;
|
||||||
|
///
|
||||||
|
/// public void Handler() {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// </summary>
|
||||||
|
public class ButtonOptionsEntry : OptionsEntry {
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value is Action<object> newValue)
|
||||||
|
this.value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The action to invoke when the button is pushed.
|
||||||
|
/// </summary>
|
||||||
|
private Action<object> value;
|
||||||
|
|
||||||
|
public ButtonOptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||||
|
|
||||||
|
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||||
|
parent.AddChild(new PButton(Field) {
|
||||||
|
Text = LookInStrings(Title), ToolTip = LookInStrings(Tooltip),
|
||||||
|
OnClick = OnButtonClicked
|
||||||
|
}.SetKleiPinkStyle(), new GridComponentSpec(row, 0) {
|
||||||
|
Margin = CONTROL_MARGIN, Alignment = TextAnchor.MiddleCenter, ColumnSpan = 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnButtonClicked(GameObject _) {
|
||||||
|
value?.Invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
// Will not be invoked
|
||||||
|
return new GameObject("Empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// Handles events for expanding and contracting options categories.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CategoryExpandHandler {
|
||||||
|
/// <summary>
|
||||||
|
/// The realized panel containing the options.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject contents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The initial state of the button.
|
||||||
|
/// </summary>
|
||||||
|
private readonly bool initialState;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized toggle button.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject toggle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new options category.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="initialState">true to start expanded, or false to start collapsed.</param>
|
||||||
|
public CategoryExpandHandler(bool initialState = true) {
|
||||||
|
this.initialState = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the options category is expanded or contracted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="on">true if the button is on, or false if it is off.</param>
|
||||||
|
public void OnExpandContract(GameObject _, bool on) {
|
||||||
|
var scale = on ? Vector3.one : Vector3.zero;
|
||||||
|
if (contents != null) {
|
||||||
|
var rt = contents.rectTransform();
|
||||||
|
rt.localScale = scale;
|
||||||
|
if (rt != null)
|
||||||
|
UnityEngine.UI.LayoutRebuilder.MarkLayoutForRebuild(rt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the header is clicked.
|
||||||
|
/// </summary>
|
||||||
|
private void OnHeaderClicked() {
|
||||||
|
if (toggle != null) {
|
||||||
|
bool state = PToggle.GetToggleState(toggle);
|
||||||
|
PToggle.SetToggleState(toggle, !state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the category label is realized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="header">The realized header label of the category.</param>
|
||||||
|
public void OnRealizeHeader(GameObject header) {
|
||||||
|
var button = header.AddComponent<UnityEngine.UI.Button>();
|
||||||
|
button.onClick.AddListener(new UnityAction(OnHeaderClicked));
|
||||||
|
button.interactable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the body is realized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="panel">The realized body of the category.</param>
|
||||||
|
public void OnRealizePanel(GameObject panel) {
|
||||||
|
contents = panel;
|
||||||
|
OnExpandContract(null, initialState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the toggle button is realized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="toggle">The realized expand/contract button.</param>
|
||||||
|
public void OnRealizeToggle(GameObject toggle) {
|
||||||
|
this.toggle = toggle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents bool and displays a check box.
|
||||||
|
/// </summary>
|
||||||
|
public class CheckboxOptionsEntry : OptionsEntry {
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value is bool newCheck) {
|
||||||
|
check = newCheck;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// true if it is checked, or false otherwise
|
||||||
|
/// </summary>
|
||||||
|
private bool check;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized item checkbox.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject checkbox;
|
||||||
|
|
||||||
|
public CheckboxOptionsEntry(string field, IOptionSpec spec) : base(field, spec) {
|
||||||
|
check = false;
|
||||||
|
checkbox = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
checkbox = new PCheckBox() {
|
||||||
|
OnChecked = (source, state) => {
|
||||||
|
// Swap the check: checked and partial -> unchecked
|
||||||
|
check = state == PCheckBox.STATE_UNCHECKED;
|
||||||
|
Update();
|
||||||
|
}, ToolTip = LookInStrings(Tooltip)
|
||||||
|
}.SetKleiBlueStyle().Build();
|
||||||
|
Update();
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update() {
|
||||||
|
PCheckBox.SetCheckState(checkbox, check ? PCheckBox.STATE_CHECKED : PCheckBox.
|
||||||
|
STATE_UNCHECKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents Color32 and displays a color picker with sliders.
|
||||||
|
/// </summary>
|
||||||
|
internal class Color32OptionsEntry : ColorBaseOptionsEntry {
|
||||||
|
public override object Value {
|
||||||
|
get => (Color32)value;
|
||||||
|
set {
|
||||||
|
if (value is Color32 newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
UpdateAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color32OptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,328 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// The abstract base of options entries that display a color picker with sliders.
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class ColorBaseOptionsEntry : OptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The margin between the color sliders and the rest of the dialog.
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly RectOffset ENTRY_MARGIN = new RectOffset(10, 10, 2, 5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The margin around each slider.
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly RectOffset SLIDER_MARGIN = new RectOffset(10, 0, 2, 2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size of the sample swatch.
|
||||||
|
/// </summary>
|
||||||
|
protected const float SWATCH_SIZE = 32.0f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hue displayed gradient.
|
||||||
|
/// </summary>
|
||||||
|
protected ColorGradient hueGradient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hue slider.
|
||||||
|
/// </summary>
|
||||||
|
protected KSlider hueSlider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field for BLUE.
|
||||||
|
/// </summary>
|
||||||
|
protected TMP_InputField blue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field for GREEN.
|
||||||
|
/// </summary>
|
||||||
|
protected TMP_InputField green;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field for RED.
|
||||||
|
/// </summary>
|
||||||
|
protected TMP_InputField red;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The saturation displayed gradient.
|
||||||
|
/// </summary>
|
||||||
|
protected ColorGradient satGradient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The saturation slider.
|
||||||
|
/// </summary>
|
||||||
|
protected KSlider satSlider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The color sample swatch.
|
||||||
|
/// </summary>
|
||||||
|
protected Image swatch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value displayed gradient.
|
||||||
|
/// </summary>
|
||||||
|
protected ColorGradient valGradient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value slider.
|
||||||
|
/// </summary>
|
||||||
|
protected KSlider valSlider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value as a Color.
|
||||||
|
/// </summary>
|
||||||
|
protected Color value;
|
||||||
|
|
||||||
|
protected ColorBaseOptionsEntry(string field, IOptionSpec spec) : base(field, spec) {
|
||||||
|
value = Color.white;
|
||||||
|
blue = null;
|
||||||
|
green = null;
|
||||||
|
red = null;
|
||||||
|
hueGradient = null; hueSlider = null;
|
||||||
|
satGradient = null; satSlider = null;
|
||||||
|
valGradient = null; valSlider = null;
|
||||||
|
swatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||||
|
base.CreateUIEntry(parent, ref row);
|
||||||
|
// Add 3 rows for the H, S, and V
|
||||||
|
parent.AddRow(new GridRowSpec());
|
||||||
|
var h = new PSliderSingle("Hue") {
|
||||||
|
ToolTip = PLibStrings.TOOLTIP_HUE, MinValue = 0.0f, MaxValue = 1.0f,
|
||||||
|
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnHueChanged
|
||||||
|
}.AddOnRealize(OnHueRealized);
|
||||||
|
var s = new PSliderSingle("Saturation") {
|
||||||
|
ToolTip = PLibStrings.TOOLTIP_SATURATION, MinValue = 0.0f, MaxValue = 1.0f,
|
||||||
|
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnSatChanged
|
||||||
|
}.AddOnRealize(OnSatRealized);
|
||||||
|
var v = new PSliderSingle("Value") {
|
||||||
|
ToolTip = PLibStrings.TOOLTIP_VALUE, MinValue = 0.0f, MaxValue = 1.0f,
|
||||||
|
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnValChanged
|
||||||
|
}.AddOnRealize(OnValRealized);
|
||||||
|
var sw = new PLabel("Swatch") {
|
||||||
|
ToolTip = LookInStrings(Tooltip), DynamicSize = false,
|
||||||
|
Sprite = PUITuning.Images.BoxBorder, SpriteMode = Image.Type.Sliced,
|
||||||
|
SpriteSize = new Vector2(SWATCH_SIZE, SWATCH_SIZE)
|
||||||
|
}.AddOnRealize(OnSwatchRealized);
|
||||||
|
var panel = new PRelativePanel("ColorPicker") {
|
||||||
|
FlexSize = Vector2.right, DynamicSize = false
|
||||||
|
}.AddChild(h).AddChild(s).AddChild(v).AddChild(sw).SetRightEdge(h, fraction: 1.0f).
|
||||||
|
SetRightEdge(s, fraction: 1.0f).SetRightEdge(v, fraction: 1.0f).
|
||||||
|
SetLeftEdge(sw, fraction: 0.0f).SetMargin(h, SLIDER_MARGIN).
|
||||||
|
SetMargin(s, SLIDER_MARGIN).SetMargin(v, SLIDER_MARGIN).AnchorYAxis(sw).
|
||||||
|
SetLeftEdge(h, toRight: sw).SetLeftEdge(s, toRight: sw).
|
||||||
|
SetLeftEdge(v, toRight: sw).SetTopEdge(h, fraction: 1.0f).
|
||||||
|
SetBottomEdge(v, fraction: 0.0f).SetTopEdge(s, below: h).
|
||||||
|
SetTopEdge(v, below: s);
|
||||||
|
parent.AddChild(panel, new GridComponentSpec(++row, 0) {
|
||||||
|
ColumnSpan = 2, Margin = ENTRY_MARGIN
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
Color32 rgb = value;
|
||||||
|
var go = new PPanel("RGB") {
|
||||||
|
DynamicSize = false, Alignment = TextAnchor.MiddleRight, Spacing = 5,
|
||||||
|
Direction = PanelDirection.Horizontal
|
||||||
|
}.AddChild(new PLabel("Red") {
|
||||||
|
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_R
|
||||||
|
}).AddChild(new PTextField("RedValue") {
|
||||||
|
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_RED,
|
||||||
|
Text = rgb.r.ToString(), MinWidth = 32, MaxLength = 3,
|
||||||
|
Type = PTextField.FieldType.Integer
|
||||||
|
}.AddOnRealize(OnRedRealized)).AddChild(new PLabel("Green") {
|
||||||
|
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_G
|
||||||
|
}).AddChild(new PTextField("GreenValue") {
|
||||||
|
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_GREEN,
|
||||||
|
Text = rgb.g.ToString(), MinWidth = 32, MaxLength = 3,
|
||||||
|
Type = PTextField.FieldType.Integer
|
||||||
|
}.AddOnRealize(OnGreenRealized)).AddChild(new PLabel("Blue") {
|
||||||
|
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_B
|
||||||
|
}).AddChild(new PTextField("BlueValue") {
|
||||||
|
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_BLUE,
|
||||||
|
Text = rgb.b.ToString(), MinWidth = 32, MaxLength = 3,
|
||||||
|
Type = PTextField.FieldType.Integer
|
||||||
|
}.AddOnRealize(OnBlueRealized)).Build();
|
||||||
|
UpdateAll();
|
||||||
|
return go;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBlueRealized(GameObject realized) {
|
||||||
|
blue = realized.GetComponentInChildren<TMP_InputField>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGreenRealized(GameObject realized) {
|
||||||
|
green = realized.GetComponentInChildren<TMP_InputField>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHueChanged(GameObject _, float newHue) {
|
||||||
|
if (hueGradient != null && hueSlider != null) {
|
||||||
|
float oldAlpha = value.a;
|
||||||
|
hueGradient.Position = hueSlider.value;
|
||||||
|
value = hueGradient.SelectedColor;
|
||||||
|
value.a = oldAlpha;
|
||||||
|
UpdateRGB();
|
||||||
|
UpdateSat(false);
|
||||||
|
UpdateVal(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHueRealized(GameObject realized) {
|
||||||
|
hueGradient = realized.AddOrGet<ColorGradient>();
|
||||||
|
realized.TryGetComponent(out hueSlider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRedRealized(GameObject realized) {
|
||||||
|
red = realized.GetComponentInChildren<TMP_InputField>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the red, green, or blue field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new color value.</param>
|
||||||
|
protected void OnRGBChanged(GameObject _, string text) {
|
||||||
|
Color32 rgb = value;
|
||||||
|
if (byte.TryParse(red.text, out byte r))
|
||||||
|
rgb.r = r;
|
||||||
|
if (byte.TryParse(green.text, out byte g))
|
||||||
|
rgb.g = g;
|
||||||
|
if (byte.TryParse(blue.text, out byte b))
|
||||||
|
rgb.b = b;
|
||||||
|
value = rgb;
|
||||||
|
UpdateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSatChanged(GameObject _, float newSat) {
|
||||||
|
if (satGradient != null && satSlider != null) {
|
||||||
|
float oldAlpha = value.a;
|
||||||
|
satGradient.Position = satSlider.value;
|
||||||
|
value = satGradient.SelectedColor;
|
||||||
|
value.a = oldAlpha;
|
||||||
|
UpdateRGB();
|
||||||
|
UpdateHue(false);
|
||||||
|
UpdateVal(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSatRealized(GameObject realized) {
|
||||||
|
satGradient = realized.AddOrGet<ColorGradient>();
|
||||||
|
realized.TryGetComponent(out satSlider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSwatchRealized(GameObject realized) {
|
||||||
|
swatch = realized.GetComponentInChildren<Image>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValChanged(GameObject _, float newValue) {
|
||||||
|
if (valGradient != null && valSlider != null) {
|
||||||
|
float oldAlpha = value.a;
|
||||||
|
valGradient.Position = valSlider.value;
|
||||||
|
value = valGradient.SelectedColor;
|
||||||
|
value.a = oldAlpha;
|
||||||
|
UpdateRGB();
|
||||||
|
UpdateHue(false);
|
||||||
|
UpdateSat(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValRealized(GameObject realized) {
|
||||||
|
valGradient = realized.AddOrGet<ColorGradient>();
|
||||||
|
realized.TryGetComponent(out valSlider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If the color is changed externally, updates all sliders.
|
||||||
|
/// </summary>
|
||||||
|
protected void UpdateAll() {
|
||||||
|
UpdateRGB();
|
||||||
|
UpdateHue(true);
|
||||||
|
UpdateSat(true);
|
||||||
|
UpdateVal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the position of the hue slider with the currently selected color.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||||
|
/// leave it where it is.</param>
|
||||||
|
protected void UpdateHue(bool moveSlider) {
|
||||||
|
if (hueGradient != null && hueSlider != null) {
|
||||||
|
Color.RGBToHSV(value, out _, out float s, out float v);
|
||||||
|
hueGradient.SetRange(0.0f, 1.0f, s, s, v, v);
|
||||||
|
hueGradient.SelectedColor = value;
|
||||||
|
if (moveSlider)
|
||||||
|
hueSlider.value = hueGradient.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected void UpdateRGB() {
|
||||||
|
Color32 rgb = value;
|
||||||
|
if (red != null)
|
||||||
|
red.text = rgb.r.ToString();
|
||||||
|
if (green != null)
|
||||||
|
green.text = rgb.g.ToString();
|
||||||
|
if (blue != null)
|
||||||
|
blue.text = rgb.b.ToString();
|
||||||
|
if (swatch != null)
|
||||||
|
swatch.color = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the position of the saturation slider with the currently selected color.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||||
|
/// leave it where it is.</param>
|
||||||
|
protected void UpdateSat(bool moveSlider) {
|
||||||
|
if (satGradient != null && satSlider != null) {
|
||||||
|
Color.RGBToHSV(value, out float h, out _, out float v);
|
||||||
|
satGradient.SetRange(h, h, 0.0f, 1.0f, v, v);
|
||||||
|
satGradient.SelectedColor = value;
|
||||||
|
if (moveSlider)
|
||||||
|
satSlider.value = satGradient.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the position of the value slider with the currently selected color.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||||
|
/// leave it where it is.</param>
|
||||||
|
protected void UpdateVal(bool moveSlider) {
|
||||||
|
if (valGradient != null && valSlider != null) {
|
||||||
|
Color.RGBToHSV(value, out float h, out float s, out _);
|
||||||
|
valGradient.SetRange(h, h, s, s, 0.0f, 1.0f);
|
||||||
|
valGradient.SelectedColor = value;
|
||||||
|
if (moveSlider)
|
||||||
|
valSlider.value = valGradient.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// A background image which displays a gradient between two different colors using HSV
|
||||||
|
/// interpolation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ColorGradient : Image {
|
||||||
|
/// <summary>
|
||||||
|
/// The position to use on the track.
|
||||||
|
/// </summary>
|
||||||
|
public float Position {
|
||||||
|
get => position;
|
||||||
|
set {
|
||||||
|
position = Mathf.Clamp01(value);
|
||||||
|
SetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently selected color.
|
||||||
|
/// </summary>
|
||||||
|
public Color SelectedColor {
|
||||||
|
get => current;
|
||||||
|
set {
|
||||||
|
current = value;
|
||||||
|
EstimatePosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently selected color.
|
||||||
|
/// </summary>
|
||||||
|
private Color current;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the image texture needs to be regenerated.
|
||||||
|
/// </summary>
|
||||||
|
private bool dirty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gradient unfortunately always uses RGB. Run curves manually using HSV.
|
||||||
|
/// </summary>
|
||||||
|
private Vector2 hue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position to use on the track.
|
||||||
|
/// </summary>
|
||||||
|
private float position;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The texture used when drawing the background.
|
||||||
|
/// </summary>
|
||||||
|
private Texture2D preview;
|
||||||
|
|
||||||
|
private Vector2 sat;
|
||||||
|
|
||||||
|
private Vector2 val;
|
||||||
|
|
||||||
|
public ColorGradient() {
|
||||||
|
current = Color.black;
|
||||||
|
position = 0.0f;
|
||||||
|
preview = null;
|
||||||
|
dirty = true;
|
||||||
|
hue = Vector2.right;
|
||||||
|
sat = Vector2.right;
|
||||||
|
val = Vector2.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimates a position based on the selected color.
|
||||||
|
/// </summary>
|
||||||
|
internal void EstimatePosition() {
|
||||||
|
Color.RGBToHSV(current, out float h, out float s, out float v);
|
||||||
|
float newPos = 0.0f, divider = 0.0f;
|
||||||
|
if (!Mathf.Approximately(hue.x, hue.y)) {
|
||||||
|
newPos += Mathf.Clamp01(Mathf.InverseLerp(hue.x, hue.y, h));
|
||||||
|
divider += 1.0f;
|
||||||
|
}
|
||||||
|
if (!Mathf.Approximately(sat.x, sat.y)) {
|
||||||
|
newPos += Mathf.Clamp01(Mathf.InverseLerp(sat.x, sat.y, s));
|
||||||
|
divider += 1.0f;
|
||||||
|
}
|
||||||
|
if (!Mathf.Approximately(val.x, val.y)) {
|
||||||
|
newPos += Mathf.Clamp01(Mathf.InverseLerp(val.x, val.y, v));
|
||||||
|
divider += 1.0f;
|
||||||
|
}
|
||||||
|
if (divider > 0.0f) {
|
||||||
|
position = newPos / divider;
|
||||||
|
SetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up the preview when the component is disposed.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnDestroy() {
|
||||||
|
if (preview != null) {
|
||||||
|
Destroy(preview);
|
||||||
|
preview = null;
|
||||||
|
}
|
||||||
|
if (sprite != null) {
|
||||||
|
Destroy(sprite);
|
||||||
|
sprite = null;
|
||||||
|
}
|
||||||
|
base.OnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the image needs to be resized.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnRectTransformDimensionsChange() {
|
||||||
|
base.OnRectTransformDimensionsChange();
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the currently selected color with the interpolated value based on the
|
||||||
|
/// current position.
|
||||||
|
/// </summary>
|
||||||
|
internal void SetPosition() {
|
||||||
|
float h = Mathf.Clamp01(Mathf.Lerp(hue.x, hue.y, position));
|
||||||
|
float s = Mathf.Clamp01(Mathf.Lerp(sat.x, sat.y, position));
|
||||||
|
float v = Mathf.Clamp01(Mathf.Lerp(val.x, val.y, position));
|
||||||
|
current = Color.HSVToRGB(h, s, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the range of colors to be displayed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hMin">The minimum hue.</param>
|
||||||
|
/// <param name="hMax">The maximum hue.</param>
|
||||||
|
/// <param name="sMin">The minimum saturation.</param>
|
||||||
|
/// <param name="sMax">The maximum saturation.</param>
|
||||||
|
/// <param name="vMin">The minimum value.</param>
|
||||||
|
/// <param name="vMax">The maximum value.</param>
|
||||||
|
public void SetRange(float hMin, float hMax, float sMin, float sMax, float vMin,
|
||||||
|
float vMax) {
|
||||||
|
// Ensure correct order
|
||||||
|
if (hMin > hMax) {
|
||||||
|
hue.x = hMax; hue.y = hMin;
|
||||||
|
} else {
|
||||||
|
hue.x = hMin; hue.y = hMax;
|
||||||
|
}
|
||||||
|
if (sMin > sMax) {
|
||||||
|
sat.x = sMax; sat.y = sMin;
|
||||||
|
} else {
|
||||||
|
sat.x = sMin; sat.y = sMax;
|
||||||
|
}
|
||||||
|
if (vMin > vMax) {
|
||||||
|
val.x = vMax; val.y = vMin;
|
||||||
|
} else {
|
||||||
|
val.x = vMin; val.y = vMax;
|
||||||
|
}
|
||||||
|
EstimatePosition();
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by Unity when the component is created.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Start() {
|
||||||
|
base.Start();
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regenerates the color gradient image if necessary.
|
||||||
|
/// </summary>
|
||||||
|
internal void Update() {
|
||||||
|
if (dirty || preview == null || sprite == null) {
|
||||||
|
var rt = rectTransform.rect;
|
||||||
|
int w = Mathf.RoundToInt(rt.width), h = Mathf.RoundToInt(rt.height);
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
var newData = new Color[w * h];
|
||||||
|
float hMin = hue.x, hMax = hue.y, sMin = sat.x, sMax = sat.y, vMin = val.x,
|
||||||
|
vMax = val.y;
|
||||||
|
var oldSprite = sprite;
|
||||||
|
bool rebuild = preview == null || oldSprite == null || preview.width !=
|
||||||
|
w || preview.height != h;
|
||||||
|
if (rebuild) {
|
||||||
|
if (preview != null)
|
||||||
|
Destroy(preview);
|
||||||
|
if (oldSprite != null)
|
||||||
|
Destroy(oldSprite);
|
||||||
|
preview = new Texture2D(w, h);
|
||||||
|
}
|
||||||
|
// Generate one row of colors
|
||||||
|
for (int i = 0; i < w; i++) {
|
||||||
|
float t = (float)i / w;
|
||||||
|
newData[i] = Color.HSVToRGB(Mathf.Lerp(hMin, hMax, t), Mathf.Lerp(sMin,
|
||||||
|
sMax, t), Mathf.Lerp(vMin, vMax, t));
|
||||||
|
}
|
||||||
|
// Native copy row to all others
|
||||||
|
for (int i = 1; i < h; i++)
|
||||||
|
Array.Copy(newData, 0, newData, i * w, w);
|
||||||
|
preview.SetPixels(newData);
|
||||||
|
preview.Apply();
|
||||||
|
if (rebuild)
|
||||||
|
sprite = Sprite.Create(preview, new Rect(0.0f, 0.0f, w, h),
|
||||||
|
new Vector2(0.5f, 0.5f));
|
||||||
|
}
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents Color and displays a color picker with sliders.
|
||||||
|
/// </summary>
|
||||||
|
internal class ColorOptionsEntry : ColorBaseOptionsEntry {
|
||||||
|
public override object Value {
|
||||||
|
get => value;
|
||||||
|
set {
|
||||||
|
if (value is Color newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
UpdateAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorOptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry that encapsulates other options. The category annotation on those
|
||||||
|
/// objects will be ignored, and the category of the Option attribute on the property
|
||||||
|
/// that declared those options (to avoid infinite loops) will be used instead.
|
||||||
|
///
|
||||||
|
/// <b>This object is not in the scene graph.</b> Any events in OnRealize will never be
|
||||||
|
/// invoked, and it is never "built".
|
||||||
|
/// </summary>
|
||||||
|
internal class CompositeOptionsEntry : OptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an options entry wrapper for the specified property, iterating its internal
|
||||||
|
/// fields to create sub-options if needed (recursively).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">The property to wrap.</param>
|
||||||
|
/// <param name="spec">The option title and tool tip.</param>
|
||||||
|
/// <param name="depth">The current depth of iteration to avoid infinite loops.</param>
|
||||||
|
/// <returns>An options wrapper, or null if no inner properties are themselves options.</returns>
|
||||||
|
internal static CompositeOptionsEntry Create(IOptionSpec spec, PropertyInfo info,
|
||||||
|
int depth) {
|
||||||
|
var type = info.PropertyType;
|
||||||
|
var composite = new CompositeOptionsEntry(info.Name, spec, type);
|
||||||
|
// Skip static properties if they exist
|
||||||
|
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.
|
||||||
|
Instance)) {
|
||||||
|
var entry = TryCreateEntry(prop, depth + 1);
|
||||||
|
if (entry != null)
|
||||||
|
composite.AddField(prop, entry);
|
||||||
|
}
|
||||||
|
return composite.ChildCount > 0 ? composite : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports the number of options contained inside this one.
|
||||||
|
/// </summary>
|
||||||
|
public int ChildCount {
|
||||||
|
get {
|
||||||
|
return subOptions.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value != null && targetType.IsAssignableFrom(value.GetType()))
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The options encapsulated in this object.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly IDictionary<PropertyInfo, IOptionsEntry> subOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the encapsulated object.
|
||||||
|
/// </summary>
|
||||||
|
protected readonly Type targetType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object thus wrapped.
|
||||||
|
/// </summary>
|
||||||
|
protected object value;
|
||||||
|
|
||||||
|
public CompositeOptionsEntry(string field, IOptionSpec spec, Type fieldType) :
|
||||||
|
base(field, spec) {
|
||||||
|
subOptions = new Dictionary<PropertyInfo, IOptionsEntry>(16);
|
||||||
|
targetType = fieldType ?? throw new ArgumentNullException(nameof(fieldType));
|
||||||
|
value = OptionsDialog.CreateOptions(fieldType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an options entry object that operates on Option fields of the encapsulated
|
||||||
|
/// object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">The property that is wrapped.</param>
|
||||||
|
/// <param name="entry">The entry to add.</param>
|
||||||
|
public void AddField(PropertyInfo info, IOptionsEntry entry) {
|
||||||
|
if (entry == null)
|
||||||
|
throw new ArgumentNullException(nameof(entry));
|
||||||
|
if (info == null)
|
||||||
|
throw new ArgumentNullException(nameof(info));
|
||||||
|
subOptions.Add(info, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||||
|
int i = row;
|
||||||
|
bool first = true;
|
||||||
|
parent.AddOnRealize(WhenRealized);
|
||||||
|
// Render each sub-entry - order is always Add Spec, Create Entry, Increment Row
|
||||||
|
foreach (var pair in subOptions) {
|
||||||
|
if (!first) {
|
||||||
|
i++;
|
||||||
|
parent.AddRow(new GridRowSpec());
|
||||||
|
}
|
||||||
|
pair.Value.CreateUIEntry(parent, ref i);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
row = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
// Will not be invoked
|
||||||
|
return new GameObject("Empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ReadFrom(object settings) {
|
||||||
|
base.ReadFrom(settings);
|
||||||
|
foreach (var pair in subOptions)
|
||||||
|
pair.Value.ReadFrom(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "{1}[field={0},title={2},children=[{3}]]".F(Field, GetType().Name, Title,
|
||||||
|
subOptions.Join());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the child objects for the first time when the panel is realized.
|
||||||
|
/// </summary>
|
||||||
|
private void WhenRealized(GameObject _) {
|
||||||
|
foreach (var pair in subOptions)
|
||||||
|
pair.Value.ReadFrom(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteTo(object settings) {
|
||||||
|
foreach (var pair in subOptions)
|
||||||
|
// Cannot detour as the types are not known at compile time, and delegates
|
||||||
|
// bake in the target object which changes upon each update
|
||||||
|
pair.Value.WriteTo(value);
|
||||||
|
base.WriteTo(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute placed on an options class only (will not function on a member property)
|
||||||
|
/// which denotes the config file name to use for that mod, and allows save/load options
|
||||||
|
/// to be set.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class ConfigFileAttribute : Attribute {
|
||||||
|
/// <summary>
|
||||||
|
/// The configuration file name. If null, the default file name will be used.
|
||||||
|
/// </summary>
|
||||||
|
public string ConfigFileName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the output should be indented nicely. Defaults to false for smaller
|
||||||
|
/// config files.
|
||||||
|
/// </summary>
|
||||||
|
public bool IndentOutput { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, the config file will be moved from the mod folder to a folder in the
|
||||||
|
/// config directory shared across mods. This change preserves the mod configuration
|
||||||
|
/// across updates, but may not be cleared when the mod is uninstalled. Use with
|
||||||
|
/// caution.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseSharedConfigLocation { get; }
|
||||||
|
|
||||||
|
public ConfigFileAttribute(string FileName = POptions.CONFIG_FILE_NAME,
|
||||||
|
bool IndentOutput = false, bool SharedConfigLocation = false) {
|
||||||
|
ConfigFileName = FileName;
|
||||||
|
this.IndentOutput = IndentOutput;
|
||||||
|
UseSharedConfigLocation = SharedConfigLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return ConfigFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib {
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute placed on an option property for a class used as mod options in order to
|
||||||
|
/// make PLib use a custom options handler. The type used for the handler must inherit
|
||||||
|
/// from IOptionsEntry.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class DynamicOptionAttribute : Attribute {
|
||||||
|
/// <summary>
|
||||||
|
/// The option category.
|
||||||
|
/// </summary>
|
||||||
|
public string Category { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The option handler.
|
||||||
|
/// </summary>
|
||||||
|
public Type Handler { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Denotes a mod option field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type that will handle this dynamic option.</param>
|
||||||
|
/// <param name="category">The category to use, or null for the default category.</param>
|
||||||
|
public DynamicOptionAttribute(Type type, string category = null) {
|
||||||
|
Category = category;
|
||||||
|
Handler = type ?? throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "DynamicOption[handler={0},category={1}]".F(Handler.FullName, Category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents float and displays a text field and slider.
|
||||||
|
/// </summary>
|
||||||
|
public class FloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The format to use if none is provided.
|
||||||
|
/// </summary>
|
||||||
|
public const string DEFAULT_FORMAT = "F2";
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value is float newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject textField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value in the text field.
|
||||||
|
/// </summary>
|
||||||
|
private float value;
|
||||||
|
|
||||||
|
public FloatOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit = null) :
|
||||||
|
base(field, spec, limit) {
|
||||||
|
textField = null;
|
||||||
|
value = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PSliderSingle GetSlider() {
|
||||||
|
return new PSliderSingle() {
|
||||||
|
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
MinValue = (float)limits.Minimum, MaxValue = (float)limits.Maximum,
|
||||||
|
InitialValue = value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
textField = new PTextField() {
|
||||||
|
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
Text = value.ToString(Format ?? DEFAULT_FORMAT), MinWidth = 64, MaxLength = 16,
|
||||||
|
Type = PTextField.FieldType.Float
|
||||||
|
}.Build();
|
||||||
|
Update();
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the slider's value is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newValue">The new slider value.</param>
|
||||||
|
private void OnSliderChanged(GameObject _, float newValue) {
|
||||||
|
// Record the value
|
||||||
|
if (limits != null)
|
||||||
|
value = limits.ClampToRange(newValue);
|
||||||
|
else
|
||||||
|
value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the input field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new text.</param>
|
||||||
|
private void OnTextChanged(GameObject _, string text) {
|
||||||
|
if (float.TryParse(text, out float newValue)) {
|
||||||
|
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||||
|
newValue *= 0.01f;
|
||||||
|
if (limits != null)
|
||||||
|
newValue = limits.ClampToRange(newValue);
|
||||||
|
// Record the valid value
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Update() {
|
||||||
|
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||||
|
if (field != null)
|
||||||
|
field.text = value.ToString(Format ?? DEFAULT_FORMAT);
|
||||||
|
if (slider != null)
|
||||||
|
PSliderSingle.SetCurrentValue(slider, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// The common parent of all classes that can specify the user visible attributes of an
|
||||||
|
/// option.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOptionSpec {
|
||||||
|
/// <summary>
|
||||||
|
/// The option category. Ignored and replaced with the parent option's category if
|
||||||
|
/// this option is part of a custom grouped type.
|
||||||
|
/// </summary>
|
||||||
|
string Category { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The format string to use when displaying this option value. Only applicable for
|
||||||
|
/// some types of options.
|
||||||
|
///
|
||||||
|
/// <b>Warning</b>: Attribute may have issues on nested classes that are used as custom
|
||||||
|
/// grouped options. To mitigate, try declaring the custom class in a non-nested
|
||||||
|
/// context (i.e. not declared inside another class).
|
||||||
|
/// </summary>
|
||||||
|
string Format { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The option title. Ignored for fields which are displayed as custom grouped types
|
||||||
|
/// types of other options.
|
||||||
|
/// </summary>
|
||||||
|
string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The option description tooltip. Ignored for fields which are displayed as custom
|
||||||
|
/// grouped types of other options.
|
||||||
|
/// </summary>
|
||||||
|
string Tooltip { get; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An optional interface which can be implemented to give mods the ability to dynamically
|
||||||
|
/// add new options at runtime, or to get a notification when options are updated to the
|
||||||
|
/// options file.
|
||||||
|
///
|
||||||
|
/// This interface is <b>optional</b>. There is no need to implement it to use PLib
|
||||||
|
/// Options. But if one method is implemented, the other must also be. If not used,
|
||||||
|
/// OnOptionsChanged should be empty, and CreateOptions should return an empty collection.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOptions {
|
||||||
|
/// <summary>
|
||||||
|
/// Called to create additional options. After the options in this class have been
|
||||||
|
/// read from the data file, but before the dialog is shown, this method will be
|
||||||
|
/// invoked. Each return value must be of a type that implements IOptionsEntry.
|
||||||
|
///
|
||||||
|
/// The options will be sorted and categorized normally as if they were present at the
|
||||||
|
/// end of the property list in a regular options class.
|
||||||
|
///
|
||||||
|
/// This method can be an enumerator using code like
|
||||||
|
/// yield return new MyOptionsHandler();
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The custom options to implement.</returns>
|
||||||
|
IEnumerable<IOptionsEntry> CreateOptions();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when options are written to the file. The current object will have the same
|
||||||
|
/// values as the data that was just written to the file. This call happens after the
|
||||||
|
/// options have been stored to disk, but before any restart required dialog is shown
|
||||||
|
/// (if [RestartRequired] is also on this class).
|
||||||
|
/// </summary>
|
||||||
|
void OnOptionsChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// All options handlers, including user dynamic option handlers, implement this type.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOptionsEntry : IOptionSpec {
|
||||||
|
/// <summary>
|
||||||
|
/// Creates UI components that will present this option.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parent">The parent panel where the components should be added.</param>
|
||||||
|
/// <param name="row">The row index where the component should be placed. If multiple
|
||||||
|
/// rows of components are added, increment this value for each additional row.</param>
|
||||||
|
void CreateUIEntry(PGridPanel parent, ref int row);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the option value into the UI from the provided settings object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The settings object.</param>
|
||||||
|
void ReadFrom(object settings);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the option value from the UI into the provided settings object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">The settings object.</param>
|
||||||
|
void WriteTo(object settings);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents int and displays a text field and slider.
|
||||||
|
/// </summary>
|
||||||
|
public class IntOptionsEntry : SlidingBaseOptionsEntry {
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value is int newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject textField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value in the text field.
|
||||||
|
/// </summary>
|
||||||
|
private int value;
|
||||||
|
|
||||||
|
public IntOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit = null) :
|
||||||
|
base(field, spec, limit) {
|
||||||
|
textField = null;
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PSliderSingle GetSlider() {
|
||||||
|
return new PSliderSingle() {
|
||||||
|
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
MinValue = (float)limits.Minimum, MaxValue = (float)limits.Maximum,
|
||||||
|
InitialValue = value, IntegersOnly = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
textField = new PTextField() {
|
||||||
|
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
Text = value.ToString(Format ?? "D"), MinWidth = 64, MaxLength = 10,
|
||||||
|
Type = PTextField.FieldType.Integer
|
||||||
|
}.Build();
|
||||||
|
Update();
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the slider's value is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newValue">The new slider value.</param>
|
||||||
|
private void OnSliderChanged(GameObject _, float newValue) {
|
||||||
|
int newIntValue = Mathf.RoundToInt(newValue);
|
||||||
|
if (limits != null)
|
||||||
|
newIntValue = limits.ClampToRange(newIntValue);
|
||||||
|
// Record the value
|
||||||
|
value = newIntValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the input field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new text.</param>
|
||||||
|
private void OnTextChanged(GameObject _, string text) {
|
||||||
|
if (int.TryParse(text, out int newValue)) {
|
||||||
|
if (limits != null)
|
||||||
|
newValue = limits.ClampToRange(newValue);
|
||||||
|
// Record the valid value
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Update() {
|
||||||
|
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||||
|
if (field != null)
|
||||||
|
field.text = value.ToString(Format ?? "D");
|
||||||
|
if (slider != null)
|
||||||
|
PSliderSingle.SetCurrentValue(slider, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute placed on an option field for a property used as mod options to define
|
||||||
|
/// minimum and maximum acceptable values.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class LimitAttribute : Attribute {
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum value (inclusive).
|
||||||
|
/// </summary>
|
||||||
|
public double Maximum { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum value (inclusive).
|
||||||
|
/// </summary>
|
||||||
|
public double Minimum { get; }
|
||||||
|
|
||||||
|
public LimitAttribute(double min, double max) {
|
||||||
|
Minimum = min.IsNaNOrInfinity() ? 0.0 : min;
|
||||||
|
Maximum = (max.IsNaNOrInfinity() || max < min) ? min : max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps the specified value to the range of this Limits object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to coerce.</param>
|
||||||
|
/// <returns>The nearest value included by these limits to the specified value.</returns>
|
||||||
|
public float ClampToRange(float value) {
|
||||||
|
return value.InRange((float)Minimum, (float)Maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps the specified value to the range of this Limits object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to coerce.</param>
|
||||||
|
/// <returns>The nearest value included by these limits to the specified value.</returns>
|
||||||
|
public int ClampToRange(int value) {
|
||||||
|
return value.InRange((int)Minimum, (int)Maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports whether a value is in the range included in these limits.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to check.</param>
|
||||||
|
/// <returns>true if it is included in the limits, or false otherwise.</returns>
|
||||||
|
public bool InRange(double value) {
|
||||||
|
return value >= Minimum && value <= Maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return "{0:F2} to {1:F2}".F(Minimum, Maximum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using System;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents float and displays a text field and slider.
|
||||||
|
/// This entry uses a logarithmic scale.
|
||||||
|
/// </summary>
|
||||||
|
public class LogFloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The format to use if none is provided.
|
||||||
|
/// </summary>
|
||||||
|
public const string DEFAULT_FORMAT = "F2";
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value is float newValue) {
|
||||||
|
this.value = limits.ClampToRange(newValue);
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject textField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value in the text field.
|
||||||
|
/// </summary>
|
||||||
|
private float value;
|
||||||
|
|
||||||
|
public LogFloatOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit) :
|
||||||
|
base(field, spec, limit) {
|
||||||
|
if (limit == null)
|
||||||
|
throw new ArgumentNullException(nameof(limit));
|
||||||
|
if (limit.Minimum <= 0.0f || limit.Maximum <= 0.0f)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(limit),
|
||||||
|
"Logarithmic values must be positive");
|
||||||
|
textField = null;
|
||||||
|
value = limit.ClampToRange(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PSliderSingle GetSlider() {
|
||||||
|
return new PSliderSingle() {
|
||||||
|
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
MinValue = Mathf.Log((float)limits.Minimum), MaxValue = Mathf.Log(
|
||||||
|
(float)limits.Maximum), InitialValue = Mathf.Log(value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
textField = new PTextField() {
|
||||||
|
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
Text = value.ToString(Format ?? DEFAULT_FORMAT), MinWidth = 64, MaxLength = 16,
|
||||||
|
Type = PTextField.FieldType.Float
|
||||||
|
}.Build();
|
||||||
|
Update();
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the slider's value is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newValue">The new slider value.</param>
|
||||||
|
private void OnSliderChanged(GameObject _, float newValue) {
|
||||||
|
value = limits.ClampToRange(Mathf.Exp(newValue));
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the input field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new text.</param>
|
||||||
|
private void OnTextChanged(GameObject _, string text) {
|
||||||
|
if (float.TryParse(text, out float newValue)) {
|
||||||
|
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||||
|
newValue *= 0.01f;
|
||||||
|
value = limits.ClampToRange(newValue);
|
||||||
|
}
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Update() {
|
||||||
|
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||||
|
if (field != null)
|
||||||
|
field.text = value.ToString(Format ?? DEFAULT_FORMAT);
|
||||||
|
if (slider != null)
|
||||||
|
PSliderSingle.SetCurrentValue(slider, Mathf.Log(Mathf.Max(float.Epsilon,
|
||||||
|
value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.Core;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the information displayed about a mod in its options dialog.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ModDialogInfo {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text shown for a mod's version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="optionsType">The type used for the mod settings.</param>
|
||||||
|
/// <returns>The mod version description.</returns>
|
||||||
|
private static string GetModVersionText(Type optionsType) {
|
||||||
|
var asm = optionsType.Assembly;
|
||||||
|
string version = asm.GetFileVersion();
|
||||||
|
// Use FileVersion if available, else assembly version
|
||||||
|
if (string.IsNullOrEmpty(version))
|
||||||
|
version = string.Format(PLibStrings.MOD_ASSEMBLY_VERSION, asm.GetName().
|
||||||
|
Version);
|
||||||
|
else
|
||||||
|
version = string.Format(PLibStrings.MOD_VERSION, version);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path to the image displayed (on the file system) for this mod.
|
||||||
|
/// </summary>
|
||||||
|
public string Image { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod title. The title is taken directly from the mod version information.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL which will be displayed. If none was provided, the Steam workshop page URL
|
||||||
|
/// will be reported for Steam mods, and an empty string for local/dev mods.
|
||||||
|
/// </summary>
|
||||||
|
public string URL { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mod version.
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; }
|
||||||
|
|
||||||
|
internal ModDialogInfo(Type type, string url, string image) {
|
||||||
|
var mod = POptions.GetModFromType(type);
|
||||||
|
string title, version;
|
||||||
|
Image = image ?? "";
|
||||||
|
if (mod != null) {
|
||||||
|
string modInfoVersion = mod.packagedModInfo?.version;
|
||||||
|
title = mod.title;
|
||||||
|
if (string.IsNullOrEmpty(url) && mod.label.distribution_platform == KMod.Label.
|
||||||
|
DistributionPlatform.Steam)
|
||||||
|
url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.
|
||||||
|
label.id;
|
||||||
|
if (!string.IsNullOrEmpty(modInfoVersion))
|
||||||
|
version = string.Format(PLibStrings.MOD_VERSION, modInfoVersion);
|
||||||
|
else
|
||||||
|
version = GetModVersionText(type);
|
||||||
|
} else {
|
||||||
|
title = type.Assembly.GetNameSafe();
|
||||||
|
version = GetModVersionText(type);
|
||||||
|
}
|
||||||
|
Title = title ?? "";
|
||||||
|
URL = url ?? "";
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return base.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// Allows mod authors to specify attributes for their mods to be shown in the Options
|
||||||
|
/// dialog.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class ModInfoAttribute : Attribute {
|
||||||
|
/// <summary>
|
||||||
|
/// If true, forces all categories in the options screen to begin collapsed (except
|
||||||
|
/// the default category).
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceCollapseCategories { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the image file (in the mod's root directory) to display in the options
|
||||||
|
/// dialog. If null or empty (or it cannot be loaded), no image is displayed.
|
||||||
|
/// </summary>
|
||||||
|
public string Image { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL to use for the mod. If null or empty, the Steam workshop link will be used
|
||||||
|
/// if possible, or otherwise the button will not be shown.
|
||||||
|
/// </summary>
|
||||||
|
public string URL { get; }
|
||||||
|
|
||||||
|
public ModInfoAttribute(string url, string image = null, bool collapse = false)
|
||||||
|
{
|
||||||
|
ForceCollapseCategories = collapse;
|
||||||
|
Image = image;
|
||||||
|
URL = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return string.Format("ModInfoAttribute[URL={1},Image={2}]", URL, Image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents float? and displays a text field and slider.
|
||||||
|
/// </summary>
|
||||||
|
public class NullableFloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The text that is rendered for the current value of the entry.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string FieldText {
|
||||||
|
get {
|
||||||
|
return value?.ToString(Format ?? FloatOptionsEntry.DEFAULT_FORMAT) ?? string.
|
||||||
|
Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value == null) {
|
||||||
|
this.value = null;
|
||||||
|
Update();
|
||||||
|
} else if (value is float newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject textField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value in the text field.
|
||||||
|
/// </summary>
|
||||||
|
private float? value;
|
||||||
|
|
||||||
|
public NullableFloatOptionsEntry(string field, IOptionSpec spec,
|
||||||
|
LimitAttribute limit = null) : base(field, spec, limit) {
|
||||||
|
textField = null;
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PSliderSingle GetSlider() {
|
||||||
|
float minLimit = (float)limits.Minimum, maxLimit = (float)limits.Maximum;
|
||||||
|
return new PSliderSingle() {
|
||||||
|
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
MinValue = minLimit, MaxValue = maxLimit, InitialValue = 0.5f * (minLimit +
|
||||||
|
maxLimit)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
textField = new PTextField() {
|
||||||
|
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
Text = FieldText, MinWidth = 64, MaxLength = 16,
|
||||||
|
Type = PTextField.FieldType.Float
|
||||||
|
}.Build();
|
||||||
|
Update();
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the slider's value is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newValue">The new slider value.</param>
|
||||||
|
private void OnSliderChanged(GameObject _, float newValue) {
|
||||||
|
// Record the value
|
||||||
|
if (limits != null)
|
||||||
|
value = limits.ClampToRange(newValue);
|
||||||
|
else
|
||||||
|
value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the input field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new text.</param>
|
||||||
|
private void OnTextChanged(GameObject _, string text) {
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
// Limits are assumed to allow null, because why not use non-nullable
|
||||||
|
// otherwise?
|
||||||
|
value = null;
|
||||||
|
else if (float.TryParse(text, out float newValue)) {
|
||||||
|
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||||
|
newValue *= 0.01f;
|
||||||
|
if (limits != null)
|
||||||
|
newValue = limits.ClampToRange(newValue);
|
||||||
|
// Record the valid value
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Update() {
|
||||||
|
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||||
|
if (field != null)
|
||||||
|
field.text = FieldText;
|
||||||
|
if (slider != null && value != null)
|
||||||
|
PSliderSingle.SetCurrentValue(slider, (float)value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Peter Han
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||||
|
* and associated documentation files (the "Software"), to deal in the Software without
|
||||||
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
* Software is furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
* substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||||
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using PeterHan.PLib.UI;
|
||||||
|
using System.Reflection;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace PeterHan.PLib.Options {
|
||||||
|
/// <summary>
|
||||||
|
/// An options entry which represents int? and displays a text field and slider.
|
||||||
|
/// </summary>
|
||||||
|
public class NullableIntOptionsEntry : SlidingBaseOptionsEntry {
|
||||||
|
/// <summary>
|
||||||
|
/// The text that is rendered for the current value of the entry.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string FieldText {
|
||||||
|
get {
|
||||||
|
return value?.ToString(Format ?? "D") ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (value == null) {
|
||||||
|
this.value = null;
|
||||||
|
Update();
|
||||||
|
} else if (value is int newValue) {
|
||||||
|
this.value = newValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The realized text field.
|
||||||
|
/// </summary>
|
||||||
|
private GameObject textField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The value in the text field.
|
||||||
|
/// </summary>
|
||||||
|
private int? value;
|
||||||
|
|
||||||
|
public NullableIntOptionsEntry(string field, IOptionSpec spec,
|
||||||
|
LimitAttribute limit = null) : base(field, spec, limit) {
|
||||||
|
textField = null;
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override PSliderSingle GetSlider() {
|
||||||
|
float minLimit = (float)limits.Minimum, maxLimit = (float)limits.Maximum;
|
||||||
|
return new PSliderSingle() {
|
||||||
|
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
MinValue = minLimit, MaxValue = maxLimit, InitialValue = minLimit,
|
||||||
|
IntegersOnly = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override GameObject GetUIComponent() {
|
||||||
|
textField = new PTextField() {
|
||||||
|
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||||
|
Text = FieldText, MinWidth = 64, MaxLength = 10,
|
||||||
|
Type = PTextField.FieldType.Integer
|
||||||
|
}.Build();
|
||||||
|
Update();
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the slider's value is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newValue">The new slider value.</param>
|
||||||
|
private void OnSliderChanged(GameObject _, float newValue) {
|
||||||
|
int newIntValue = Mathf.RoundToInt(newValue);
|
||||||
|
if (limits != null)
|
||||||
|
newIntValue = limits.ClampToRange(newIntValue);
|
||||||
|
// Record the value
|
||||||
|
value = newIntValue;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the input field's text is changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The new text.</param>
|
||||||
|
private void OnTextChanged(GameObject _, string text) {
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
// Limits are assumed to allow null, because why not use non-nullable
|
||||||
|
// otherwise?
|
||||||
|
value = null;
|
||||||
|
else if (int.TryParse(text, out int newValue)) {
|
||||||
|
if (limits != null)
|
||||||
|
newValue = limits.ClampToRange(newValue);
|
||||||
|
// Record the valid value
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the displayed value.
|
||||||
|
/// </summary>
|
||||||
|
protected override void Update() {
|
||||||
|
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||||
|
if (field != null)
|
||||||
|
field.text = FieldText;
|
||||||
|
if (slider != null && value != null)
|
||||||
|
PSliderSingle.SetCurrentValue(slider, (int)value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue