Initial Push

This commit is contained in:
Savya Bikram Shah
2026-05-26 16:47:16 +05:45
commit f901f70c10
219 changed files with 11642 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

107
.gitignore vendored Normal file
View File

@@ -0,0 +1,107 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
# Recommended: add any editor/OS/tool-specific ignore rules from the Global/ templates as needed.
# See: https://github.com/github/gitignore/tree/main/Global
#
.utmp/
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
*.log
# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control.
*.blend1
*.blend1.meta
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Jetbrains Rider personal-layer settings
*.DotSettings.user
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.slnx
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Mono auto generated files
mono_crash.*
# Builds
*.apk
*.aab
*.unitypackage
*.unitypackage.meta
*.app
# Crashlytics generated file
crashlytics-build.properties
# TestRunner generated files
InitTestScene*.unity*
# Addressables default ignores, before user customizations
/ServerData
/[Aa]ssets/StreamingAssets/aa*
/[Aa]ssets/AddressableAssetsData/link.xml*
/[Aa]ssets/Addressables_Temp*
# By default, Addressables content builds will generate addressables_content_state.bin
# files in platform-specific subfolders, for example:
# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin
/[Aa]ssets/AddressableAssetsData/*/*.bin*
# Visual Scripting auto-generated files
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
# Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
# Auto-generated cache in Assets folder
/[Aa]ssets/[Ss]ceneDependencyCache*

15
.idea/.idea.Colorbook/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/projectSettingsUpdater.xml
/modules.xml
/.idea.Colorbook.iml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

BIN
Assets/.DS_Store vendored Normal file

Binary file not shown.

8
Assets/Darkmatter.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: db8662bfd07624a4a89c80b7441bae0e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Darkmatter/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 77eb0c56d882f4031a235f0ea6202922
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Darkmatter/Code/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 04fd8e2c965eb4fb49f4deca22ee7c1d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1275b0c93b187472ab525938a5f6308a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
#if !NET5_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
// Enables C# 9 records/init-only members on older Unity/.NET profiles.
internal static class IsExternalInit
{
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 30b275c4b60da48f7a836b26c9576f6c

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bca93034b2c4d4539936e716adb298a9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 100aa93d3292d46cda004ce163762e13
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7d0243ec32cd1418f96bd1a3930eadc3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Darkmatter.Core.Contracts.Services.Assets
{
public interface IAssetProviderService
{
UniTask<GameObject> InstantiateAsync(
string assetId,
Vector3 position,
Quaternion rotation,
IProgress<float> progress,
CancellationToken cancellationToken);
UniTask<GameObject> InstantiateAsync(
string assetId,
IProgress<float> progress,
CancellationToken cancellationToken);
UniTask<T> LoadAssetAsync<T>(
string assetId,
IProgress<float> progress,
CancellationToken cancellationToken);
UniTask<IReadOnlyList<T>> LoadAssetsAsync<T>(
IProgress<float> progress,
CancellationToken cancellationToken);
UniTask<IReadOnlyList<T>> LoadAssetsAsync<T>(
object key,
IProgress<float> progress,
CancellationToken cancellationToken);
void UnloadAsset(string assetId);
UniTask LoadExternalCatalogAsync(
string catalogUrl,
IProgress<float> progress,
CancellationToken cancellationToken);
void ReleaseInstance(GameObject instance);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d3f25dd3dcb494f9dabbf23a0114b562

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9ecc165686aa0429daf34d55396c1bfa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Data.Dynamic.Services.Audio;
using Darkmatter.Core.Enums.Services.Audio;
using UnityEngine;
namespace Darkmatter.Core.Contracts.Services.Audio
{
public interface IAudioService
{
AudioHandle Play(in AudioRequest request);
UniTask InitializeAsync(CancellationToken cancellationToken);
UniTask<AudioClip> LoadClipFromPath(string path, CancellationToken cancellationToken);
void Stop(AudioHandle handle, float fadeOutSeconds = 0f);
void SetPitch(AudioHandle handle, float pitch);
void SetVolume(AudioHandle handle, float volume01);
bool IsPlaying(AudioHandle handle);
void SetChannelPaused(AudioChannel channel, bool paused);
void SetChannelVolume(AudioChannel channel, float volume01);
void SetSnapshotWeights(params float[] weights);
void TransitionSnapshotWeights(float duration, params float[] weights);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4a49dff8ccb645b9b282b303b718fb5c
timeCreated: 1770634613

View File

@@ -0,0 +1,14 @@
using Darkmatter.Core.Data.Dynamic.Services.Audio;
using Darkmatter.Core.Enums.Services.Audio;
using UnityEngine;
namespace Darkmatter.Core.Contracts.Services.Audio
{
public interface ISfxPlayer
{
AudioHandle Play(SfxId id, Transform follow = null);
AudioHandle PlayLoop(SfxId id, Transform follow = null);
void Stop(AudioHandle handle, float fadeOutSeconds = 0f);
bool IsPlaying(AudioHandle handle);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8b2cab46f32d7466b9decc9c7ff94eb2

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1903ca430080472fbe36c5c1577bc502
timeCreated: 1770642710

View File

@@ -0,0 +1,27 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Enums.Services.Scenes;
using UnityEngine.SceneManagement;
namespace Darkmatter.Core.Contracts.Services.Scenes
{
public interface ISceneService
{
UniTask LoadSceneAsync(GameScene scene, IProgress<float> progress, CancellationToken cancellationToken);
UniTask UnloadSceneAsync(GameScene scene, IProgress<float> progress, CancellationToken cancellationToken);
UniTask LoadSceneAsync(
string sceneKey,
IProgress<float> progress,
CancellationToken cancellationToken,
bool setActiveScene = true);
UniTask UnloadSceneAsync(
string sceneKey,
IProgress<float> progress,
CancellationToken cancellationToken);
bool TryGetLoadedScene(string sceneKey, out Scene scene);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3ea399e8d284f538e256460c1def877
timeCreated: 1770642720

View File

@@ -0,0 +1,16 @@
{
"name": "Core",
"rootNamespace": "Darkmatter.Core",
"references": [
"GUID:f51ebe6a0ceec4240a699833d6309b23"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6a0a834eb41764f12ba55c3fb04a40cb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b2cc64aeeed544509ce5b2191154d78
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e087f5b8739414488be6e94f4cf8f3ce
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 56cb41057044542528a751d0be05d961
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e5b0eb514db9e488ea6cf383b759a2e4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
namespace Darkmatter.Core.Data.Dynamic.Services.Audio
{
public readonly struct AudioHandle
{
public static readonly AudioHandle Invalid = new(0, 0);
public readonly int Id;
public readonly ushort Generation;
public bool IsValid => Id > 0;
public AudioHandle(int id, ushort generation)
{
Id = id;
Generation = generation;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a062321bebaa428d84c9e6ce14edc4d9
timeCreated: 1770636619

View File

@@ -0,0 +1,36 @@
using Darkmatter.Core.Enums.Services.Audio;
using UnityEngine;
namespace Darkmatter.Core.Data.Dynamic.Services.Audio
{
public readonly struct AudioRequest
{
public readonly AudioClip Clip;
public readonly AudioChannel Channel;
public readonly AudioPlayMode PlayMode;
public readonly bool StopChannelBeforePlay;
public readonly float Volume01;
public readonly float Pitch;
public readonly bool Spatial3D;
public readonly Transform FollowTarget;
public readonly float MinDistance;
public readonly float MaxDistance;
public AudioRequest(AudioClip clip, AudioChannel channel, AudioPlayMode mode,
bool stopChannelBeforePlay = false,
float volume01 = 1f, float pitch = 1f, bool spatial3D = false, Transform followTarget = null,
float minDistance = 1f, float maxDistance = 500f)
{
Clip = clip;
Channel = channel;
PlayMode = mode;
StopChannelBeforePlay = stopChannelBeforePlay;
Volume01 = volume01;
Pitch = pitch;
Spatial3D = spatial3D;
FollowTarget = followTarget;
MinDistance = minDistance;
MaxDistance = maxDistance;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0d94a32bb4b841a88971f46d7fc3984b
timeCreated: 1770636723

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7e90b7ea022ae4de5962d79e5c36846d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4e05cfff249d84f78a2dc81dc36772c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3346e766e438c4a46a5f2c70019c93da
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Enums.Services.Audio;
using UnityEngine;
namespace Darkmatter.Core.Data.Static.Services.Audio
{
[CreateAssetMenu(fileName = "SfxCatalog", menuName = "Darkmatter/Audio/Sfx Catalog")]
public class SfxCatalogSO : ScriptableObject
{
[Serializable]
public class Entry
{
public SfxId Id;
public AudioClip Clip;
public AudioChannel Channel = AudioChannel.Sfx;
[Range(0f, 1f)] public float Volume = 1f;
[Range(0.1f, 3f)] public float Pitch = 1f;
public bool Loop;
}
[SerializeField] private List<Entry> entries = new();
private Dictionary<SfxId, Entry> _index;
public bool TryGet(SfxId id, out Entry entry)
{
if (_index == null) BuildIndex();
return _index.TryGetValue(id, out entry);
}
private void BuildIndex()
{
_index = new Dictionary<SfxId, Entry>(entries.Count);
foreach (var e in entries)
{
if (e != null && e.Id != SfxId.None && e.Clip != null)
_index[e.Id] = e;
}
}
private void OnValidate() => _index = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8dc05770caf7f49e9a59dab81283a278

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 393a0b77d95da48f3ba6251f868bcf96
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e97b83c35ee0e4b0d91e6ab4a333abe5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2a7ce1f001dba4bc680c52517e4b5c68
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
namespace Darkmatter.Core.Enums.Services.Audio
{
public enum AudioChannel
{
Sfx,
VehicleSfx,
AISfx,
Engine,
VehicleBrake,
PassengerChatter,
Music,
Ambience,
Weather,
BGM,
UI,
Radio
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d2031508f5c44c969e2a03200a867b36
timeCreated: 1770636948

View File

@@ -0,0 +1,8 @@
namespace Darkmatter.Core.Enums.Services.Audio
{
public enum AudioPlayMode
{
OneShot,
Loop
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 63f44faaf2da4474ba7c3850f21dd02f
timeCreated: 1770636914

View File

@@ -0,0 +1,16 @@
namespace Darkmatter.Core.Enums.Services.Audio
{
public enum SfxId
{
None = 0,
WiperUp = 100,
WiperDown = 101,
BlinkerTick = 200,
GearShift = 300,
ReverseBeep = 400,
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9a490eff4a4c14cbc9ee02b9fcd543b8

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 92344a3224a2f4e88ad7438669c9dfb9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
namespace Darkmatter.Core.Enums.Services.Scenes
{
public enum GameScene
{
Boot,
MainMenu,
Gameplay,
TestTraffic,
VehicleTest,
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2ae040ecfda14b5e9f4082294637b020
timeCreated: 1770642840

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea36b1548e1bb42feafed8de99668386
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a60fee6d8a3034037ac8cc01c45baf11
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 395d10529759141ddabeab8039459e38
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fa62a43934add4beb92f51d9effd9d84
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace Darkmatter.Libs.FSM
{
public interface IState
{
void Enter();
void Tick();
void Exit();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2f814fe27db8b43349a93943bf71fcd5

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c176ee863a5e74e88a6517f9f102cf92
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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() { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5d044f466d7a46379be2ebb74292a74

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c9c75568b26bd4667ad07404934fc0c9

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d669ee184e2754ff6af32edd4c87fdbc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e5889f574d4c4d1ca18fb324f33938d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6c035b34d3ec341d58cdb799f2326799
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
using VContainer;
namespace Darkmatter.Libs.Installers
{
public interface IServiceModule
{
void Register(IContainerBuilder builder);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a4fccd9c5f4934143929c601592e9616

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c1c03c0e5b2f4412b9f2be1c20d6a9b1
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8fea732420104e728b3290bc8e9bfc1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 62a255738299947f5aa75ed233adb93a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: aeb1895d9f8424537b1a062bba8f6f14
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3360f02303be544ecbe8c21dc0c4e33e

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22922a82d6e00458f8e34b78a9599a66

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b4c9f7fbf1e144933a1797dc208ece5f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4c1e3acaf8f7448a847fd93afeb9123
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 85a19104921a942afae4ed40c49ef826
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3c99b2d93e5a945328d6fc323c99142d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fad8e38235e6e45a3b3d7fecc3323ea7
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: af3bb097acf624c119d24f77a7c70eb3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3f4f71cd22f5047c78946bd1928ea7b1
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b322d8950c37b4852a2ce171c9b96e5b

View File

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b13eca0e3647841f58589a5146434751

View File

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e0a53d97fb18e40cf9c86a04e8ba50a8

Some files were not shown because too many files have changed in this diff Show More