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