Initial commit

This commit is contained in:
Guus Waals 2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
reference*/
TemplateMod/
OxygenNotIncluded*
Libs
Logs
Backups/
.vscode/
IlSpy
.DS_Store

3
mod/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
obj/
bin/
dist/

63
mod/Mod.cs Normal file
View File

@ -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());
// }
// }
// }
}

28
mod/Options.cs Normal file
View File

@ -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;
}
}

49
mod/PLib/PLib.csproj Normal file
View File

@ -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>

BIN
mod/PLib/PLib.xcf Normal file

Binary file not shown.

239
mod/PLib/README.md Normal file
View File

@ -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:
![mod menu example screenshot](https://i.imgur.com/1S1i9ru.png)
## 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.

BIN
mod/PLib/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 ?? "");
}
}
}

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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) { }
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

143
mod/PLibCore/PGameUtils.cs Normal file
View File

@ -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();
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

204
mod/PLibCore/PLibStrings.cs Normal file
View File

@ -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.";
}
}

1028
mod/PLibCore/PPatchTools.cs Normal file

File diff suppressed because it is too large Load Diff

137
mod/PLibCore/PRegistry.cs Normal file
View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

252
mod/PLibCore/PUtil.cs Normal file
View File

@ -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));
}
}
}

30
mod/PLibCore/PVersion.cs Normal file
View File

@ -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";
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)");
}
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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!");
}
}
}

View File

@ -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();
}
}
}

View File

@ -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&lt;<typeparamref name="T"/>&gt; with the default
/// initial capacity.
/// </summary>
public PriorityQueue() : this(32) { }
/// <summary>
/// Creates a new PriorityQueue&lt;<typeparamref name="T"/>&gt; 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&lt;<typeparamref name="T"/>&gt;.
/// </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&lt;<typeparamref name="T"/>&gt;.
///
/// 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&lt;<typeparamref name="T"/>&gt;.
/// </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&lt;<typeparamref name="T"/>&gt;
/// 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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; with the default initial capacity.
/// </summary>
public PriorityDictionary() : base() { }
/// <summary>
/// Creates a new PriorityDictionary&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; 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&lt;<typeparamref name="K"/>, <typeparamref name="V"/>&gt;.
///
/// 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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt;.
/// </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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; 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 + "]";
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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."

View File

@ -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Сравните эту версию с примечаниями к выпуску мода, чтобы узнать, не устарел ли он."

View File

@ -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 ""

View File

@ -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"
"请将此版本与模组的发行说明进行比较,以查看它是否过时。"

View File

@ -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
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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 + "]";
}
}
}

View File

@ -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);
}
}
}

View File

@ -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&lt;object&gt; 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");
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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) { }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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) { }
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)));
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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