diff --git a/TinkStateSharp.sln b/TinkStateSharp.sln
index f601ccb..9168e72 100644
--- a/TinkStateSharp.sln
+++ b/TinkStateSharp.sln
@@ -15,6 +15,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Unity", "projects
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Unity-Test", "projects\TinkState-Unity-Test\TinkState-Unity-Test.csproj", "{7CC48BB6-A797-449D-B681-44A364271C97}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Model", "projects\TinkState-Model\TinkState-Model.csproj", "{05A279B3-BE32-44D6-90B2-EB1DE219B6FE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Model-Weaver", "projects\TinkState-Model-Weaver\TinkState-Model-Weaver.csproj", "{2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Model-Weaver-Unity", "projects\TinkState-Model-Weaver-Unity\TinkState-Model-Weaver-Unity.csproj", "{FD579783-E58A-4D86-8CF9-C52D3FE4D96E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinkState-Model-Weaver-Tool", "projects\TinkState-Model-Weaver-Tool\TinkState-Model-Weaver-Tool.csproj", "{E6F1F0D7-47AC-4986-A455-D32637E0A26B}"
+EndProject
Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -88,11 +96,63 @@ Global
{7CC48BB6-A797-449D-B681-44A364271C97}.Release|x64.Build.0 = Release|Any CPU
{7CC48BB6-A797-449D-B681-44A364271C97}.Release|x86.ActiveCfg = Release|Any CPU
{7CC48BB6-A797-449D-B681-44A364271C97}.Release|x86.Build.0 = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|x64.Build.0 = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Debug|x86.Build.0 = Debug|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|x64.ActiveCfg = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|x64.Build.0 = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|x86.ActiveCfg = Release|Any CPU
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE}.Release|x86.Build.0 = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|x64.Build.0 = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Debug|x86.Build.0 = Debug|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|x64.ActiveCfg = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|x64.Build.0 = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|x86.ActiveCfg = Release|Any CPU
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0}.Release|x86.Build.0 = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|x64.Build.0 = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Debug|x86.Build.0 = Debug|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|x64.ActiveCfg = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|x64.Build.0 = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|x86.ActiveCfg = Release|Any CPU
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E}.Release|x86.Build.0 = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|x64.Build.0 = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Debug|x86.Build.0 = Debug|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|x64.ActiveCfg = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|x64.Build.0 = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|x86.ActiveCfg = Release|Any CPU
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6B383783-3B6F-4685-A506-1E502392E83B} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
{DA5CD7B7-76F9-4C6D-875D-36219E9D1086} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
{FBE6AC15-3025-45FA-A8EA-FBFD05A7B25F} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
{7CC48BB6-A797-449D-B681-44A364271C97} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
+ {05A279B3-BE32-44D6-90B2-EB1DE219B6FE} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
+ {2B3E252A-79C9-4190-B03A-CA2C4C4D79C0} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
+ {FD579783-E58A-4D86-8CF9-C52D3FE4D96E} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
+ {E6F1F0D7-47AC-4986-A455-D32637E0A26B} = {C6990338-31DB-4CD2-BC63-DA0C25A8F043}
EndGlobalSection
EndGlobal
diff --git a/playground-unity/Assets/HelloWorld/HelloWorld.cs b/playground-unity/Assets/HelloWorld/HelloWorld.cs
index b04197a..9475852 100644
--- a/playground-unity/Assets/HelloWorld/HelloWorld.cs
+++ b/playground-unity/Assets/HelloWorld/HelloWorld.cs
@@ -1,7 +1,20 @@
-using TinkState;
+using TinkState.Model;
using TMPro;
using UnityEngine;
+class Player : Model
+{
+ [Observable] public string Name { get; set; }
+ [Observable] public int Age { get; set; }
+}
+
+class GreetingViewModel : Model
+{
+ [Observable] public Player Player { get; set; }
+
+ [Observable] public string Greeting => $"Hello, {Player.Name}!";
+}
+
public class HelloWorld : MonoBehaviour
{
[SerializeField] TMP_InputField nameInput;
@@ -9,17 +22,15 @@ public class HelloWorld : MonoBehaviour
void Start()
{
- // define piece of mutable observable state
- var name = Observable.State("World");
+ var player = new Player {Name = "Dan"};
+ var greetingViewModel = new GreetingViewModel {Player = player};
- // bind the state two-ways to an input field
- name.Bind(nameInput.SetTextWithoutNotify);
- nameInput.onValueChanged.AddListener(newValue => name.Value = newValue);
+ var greeting = greetingViewModel.GetObservable(m => m.Greeting);
+ var name = player.GetObservable(p => p.Name);
- // derive automatically updated observable value from it
- var greeting = Observable.Auto(() => $"Hello, {name.Value}!");
+ name.Bind(nameInput.SetTextWithoutNotify);
+ nameInput.onValueChanged.AddListener(newValue => player.Name = newValue);
- // bind the auto-observable to a text field
greeting.Bind(text => greetingLabel.text = text);
}
}
\ No newline at end of file
diff --git a/playground-unity/Packages/packages-lock.json b/playground-unity/Packages/packages-lock.json
index a4c3f0c..5fba1f5 100644
--- a/playground-unity/Packages/packages-lock.json
+++ b/playground-unity/Packages/packages-lock.json
@@ -32,6 +32,13 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
+ "com.unity.nuget.mono-cecil": {
+ "version": "1.11.4",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
"com.unity.test-framework": {
"version": "2.0.1-exp.2",
"depth": 0,
@@ -65,7 +72,9 @@
"version": "file:../../src",
"depth": 0,
"source": "local",
- "dependencies": {}
+ "dependencies": {
+ "com.unity.nuget.mono-cecil": "1.11.4"
+ }
},
"com.unity.modules.ai": {
"version": "1.0.0",
diff --git a/playground/Playground.cs b/playground/Playground.cs
index 61cb373..f5647da 100644
--- a/playground/Playground.cs
+++ b/playground/Playground.cs
@@ -1,35 +1,17 @@
using System;
-using System.Diagnostics;
-using System.Threading.Tasks;
-using TinkState;
+using TinkState.Model;
+
+class MyModel : Model
+{
+ [Observable] public string Name { get; set; }
+}
class Playground
{
- static async Task Main()
+ static void Main()
{
- var stateA = Observable.State("hello");
- var stateB = Observable.State("world");
-
- var o = Observable.Auto(async () =>
- {
- Console.WriteLine("computing");
- var a = stateA.Value;
- await Task.Delay(1000);
- var b = stateB.Value;
- return a + " " + b;
- });
-
- o.Bind(result => Console.WriteLine(result.Status switch
- {
- AsyncComputeStatus.Loading => "Loading...",
- AsyncComputeStatus.Done => "Done: " + result.Result,
- AsyncComputeStatus.Failed => "Failed: " + result.Exception,
- }));
-
- await Task.Delay(1500);
-
- stateB.Value = "Dan";
-
- Process.GetCurrentProcess().WaitForExit();
+ var model = new MyModel {Name = "Dan"};
+ var obs = model.GetObservable(_ => _.Name);
+ obs.Bind(name => Console.WriteLine("Name is: " + name));
}
}
diff --git a/playground/Playground.csproj b/playground/Playground.csproj
index 6ade01a..10c6cbd 100644
--- a/playground/Playground.csproj
+++ b/playground/Playground.csproj
@@ -6,7 +6,13 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/TinkState-Model-Weaver-Tool/TinkState-Model-Weaver-Tool.csproj b/projects/TinkState-Model-Weaver-Tool/TinkState-Model-Weaver-Tool.csproj
new file mode 100644
index 0000000..bcdf530
--- /dev/null
+++ b/projects/TinkState-Model-Weaver-Tool/TinkState-Model-Weaver-Tool.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net6.0
+ 9.0
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/TinkState-Model-Weaver-Unity/TinkState-Model-Weaver-Unity.csproj b/projects/TinkState-Model-Weaver-Unity/TinkState-Model-Weaver-Unity.csproj
new file mode 100644
index 0000000..f9402f9
--- /dev/null
+++ b/projects/TinkState-Model-Weaver-Unity/TinkState-Model-Weaver-Unity.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net6.0
+ 9.0
+ Unity.TinkState.Model.CodeGen
+
+
+
+ false
+
+
+
+
+
+
+
+
+ ../../unity-dlls/Unity.CompilationPipeline.Common.dll
+
+
+
+
+
+
+
+
+
diff --git a/projects/TinkState-Model-Weaver/TinkState-Model-Weaver.csproj b/projects/TinkState-Model-Weaver/TinkState-Model-Weaver.csproj
new file mode 100644
index 0000000..07cac56
--- /dev/null
+++ b/projects/TinkState-Model-Weaver/TinkState-Model-Weaver.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net6.0
+ 9.0
+ Nadako.TinkState.Model.Weaver
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/TinkState-Model/TinkState-Model.csproj b/projects/TinkState-Model/TinkState-Model.csproj
new file mode 100644
index 0000000..af9d1d3
--- /dev/null
+++ b/projects/TinkState-Model/TinkState-Model.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net6.0
+ 9.0
+ Nadako.TinkState.Model
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-weaver/TinkStateModelWeaver.cs b/src-weaver/TinkStateModelWeaver.cs
new file mode 100644
index 0000000..6cdebcc
--- /dev/null
+++ b/src-weaver/TinkStateModelWeaver.cs
@@ -0,0 +1,55 @@
+using System;
+using Mono.Cecil;
+using System.IO;
+using TinkState.Model.Weaver;
+using Logger = TinkState.Model.Weaver.Logger;
+
+namespace TinkState.Model
+{
+ public class TinkStateModelWeaver
+ {
+ class ConsoleLogger : Logger
+ {
+ public void Debug(string message)
+ {
+ Console.WriteLine(message);
+ }
+
+ public void Error(string message, string file, int line, int column)
+ {
+ Console.WriteLine("ERROR: " + message);
+ }
+ }
+
+ static void Main(string[] args)
+ {
+ var assemblyPath = args[0];
+
+ var logger = new ConsoleLogger();
+ logger.Debug("Processing " + assemblyPath);
+
+ var resolver = new DefaultAssemblyResolver();
+ resolver.AddSearchDirectory(Path.GetDirectoryName(assemblyPath));
+
+ var readParams = new ReaderParameters
+ {
+ ReadWrite = true,
+ ReadSymbols = true,
+ AssemblyResolver = resolver,
+ };
+ var module = ModuleDefinition.ReadModule(assemblyPath, readParams);
+
+ if (ModelWeaver.Weave(module, logger, out var modified))
+ {
+ if (modified)
+ {
+ var writeParameters = new WriterParameters
+ {
+ WriteSymbols = true
+ };
+ module.Write(writeParameters);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver-Unity.meta b/src/TinkState-Model-Weaver-Unity.meta
new file mode 100644
index 0000000..d8ce7e7
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d3d2f3490f465d941b2b84b6e3b18b34
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver-Unity/Editor.meta b/src/TinkState-Model-Weaver-Unity/Editor.meta
new file mode 100644
index 0000000..d10de4c
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d9a9e94c38f752b439f9085ea2495bc0
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef b/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef
new file mode 100644
index 0000000..d54215b
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef
@@ -0,0 +1,11 @@
+{
+ "name": "Unity.TinkState.Model.CodeGen",
+ "references": [
+ "Nadako.TinkState.Model.Weaver"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "autoReferenced": false
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef.meta b/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef.meta
new file mode 100644
index 0000000..ded279d
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity/Editor/Unity.TinkState.Model.CodeGen.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bc876a066c74e92499d3cf946da670c4
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs b/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs
new file mode 100644
index 0000000..402e75b
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs
@@ -0,0 +1,129 @@
+using Mono.Cecil;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Mono.Cecil.Cil;
+using TinkState.Model.Weaver;
+using Unity.CompilationPipeline.Common.Diagnostics;
+using Unity.CompilationPipeline.Common.ILPostProcessing;
+using Logger = TinkState.Model.Weaver.Logger;
+
+namespace TinkState.Model
+{
+ public class UnityCompilationHook : ILPostProcessor
+ {
+ class UnityDebugLogger : Logger
+ {
+ public readonly List Messages = new List();
+
+ public void Debug(string message)
+ {
+ Messages.Add(new DiagnosticMessage
+ {
+ DiagnosticType = DiagnosticType.Warning,
+ MessageData = message
+ });
+ }
+
+ public void Error(string message, string file, int line, int column)
+ {
+ Messages.Add(new DiagnosticMessage
+ {
+ DiagnosticType = DiagnosticType.Error,
+ MessageData = message,
+ File = file,
+ Line = line,
+ Column = column
+ });
+ }
+ }
+
+ public override ILPostProcessor GetInstance() => this;
+
+ public override bool WillProcess(ICompiledAssembly compiledAssembly)
+ {
+ return compiledAssembly.References.Any(path => Path.GetFileNameWithoutExtension(path) == "Nadako.TinkState.Model");
+ }
+
+ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
+ {
+ var logger = new UnityDebugLogger();
+ logger.Debug("Processing " + compiledAssembly.Name);
+
+ using (var stream = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData))
+ using (var symbols = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData))
+ {
+ var resolver = new DefaultAssemblyResolver();
+ var dirs = compiledAssembly.References.Select(Path.GetDirectoryName).Distinct();
+ foreach (var path in dirs)
+ {
+ resolver.AddSearchDirectory(path);
+ }
+ // logger.Log(string.Join(", ", resolver.GetSearchDirectories()));
+
+ // return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, logger.Messages);
+
+ var readParams = new ReaderParameters
+ {
+ SymbolStream = symbols,
+ ReadWrite = true,
+ ReadSymbols = true,
+ AssemblyResolver = resolver,
+ ReflectionImporterProvider = new FixedReflectionImporterProvider()
+ };
+ var module = ModuleDefinition.ReadModule(stream, readParams);
+
+ if (ModelWeaver.Weave(module, logger, out var modified))
+ {
+ if (modified)
+ {
+ var peOut = new MemoryStream();
+ var pdbOut = new MemoryStream();
+ var writeParameters = new WriterParameters
+ {
+ SymbolWriterProvider = new PortablePdbWriterProvider(),
+ SymbolStream = pdbOut,
+ WriteSymbols = true
+ };
+ module.Write(peOut, writeParameters);
+
+ var newAsm = new InMemoryAssembly(peOut.ToArray(), pdbOut.ToArray());
+ return new ILPostProcessResult(newAsm, logger.Messages);
+ }
+ }
+
+ return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, logger.Messages);
+ }
+ }
+
+ public class FixedReflectionImporterProvider : IReflectionImporterProvider
+ {
+ public IReflectionImporter GetReflectionImporter(ModuleDefinition module)
+ {
+ return new FixedReflectionImporter(module);
+ }
+
+ public class FixedReflectionImporter : DefaultReflectionImporter
+ {
+ const string SystemPrivateCoreLib = "System.Private.CoreLib";
+
+ readonly AssemblyNameReference fixedCoreLib;
+
+ public FixedReflectionImporter(ModuleDefinition module) : base(module)
+ {
+ fixedCoreLib = module.AssemblyReferences.FirstOrDefault(a =>
+ a.Name is "mscorlib" or "netstandard" or SystemPrivateCoreLib);
+ }
+
+ public override AssemblyNameReference ImportReference(AssemblyName name)
+ {
+ if (name.Name == SystemPrivateCoreLib && fixedCoreLib != null)
+ return fixedCoreLib;
+
+ return base.ImportReference(name);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs.meta b/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs.meta
new file mode 100644
index 0000000..3230267
--- /dev/null
+++ b/src/TinkState-Model-Weaver-Unity/Editor/UnityCompilationHook.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 146c2a6df7413b441a9017699b4d807f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver.meta b/src/TinkState-Model-Weaver.meta
new file mode 100644
index 0000000..c7bafee
--- /dev/null
+++ b/src/TinkState-Model-Weaver.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b9e8eb8470835184bb053e2832c7b8e8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver/Logger.cs b/src/TinkState-Model-Weaver/Logger.cs
new file mode 100644
index 0000000..e33313a
--- /dev/null
+++ b/src/TinkState-Model-Weaver/Logger.cs
@@ -0,0 +1,8 @@
+namespace TinkState.Model.Weaver
+{
+ public interface Logger
+ {
+ void Debug(string message);
+ void Error(string message, string file, int line, int column);
+ }
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver/Logger.cs.meta b/src/TinkState-Model-Weaver/Logger.cs.meta
new file mode 100644
index 0000000..c06e897
--- /dev/null
+++ b/src/TinkState-Model-Weaver/Logger.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc65efdfa164dd94aa8a7b23286c7974
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver/ModelWeaver.cs b/src/TinkState-Model-Weaver/ModelWeaver.cs
new file mode 100644
index 0000000..bc0b9dd
--- /dev/null
+++ b/src/TinkState-Model-Weaver/ModelWeaver.cs
@@ -0,0 +1,415 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+
+// TODO: this code here is highly experimental and subject to large changes and cleanups
+
+namespace TinkState.Model.Weaver
+{
+ public class ModelWeaver
+ {
+ class ModelWeaverException : Exception
+ {
+ public readonly string File;
+ public readonly int Line;
+ public readonly int Column;
+
+ public ModelWeaverException(string message, string file, int line, int column) : base(message)
+ {
+ File = file;
+ Line = line;
+ Column = column;
+ }
+
+ public ModelWeaverException(string message, SequencePoint sequencePoint)
+ : this(message, sequencePoint.Document.Url, sequencePoint.StartLine, sequencePoint.StartColumn) { }
+
+ public ModelWeaverException(string message, MethodDefinition method) : this(message, GetMethodSequencePoint(method)) { }
+
+ static SequencePoint GetMethodSequencePoint(MethodDefinition method)
+ {
+ return method.DebugInformation.SequencePoints[0];
+ }
+ }
+
+ const string TinkStateAssemblyName = "Nadako.TinkState";
+ const string TinkStateModelAssemblyName = "Nadako.TinkState.Model";
+
+ public static bool Weave(ModuleDefinition module, Logger logger, out bool modified)
+ {
+ modified = false;
+
+ try
+ {
+ if (!IsUsingModels(module)) return true;
+
+ logger.Debug($"Weaving {module.Assembly.FullName}");
+
+ ModelWeaver weaver = null;
+ foreach (var type in module.Types)
+ {
+ if (IsModelClass(type))
+ {
+ weaver ??= new ModelWeaver(module, logger);
+ weaver.WeaveModelClass(type);
+ modified = true;
+ }
+ }
+ }
+ catch (ModelWeaverException e)
+ {
+ logger.Error(e.Message, e.File, e.Line, e.Column);
+ return false;
+ }
+ catch (Exception e)
+ {
+ logger.Error(e.ToString(), null, 0, 0);
+ return false;
+ }
+
+ return true;
+ }
+
+ static bool IsUsingModels(ModuleDefinition module)
+ {
+ return module.HasAssemblyReferences && module.AssemblyReferences.Any(r => r.Name == TinkStateModelAssemblyName);
+ }
+
+ static bool IsModelClass(TypeDefinition type)
+ {
+ return type.IsClass && type.HasInterfaces && type.Interfaces.Any(i => i.InterfaceType.FullName == "TinkState.Model.Model");
+ }
+
+ const string ObservableAttributeName = "TinkState.Model.ObservableAttribute";
+ const string CompilerGeneratedAttributeName = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
+
+ readonly ModuleDefinition module;
+ readonly Logger logger;
+
+ readonly TypeDefinition observableType;
+ readonly TypeDefinition stateType;
+ readonly TypeDefinition modelInternalType;
+ readonly MethodReference modelInternalGetObservableMethod;
+ readonly MethodReference stateCtorMethod;
+ readonly MethodReference autoCtorMethod;
+ readonly MethodReference observableGetValueMethod;
+ readonly MethodReference stateGetValueMethod;
+ readonly MethodReference stateSetValueMethod;
+ readonly MethodReference stringEqualsMethod;
+ readonly TypeReference funcType;
+
+ ModelWeaver(ModuleDefinition module, Logger logger)
+ {
+ this.module = module;
+ this.logger = logger;
+
+ var tinkStateRef = module.AssemblyReferences.First(r => r.Name == TinkStateAssemblyName);
+ var tinkStateAssembly = module.AssemblyResolver.Resolve(tinkStateRef);
+ observableType = tinkStateAssembly.MainModule.Types.First(t => t.FullName == "TinkState.Observable`1");
+ observableGetValueMethod = observableType.Methods.First(m => m.Name == "get_Value");
+ stateType = tinkStateAssembly.MainModule.Types.First(t => t.FullName == "TinkState.State`1");
+ stateGetValueMethod = stateType.Methods.First(m => m.Name == "get_Value");
+ stateSetValueMethod = stateType.Methods.First(m => m.Name == "set_Value");
+
+ var observableClass = tinkStateAssembly.MainModule.Types.First(t => t.FullName == "TinkState.Observable");
+ stateCtorMethod = observableClass.Methods.First(m => m.Name == "State");
+ autoCtorMethod = observableClass.Methods.First(m => m.Name == "Auto");
+
+ var modelAssemblyRef = module.AssemblyReferences.First(r => r.Name == TinkStateModelAssemblyName);
+ var modelAssembly = module.AssemblyResolver.Resolve(modelAssemblyRef);
+ modelInternalType = modelAssembly.MainModule.Types.First(t => t.FullName == "TinkState.Model.ModelInternal");
+ modelInternalGetObservableMethod = modelInternalType.Methods.First(m => m.Name == "GetObservable");
+
+ stringEqualsMethod = module.TypeSystem.String.Resolve().Methods.First(m => m.Name == "op_Equality");
+ funcType = module.ImportReference(typeof(Func<>));
+ }
+
+ struct FieldData
+ {
+ public string PropertyName;
+ public FieldReference BackingField;
+ public Func InitCode;
+ }
+
+ void WeaveModelClass(TypeDefinition type)
+ {
+ logger.Debug("Weaving " + type.FullName);
+
+ var backingFields = new List();
+
+ foreach (var prop in type.Properties)
+ {
+ if (!prop.HasCustomAttribute(ObservableAttributeName)) continue;
+
+ if (prop.GetMethod == null) throw new Exception("Observable properties must have a get method");
+
+ if (prop.SetMethod == null)
+ {
+ CreateAutoObservable(type, prop, backingFields);
+ }
+ else
+ {
+ CreateState(type, prop, backingFields);
+ }
+ }
+
+ AddBackingFieldInits(type, backingFields);
+ ImplementModelInternal(type, backingFields);
+ }
+
+ void CreateAutoObservable(TypeDefinition type, PropertyDefinition prop, List backingFields)
+ {
+ if (prop.GetMethod.HasCustomAttribute(CompilerGeneratedAttributeName))
+ {
+ // auto-observables require actual computation code
+ throw new ModelWeaverException("Read-only Observable properties must have a non-automatic get method", prop.GetMethod);
+ }
+
+ var getMethod = prop.GetMethod;
+
+ // move get logic into a separate method since it'll be used as a computation for the auto-observable
+ var hoistedGetMethod = new MethodDefinition("<" + prop.Name + ">TinkStateModel_Compute",
+ MethodAttributes.Private, getMethod.ReturnType); // TODO: check attributes
+ hoistedGetMethod.Body = new MethodBody(hoistedGetMethod);
+ foreach (var i in getMethod.Body.Instructions)
+ hoistedGetMethod.Body.Instructions.Add(i);
+ type.Methods.Add(hoistedGetMethod);
+
+ // add a backing field for the auto-observable
+ var backingFieldType = module.ImportReference(observableType).MakeGenericInstanceType(prop.PropertyType);
+ var backingField = new FieldDefinition($"<{prop.Name}>k__BackingField", FieldAttributes.Private, backingFieldType);
+ type.Fields.Add(backingField);
+
+ var getValue = new MethodReference(observableGetValueMethod.Name, observableGetValueMethod.ReturnType, backingFieldType) {HasThis = true};
+
+ var il = getMethod.Body.GetILProcessor();
+ il.Clear();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, backingField);
+ il.Emit(OpCodes.Callvirt, getValue);
+ il.Emit(OpCodes.Ret);
+
+ var autoCtorMethodInstance = new GenericInstanceMethod(module.ImportReference(autoCtorMethod));
+ autoCtorMethodInstance.GenericArguments.Add(prop.PropertyType);
+
+ var appliedFuncType = module.ImportReference(funcType).MakeGenericInstanceType(prop.PropertyType);
+ var funcCtor = module.ImportReference(appliedFuncType.Resolve().GetConstructors().First());
+
+ backingFields.Add(new FieldData
+ {
+ PropertyName = prop.Name,
+ BackingField = backingField,
+ InitCode = _ => new[]
+ {
+ Instruction.Create(OpCodes.Ldarg_0),
+
+ Instruction.Create(OpCodes.Ldftn, hoistedGetMethod), // TODO: add caching for non-closures like roslyn does?
+ Instruction.Create(OpCodes.Newobj, funcCtor.MakeHostInstanceGeneric(module, appliedFuncType)),
+
+ Instruction.Create(OpCodes.Ldnull), // comparer
+
+ Instruction.Create(OpCodes.Call, autoCtorMethodInstance),
+ }
+ });
+ }
+
+ void CreateState(TypeDefinition type, PropertyDefinition prop, List backingFields)
+ {
+ if (!prop.GetMethod.CheckAndRemoveCustomAttribute(CompilerGeneratedAttributeName))
+ throw new ModelWeaverException("Observable state properties must have an automatic get method", prop.GetMethod);
+ if (!prop.SetMethod.CheckAndRemoveCustomAttribute(CompilerGeneratedAttributeName))
+ throw new ModelWeaverException("Observable state properties must have an automatic set method", prop.SetMethod);
+
+ var originalBackingField = prop.GetBackingField();
+ type.Fields.Remove(originalBackingField);
+
+ var backingFieldType = module.ImportReference(stateType).MakeGenericInstanceType(prop.PropertyType);
+ var backingField =
+ new FieldDefinition(originalBackingField.Name, FieldAttributes.Private, backingFieldType);
+ type.Fields.Add(backingField);
+
+ var getValue = new MethodReference(stateGetValueMethod.Name, stateGetValueMethod.ReturnType, backingFieldType) {HasThis = true};
+
+ var il = prop.GetMethod.Body.GetILProcessor();
+ il.Clear();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, backingField);
+ il.Emit(OpCodes.Callvirt, getValue);
+ il.Emit(OpCodes.Ret);
+
+ var setValue = new MethodReference(stateSetValueMethod.Name, stateSetValueMethod.ReturnType, backingFieldType) {HasThis = true};
+ setValue.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, stateSetValueMethod.Parameters[0].ParameterType));
+
+ il = prop.SetMethod.Body.GetILProcessor();
+ il.Clear();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, backingField);
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Callvirt, setValue);
+ il.Emit(OpCodes.Ret);
+
+ var stateCtorMethodInstance = new GenericInstanceMethod(module.ImportReference(stateCtorMethod));
+ stateCtorMethodInstance.GenericArguments.Add(prop.PropertyType);
+
+ backingFields.Add(new FieldData
+ {
+ PropertyName = prop.Name,
+ BackingField = backingField,
+ InitCode = body =>
+ {
+ // TODO: find initial value in ctor (assignment to the original backing field) and push it instead of default
+ var initInstructions = new List();
+ if (prop.PropertyType.IsValueType)
+ {
+ // TODO: use built-in instructions for numbers (ldc.i4.0, ldc.i8, etc.)
+ var tempVar = new VariableDefinition(prop.PropertyType);
+ body.Variables.Add(tempVar);
+ body.InitLocals = true;
+ initInstructions.Add(Instruction.Create(OpCodes.Ldloca_S, tempVar));
+ initInstructions.Add(Instruction.Create(OpCodes.Initobj, prop.PropertyType));
+ initInstructions.Add(Instruction.Create(OpCodes.Ldloc, tempVar));
+ }
+ else
+ {
+ initInstructions.Add(Instruction.Create(OpCodes.Ldnull));
+ }
+ initInstructions.Add(Instruction.Create(OpCodes.Ldnull)); // comparer
+ initInstructions.Add(Instruction.Create(OpCodes.Call, stateCtorMethodInstance));
+ return initInstructions.ToArray();
+ },
+ });
+ }
+
+ void ImplementModelInternal(TypeDefinition type, List fields)
+ {
+ // add interface implementation record
+ type.Interfaces.Add(new InterfaceImplementation(module.ImportReference(modelInternalType)));
+
+ // create GetObservable method
+ var getObservableMethod = new MethodDefinition("TinkState.Model.ModelInternal.GetObservable",
+ MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.HideBySig |
+ MethodAttributes.Virtual, // TODO: check attributes
+ module.TypeSystem.Void);
+
+ var typeParam = new GenericParameter("T", getObservableMethod);
+ getObservableMethod.GenericParameters.Add(typeParam);
+
+ getObservableMethod.MethodReturnType.ReturnType = module.ImportReference(observableType).MakeGenericInstanceType(typeParam);
+
+ getObservableMethod.Body = new MethodBody(getObservableMethod);
+ var il = getObservableMethod.Body.GetILProcessor();
+ foreach (var field in fields)
+ {
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Ldstr, field.PropertyName);
+ il.Emit(OpCodes.Call, module.ImportReference(stringEqualsMethod));
+ var elseLabel = Instruction.Create(OpCodes.Nop);
+ il.Emit(OpCodes.Brfalse, elseLabel);
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, field.BackingField);
+ il.Emit(OpCodes.Ret);
+ il.Append(elseLabel);
+ }
+
+ il.Emit(OpCodes.Ldnull); // TODO: throw instead
+ il.Emit(OpCodes.Ret);
+
+ getObservableMethod.Overrides.Add(module.ImportReference(modelInternalGetObservableMethod));
+ getObservableMethod.Parameters.Add(new ParameterDefinition("field", ParameterAttributes.None, module.TypeSystem.String));
+
+ type.Methods.Add(getObservableMethod);
+ }
+
+ void AddBackingFieldInits(TypeDefinition type, List fields)
+ {
+ if (fields.Count == 0) return;
+
+ // TODO: hoist inits into a method if there's more than 1 ctor?
+ var hasCtor = false;
+ foreach (var ctor in type.GetConstructors())
+ {
+ hasCtor = true;
+ for (var i = fields.Count - 1; i >= 0; i--)
+ {
+ var field = fields[i];
+
+ ctor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Stfld, field.BackingField));
+ var initCode = field.InitCode(ctor.Body);
+
+ for (var j = initCode.Length - 1; j >= 0; j--)
+ {
+ ctor.Body.Instructions.Insert(0, initCode[j]);
+ }
+
+ ctor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldarg_0));
+ }
+ }
+
+ if (!hasCtor) throw new Exception("No constructor O_o");
+ }
+ }
+
+ static class HelperExtensions
+ {
+ public static FieldDefinition GetBackingField(this PropertyDefinition prop)
+ {
+ foreach (var instruction in prop.GetMethod.Body.Instructions)
+ {
+ if (instruction.OpCode == OpCodes.Ldfld) return ((FieldReference) instruction.Operand).Resolve();
+ }
+
+ throw new Exception("Couldn't find backing field load instruction in the get method");
+ }
+
+ public static bool HasCustomAttribute(this ICustomAttributeProvider provider, string attributeName)
+ {
+ return provider.HasCustomAttributes &&
+ provider.CustomAttributes.Any(a => a.AttributeType.FullName == attributeName);
+ }
+
+ public static bool CheckAndRemoveCustomAttribute(this ICustomAttributeProvider provider, string attributeName)
+ {
+ if (!provider.HasCustomAttributes) return false;
+
+ var removed = false;
+ var attributes = provider.CustomAttributes;
+ var i = 0;
+ while (i < attributes.Count)
+ {
+ var attribute = attributes[i];
+ if (attribute.AttributeType.FullName == attributeName)
+ {
+ attributes.RemoveAt(i);
+ removed = true;
+ }
+ else
+ {
+ i++;
+ }
+ }
+
+ return removed;
+ }
+
+ public static MethodReference MakeHostInstanceGeneric(this MethodReference self, ModuleDefinition module, GenericInstanceType instanceType)
+ {
+ MethodReference reference = new MethodReference(self.Name, self.ReturnType, instanceType)
+ {
+ CallingConvention = self.CallingConvention,
+ HasThis = self.HasThis,
+ ExplicitThis = self.ExplicitThis
+ };
+
+ foreach (ParameterDefinition parameter in self.Parameters)
+ reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
+
+ foreach (GenericParameter generic_parameter in self.GenericParameters)
+ reference.GenericParameters.Add(new GenericParameter(generic_parameter.Name, reference));
+
+ return module.ImportReference(reference);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver/ModelWeaver.cs.meta b/src/TinkState-Model-Weaver/ModelWeaver.cs.meta
new file mode 100644
index 0000000..d32b401
--- /dev/null
+++ b/src/TinkState-Model-Weaver/ModelWeaver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 699c8a776e77629478f4a969dc6a2037
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef b/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef
new file mode 100644
index 0000000..b686ae3
--- /dev/null
+++ b/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef
@@ -0,0 +1,13 @@
+{
+ "name": "Nadako.TinkState.Model.Weaver",
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "Mono.Cecil.dll",
+ "Mono.Cecil.Rocks.dll"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "noEngineReferences": true
+}
\ No newline at end of file
diff --git a/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef.meta b/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef.meta
new file mode 100644
index 0000000..61fd6ed
--- /dev/null
+++ b/src/TinkState-Model-Weaver/Nadako.TinkState.Model.Weaver.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0c5be3d428cbcf842a63df7d4e6fda13
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model.meta b/src/TinkState-Model.meta
new file mode 100644
index 0000000..d48a412
--- /dev/null
+++ b/src/TinkState-Model.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c8ae8bfaad3653a448b3306abd8c399a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model/Runtime.meta b/src/TinkState-Model/Runtime.meta
new file mode 100644
index 0000000..3d8aa87
--- /dev/null
+++ b/src/TinkState-Model/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6f42b49a8b4bc834f893814b230c7e64
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model/Runtime/Attributes.cs b/src/TinkState-Model/Runtime/Attributes.cs
new file mode 100644
index 0000000..bb88ac3
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Attributes.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace TinkState.Model
+{
+ ///
+ /// Use Observable or State backing field for the property.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ObservableAttribute : Attribute {}
+}
diff --git a/src/TinkState-Model/Runtime/Attributes.cs.meta b/src/TinkState-Model/Runtime/Attributes.cs.meta
new file mode 100644
index 0000000..8fc0472
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Attributes.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7b9109ea020b27e4c820b25d50158d80
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/TinkState-Model/Runtime/Model.cs b/src/TinkState-Model/Runtime/Model.cs
new file mode 100644
index 0000000..8b9ab30
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Model.cs
@@ -0,0 +1,55 @@
+using System;
+using System.ComponentModel;
+using System.Linq.Expressions;
+
+namespace TinkState.Model
+{
+ ///
+ /// Enable support for the [Observable] attribute on properties of the implementing class.
+ ///
+ public interface Model { }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public interface ModelInternal
+ {
+ Observable GetObservable(string field);
+ }
+
+ ///
+ /// Extension methods for implementors of the interface.
+ ///
+ public static class ModelExtensions
+ {
+ ///
+ /// Get the underlying backing object for given property with the [Observable] attribute.
+ ///
+ /// Model class instance
+ /// Lambda expression in form of _ => c.Property
+ /// Type of the model to retrieve an observable from.
+ /// Type of the property to retrieve an observable for.
+ /// Backing object for given property.
+ public static Observable GetObservable(this M model, Expression> expr) where M : Model
+ {
+ var memberExpr = expr.Body as MemberExpression;
+ if (memberExpr == null) throw new Exception("Member expression is expected");
+ if (!(memberExpr.Expression is ParameterExpression)) throw new Exception("Expression in form of `v => v.Field` expected");
+ var fieldName = memberExpr.Member.Name;
+ return GetObservable(model, fieldName);
+ }
+
+ ///
+ /// Get the underlying backing object for given property with the [Observable] attribute.
+ ///
+ ///
+ /// This method is not type safe, prefer instead.
+ ///
+ /// Model class instance
+ /// Name of the string
+ /// Type of the property to retrieve an observable for.
+ /// Backing object for given property.
+ public static Observable GetObservable(this Model model, string field)
+ {
+ return ((ModelInternal)model).GetObservable(field);
+ }
+ }
+}
diff --git a/src/TinkState-Model/Runtime/Model.cs.meta b/src/TinkState-Model/Runtime/Model.cs.meta
new file mode 100644
index 0000000..4c22256
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Model.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5e07c25fe81241be8384d9b86f50c26d
+timeCreated: 1670584965
\ No newline at end of file
diff --git a/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef b/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef
new file mode 100644
index 0000000..24a9ae8
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef
@@ -0,0 +1,7 @@
+{
+ "name": "Nadako.TinkState.Model",
+ "references": [
+ "Nadako.TinkState"
+ ],
+ "noEngineReferences": true
+}
\ No newline at end of file
diff --git a/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef.meta b/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef.meta
new file mode 100644
index 0000000..5cbbb7a
--- /dev/null
+++ b/src/TinkState-Model/Runtime/Nadako.TinkState.Model.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 055f04631c2c88340813580615fde26a
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/package.json b/src/package.json
index eadc49a..c5c4422 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,18 +1,21 @@
{
- "name": "me.nadako.tinkstate",
- "version": "0.1.0",
- "displayName": "TinkStateSharp for Unity",
- "description": "Reactive State Handling",
- "unity": "2021.3",
- "author": {
- "name": "Dan Korostelev",
- "email": "nadako@gmail.com"
- },
- "license": "Unlicense",
- "keywords": [
- "react",
- "reactive",
- "observable",
- "state"
- ]
+ "name": "me.nadako.tinkstate",
+ "version": "0.1.0",
+ "displayName": "TinkStateSharp for Unity",
+ "description": "Reactive State Handling",
+ "unity": "2021.3",
+ "author": {
+ "name": "Dan Korostelev",
+ "email": "nadako@gmail.com"
+ },
+ "dependencies": {
+ "com.unity.nuget.mono-cecil": "1.11.4"
+ },
+ "license": "Unlicense",
+ "keywords": [
+ "react",
+ "reactive",
+ "observable",
+ "state"
+ ]
}
\ No newline at end of file
diff --git a/unity-dlls/Unity.CompilationPipeline.Common.dll b/unity-dlls/Unity.CompilationPipeline.Common.dll
new file mode 100644
index 0000000..5e0adfc
Binary files /dev/null and b/unity-dlls/Unity.CompilationPipeline.Common.dll differ