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