Initial Push
This commit is contained in:
8
Assets/Darkmatter/Code/Libs/FSM.meta
Normal file
8
Assets/Darkmatter/Code/Libs/FSM.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a60fee6d8a3034037ac8cc01c45baf11
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/FSM/Docs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/FSM/Docs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 395d10529759141ddabeab8039459e38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/Darkmatter/Code/Libs/FSM/Docs/FSMLib.md
Normal file
19
Assets/Darkmatter/Code/Libs/FSM/Docs/FSMLib.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# FSM Lib
|
||||
|
||||
## Purpose
|
||||
|
||||
`Darkmatter.Libs.FSM` holds reusable finite-state-machine primitives intended for feature-local orchestration without creating cross-feature dependencies.
|
||||
|
||||
## Public Entry Points
|
||||
|
||||
- FSM runtime types under `Assets/Darkmatter/Code/Libs/FSM`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Intended to remain generic and feature-agnostic
|
||||
- May be referenced by any feature that needs local state transitions
|
||||
|
||||
## Extension Notes
|
||||
|
||||
- Keep the API deterministic and side-effect free.
|
||||
- Domain-specific states, transitions, or scene references belong in the consuming feature or service slice.
|
||||
7
Assets/Darkmatter/Code/Libs/FSM/Docs/FSMLib.md.meta
Normal file
7
Assets/Darkmatter/Code/Libs/FSM/Docs/FSMLib.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa62a43934add4beb92f51d9effd9d84
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/Darkmatter/Code/Libs/FSM/IState.cs
Normal file
9
Assets/Darkmatter/Code/Libs/FSM/IState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Darkmatter.Libs.FSM
|
||||
{
|
||||
public interface IState
|
||||
{
|
||||
void Enter();
|
||||
void Tick();
|
||||
void Exit();
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/FSM/IState.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/FSM/IState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f814fe27db8b43349a93943bf71fcd5
|
||||
14
Assets/Darkmatter/Code/Libs/FSM/Libs.FSM.asmdef
Normal file
14
Assets/Darkmatter/Code/Libs/FSM/Libs.FSM.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Libs.FSM",
|
||||
"rootNamespace": "Darkmatter.Libs.FSM",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Darkmatter/Code/Libs/FSM/Libs.FSM.asmdef.meta
Normal file
7
Assets/Darkmatter/Code/Libs/FSM/Libs.FSM.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c176ee863a5e74e88a6517f9f102cf92
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/Darkmatter/Code/Libs/FSM/State.cs
Normal file
15
Assets/Darkmatter/Code/Libs/FSM/State.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Darkmatter.Libs.FSM
|
||||
{
|
||||
public abstract class State<T> : IState
|
||||
{
|
||||
protected readonly T runner;
|
||||
protected State(T runner)
|
||||
{
|
||||
this.runner = runner;
|
||||
}
|
||||
|
||||
public virtual void Enter() { }
|
||||
public virtual void Tick() { }
|
||||
public virtual void Exit() { }
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/FSM/State.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/FSM/State.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5d044f466d7a46379be2ebb74292a74
|
||||
26
Assets/Darkmatter/Code/Libs/FSM/StateMachine.cs
Normal file
26
Assets/Darkmatter/Code/Libs/FSM/StateMachine.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Darkmatter.Libs.FSM
|
||||
{
|
||||
public abstract class StateMachine
|
||||
{
|
||||
public IState CurrentState { get; private set; }
|
||||
public IState PreviousState { get; private set; }
|
||||
|
||||
public void ChangeState(IState newState)
|
||||
{
|
||||
if (newState == null) return;
|
||||
if (CurrentState == newState) return;
|
||||
|
||||
CurrentState?.Exit();
|
||||
|
||||
PreviousState = CurrentState;
|
||||
CurrentState = newState;
|
||||
|
||||
CurrentState.Enter();
|
||||
}
|
||||
|
||||
public virtual void Tick()
|
||||
{
|
||||
CurrentState?.Tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/FSM/StateMachine.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/FSM/StateMachine.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9c75568b26bd4667ad07404934fc0c9
|
||||
8
Assets/Darkmatter/Code/Libs/Installers.meta
Normal file
8
Assets/Darkmatter/Code/Libs/Installers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d669ee184e2754ff6af32edd4c87fdbc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/Installers/Docs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/Installers/Docs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e5889f574d4c4d1ca18fb324f33938d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/Darkmatter/Code/Libs/Installers/Docs/InstallersLib.md
Normal file
19
Assets/Darkmatter/Code/Libs/Installers/Docs/InstallersLib.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Installers Lib
|
||||
|
||||
## Purpose
|
||||
|
||||
`Darkmatter.Libs.Installers` provides the shared installer contract used by App and scene scopes to register feature/service modules through explicit DI composition.
|
||||
|
||||
## Public Entry Points
|
||||
|
||||
- `IServiceModule`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends only on `VContainer` so modules can register against `IContainerBuilder`.
|
||||
- Is consumed by App scopes (`RootLifetimeScope`, `GameplayLifetimeScope`, `MainMenuLifetimeScope`) and scene/entity scopes that register `MonoBehaviour` modules through explicit inspector wiring.
|
||||
|
||||
## Extension Notes
|
||||
|
||||
- Keep this lib minimal and contract-only. It should stay reusable by both feature and service slices without pulling in higher-layer code.
|
||||
- New reusable installer helpers belong here only if they remain generic and do not encode game-specific behavior.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c035b34d3ec341d58cdb799f2326799
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs
Normal file
9
Assets/Darkmatter/Code/Libs/Installers/IServiceModule.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using VContainer;
|
||||
|
||||
namespace Darkmatter.Libs.Installers
|
||||
{
|
||||
public interface IServiceModule
|
||||
{
|
||||
void Register(IContainerBuilder builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4fccd9c5f4934143929c601592e9616
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Libs.Installers",
|
||||
"rootNamespace": "Darkmatter.Libs.Installers",
|
||||
"references": [
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1c03c0e5b2f4412b9f2be1c20d6a9b1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/Observer.meta
Normal file
8
Assets/Darkmatter/Code/Libs/Observer.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8fea732420104e728b3290bc8e9bfc1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/Observer/Docs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/Observer/Docs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62a255738299947f5aa75ed233adb93a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Darkmatter/Code/Libs/Observer/Docs/ObserverLib.md
Normal file
20
Assets/Darkmatter/Code/Libs/Observer/Docs/ObserverLib.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Observer Lib
|
||||
|
||||
## Purpose
|
||||
|
||||
`Darkmatter.Libs.Observer` provides the shared event-bus/observer primitives used for fire-and-forget notifications across slices.
|
||||
|
||||
## Public Entry Points
|
||||
|
||||
- `IEventBus`
|
||||
- `EventBus`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Used broadly by App, Features, and Services for cross-slice notifications
|
||||
- Should remain infrastructure-only; business rules stay in the caller/subscriber
|
||||
|
||||
## Extension Notes
|
||||
|
||||
- Use this lib for notifications, not request/response flows.
|
||||
- Preserve disposable subscription semantics so lifetime scopes can cleanly unsubscribe on teardown.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aeb1895d9f8424537b1a062bba8f6f14
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
75
Assets/Darkmatter/Code/Libs/Observer/EventBus.cs
Normal file
75
Assets/Darkmatter/Code/Libs/Observer/EventBus.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using VContainer;
|
||||
|
||||
namespace Darkmatter.Libs.Observer
|
||||
{
|
||||
public class EventBus : IEventBus
|
||||
{
|
||||
[Inject]
|
||||
public EventBus() { }
|
||||
|
||||
private readonly Dictionary<Type, object> _map = new();
|
||||
public void Publish<T>(T evt) where T : struct
|
||||
{
|
||||
if (_map.TryGetValue(typeof(T), out var handlerObj))
|
||||
{
|
||||
var action = (Action<T>)handlerObj;
|
||||
action?.Invoke(evt);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable Subscribe<T>(Action<T> handler) where T : struct
|
||||
{
|
||||
var t = typeof(T);
|
||||
|
||||
if (_map.TryGetValue(t, out var existingHandlers))
|
||||
{
|
||||
var action = (Action<T>)existingHandlers;
|
||||
_map[t] = action + handler;
|
||||
}
|
||||
else
|
||||
{
|
||||
_map[t] = handler;
|
||||
}
|
||||
|
||||
return new Unsub<T>(this, handler);
|
||||
}
|
||||
|
||||
internal void Unsubscribe<T>(Action<T> handler) where T : struct
|
||||
{
|
||||
var t = typeof(T);
|
||||
if (_map.TryGetValue(t, out var existingHandlers))
|
||||
{
|
||||
var action = (Action<T>)existingHandlers;
|
||||
action -= handler;
|
||||
|
||||
if (action == null)
|
||||
_map.Remove(t);
|
||||
else
|
||||
_map[t] = action;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Unsub<T> : IDisposable where T : struct
|
||||
{
|
||||
private readonly EventBus _bus;
|
||||
private Action<T> _handler;
|
||||
|
||||
public Unsub(EventBus bus, Action<T> handler)
|
||||
{
|
||||
_bus = bus;
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_handler != null)
|
||||
{
|
||||
_bus.Unsubscribe(_handler);
|
||||
_handler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/Observer/EventBus.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/Observer/EventBus.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3360f02303be544ecbe8c21dc0c4e33e
|
||||
18
Assets/Darkmatter/Code/Libs/Observer/IEventBus.cs
Normal file
18
Assets/Darkmatter/Code/Libs/Observer/IEventBus.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Libs.Observer
|
||||
{
|
||||
public interface IEventBus
|
||||
{
|
||||
/// <summary>
|
||||
/// Publish an event of type T to all subscribers.
|
||||
/// </summary>
|
||||
void Publish<T>(T evt) where T : struct;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to an event of type T.
|
||||
/// Returns IDisposable – calling Dispose() unsubscribes.
|
||||
/// </summary>
|
||||
IDisposable Subscribe<T>(Action<T> handler) where T : struct;
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/Observer/IEventBus.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/Observer/IEventBus.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22922a82d6e00458f8e34b78a9599a66
|
||||
16
Assets/Darkmatter/Code/Libs/Observer/Libs.Observer.asmdef
Normal file
16
Assets/Darkmatter/Code/Libs/Observer/Libs.Observer.asmdef
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Libs.Observer",
|
||||
"rootNamespace": "Darkmatter.Libs.Observer",
|
||||
"references": [
|
||||
"VContainer"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4c9f7fbf1e144933a1797dc208ece5f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/PlayerPrefs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/PlayerPrefs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4c1e3acaf8f7448a847fd93afeb9123
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Docs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Docs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85a19104921a942afae4ed40c49ef826
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,175 @@
|
||||
# Protected PlayerPrefs Toolkit
|
||||
|
||||
Protected PlayerPrefs Toolkit is a lightweight Unity package for project-local data protection, key management, and editor-side inspection of stored values. It wraps Unity `PlayerPrefs`, hashes keys, encrypts values, and adds a guided setup flow so the package can be configured before it ships.
|
||||
|
||||
[[SCREENSHOT: Package Overview - Add a screenshot of the package files and folders inside the Unity Project window.]]
|
||||
|
||||
## What Is Included
|
||||
|
||||
- `ProtectedPlayerPrefs`: runtime API for string, int, float, and bool values.
|
||||
- `ProtectedPlayerPrefsSettings`: project settings asset that stores the SHA-256 hash derived from your password.
|
||||
- Getting Started window: first-run setup that appears automatically when the hash password has not been configured.
|
||||
- `PlayerPrefsKeyRegistry`: registry asset for known keys and descriptions.
|
||||
- `PlayerPrefs Editor`: editor window for registered keys, raw lookup, delete actions, and key-code generation.
|
||||
- `PlayerPrefsKeys`: generated constants class that removes string literals from gameplay and service code.
|
||||
|
||||
## Quick Start
|
||||
|
||||
- Import the package into your project.
|
||||
- If no hash password is configured, Unity creates the settings asset and opens `Tools/Darkmatter/Protected PlayerPrefs/Getting Started`.
|
||||
- Enter a strong password and save it. The package stores only the SHA-256 hash, not the raw password.
|
||||
- Keep the original password in a secure team password manager.
|
||||
- Use `ProtectedPlayerPrefs` from runtime code and `Tools/Darkmatter/PlayerPrefs Editor` from the editor.
|
||||
|
||||
[[SCREENSHOT: Getting Started Window - Add a screenshot of the first-run setup window with the password fields and action buttons.]]
|
||||
|
||||
## Settings Asset and Hash Password
|
||||
|
||||
The package creates or reuses this asset path by default:
|
||||
|
||||
```text
|
||||
Assets/Darkmatter/Data/Settings/Persistance/Resources/ProtectedPlayerPrefsSettings.asset
|
||||
```
|
||||
|
||||
Important behavior:
|
||||
|
||||
- The runtime loads the settings asset through `Resources`, so the configured hash is available in builds.
|
||||
- If no configured hash exists, the runtime falls back to the built-in default passphrase for backward compatibility.
|
||||
- Changing the password later changes the derived encryption key and IV. Existing protected values will no longer decrypt.
|
||||
- The package stores only the SHA-256 hash of the password. The original password cannot be recovered from the asset.
|
||||
|
||||
[[SCREENSHOT: Settings Asset Inspector - Add a screenshot of the generated ProtectedPlayerPrefsSettings asset in the Inspector.]]
|
||||
|
||||
## Runtime API
|
||||
|
||||
Use `ProtectedPlayerPrefs` exactly like a narrow, protected subset of Unity `PlayerPrefs`.
|
||||
|
||||
```csharp
|
||||
using Darkmatter.Libs.PlayerPrefs;
|
||||
|
||||
ProtectedPlayerPrefs.SetString(PlayerPrefsKeys.Accounts.SavedAuthRequest, json);
|
||||
ProtectedPlayerPrefs.SetInt("Profile.Level", 12);
|
||||
ProtectedPlayerPrefs.SetFloat("Audio.MasterVolume", 0.8f);
|
||||
ProtectedPlayerPrefs.SetBool("Onboarding.Completed", true);
|
||||
ProtectedPlayerPrefs.Save();
|
||||
```
|
||||
|
||||
Reading values:
|
||||
|
||||
```csharp
|
||||
var savedAuth = ProtectedPlayerPrefs.GetString(PlayerPrefsKeys.Accounts.SavedAuthRequest, string.Empty);
|
||||
var level = ProtectedPlayerPrefs.GetInt("Profile.Level", 1);
|
||||
var volume = ProtectedPlayerPrefs.GetFloat("Audio.MasterVolume", 1f);
|
||||
var onboardingDone = ProtectedPlayerPrefs.GetBool("Onboarding.Completed", false);
|
||||
```
|
||||
|
||||
Available API surface:
|
||||
|
||||
- `Init(string passphrase)`: optional manual initialization from a raw password.
|
||||
- `InitWithHash(string hashedPassphrase)`: optional manual initialization from a SHA-256 hash.
|
||||
- `ComputePassphraseHash(string passphrase)`: helper used by the setup flow.
|
||||
- `SetString`, `GetString`
|
||||
- `SetInt`, `GetInt`
|
||||
- `SetFloat`, `GetFloat`
|
||||
- `SetBool`, `GetBool`
|
||||
- `HasKey`, `DeleteKey`, `DeleteAll`, `Save`
|
||||
|
||||
## Key Registry and Code Generation
|
||||
|
||||
Use `PlayerPrefsKeyRegistry` to maintain a curated list of known keys, types, and descriptions. The editor window can generate a strongly named `PlayerPrefsKeys` class from the registry.
|
||||
|
||||
Benefits:
|
||||
|
||||
- Central place to document keys.
|
||||
- Safer refactoring than raw string literals.
|
||||
- Cleaner service and feature code.
|
||||
- Easier QA and debugging in the editor window.
|
||||
|
||||
Example generated structure:
|
||||
|
||||
```csharp
|
||||
public static class PlayerPrefsKeys
|
||||
{
|
||||
public static class Accounts
|
||||
{
|
||||
public const string SavedAuthRequest = "Accounts.SavedAuthRequest";
|
||||
}
|
||||
|
||||
public static class SaveGame
|
||||
{
|
||||
public const string Session = "SaveGame.Session";
|
||||
public const string Vehicle = "SaveGame.Vehicle";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PlayerPrefs Editor
|
||||
|
||||
Open the tool from:
|
||||
|
||||
```text
|
||||
Tools/Darkmatter/PlayerPrefs Editor
|
||||
```
|
||||
|
||||
The editor window includes three areas:
|
||||
|
||||
- Registered Keys: edit keys defined in the registry and save protected values.
|
||||
- Raw Lookup: inspect or delete unprotected third-party or legacy keys by name.
|
||||
- Settings: manage registry generation output and open the Protected PlayerPrefs setup flow.
|
||||
|
||||
[[SCREENSHOT: Registered Keys Tab - Add a screenshot of the Registered Keys tab with a few example entries.]]
|
||||
|
||||
[[SCREENSHOT: Raw Lookup Tab - Add a screenshot of the Raw Lookup tab showing a direct key lookup.]]
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- Treat this package as protection against casual local tampering, not as a replacement for a trusted backend.
|
||||
- Use generated keys for feature and service code whenever the key is known ahead of time.
|
||||
- Save immediately after write operations that must survive app termination.
|
||||
- For sensitive gameplay state, pair local protection with server validation when the product design requires trust.
|
||||
|
||||
## Recommended Workflow for Shipping
|
||||
|
||||
- Configure the hash password on first import.
|
||||
- Register the keys that belong to your project.
|
||||
- Generate `PlayerPrefsKeys.cs`.
|
||||
- Replace raw string literals with generated constants.
|
||||
- Run a smoke test with a fresh install and with existing saved data.
|
||||
- Capture the screenshots marked in this documentation before publishing the package.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Problem: protected values return defaults after you change the password.
|
||||
|
||||
- Cause: the encryption key changed because the stored hash changed.
|
||||
- Fix: restore the original password or clear and rebuild the protected local cache.
|
||||
|
||||
Problem: the setup window keeps appearing.
|
||||
|
||||
- Cause: the settings asset exists, but its hash password is still blank.
|
||||
- Fix: open the Getting Started window and save a password.
|
||||
|
||||
Problem: a key is visible in PlayerPrefs but not in the Registered Keys tab.
|
||||
|
||||
- Cause: the value exists, but the key is not in `PlayerPrefsKeyRegistry`.
|
||||
- Fix: add the key to the registry or inspect it from Raw Lookup.
|
||||
|
||||
## Package Files
|
||||
|
||||
Core files:
|
||||
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime/ProtectedPlayerPrefs.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime/ProtectedPlayerPrefsSettings.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime/PlayerPrefsKeyRegistry.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime/PlayerPrefsKeys.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Editor/PlayerPrefsEditorWindow.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Editor/ProtectedPlayerPrefsGettingStartedWindow.cs`
|
||||
- `Assets/Darkmatter/Code/Libs/PlayerPrefs/Editor/ProtectedPlayerPrefsSetupBootstrap.cs`
|
||||
|
||||
## Final Checklist Before Asset Store Submission
|
||||
|
||||
- Verify the configured hash password is set.
|
||||
- Remove project-specific sample keys that should not ship.
|
||||
- Replace every screenshot placeholder in this PDF.
|
||||
- Confirm the generated documentation opens correctly on a clean machine.
|
||||
- Test the package in a fresh Unity project before export.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c99b2d93e5a945328d6fc323c99142d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 12 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 12 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Author (Codex) /CreationDate (D:20260412135420+05'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260412135420+05'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (Protected PlayerPrefs Toolkit Documentation) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Count 5 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1830
|
||||
>>
|
||||
stream
|
||||
Gau`T9lo&I&A@sBlr!.@1r4+mQ;0X/8S@>,b4Leh!0`M_L]eOpZDrE3"\;7i/DKeb;qj*o9`iu'[r:.V9Ye\e@Xf.&^i+;(r'5c80OFg:]EmYL_#*rHNsdV!ZRgpJbOUIN$p=-D-Lg-W"c&IiILmh,Ufm&H4NX:J3/Y?kFro1Qj":d^0?IZfEgH,8RAHM4)t\1npObu!..n78b7!WI!4d7#pTiXIHqaB]%M%TW(Pa`bdD4gH$I&q&9";%gNlc(Y((01jeE]a,!qYk7s4&+M7;1&g(aHC)kMX>!"0sj[+>]nBgP;)i3;(@T=^%okP/04WC`:;NeWo'Q3G3Pt735XT+Gd]J@4(pR\=5H>5%?qQTr(2U@H*D#a#of44c(65VqlZ4^^P&%!uk*l+f$hanbp*?$/=hG-L9`,2hPLWoAocoN=KCPo/-r8Zc$n4ZICfcH[^QRWJRu,-A_j&%3p;@F/TdLW2o#PDjgD-?[8Jld.Z^-#RRUIhsl5"/IGZK8MrAdbaFj8.BM3?Lk!iLNk7u6Va,M6XK8dj1KB\K.^%KC%u:B;I#]i8d<`]p#?MhPP.%ALD\'MY&GG'W*7+L`6faF[XJf;WAc,BR]dJaTEn>'V>Njk9ASK"eDU>!g'[rNe(:DeH&ti<B+*Bk0gu=+IMe]dWIblhJ!b9TM<`Q35HTXE]_BaF_`Q2lAP.RLS,LqOknh#FV+5C(pE0rGUN-IXM+Z=?(q5Ktg3d,T7X]tBV'*cF#TPG%P8/e-,Tm%c@]<b5rh%Z#,Y#[k.$5IA'lB4_?.tZV[BYX$MlRn,:XltpZ9Kt5!!$RB-5q)`FA+Hd3>F<7]&lT&#i!&\l:"Q,>1&iT:$gX39>VhU#M`O11G8u?S`CmJlVf2-[[3d)&M1on!rl#%eAbRc+Nbfe236$:pW6K0X.>;jb6&+#&pW5V0+A,"f%5/GK"ORLADr@2a[A"SB'O:1&hB:4<G;"gaf)(+@pD&&tVEp0?[D$&Wm"fuS;J_Ql2bDIgUcKeLN;bdKf:[-B9/Q&CTm-Z[_faK`G^FjMKhZ8an`:J<JoZQ+5^V%m7;X$/B`rZ+58SPi+-3"'oa!_!jp^b80WbqX'/9.Y_!%\%!EA$]=>WQ1/*k=nd*;#tB;d+WScPmt\jIKD8C?d^D^RHq?#]O=aDqF3Tp_YSGGJNF5G$9Sj!6V4B@AU2fO4tCRS)%l4$@nBC*T"8+[Q".AAP'Yb:'A.h.Y3o^kD*ahSFBS\-:Ld42>M.!cI+VmH:LkTnA>U2`Vhij&NlPqU6p5@.,JOqneX6<qu*9%eO[ciYj7JF0XhMKWke[1)0Yd28dQR#m`oh)T3'+)(!(d%H#p>m3-B'1tQg"_U]mPb_0NI09C8`[1MXu4%0]rVAW2loCWb6Y*p*"A:'%`)/%B(#"IMjjQS2QlrlP"cH+BHlIl>lLgom(9f^M'7?(".1dSE:gnDT.]aH337%`&Q$7dZAA*02MKj'o?WF1`%P\^cJ=o.ch'#a%f7[;;hW`XIS35W#kCtTo6:Ti+8l5uQ2s/W"3;VrF!EV'ghotdf3NldB4[V>+T<T$)&Y2-:50mD'H"nnsk#)36TTj:Ci\K/_.XG(Pb31;c7a$knRq;Af)?So5rFnq'?2>0bGONWiSi6.m5HsDUTE^@!4R%.HW5Kh1C*8rW].kLoKZ-S/)&:.ul]H59;`qC(;i&HT6O*OYcJXE@4&%l;U,KX`:R%+],:Q2\>E$;8nIrcTSbmh=e5[&ePZoPe`r`^Sei"NcC*Q,;]dj>#oZ%rA1)0'NZ3nnUr1JQ)6oD#8>('!&SA"02UguPT,D8rBg\Tg9C+EeO9`IAVRQsK~>endstream
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1690
|
||||
>>
|
||||
stream
|
||||
Gb"/&>Ar7S'RnB33%-Jr*>1ik?/(Qi&(Y/qicYjW*b^tIfuTmT;K;[(G=]i]in0*C'^Np^+?%0UF8<Ok'*SpHW;E2?!+(ZDr#Gt+_mf[bOq>VTiU-F$p4T5=Ree1h1^ba,J_7.OL<K<='h":2ILuC<U_0;'SA>9$0I2+sHW7j^?SohpS46i;'C?iD;<"tf^!+Ck5\M,a?4:Rlik4n,F&%$,D4SVc[#J_a:_o6CLbOhm$q<i-Sf9kI;9@BoN>+$&N28(DKR!maY;#t/.O$#SoJ"8r/OgV>aEg*5dhG\q)SLQ43K+S;GKdAjL<V]jOa4XuQ!m/'Wb8JYgJ:1gQ;[2s&g>K+<IhRWhMja^O=o%Y%5mi7ii_>MbL_:^OKU;lKR`%<.FMXIKIJ4]T*@QUJ9Zp;J`959QUZ^S0n'bY6UNtqI/MOTiBapE!htO4e=q.DL`5)-TW;=;?Ko!WQAM-XANAkPc[amk$eu!%0c,G?`rPe&;1FYK(b"bhHd@Z`eI8Sh;jR<q6oC5[3.p13$4QRX(Kl9VF!u^ik"O;?BQ!)]XI9MI@HR!*ag>AG2+9f;:,T6_p#ME<R+0-s8s;stK1iqc!TH89@3WI'[1o?E-0f5cUZ]UL(D7&$ojf5Q7]k_mo*;e'*VXmaU4i6(j3cST>uP_<lR7cFg:h4I*aCRQC)C[gaG'YFkrF]+>b6*tMXGi_=GB!.VW6O0gL0@bBi%2/RGf'[_A'.1rc5M)kNA!Onrkq)H,8dor,RI[6.iaDPAM(5/XX?o)DT3+4=>U88b<G_^%!n/PdinCWH6R_V9e$QFlThWr+XgZ#0l`O='B:TWgo\gE\2'lF*tR`=hU-/\O'P/:\BEtqVMbKMoARDFO=^bSD=LNNdrk.[LT[p]A'pe4@oAUleH%u:77tpm<TMXMS>rZEZjY#mP0%"-aY4nD1IC]Bi5AoJIr8"V.<Nl$<V5hTLgdm=0eFB_)Z,\DtFN#.bfT8_8.R6knN6b.`ADhfUM$3Nbl_l0i>5Ja[+RV,-]<3d07B8rE_b<m!,=Q<fbQ[4PH94"]Y$n,5`AqL-B`nD29TB=^k%Cap6DRBQ5e?#Zt<iY#-k8MJ8<pQ=+9*CtkQd\QiN@&/.nJZT\S#5Pu,gp8gFshq)pMc0R:"2onVXRL4(^`gc&/E@gMhIU;,N]l^UJL9Fmd^\CL\XX1oc=`u=G*@kp<]Fne4'Qt=[&p5\RkO]_pEfbQ2!9ClEB7nt]8_W)Va,*c\CBs4VLZ'+'6qq!s$cdu,!qrKj@b0rO/Nl+p.^p8Q<6OADa1CL(/X0L"-*@UrK5^$<^+'7Wr4%:Gj-gf3rr7o=rC(d?0=n+S&k`<Gcf%lF`[e3ZHR!fHcF4Gk4.Eq4l2Ar;9]&=o'#.LuVGTQ6T!`-9[XETa\p2pb)YX\*Dq]]XENqUDpT_%dcZ)S9>6't7.;l@0TjgX9eS23U,1r2OjSi)3'Bc'S_oe$J-hSJ_iP4)Ij7L^0>&2mM\(`D.-mkDT>l8a)P"WW>#SBbg/#,qeF[*=lpf`kiSkET2AJi^\@;Y.nr"]Nn3JFY?<nAudo`nK(<9AMr!`'2$Eq5@MnaaXJ<pDD`hr,VhD\)05OQ:V_0uD8B`:n=PJWlrj7`d/09@WK%23GCL@$p"4DA4Be8^q3Kc(ER=d9dE1@Y`k/\*Q;&Mne>sHJ=JHh$kE#rW*!X[:'~>endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1760
|
||||
>>
|
||||
stream
|
||||
Gau`UD/\/e&H;*)E<n86*AU*K7(otk#Id:RU)f]R?M>aQ(n%\;d#q%!cFfkNI7IF@A0lEL<JV;2[Uo#9*o9&(a&'#0Q^3,r"i./FS@tU]D4&7,?/i`^"'eM(?O%ia6"%;iK$^>N!+K8^%mF>1;IG_k[c?'!#E7$V$Xj([Fc"?]O!K[R9:_/h!OLS$de])/8utXY'jA*h#k_H[nj(G(V3NrlV3)N".O4I<Xo,^g..ddd)#CLfiK_QmLpWPdQ/l_SY*=``Qt2Vt:%U6/qur9kN?I5T6483`;'QI"0Ok-c<Ds0^C:*].As`=H[aam3QHo](B2PoFWTH7`dY>?QDEDeDC'qagKH\RH&qITb`f<i>+LS9Lel<V")ME?qK3sRMR9FQhc<bM0#?JB]j1ogc[D64h2Cd#/3"V2".nYe"_\H77o9G]'$9a02:h/+-Na)oSega7(f>GVgpL9\W;&!S1WcKrP$Mc6e(*_A.OI'1m`F>guf?[c>r@1f>YnHVX\p(-f*hM<!E?YS^?rini!c<iuAJ1\#+Qna.\</TtVFa7+kIDAn9=`0#Mqdg.oHrU5MIrJnkomU@0UP]'a*KmoSLp.*i(=\'F.%r@m!VCg@JEelM4&"%pu@i3fOQ%72SNrIV?QDM+NngXZaW';d>B<7haddCX*9XA.cuk<^14t=-MXtT<Qqr$NDSp82?Vs'R>T4mad0A@XGl+RlMGq`p?=r8?Q\fTj,jYuNJIC^J`43t_;j?>)D$0*0B\33lq8;c;&VWRCh`I&93C\j^m>25-<qntP^*V@aqhcYn[h]E?)FKp"+9G@n?H<Yq_QXk[lg;70[B3@Tp))RF8U4RS'ATD@M^_h,Co%N#,YgZ6Ap3KG#1=AS9#j?6:$`DdCNJ5SdWVn/s['lm&g_<oE`gkhOkg+$VVM-;lH:(V$o#$Q6$0_Khi3d$i:&1b?u\c[d+F@E<N:SrDEO'WofiP\R0V!OsA=4l]8d#Rms1e,5&gjJ/j,l=aq-EN/M!+p[L3ZVH*MEV'FXoA^^^)JCq$\IIql1[jaEr*Be'CD4mIT`O<pd&06GM@[:C.=p%Qun2d$G\.ko=mGVT:Z*%a.3?(B#EB(>CiV\gr#E\>5'3e\q:kR&td7A5P8lBIUU&rT[67Hn/A22l:fj+ro30'M\?7uJr(h]`jO&T"8.QN(%]KVgYQmOjmE0mBLljb52_JFj]5j;rIQHnP0;SGCBLFpdnQl8OIoU?'+<,kpKe=hW/Y2.\dn-\!.':j=a!*n9-nr.MNr)lg&j.NNY!F8<fG>+N#X6I6TLG#N08+>F$c2TMZZ#@Qoj:R-RBf,K&j4TAG<T>MAS9])sm=9m(4?XGhSNe,&;K,<>m$uTU_.YMJ&.>WK,p5P:l[jY2"9cBo&@gYDJcL0:<<RPFb[r1+O>[OcmI4Eu1mi]q<&m^'QN,4SrCK`%;\Ql6ABq=[M:'-7iB&dZ\GIntQGnhW,.%?NA5k;[A%Z_pahRL1^n8YjDQh-)(@H'UClPZ&l##js0@eu,SF!q/4*e(Uo@ruf60!oc9.Y2ba#K4EB%TVC[SfZhXC]O<$"(V5M58dd:d*RrbjaAYFLmM:k4I1g26W&ob1u4mMN6.e:4-<(:#9XOh92+Y9.J^mjdf>dhNnN1c9+<QkN]JiU<^K*a<UD3/t*H,5p(VNg)Pjc(<HUs4bQB5A:,\JQ_qfA)"hE%n+VXVVGe6;H.&qEo+I@pj[%IYP8'WjS?JC[^U`TaAc;(['phH2m6Uf*rr<&AA%M~>endstream
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1656
|
||||
>>
|
||||
stream
|
||||
Gb"/'9on$e&A@P9R,bVG'N#s<S6N`@'Ffq1f0p3a&%"aZN$V=EWV-,FkE1q@b'ldu)VIc#^c&!^L@aQPS"U^RGVfA&e3lMSkg^VA/>C+@=:umdGXoM#c9@j(H(*nn6:!e2(s=@I?eLAjhOZThZ65+c=[ZDa#&HJm;b^th"Aq4k<70g[1nFrY4:RL:l1'om%Doqi3"N9c!]E]WE(5/%k'%]jgi"qVnmR`lb_C@[2Dh$0Bq-B-Ra0f+AOU#r8ae1D(](f\.mj]@Wi`Wmd,G]jIWQ)-&i^:3@fh_US:]8XJ/qoOb+3>7p9*R(blW(3/MFP[/a-89FL>d5[o,(.GGE\cScnPI;j7J8?tR:YYW<Z(pje#;6d6+7-@*Dt3V%FIj_]ML`o8OA.>U-qaCQH#*u@"Z+n]1A@9N;Sc]$]Q$&XBl]J>hYSg#_FnE\B[4Y!%8@8A\g^%2WGUN]=I]Cn,(`jA8N.8?Y;KGDuom4im0,B`gfjkB-n74$Ntf[Emf%5%>(n6,s%mJD@>U1Z_A3Z2+f_HPWoc"j>)j632U=rOI9ZkeYJSmnKVgNJm-;j(la"5j=<?NWdMY,,-pG#P&Da"SRET0.EaS[#tO]>rl]@Y:n4S5Ji1es=YlpP^A15nrE8Yqus-/4;)nPi?1ko/NC<0^d$9#\]Mmclh\8`G,?gQds8G$dk?$Z4=$(CfFCA4dLt=E#/<e^-S#D*D+53[ced!1Lb1]m)#YsB]S:d8*0t^dS'G9/;]S$fn`i-7,GA\Yhhs`JalA!86kbtXTUq.ZkVYi<d8=a,d*#$0C$,UjIWlE%Bl]Ne8"&a.96dHRr2A%A&,/P,Y=,m6^$ms8EPsP`6cjbaeJfLEf287U&5ZnD@@OGE,,^#J8$I?;cb@o<W;uAB*Z:<k4X7V1`n+Fd95Cf<^mQTTG`Gi.t;K==:^Si-e-I9fO'j_h*8/-YkdAZ0_RQ@6/^=B*Fb]G[s];.8`TU!bJ/[]Y;uH\V1P0HRB9G>:CG\a)Dl_*%<@@A*7a(0iC4VMWc?#C%an8s=gNb`d?IQB+.`nnl$[dBoVmiE0-^rY(<Yru%Yr.5@!AONf5$Am<t[AiW)Cu["61o*I1<`E6U%r<g&_55i2*VW3GDrBZf-+`DX@#;1CLr1,jSWu'&Zc:i[aXSg1cBE%g,F2U?H,,&@Vk]TIE,=Mg,U9'=ro$fkCL$V;X3EnC[&j$ht0EAY?<V3XF:RCJfh51,qLS(T;&ILRK76Zr_%`5N$k,.p;SGk=FeC#h-)3oU!G9Fb@j9gn>N("nC@ap(Vb8n8\De=ElV"^(>u"CRPk>pUART2l?GG]A1K9KiWVK^MJ?S3_Z`JZ.#W%c7c2#agG%aQc9In)_f'pS^NU]#0g;G>\)Y?4XGM-HO`$@e%p#@ILiG>LrN=l$58G"Zr`fAD#]1*Qib@njPiB(ns*D6NOk2D-ME3<U[tr^9-WiseXLecF$;YH\RRs&R).eOeFP-Y`6SH[HqM-FA$*aAN)R#$K.D$abq%7Yk"SK%d"3feXI4YJHOa<5$$#2JGBBe6(;_7<+bp^VR_m)C%:aZ`q4Ygc&D*jA#G+430+$Hg:I&WU_;r5&1j8_+n`?l98FOg>a&(%R(,3h,+1'/\s*KN2;;3ZQBf6GXhgkT$Hie'Y."Dh@FS5ldq*o?~>endstream
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1554
|
||||
>>
|
||||
stream
|
||||
Gb!$H9ip@5'ScA[MRuQ/>0$28*-MK4ZR?2"dWX*!@On]1[cNW^+THnn'ZW0A0EsX2i>h?LmC^$VrK+0K^r9Yis!@ob`<+%8!lT`1$:s(XYdfE`MmKU":)q9Y(P!/p"%$I9beH^Po?V%qKZ%jn5Or1pLCT_A;^`as!lk[16TNqF+8)O,'SgSQH*eSIO#h/'kY>E2*ukms'/p!'5-qTf?tq(R3cQrI*%$Y.Nt[sO:?[IH7ffA(.5X[S9Aq4q>:+86>:EY[O`.c/"VtHQnZ2I.-U9s6D%'5qS:VEC)3@mf5]!69[biS?iPZm8A8/?CCBdiaW=)aAWjsN>Eh0\Q3,h\C"tt'b`(ZU1^ph5,)h4PK>#Qeq;Zk>hnJ(q8h"<-[d^n8[d(goh1!+Kr7AGPNoeL/0'12;2IA$MN[WeAbiQoGUmm;_]*aZOUNT'$f_TchjP>JhMLT:$u*R/As5VcWg&lQ04FflJ+Q"CZ;U4sqg,pLbTK8M.$04aA?R'bUC\oI&bLiNdk-XI(=DfK7UmfMLJ-)#J1Iq1<[_+>=2^#hgN?.)o]F)*T_'=k5==M]`/)]U:X=^mu-O!lS*%V8UNqtiMf7Ifm[FYl=?BZ;.>0l3rYQ@b3GbtFM7@=f_cjGZ^(AVuO$CS%H95\e1i(C@g<cm3+LOA4Hn5CYm;5#n\:0fpkpk@H.20a_$:#uXc:)('<pC&9:I[/VqF!C<*IL#snu"STWr<klJVf"eP*fF2HLcP:$rcZ<4N[+t<46r(OS%%@j'@\lNdH%u`E23fN:S*2T+T]dBuqNc.?\ZZ6e3*]e!JPU-r6RU`1`Gl0.H*F^rVjB`>@:SbrXYCZ[.>32VXTD4`4U_^nj1Dt!q=PUL3t@<V?H.8P)Km8ea9XTU+Da$jR>kj.o?!a;:1UdX.(@H"Hi6e`N55fS#/b$_:"'lG(A:Wa7*J9?93'r>JTV/t;A:g_pM__&VQTdbnUHWbmU5q(rJl!>EKg.WR0CK>m\O'C=%;nrL[GH]08SJ'X++gK+(kIKj!uT<"k?^u`g7l\j(lA.j%`X7kB3bL_8^>=d^eTfIJi-E"*#O6rh(<$qh)ai5E"$.Zg>/h2X6"A*=CUA6Hji_\Ud*6fj`egaSbi.UcOsB-.OXZrTFB?U8&t0P]CT=\jO)lJ*l[/+"9E6I`:T.*j&o==Zo]fRa!eZ^"\EU2W8SJkdi.7paGugTQk8+h?tKKbA3n"Xa9O/VXCKH/h_7#g2YC(WP`;eP"gro(G=@_m"?niCqn3eD7qL1;MSn;0?lTrb4GQ^9h4fM]>/o`jK#E=4t!(0nTEZZ9c\?<9_[0uHeo'2G_<8gZU3I#j/+IHgV93^K!0EN%<tb3[aW#U25;'h-L*_$D48UlT8pc-X?O:Jb^7LM94Y&m-J5NY0l4$m+rMmCJd+<,M*,?if@aZ8gg&*M&0^T6M0(R=hE>;QaMK:LR[$QEF)$n^^K7Xp3ssG/8dqU;gX6_54lS#dTLm/H5)5LN3WZ-l0MW"QFRs@bY0l>8_FcR70RoEoDtiK#b?Z-#&!`6)M@BkR9*Uo7")?2C`;~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 18
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000436 00000 n
|
||||
0000000641 00000 n
|
||||
0000000846 00000 n
|
||||
0000001051 00000 n
|
||||
0000001256 00000 n
|
||||
0000001461 00000 n
|
||||
0000001531 00000 n
|
||||
0000001834 00000 n
|
||||
0000001918 00000 n
|
||||
0000003840 00000 n
|
||||
0000005622 00000 n
|
||||
0000007474 00000 n
|
||||
0000009222 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<eece393ebfb26a539f7b54ebcb7e7fe0><eece393ebfb26a539f7b54ebcb7e7fe0>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 11 0 R
|
||||
/Root 10 0 R
|
||||
/Size 18
|
||||
>>
|
||||
startxref
|
||||
10868
|
||||
%%EOF
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fad8e38235e6e45a3b3d7fecc3323ea7
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Editor.meta
Normal file
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af3bb097acf624c119d24f77a7c70eb3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Libs.PlayerPrefs.Editor",
|
||||
"rootNamespace": "Darkmatter.Libs.PlayerPrefs.Editor",
|
||||
"references": [
|
||||
"Libs.PlayerPrefs"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f4f71cd22f5047c78946bd1928ea7b1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,559 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs.Editor
|
||||
{
|
||||
public class PlayerPrefsEditorWindow : EditorWindow
|
||||
{
|
||||
private PlayerPrefsKeyRegistry _registry;
|
||||
private Vector2 _scrollPos;
|
||||
private Dictionary<string, string> _editValues = new();
|
||||
private Dictionary<string, bool> _dirty = new();
|
||||
|
||||
// Add-key fields
|
||||
private string _newKey = "";
|
||||
private PlayerPrefType _newType = PlayerPrefType.String;
|
||||
private string _newDescription = "";
|
||||
|
||||
private enum Tab { Registered, Raw, Settings }
|
||||
private Tab _currentTab = Tab.Registered;
|
||||
|
||||
[MenuItem("Tools/Darkmatter/PlayerPrefs Editor")]
|
||||
public static void Open()
|
||||
{
|
||||
var window = GetWindow<PlayerPrefsEditorWindow>("PlayerPrefs Editor");
|
||||
window.minSize = new Vector2(450, 300);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
FindRegistry();
|
||||
}
|
||||
|
||||
private void FindRegistry()
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets("t:PlayerPrefsKeyRegistry");
|
||||
if (guids.Length > 0)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
_registry = AssetDatabase.LoadAssetAtPath<PlayerPrefsKeyRegistry>(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
|
||||
switch (_currentTab)
|
||||
{
|
||||
case Tab.Registered:
|
||||
DrawRegisteredKeys();
|
||||
break;
|
||||
case Tab.Raw:
|
||||
DrawRawSection();
|
||||
break;
|
||||
case Tab.Settings:
|
||||
DrawSettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toolbar ─────────────────────────────────────────────
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
|
||||
if (GUILayout.Toggle(_currentTab == Tab.Registered, "Registered Keys", EditorStyles.toolbarButton))
|
||||
_currentTab = Tab.Registered;
|
||||
if (GUILayout.Toggle(_currentTab == Tab.Raw, "Raw Lookup", EditorStyles.toolbarButton))
|
||||
_currentTab = Tab.Raw;
|
||||
if (GUILayout.Toggle(_currentTab == Tab.Settings, "Settings", EditorStyles.toolbarButton))
|
||||
_currentTab = Tab.Settings;
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.backgroundColor = new Color(1f, 0.4f, 0.4f);
|
||||
if (GUILayout.Button("Delete All PlayerPrefs", EditorStyles.toolbarButton))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Delete All PlayerPrefs",
|
||||
"This will permanently delete ALL PlayerPrefs (both regular and protected). Are you sure?",
|
||||
"Delete All", "Cancel"))
|
||||
{
|
||||
UnityEngine.PlayerPrefs.DeleteAll();
|
||||
UnityEngine.PlayerPrefs.Save();
|
||||
_editValues.Clear();
|
||||
_dirty.Clear();
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── Registered Keys Tab ─────────────────────────────────
|
||||
|
||||
private void DrawRegisteredKeys()
|
||||
{
|
||||
// Registry selector
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_registry = (PlayerPrefsKeyRegistry)EditorGUILayout.ObjectField(
|
||||
"Key Registry", _registry, typeof(PlayerPrefsKeyRegistry), false);
|
||||
if (_registry == null && GUILayout.Button("Create", GUILayout.Width(60)))
|
||||
{
|
||||
CreateRegistry();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (_registry == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"Assign or create a PlayerPrefsKeyRegistry asset to manage keys.",
|
||||
MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// Add new key
|
||||
DrawAddKeySection();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
|
||||
// Key list
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
|
||||
for (int i = _registry.Entries.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = _registry.Entries[i];
|
||||
DrawKeyEntry(entry, i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawAddKeySection()
|
||||
{
|
||||
EditorGUILayout.LabelField("Register New Key", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
|
||||
_newKey = EditorGUILayout.TextField("Key", _newKey);
|
||||
_newType = (PlayerPrefType)EditorGUILayout.EnumPopup("Type", _newType);
|
||||
_newDescription = EditorGUILayout.TextField("Description", _newDescription);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(string.IsNullOrWhiteSpace(_newKey));
|
||||
if (GUILayout.Button("Register Key"))
|
||||
{
|
||||
Undo.RecordObject(_registry, "Register PlayerPref Key");
|
||||
_registry.AddKey(_newKey, _newType, _newDescription);
|
||||
EditorUtility.SetDirty(_registry);
|
||||
_newKey = "";
|
||||
_newDescription = "";
|
||||
GenerateKeysClass(_generatedOutputPath);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void DrawKeyEntry(PlayerPrefKeyEntry entry, int index)
|
||||
{
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
|
||||
// Header row
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(entry.Key, EditorStyles.boldLabel);
|
||||
GUILayout.Label($"[{entry.Type}]", GUILayout.Width(50));
|
||||
|
||||
bool hasProtected = ProtectedPlayerPrefs.HasKey(entry.Key);
|
||||
bool hasRaw = UnityEngine.PlayerPrefs.HasKey(entry.Key);
|
||||
string source = hasProtected ? "Protected" : hasRaw ? "Raw" : "Not Set";
|
||||
GUILayout.Label(source, EditorStyles.miniLabel, GUILayout.Width(70));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.Description))
|
||||
{
|
||||
EditorGUILayout.LabelField(entry.Description, EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
// Value display/edit
|
||||
string currentValue = GetCurrentValue(entry);
|
||||
string editKey = $"{entry.Key}_{index}";
|
||||
|
||||
if (!_editValues.ContainsKey(editKey))
|
||||
_editValues[editKey] = currentValue;
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
string newVal = entry.Type switch
|
||||
{
|
||||
PlayerPrefType.Bool => EditorGUILayout.Toggle("Value",
|
||||
_editValues[editKey] == "1" || _editValues[editKey].Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
? "1" : "0",
|
||||
_ => EditorGUILayout.TextField("Value", _editValues[editKey])
|
||||
};
|
||||
|
||||
if (newVal != _editValues[editKey])
|
||||
{
|
||||
_editValues[editKey] = newVal;
|
||||
_dirty[editKey] = true;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
// Action buttons
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!_dirty.ContainsKey(editKey) || !_dirty[editKey]);
|
||||
if (GUILayout.Button("Save", GUILayout.Width(50)))
|
||||
{
|
||||
SaveValue(entry, _editValues[editKey]);
|
||||
_dirty[editKey] = false;
|
||||
GenerateKeysClass(_generatedOutputPath);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (GUILayout.Button("Refresh", GUILayout.Width(60)))
|
||||
{
|
||||
_editValues[editKey] = GetCurrentValue(entry);
|
||||
_dirty[editKey] = false;
|
||||
}
|
||||
|
||||
GUI.backgroundColor = new Color(1f, 0.6f, 0.4f);
|
||||
if (GUILayout.Button("Delete Value", GUILayout.Width(80)))
|
||||
{
|
||||
ProtectedPlayerPrefs.DeleteKey(entry.Key);
|
||||
UnityEngine.PlayerPrefs.DeleteKey(entry.Key);
|
||||
UnityEngine.PlayerPrefs.Save();
|
||||
_editValues[editKey] = "";
|
||||
_dirty[editKey] = false;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Unregister", GUILayout.Width(80)))
|
||||
{
|
||||
Undo.RecordObject(_registry, "Unregister PlayerPref Key");
|
||||
_registry.RemoveKey(entry.Key);
|
||||
EditorUtility.SetDirty(_registry);
|
||||
_editValues.Remove(editKey);
|
||||
_dirty.Remove(editKey);
|
||||
GenerateKeysClass(_generatedOutputPath);
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private string GetCurrentValue(PlayerPrefKeyEntry entry)
|
||||
{
|
||||
// Try protected first, then raw
|
||||
if (ProtectedPlayerPrefs.HasKey(entry.Key))
|
||||
{
|
||||
return entry.Type switch
|
||||
{
|
||||
PlayerPrefType.String => ProtectedPlayerPrefs.GetString(entry.Key),
|
||||
PlayerPrefType.Int => ProtectedPlayerPrefs.GetInt(entry.Key).ToString(),
|
||||
PlayerPrefType.Float => ProtectedPlayerPrefs.GetFloat(entry.Key).ToString(),
|
||||
PlayerPrefType.Bool => ProtectedPlayerPrefs.GetBool(entry.Key) ? "1" : "0",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
if (UnityEngine.PlayerPrefs.HasKey(entry.Key))
|
||||
{
|
||||
return entry.Type switch
|
||||
{
|
||||
PlayerPrefType.String => UnityEngine.PlayerPrefs.GetString(entry.Key),
|
||||
PlayerPrefType.Int => UnityEngine.PlayerPrefs.GetInt(entry.Key).ToString(),
|
||||
PlayerPrefType.Float => UnityEngine.PlayerPrefs.GetFloat(entry.Key).ToString(),
|
||||
PlayerPrefType.Bool => UnityEngine.PlayerPrefs.GetInt(entry.Key) == 1 ? "1" : "0",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private void SaveValue(PlayerPrefKeyEntry entry, string value)
|
||||
{
|
||||
switch (entry.Type)
|
||||
{
|
||||
case PlayerPrefType.String:
|
||||
ProtectedPlayerPrefs.SetString(entry.Key, value);
|
||||
break;
|
||||
case PlayerPrefType.Int:
|
||||
if (int.TryParse(value, out var intVal))
|
||||
ProtectedPlayerPrefs.SetInt(entry.Key, intVal);
|
||||
break;
|
||||
case PlayerPrefType.Float:
|
||||
if (float.TryParse(value, out var floatVal))
|
||||
ProtectedPlayerPrefs.SetFloat(entry.Key, floatVal);
|
||||
break;
|
||||
case PlayerPrefType.Bool:
|
||||
ProtectedPlayerPrefs.SetBool(entry.Key, value == "1");
|
||||
break;
|
||||
}
|
||||
ProtectedPlayerPrefs.Save();
|
||||
}
|
||||
|
||||
// ── Raw Lookup Tab ──────────────────────────────────────
|
||||
|
||||
private string _rawKey = "";
|
||||
private string _rawValue = "";
|
||||
private string _rawType = "String";
|
||||
|
||||
private void DrawRawSection()
|
||||
{
|
||||
EditorGUILayout.LabelField("Raw PlayerPrefs Lookup", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Look up any PlayerPrefs key directly (unprotected). Useful for debugging third-party or legacy keys.",
|
||||
MessageType.Info);
|
||||
|
||||
_rawKey = EditorGUILayout.TextField("Key", _rawKey);
|
||||
_rawType = EditorGUILayout.TextField("Type (String/Int/Float)", _rawType);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Get"))
|
||||
{
|
||||
_rawValue = _rawType.ToLower() switch
|
||||
{
|
||||
"int" => UnityEngine.PlayerPrefs.GetInt(_rawKey).ToString(),
|
||||
"float" => UnityEngine.PlayerPrefs.GetFloat(_rawKey).ToString(),
|
||||
_ => UnityEngine.PlayerPrefs.GetString(_rawKey)
|
||||
};
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Has Key?"))
|
||||
{
|
||||
_rawValue = UnityEngine.PlayerPrefs.HasKey(_rawKey)
|
||||
? "Key EXISTS"
|
||||
: "Key NOT found";
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Delete"))
|
||||
{
|
||||
UnityEngine.PlayerPrefs.DeleteKey(_rawKey);
|
||||
UnityEngine.PlayerPrefs.Save();
|
||||
_rawValue = "(deleted)";
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("Result:", EditorStyles.boldLabel);
|
||||
EditorGUILayout.SelectableLabel(_rawValue, EditorStyles.textArea, GUILayout.MinHeight(40));
|
||||
}
|
||||
|
||||
// ── Settings Tab ────────────────────────────────────────
|
||||
|
||||
private string _generatedOutputPath = "Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime/PlayerPrefsKeys.cs";
|
||||
|
||||
private void DrawSettings()
|
||||
{
|
||||
EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
EditorGUILayout.LabelField("Registry Asset", EditorStyles.boldLabel);
|
||||
_registry = (PlayerPrefsKeyRegistry)EditorGUILayout.ObjectField(
|
||||
"Active Registry", _registry, typeof(PlayerPrefsKeyRegistry), false);
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
DrawProtectionSettings();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.LabelField("Code Generation", EditorStyles.boldLabel);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Generates a static PlayerPrefsKeys class so you can use PlayerPrefsKeys.Career.Level instead of string literals.",
|
||||
MessageType.Info);
|
||||
_generatedOutputPath = EditorGUILayout.TextField("Output Path", _generatedOutputPath);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(_registry == null || _registry.Entries.Count == 0);
|
||||
if (GUILayout.Button("Generate PlayerPrefsKeys.cs"))
|
||||
{
|
||||
GenerateKeysClass(_generatedOutputPath);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.LabelField("Danger Zone", EditorStyles.boldLabel);
|
||||
|
||||
GUI.backgroundColor = new Color(1f, 0.3f, 0.3f);
|
||||
if (GUILayout.Button("Delete ALL PlayerPrefs"))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Delete All PlayerPrefs",
|
||||
"This will permanently delete ALL PlayerPrefs. This cannot be undone.",
|
||||
"Delete All", "Cancel"))
|
||||
{
|
||||
UnityEngine.PlayerPrefs.DeleteAll();
|
||||
UnityEngine.PlayerPrefs.Save();
|
||||
_editValues.Clear();
|
||||
_dirty.Clear();
|
||||
Debug.Log("[PlayerPrefs Editor] All PlayerPrefs deleted.");
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
|
||||
private void DrawProtectionSettings()
|
||||
{
|
||||
EditorGUILayout.LabelField("Protection Setup", EditorStyles.boldLabel);
|
||||
|
||||
var settingsAsset = global::Darkmatter.Libs.PlayerPrefs.Editor.ProtectedPlayerPrefsSettingsUtility.FindSettingsAsset();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.ObjectField(
|
||||
"Settings Asset",
|
||||
settingsAsset,
|
||||
typeof(ProtectedPlayerPrefsSettings),
|
||||
false);
|
||||
}
|
||||
|
||||
var status = settingsAsset != null && settingsAsset.HasConfiguredHash
|
||||
? "Configured"
|
||||
: "Missing";
|
||||
EditorGUILayout.LabelField("Hash Password", status);
|
||||
EditorGUILayout.LabelField("Runtime Source", ProtectedPlayerPrefs.InitializationSource);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Open Getting Started"))
|
||||
{
|
||||
global::Darkmatter.Libs.PlayerPrefs.Editor.ProtectedPlayerPrefsGettingStartedWindow.OpenWindow();
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(settingsAsset == null);
|
||||
if (GUILayout.Button("Ping Asset"))
|
||||
{
|
||||
EditorGUIUtility.PingObject(settingsAsset);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (GUILayout.Button("Open Docs"))
|
||||
{
|
||||
global::Darkmatter.Libs.PlayerPrefs.Editor.ProtectedPlayerPrefsSettingsUtility.OpenDocumentationPdf();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// ── Code Generation ─────────────────────────────────────
|
||||
|
||||
private void GenerateKeysClass(string outputPath)
|
||||
{
|
||||
// Keys with a dot → nested class: "career.level" → PlayerPrefsKeys.Career.Level
|
||||
// Keys without a dot → flat const on PlayerPrefsKeys directly: "volume" → PlayerPrefsKeys.Volume
|
||||
var groups = new SortedDictionary<string, SortedDictionary<string, PlayerPrefKeyEntry>>();
|
||||
var flat = new SortedDictionary<string, PlayerPrefKeyEntry>();
|
||||
|
||||
foreach (var entry in _registry.Entries)
|
||||
{
|
||||
var dotIndex = entry.Key.IndexOf('.');
|
||||
if (dotIndex > 0)
|
||||
{
|
||||
var group = ToClassName(entry.Key.Substring(0, dotIndex));
|
||||
var member = ToConstName(entry.Key.Substring(dotIndex + 1));
|
||||
if (!groups.ContainsKey(group))
|
||||
groups[group] = new SortedDictionary<string, PlayerPrefKeyEntry>();
|
||||
groups[group][member] = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
flat[ToConstName(entry.Key)] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated>");
|
||||
sb.AppendLine("// Generated by PlayerPrefs Editor — do not edit by hand.");
|
||||
sb.AppendLine("// Re-generate via Tools > Darkmatter > PlayerPrefs Editor > Settings.");
|
||||
sb.AppendLine("// </auto-generated>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace Darkmatter.Libs.PlayerPrefs");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" public static class PlayerPrefsKeys");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var (constName, entry) in flat)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(entry.Description))
|
||||
sb.AppendLine($" /// <summary>{entry.Description} ({entry.Type})</summary>");
|
||||
sb.AppendLine($" public const string {constName} = \"{entry.Key}\";");
|
||||
}
|
||||
|
||||
if (flat.Count > 0 && groups.Count > 0)
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var (groupName, members) in groups)
|
||||
{
|
||||
sb.AppendLine($" public static class {groupName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var (constName, entry) in members)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(entry.Description))
|
||||
sb.AppendLine($" /// <summary>{entry.Description} ({entry.Type})</summary>");
|
||||
sb.AppendLine($" public const string {constName} = \"{entry.Key}\";");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
var fullPath = Path.Combine(
|
||||
Path.GetDirectoryName(Application.dataPath),
|
||||
outputPath.TrimStart('/'));
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
|
||||
File.WriteAllText(fullPath, sb.ToString(), Encoding.UTF8);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Debug.Log($"[PlayerPrefs Editor] Generated {outputPath}");
|
||||
}
|
||||
|
||||
private static string ToClassName(string segment)
|
||||
{
|
||||
var clean = Regex.Replace(segment, @"[^a-zA-Z0-9_]", "_");
|
||||
return char.ToUpper(clean[0]) + clean.Substring(1);
|
||||
}
|
||||
|
||||
private static string ToConstName(string segment)
|
||||
{
|
||||
// "some.nested.key" → "SomeNestedKey"
|
||||
var parts = segment.Split('.');
|
||||
var sb = new StringBuilder();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var clean = Regex.Replace(part, @"[^a-zA-Z0-9_]", "_");
|
||||
if (clean.Length == 0) continue;
|
||||
sb.Append(char.ToUpper(clean[0]));
|
||||
sb.Append(clean.Substring(1));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────
|
||||
|
||||
private void CreateRegistry()
|
||||
{
|
||||
var path = EditorUtility.SaveFilePanelInProject(
|
||||
"Create PlayerPrefs Key Registry",
|
||||
"PlayerPrefsKeyRegistry",
|
||||
"asset",
|
||||
"Choose where to save the registry asset.");
|
||||
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var asset = CreateInstance<PlayerPrefsKeyRegistry>();
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
_registry = asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b322d8950c37b4852a2ce171c9b96e5b
|
||||
@@ -0,0 +1,160 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs.Editor
|
||||
{
|
||||
public class ProtectedPlayerPrefsGettingStartedWindow : EditorWindow
|
||||
{
|
||||
private string _passphrase = string.Empty;
|
||||
private string _confirmPassphrase = string.Empty;
|
||||
private Vector2 _scrollPosition;
|
||||
|
||||
[MenuItem("Tools/Darkmatter/Protected PlayerPrefs/Getting Started")]
|
||||
public static void OpenWindow()
|
||||
{
|
||||
var window = GetWindow<ProtectedPlayerPrefsGettingStartedWindow>(
|
||||
true,
|
||||
"Protected PlayerPrefs Setup",
|
||||
true);
|
||||
window.minSize = new Vector2(520f, 520f);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
|
||||
EditorGUILayout.LabelField("Protected PlayerPrefs Setup", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.HelpBox(
|
||||
"Set a project-specific hash password before shipping the package. The raw password is not stored - only its SHA-256 hash is saved in the settings asset.",
|
||||
MessageType.Info);
|
||||
|
||||
DrawChecklist();
|
||||
DrawSettingsSummary();
|
||||
DrawPassphraseForm();
|
||||
DrawActions();
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private static void DrawChecklist()
|
||||
{
|
||||
EditorGUILayout.LabelField("Before You Save", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField("1. Use a unique project-specific password.");
|
||||
EditorGUILayout.LabelField("2. Store it in your team password manager.");
|
||||
EditorGUILayout.LabelField("3. Changing it later will make existing protected values unreadable.");
|
||||
EditorGUILayout.Space(8);
|
||||
}
|
||||
|
||||
private static void DrawSettingsSummary()
|
||||
{
|
||||
EditorGUILayout.LabelField("Settings Asset", EditorStyles.boldLabel);
|
||||
|
||||
var settingsAsset = ProtectedPlayerPrefsSettingsUtility.GetOrCreateSettingsAsset();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.ObjectField(
|
||||
"Asset",
|
||||
settingsAsset,
|
||||
typeof(ProtectedPlayerPrefsSettings),
|
||||
false);
|
||||
EditorGUILayout.TextField("Path", ProtectedPlayerPrefsSettingsUtility.GetSettingsPath());
|
||||
}
|
||||
|
||||
var status = settingsAsset.HasConfiguredHash
|
||||
? "Configured"
|
||||
: "Hash password missing";
|
||||
EditorGUILayout.LabelField("Status", status);
|
||||
EditorGUILayout.Space(8);
|
||||
}
|
||||
|
||||
private void DrawPassphraseForm()
|
||||
{
|
||||
EditorGUILayout.LabelField("Hash Password", EditorStyles.boldLabel);
|
||||
_passphrase = EditorGUILayout.PasswordField("Password", _passphrase);
|
||||
_confirmPassphrase = EditorGUILayout.PasswordField("Confirm", _confirmPassphrase);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Generate Strong Password"))
|
||||
{
|
||||
_passphrase = ProtectedPlayerPrefsSettingsUtility.GenerateRandomPassphrase();
|
||||
_confirmPassphrase = _passphrase;
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
_passphrase = string.Empty;
|
||||
_confirmPassphrase = string.Empty;
|
||||
GUI.FocusControl(null);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
}
|
||||
|
||||
private void DrawActions()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Save Hash Password", GUILayout.Height(32f)))
|
||||
{
|
||||
SaveHashPassword();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Open PlayerPrefs Editor", GUILayout.Height(32f)))
|
||||
{
|
||||
PlayerPrefsEditorWindow.Open();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Ping Settings Asset"))
|
||||
{
|
||||
var settingsAsset = ProtectedPlayerPrefsSettingsUtility.GetOrCreateSettingsAsset();
|
||||
EditorGUIUtility.PingObject(settingsAsset);
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!ProtectedPlayerPrefsSettingsUtility.HasDocumentationPdf());
|
||||
if (GUILayout.Button("Open Package Docs"))
|
||||
{
|
||||
ProtectedPlayerPrefsSettingsUtility.OpenDocumentationPdf();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void SaveHashPassword()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_passphrase))
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Hash Password Required",
|
||||
"Enter a password before saving the Protected PlayerPrefs settings.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passphrase.Length < 12)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Password Too Short",
|
||||
"Use at least 12 characters so the generated hash is project-specific and hard to guess.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_passphrase != _confirmPassphrase)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Passwords Do Not Match",
|
||||
"The confirmation value does not match the password.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
ProtectedPlayerPrefsSettingsUtility.SavePassphrase(_passphrase);
|
||||
ShowNotification(new GUIContent("Protected PlayerPrefs hash saved."));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b13eca0e3647841f58589a5146434751
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs.Editor
|
||||
{
|
||||
internal static class ProtectedPlayerPrefsSettingsUtility
|
||||
{
|
||||
internal const string DefaultSettingsDirectory = "Assets/Darkmatter/Data/Settings/Persistance/Resources";
|
||||
internal const string DefaultSettingsPath = DefaultSettingsDirectory + "/ProtectedPlayerPrefsSettings.asset";
|
||||
internal const string DocumentationPdfPath =
|
||||
"Assets/Darkmatter/Code/Libs/PlayerPrefs/Docs/ProtectedPlayerPrefsToolkit_Documentation.pdf";
|
||||
|
||||
internal static ProtectedPlayerPrefsSettings FindSettingsAsset()
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets("t:ProtectedPlayerPrefsSettings");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
return AssetDatabase.LoadAssetAtPath<ProtectedPlayerPrefsSettings>(path);
|
||||
}
|
||||
|
||||
internal static ProtectedPlayerPrefsSettings GetOrCreateSettingsAsset()
|
||||
{
|
||||
var existing = FindSettingsAsset();
|
||||
if (existing != null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(DefaultSettingsDirectory);
|
||||
|
||||
var asset = ScriptableObject.CreateInstance<ProtectedPlayerPrefsSettings>();
|
||||
AssetDatabase.CreateAsset(asset, DefaultSettingsPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
return asset;
|
||||
}
|
||||
|
||||
internal static bool NeedsSetup()
|
||||
{
|
||||
var settings = GetOrCreateSettingsAsset();
|
||||
return !settings.HasConfiguredHash;
|
||||
}
|
||||
|
||||
internal static void SavePassphrase(string passphrase)
|
||||
{
|
||||
var settings = GetOrCreateSettingsAsset();
|
||||
var hashedPassphrase = ProtectedPlayerPrefs.ComputePassphraseHash(passphrase);
|
||||
settings.SetHashedPassphrase(hashedPassphrase);
|
||||
EditorUtility.SetDirty(settings);
|
||||
AssetDatabase.SaveAssets();
|
||||
ProtectedPlayerPrefs.InitWithHash(hashedPassphrase);
|
||||
}
|
||||
|
||||
internal static string GetSettingsPath()
|
||||
{
|
||||
var settings = FindSettingsAsset();
|
||||
return settings == null ? DefaultSettingsPath : AssetDatabase.GetAssetPath(settings);
|
||||
}
|
||||
|
||||
internal static bool HasDocumentationPdf()
|
||||
{
|
||||
return File.Exists(Path.GetFullPath(DocumentationPdfPath));
|
||||
}
|
||||
|
||||
internal static void OpenDocumentationPdf()
|
||||
{
|
||||
var absolutePath = Path.GetFullPath(DocumentationPdfPath);
|
||||
if (!File.Exists(absolutePath))
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Documentation Missing",
|
||||
"The package PDF has not been generated yet.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
EditorUtility.OpenWithDefaultApp(absolutePath);
|
||||
}
|
||||
|
||||
internal static string GenerateRandomPassphrase()
|
||||
{
|
||||
return $"{System.Guid.NewGuid():N}{System.Guid.NewGuid():N}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0a53d97fb18e40cf9c86a04e8ba50a8
|
||||
@@ -0,0 +1,39 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs.Editor
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
internal static class ProtectedPlayerPrefsSetupBootstrap
|
||||
{
|
||||
private const string WindowShownSessionKey =
|
||||
"Darkmatter.Libs.PlayerPrefs.Editor.ProtectedPlayerPrefsSetupBootstrap.WindowShown";
|
||||
|
||||
static ProtectedPlayerPrefsSetupBootstrap()
|
||||
{
|
||||
EditorApplication.delayCall += TryOpenSetupWindow;
|
||||
}
|
||||
|
||||
private static void TryOpenSetupWindow()
|
||||
{
|
||||
if (Application.isBatchMode || EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SessionState.GetBool(WindowShownSessionKey, false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SessionState.SetBool(WindowShownSessionKey, true);
|
||||
|
||||
if (!ProtectedPlayerPrefsSettingsUtility.NeedsSetup())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProtectedPlayerPrefsGettingStartedWindow.OpenWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 696659b857f154ebca037ce7da9608eb
|
||||
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime.meta
Normal file
8
Assets/Darkmatter/Code/Libs/PlayerPrefs/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f0ff6fb20d0d4f969c15e636a161c53
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Libs.PlayerPrefs",
|
||||
"rootNamespace": "Darkmatter.Libs.PlayerPrefs",
|
||||
"references": [
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 564d11c0820a9455c8821cd85e9d0fd1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks whether a local PlayerPrefs value has unsynced writes or clears.
|
||||
/// Prevents stale backend data from overwriting local state when the player
|
||||
/// was offline during a write or clear operation.
|
||||
///
|
||||
/// Usage:
|
||||
/// - Before any local write: MarkPendingWrite(key)
|
||||
/// - Before any local clear: MarkPendingClear(key)
|
||||
/// - After a confirmed backend success: MarkSynced(key)
|
||||
/// - On load: check GetState(key) before deciding whether remote wins
|
||||
/// </summary>
|
||||
public static class LocalWriteTracker
|
||||
{
|
||||
public enum WriteState
|
||||
{
|
||||
Synced = 0,
|
||||
PendingWrite = 1,
|
||||
PendingClear = 2
|
||||
}
|
||||
|
||||
private static string StateKey(string dataKey) => $"{dataKey}.__wstate";
|
||||
|
||||
public static WriteState GetState(string key)
|
||||
=> (WriteState)ProtectedPlayerPrefs.GetInt(StateKey(key), (int)WriteState.Synced);
|
||||
|
||||
public static void MarkPendingWrite(string key)
|
||||
{
|
||||
ProtectedPlayerPrefs.SetInt(StateKey(key), (int)WriteState.PendingWrite);
|
||||
ProtectedPlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static void MarkPendingClear(string key)
|
||||
{
|
||||
ProtectedPlayerPrefs.SetInt(StateKey(key), (int)WriteState.PendingClear);
|
||||
ProtectedPlayerPrefs.Save();
|
||||
}
|
||||
|
||||
public static void MarkSynced(string key)
|
||||
{
|
||||
ProtectedPlayerPrefs.SetInt(StateKey(key), (int)WriteState.Synced);
|
||||
ProtectedPlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f5a82e78f4c64d7094368a8ff06bdb6
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
/// <summary>
|
||||
/// Fire-and-forget retry for keys stuck in <see cref="LocalWriteTracker.WriteState.PendingWrite"/>.
|
||||
/// Single-flight per key. Uses <see cref="CancellationToken.None"/> so a caller-scoped
|
||||
/// cancellation (e.g. load completing) won't abort an in-flight POST.
|
||||
/// On success, marks the key synced. Exceptions are swallowed and logged.
|
||||
/// </summary>
|
||||
public static class PendingWriteResync
|
||||
{
|
||||
private static readonly HashSet<string> InFlight = new();
|
||||
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetDomain() { lock (InFlight) InFlight.Clear(); }
|
||||
|
||||
/// <summary>
|
||||
/// Schedules a resync POST for <paramref name="key"/> if one isn't already running.
|
||||
/// </summary>
|
||||
/// <param name="key">Tracker key to mark synced on success.</param>
|
||||
/// <param name="post">POST closure; returns true on success.</param>
|
||||
public static void Schedule(string key, Func<CancellationToken, UniTask<bool>> post)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || post == null) return;
|
||||
|
||||
lock (InFlight)
|
||||
{
|
||||
if (!InFlight.Add(key)) return;
|
||||
}
|
||||
|
||||
RunAsync(key, post).Forget();
|
||||
}
|
||||
|
||||
private static async UniTaskVoid RunAsync(string key, Func<CancellationToken, UniTask<bool>> post)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await post(CancellationToken.None);
|
||||
if (ok) LocalWriteTracker.MarkSynced(key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[PendingWriteResync] '{key}' failed: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (InFlight) InFlight.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acc95e217aee14a7da0cfbdbf1358277
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
public enum PlayerPrefType
|
||||
{
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Bool
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class PlayerPrefKeyEntry
|
||||
{
|
||||
public string Key;
|
||||
public PlayerPrefType Type;
|
||||
public string Description;
|
||||
}
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "PlayerPrefsKeyRegistry",
|
||||
menuName = "Darkmatter/Libs/PlayerPrefs Key Registry")]
|
||||
public class PlayerPrefsKeyRegistry : ScriptableObject
|
||||
{
|
||||
[SerializeField] private List<PlayerPrefKeyEntry> _entries = new();
|
||||
|
||||
public IReadOnlyList<PlayerPrefKeyEntry> Entries => _entries;
|
||||
|
||||
public void AddKey(string key, PlayerPrefType type, string description = "")
|
||||
{
|
||||
if (_entries.Exists(e => e.Key == key)) return;
|
||||
_entries.Add(new PlayerPrefKeyEntry
|
||||
{
|
||||
Key = key,
|
||||
Type = type,
|
||||
Description = description
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveKey(string key)
|
||||
{
|
||||
_entries.RemoveAll(e => e.Key == key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8ac5d9908c8049e1a3c8b8ac108b5f5
|
||||
@@ -0,0 +1,51 @@
|
||||
// <auto-generated>
|
||||
// Generated by PlayerPrefs Editor — do not edit by hand.
|
||||
// Re-generate via Tools > Darkmatter > PlayerPrefs Editor > Settings.
|
||||
// </auto-generated>
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
public static class PlayerPrefsKeys
|
||||
{
|
||||
/// <summary>Saves the achievements which the user has unlocked (String)</summary>
|
||||
public const string Achievements = "Achievements";
|
||||
public const string LocalLedger = "LocalLedger";
|
||||
|
||||
public static class Accounts
|
||||
{
|
||||
public const string SavedAuthRequest = "Accounts.SavedAuthRequest";
|
||||
}
|
||||
|
||||
public static class Economy
|
||||
{
|
||||
/// <summary>Saves user's hard Currency (Int)</summary>
|
||||
public const string Gold = "Economy.Gold";
|
||||
/// <summary>Saves User's soft currency (Int)</summary>
|
||||
public const string Rupees = "Economy.Rupees";
|
||||
}
|
||||
|
||||
public static class Garage
|
||||
{
|
||||
/// <summary>Json of ids of the user owned buses (String)</summary>
|
||||
public const string OwnedBusIds = "Garage.OwnedBusIds";
|
||||
/// <summary>Id of the Bus that the user has selected (String)</summary>
|
||||
public const string SelectedBusId = "Garage.SelectedBusId";
|
||||
}
|
||||
|
||||
public static class Progression
|
||||
{
|
||||
/// <summary>Saves the user's Level (Int)</summary>
|
||||
public const string Level = "Progression.Level";
|
||||
/// <summary>Saves Xp of the user (Int)</summary>
|
||||
public const string Xp = "Progression.Xp";
|
||||
}
|
||||
|
||||
public static class SaveGame
|
||||
{
|
||||
/// <summary>Saves the users session 's json (String)</summary>
|
||||
public const string Session = "SaveGame.Session";
|
||||
public const string Vehicle = "SaveGame.Vehicle";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b42d435175264ee197e9f3d62858500
|
||||
@@ -0,0 +1,354 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper around Unity's PlayerPrefs that obscures keys and encrypts values
|
||||
/// to discourage casual tampering via plist/registry editors.
|
||||
/// </summary>
|
||||
public static class ProtectedPlayerPrefs
|
||||
{
|
||||
private static byte[] s_key;
|
||||
private static byte[] s_iv;
|
||||
private static bool s_initialized;
|
||||
private static bool s_missingSettingsWarningLogged;
|
||||
|
||||
private const string DefaultPassphrase = "DM_BusGame_2026";
|
||||
private const string SettingsResourcePath = "ProtectedPlayerPrefsSettings";
|
||||
|
||||
public static bool IsUsingConfiguredHash { get; private set; }
|
||||
public static string InitializationSource { get; private set; } = "Uninitialized";
|
||||
|
||||
/// <summary>
|
||||
/// Initialize with a custom passphrase. Call once at startup.
|
||||
/// If never called, the configured settings asset is used when present.
|
||||
/// </summary>
|
||||
public static void Init(string passphrase)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(passphrase))
|
||||
{
|
||||
throw new ArgumentException("Passphrase cannot be null or whitespace.", nameof(passphrase));
|
||||
}
|
||||
|
||||
ApplyHash(HashPassphraseToBytes(passphrase), "ManualPassphrase", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize from a precomputed SHA-256 hash generated by <see cref="ComputePassphraseHash"/>.
|
||||
/// </summary>
|
||||
public static void InitWithHash(string hashedPassphrase)
|
||||
{
|
||||
if (!TryParseHash(hashedPassphrase, out var hashBytes))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Hashed passphrase must be a 64-character hexadecimal SHA-256 value.",
|
||||
nameof(hashedPassphrase));
|
||||
}
|
||||
|
||||
ApplyHash(hashBytes, "ManualHash", true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a user-facing passphrase into the SHA-256 hash stored in the settings asset.
|
||||
/// </summary>
|
||||
public static string ComputePassphraseHash(string passphrase)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(passphrase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return BytesToHex(HashPassphraseToBytes(passphrase));
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (s_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryInitializeFromSettings())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyHash(HashPassphraseToBytes(DefaultPassphrase), "DefaultFallback", false);
|
||||
LogMissingSettingsWarning();
|
||||
}
|
||||
|
||||
// ── String ──────────────────────────────────────────────
|
||||
|
||||
public static void SetString(string key, string value)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
var encVal = Encrypt(value ?? string.Empty);
|
||||
UnityEngine.PlayerPrefs.SetString(encKey, encVal);
|
||||
}
|
||||
|
||||
public static string GetString(string key, string defaultValue = "")
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
if (!UnityEngine.PlayerPrefs.HasKey(encKey)) return defaultValue;
|
||||
try
|
||||
{
|
||||
return Decrypt(UnityEngine.PlayerPrefs.GetString(encKey));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Int ─────────────────────────────────────────────────
|
||||
|
||||
public static void SetInt(string key, int value)
|
||||
{
|
||||
SetString(key, value.ToString());
|
||||
}
|
||||
|
||||
public static int GetInt(string key, int defaultValue = 0)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
if (!UnityEngine.PlayerPrefs.HasKey(encKey)) return defaultValue;
|
||||
try
|
||||
{
|
||||
var decrypted = Decrypt(UnityEngine.PlayerPrefs.GetString(encKey));
|
||||
return int.TryParse(decrypted, out var result) ? result : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Float ───────────────────────────────────────────────
|
||||
|
||||
public static void SetFloat(string key, float value)
|
||||
{
|
||||
SetString(key, value.ToString("R"));
|
||||
}
|
||||
|
||||
public static float GetFloat(string key, float defaultValue = 0f)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
if (!UnityEngine.PlayerPrefs.HasKey(encKey)) return defaultValue;
|
||||
try
|
||||
{
|
||||
var decrypted = Decrypt(UnityEngine.PlayerPrefs.GetString(encKey));
|
||||
return float.TryParse(decrypted, out var result) ? result : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bool (convenience) ──────────────────────────────────
|
||||
|
||||
public static void SetBool(string key, bool value)
|
||||
{
|
||||
SetInt(key, value ? 1 : 0);
|
||||
}
|
||||
|
||||
public static bool GetBool(string key, bool defaultValue = false)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
if (!UnityEngine.PlayerPrefs.HasKey(encKey)) return defaultValue;
|
||||
return GetInt(key, defaultValue ? 1 : 0) == 1;
|
||||
}
|
||||
|
||||
// ── Long (convenience) ──────────────────────────────────
|
||||
public static void SetLong(string key, long value)
|
||||
{
|
||||
SetString(key, value.ToString());
|
||||
}
|
||||
|
||||
public static long GetLong(string key, long defaultValue = 0L)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var encKey = HashKey(key);
|
||||
if (!UnityEngine.PlayerPrefs.HasKey(encKey)) return defaultValue;
|
||||
try
|
||||
{
|
||||
var decrypted = Decrypt(UnityEngine.PlayerPrefs.GetString(encKey));
|
||||
return long.TryParse(decrypted, out var result) ? result : defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Queries ─────────────────────────────────────────────
|
||||
|
||||
public static bool HasKey(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return UnityEngine.PlayerPrefs.HasKey(HashKey(key));
|
||||
}
|
||||
|
||||
public static void DeleteKey(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
UnityEngine.PlayerPrefs.DeleteKey(HashKey(key));
|
||||
}
|
||||
|
||||
public static void DeleteAll()
|
||||
{
|
||||
UnityEngine.PlayerPrefs.DeleteAll();
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
UnityEngine.PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
// ── Internal crypto ─────────────────────────────────────
|
||||
|
||||
internal static string HashKey(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
using var hmac = new HMACSHA256(s_key);
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(key));
|
||||
return "pp_" + BitConverter.ToString(hash).Replace("-", "").Substring(0, 32).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryInitializeFromSettings()
|
||||
{
|
||||
var settings = Resources.Load<ProtectedPlayerPrefsSettings>(SettingsResourcePath);
|
||||
if (settings == null || !settings.HasConfiguredHash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseHash(settings.HashedPassphrase, out var hashBytes))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"[ProtectedPlayerPrefs] Settings asset contains an invalid hash. Falling back to the default passphrase.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplyHash(hashBytes, "SettingsAsset", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyHash(byte[] hashBytes, string source, bool usingConfiguredHash)
|
||||
{
|
||||
s_key = new byte[16];
|
||||
s_iv = new byte[16];
|
||||
Array.Copy(hashBytes, 0, s_key, 0, 16);
|
||||
Array.Copy(hashBytes, 16, s_iv, 0, 16);
|
||||
s_initialized = true;
|
||||
IsUsingConfiguredHash = usingConfiguredHash;
|
||||
InitializationSource = source;
|
||||
}
|
||||
|
||||
private static byte[] HashPassphraseToBytes(string passphrase)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
return sha.ComputeHash(Encoding.UTF8.GetBytes(passphrase));
|
||||
}
|
||||
|
||||
private static bool TryParseHash(string hashedPassphrase, out byte[] hashBytes)
|
||||
{
|
||||
hashBytes = null;
|
||||
if (string.IsNullOrWhiteSpace(hashedPassphrase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = hashedPassphrase.Trim();
|
||||
if (normalized.Length != 64)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
hashBytes = new byte[32];
|
||||
for (int index = 0; index < hashBytes.Length; index++)
|
||||
{
|
||||
var hexIndex = index * 2;
|
||||
hashBytes[index] = Convert.ToByte(normalized.Substring(hexIndex, 2), 16);
|
||||
}
|
||||
|
||||
return hashBytes.Length == 32;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BytesToHex(byte[] bytes)
|
||||
{
|
||||
var builder = new StringBuilder(bytes.Length * 2);
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
builder.Append(value.ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void LogMissingSettingsWarning()
|
||||
{
|
||||
if (s_missingSettingsWarningLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_missingSettingsWarningLogged = true;
|
||||
Debug.LogWarning(
|
||||
"[ProtectedPlayerPrefs] No configured hash password was found. Using the default fallback passphrase. Open Tools/Darkmatter/Protected PlayerPrefs/Getting Started to configure a project-specific hash.");
|
||||
}
|
||||
|
||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetDomain()
|
||||
{
|
||||
s_key = null;
|
||||
s_iv = null;
|
||||
s_initialized = false;
|
||||
s_missingSettingsWarningLogged = false;
|
||||
IsUsingConfiguredHash = false;
|
||||
InitializationSource = "Uninitialized";
|
||||
}
|
||||
|
||||
private static string Encrypt(string plainText)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = s_key;
|
||||
aes.IV = s_iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using var encryptor = aes.CreateEncryptor();
|
||||
var plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||
return Convert.ToBase64String(cipherBytes);
|
||||
}
|
||||
|
||||
internal static string Decrypt(string cipherText)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = s_key;
|
||||
aes.IV = s_iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
var cipherBytes = Convert.FromBase64String(cipherText);
|
||||
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||
return Encoding.UTF8.GetString(plainBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57fbfc2c31a3b4977bd45a9211d73b38
|
||||
@@ -0,0 +1,21 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.PlayerPrefs
|
||||
{
|
||||
[CreateAssetMenu(
|
||||
fileName = "ProtectedPlayerPrefsSettings",
|
||||
menuName = "Darkmatter/Libs/Protected PlayerPrefs Settings")]
|
||||
public class ProtectedPlayerPrefsSettings : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string _hashedPassphrase;
|
||||
|
||||
public string HashedPassphrase => _hashedPassphrase;
|
||||
|
||||
public bool HasConfiguredHash => !string.IsNullOrWhiteSpace(_hashedPassphrase);
|
||||
|
||||
public void SetHashedPassphrase(string hashedPassphrase)
|
||||
{
|
||||
_hashedPassphrase = (hashedPassphrase ?? string.Empty).Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfd2947542f794556827881bef2c9b73
|
||||
8
Assets/Darkmatter/Code/Libs/UI.meta
Normal file
8
Assets/Darkmatter/Code/Libs/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b610d653a5c38498394ce5b72e3a7e34
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Libs/UI/Docs.meta
Normal file
8
Assets/Darkmatter/Code/Libs/UI/Docs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee2cb8a3aba984b298648c1bb1a5d8bc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
Assets/Darkmatter/Code/Libs/UI/Docs/UILib.md
Normal file
27
Assets/Darkmatter/Code/Libs/UI/Docs/UILib.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# UI Library
|
||||
|
||||
Reusable UI components.
|
||||
|
||||
## Components
|
||||
|
||||
### ToggleButton
|
||||
|
||||
A single-script toggle button with distinct "On" and "Off" visual states.
|
||||
|
||||
#### Properties
|
||||
- **onObject**: GameObject active when state is ON.
|
||||
- **offObject**: GameObject active when state is OFF.
|
||||
- **button**: The Unity UI Button component for interaction.
|
||||
- **isOn**: Current state (can be set from Inspector).
|
||||
|
||||
#### Events
|
||||
- **OnValueChanged**: Action<bool> fired when the state changes.
|
||||
|
||||
#### Usage Example
|
||||
|
||||
```csharp
|
||||
// Inside a parent presenter
|
||||
view.MyToggle.OnValueChanged += (isOn) => {
|
||||
Debug.Log($"Toggle is {isOn}");
|
||||
};
|
||||
```
|
||||
7
Assets/Darkmatter/Code/Libs/UI/Docs/UILib.md.meta
Normal file
7
Assets/Darkmatter/Code/Libs/UI/Docs/UILib.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3c2446a0cfe04975b2dc693815bd03f
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Darkmatter/Code/Libs/UI/Libs.UI.asmdef
Normal file
14
Assets/Darkmatter/Code/Libs/UI/Libs.UI.asmdef
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Libs.UI",
|
||||
"rootNamespace": "Darkmatter.Libs.UI",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Darkmatter/Code/Libs/UI/Libs.UI.asmdef.meta
Normal file
7
Assets/Darkmatter/Code/Libs/UI/Libs.UI.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fef6c79ec73f4278abba9f13e29556e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
Assets/Darkmatter/Code/Libs/UI/ToggleButton.cs
Normal file
69
Assets/Darkmatter/Code/Libs/UI/ToggleButton.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Libs.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A single reusable toggle button component.
|
||||
/// Handles its own state and visual updates while exposing an event for Presenters.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class ToggleButton : MonoBehaviour
|
||||
{
|
||||
[Header("States")] [SerializeField] private GameObject onObject;
|
||||
[SerializeField] private GameObject offObject;
|
||||
|
||||
private Button _button;
|
||||
|
||||
[Header("Initial State")] [SerializeField]
|
||||
private bool isOn;
|
||||
|
||||
public event Action<bool> OnValueChanged;
|
||||
|
||||
public bool IsOn
|
||||
{
|
||||
get => isOn;
|
||||
set
|
||||
{
|
||||
if (isOn == value) return;
|
||||
isOn = value;
|
||||
UpdateVisuals();
|
||||
OnValueChanged?.Invoke(isOn);
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_button = GetComponent<Button>();
|
||||
if (_button != null)
|
||||
{
|
||||
_button.onClick.AddListener(Toggle);
|
||||
}
|
||||
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
private void Toggle()
|
||||
{
|
||||
IsOn = !IsOn;
|
||||
}
|
||||
|
||||
public void SetStateSilently(bool newState)
|
||||
{
|
||||
isOn = newState;
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
private void UpdateVisuals()
|
||||
{
|
||||
if (onObject != null) onObject.SetActive(isOn);
|
||||
if (offObject != null) offObject.SetActive(!isOn);
|
||||
}
|
||||
|
||||
public void SetInteractable(bool interactable)
|
||||
{
|
||||
if (_button != null) _button.interactable = interactable;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Darkmatter/Code/Libs/UI/ToggleButton.cs.meta
Normal file
2
Assets/Darkmatter/Code/Libs/UI/ToggleButton.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 592fc11e654ce4b5caccaa94f339d26a
|
||||
110
Assets/Darkmatter/Code/Libs/UI/ToggleButtonGroup.cs
Normal file
110
Assets/Darkmatter/Code/Libs/UI/ToggleButtonGroup.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Darkmatter.Libs.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages a group of ToggleButton components, ensuring only one can be active at a time.
|
||||
/// </summary>
|
||||
public class ToggleButtonGroup : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private List<ToggleButton> toggleButtons = new List<ToggleButton>();
|
||||
[SerializeField] private bool allowSwitchOff = false;
|
||||
|
||||
public event Action<ToggleButton> OnToggleSelected;
|
||||
|
||||
public IReadOnlyList<ToggleButton> ToggleButtons => toggleButtons;
|
||||
|
||||
// Cache the delegates to cleanly unsubscribe
|
||||
private Dictionary<ToggleButton, Action<bool>> _eventHandlers = new Dictionary<ToggleButton, Action<bool>>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
foreach (var button in toggleButtons)
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
Action<bool> handler = (isOn) => HandleToggleValueChanged(button, isOn);
|
||||
_eventHandlers[button] = handler;
|
||||
button.OnValueChanged += handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
foreach (var button in toggleButtons)
|
||||
{
|
||||
if (button != null && _eventHandlers.TryGetValue(button, out var handler))
|
||||
{
|
||||
button.OnValueChanged -= handler;
|
||||
}
|
||||
}
|
||||
_eventHandlers.Clear();
|
||||
}
|
||||
|
||||
public void AddToggle(ToggleButton toggle)
|
||||
{
|
||||
if (!toggleButtons.Contains(toggle))
|
||||
{
|
||||
toggleButtons.Add(toggle);
|
||||
if (isActiveAndEnabled)
|
||||
{
|
||||
Action<bool> handler = (isOn) => HandleToggleValueChanged(toggle, isOn);
|
||||
_eventHandlers[toggle] = handler;
|
||||
toggle.OnValueChanged += handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveToggle(ToggleButton toggle)
|
||||
{
|
||||
if (toggleButtons.Remove(toggle))
|
||||
{
|
||||
if (_eventHandlers.TryGetValue(toggle, out var handler))
|
||||
{
|
||||
toggle.OnValueChanged -= handler;
|
||||
_eventHandlers.Remove(toggle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleToggleValueChanged(ToggleButton changedButton, bool isOn)
|
||||
{
|
||||
if (isOn)
|
||||
{
|
||||
// Turn off all other toggles
|
||||
foreach (var button in toggleButtons)
|
||||
{
|
||||
if (button != changedButton && button != null)
|
||||
{
|
||||
button.IsOn = false;
|
||||
}
|
||||
}
|
||||
OnToggleSelected?.Invoke(changedButton);
|
||||
}
|
||||
else if (!allowSwitchOff)
|
||||
{
|
||||
// If it was turned off but we don't allow switching off completely,
|
||||
// check if any other toggle is on. If not, force this one back on.
|
||||
bool anyOn = false;
|
||||
foreach (var button in toggleButtons)
|
||||
{
|
||||
if (button != null && button.IsOn)
|
||||
{
|
||||
anyOn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Actually, due to event ordering, another button might have just turned on.
|
||||
// We should only force it on if really no other button is on.
|
||||
if (!anyOn)
|
||||
{
|
||||
changedButton.IsOn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Darkmatter/Code/Libs/UI/ToggleButtonGroup.cs.meta
Normal file
3
Assets/Darkmatter/Code/Libs/UI/ToggleButtonGroup.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dee326988845440b93adb25956f86f66
|
||||
timeCreated: 1777279521
|
||||
Reference in New Issue
Block a user