From b55ae7d814951f72645f40f8f0799bdfd5d9ee1d Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 7 Jan 2022 22:26:02 +0300 Subject: [PATCH] HNSW Append LAL graph --- TestHNSW/HNSWDemo/Program.cs | 28 ++++- .../HNSWDemo/Properties/launchSettings.json | 9 ++ TestHNSW/HNSWDemo/Tests/AccuracityTest.cs | 2 +- TestHNSW/HNSWDemo/Tests/HistogramTest.cs | 20 +++- TestHNSW/HNSWDemo/Tests/LALTest.cs | 101 +++++++++++++++++ TestPipeLine/Consumer/Program.cs | 9 ++ TestPipeLine/Processor/Program.cs | 9 ++ TestPipeLine/Source/Program.cs | 9 ++ TestPipeLine/Watcher/Program.cs | 9 ++ ZeroLevel.HNSW/{ => Services}/HNSWMap.cs | 10 ++ ZeroLevel.HNSW/Services/HNSWMappers.cs | 75 +++++++++++++ ZeroLevel.HNSW/Services/LAL/LALGraph.cs | 105 ++++++++++++++++++ ZeroLevel.HNSW/Services/LAL/LALLinks.cs | 79 +++++++++++++ .../Services/LAL/SplittedLALGraph.cs | 29 +++++ ZeroLevel.HNSW/Services/LinksSet.cs | 28 ++--- ZeroLevel.HNSW/SmallWorldFactory.cs | 6 +- ZeroLevel.HNSW/ZeroLevel.HNSW.csproj | 33 ++++++ ZeroLevel/ZeroLevel.csproj | 4 +- 18 files changed, 531 insertions(+), 34 deletions(-) create mode 100644 TestHNSW/HNSWDemo/Properties/launchSettings.json create mode 100644 TestHNSW/HNSWDemo/Tests/LALTest.cs rename ZeroLevel.HNSW/{ => Services}/HNSWMap.cs (87%) create mode 100644 ZeroLevel.HNSW/Services/HNSWMappers.cs create mode 100644 ZeroLevel.HNSW/Services/LAL/LALGraph.cs create mode 100644 ZeroLevel.HNSW/Services/LAL/LALLinks.cs create mode 100644 ZeroLevel.HNSW/Services/LAL/SplittedLALGraph.cs diff --git a/TestHNSW/HNSWDemo/Program.cs b/TestHNSW/HNSWDemo/Program.cs index 5779715..afc4524 100644 --- a/TestHNSW/HNSWDemo/Program.cs +++ b/TestHNSW/HNSWDemo/Program.cs @@ -1,6 +1,7 @@ using HNSWDemo.Tests; using System; -using ZeroLevel.Services.Web; +using System.IO; +using ZeroLevel.HNSW; namespace HNSWDemo { @@ -8,12 +9,29 @@ namespace HNSWDemo { static void Main(string[] args) { - var uri = new Uri("https://hack33d.ru/bpla/upload.php?path=128111&get=0J/QuNC70LjQv9C10L3QutC+INCS0LvQsNC00LjQvNC40YAg0JzQuNGF0LDQudC70L7QstC40Yc7MDQuMDkuMTk1NCAoNjYg0LvQtdGCKTvQnNC+0YHQutC+0LLRgdC60LDRjzsxMjgxMTE7TEFfUkVaVVM7RkxZXzAy"); - var parts = UrlUtility.ParseQueryString(uri.Query); - new AutoClusteringMNISTTest().Run(); - //new HistogramTest().Run(); + + + new LALTest().Run(); + // new AutoClusteringMNISTTest().Run(); + // new AccuracityTest().Run(); Console.WriteLine("Completed"); Console.ReadKey(); } + + static int GetC(string file) + { + var name = Path.GetFileNameWithoutExtension(file); + var index = name.IndexOf("_M"); + if (index > 0) + { + index = name.IndexOf("_", index + 2); + if (index > 0) + { + var num = name.Substring(index + 1, name.Length - index - 1); + return int.Parse(num); + } + } + return -1; + } } } diff --git a/TestHNSW/HNSWDemo/Properties/launchSettings.json b/TestHNSW/HNSWDemo/Properties/launchSettings.json new file mode 100644 index 0000000..0246ed6 --- /dev/null +++ b/TestHNSW/HNSWDemo/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "HNSWDemo": { + "commandName": "Project", + "hotReloadEnabled": false, + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/TestHNSW/HNSWDemo/Tests/AccuracityTest.cs b/TestHNSW/HNSWDemo/Tests/AccuracityTest.cs index 75e5795..f36dc65 100644 --- a/TestHNSW/HNSWDemo/Tests/AccuracityTest.cs +++ b/TestHNSW/HNSWDemo/Tests/AccuracityTest.cs @@ -11,7 +11,7 @@ namespace HNSWDemo.Tests : ITest { private static int K = 200; - private static int count = 3000; + private static int count = 10000; private static int testCount = 500; private static int dimensionality = 128; diff --git a/TestHNSW/HNSWDemo/Tests/HistogramTest.cs b/TestHNSW/HNSWDemo/Tests/HistogramTest.cs index 9ee0754..9b803eb 100644 --- a/TestHNSW/HNSWDemo/Tests/HistogramTest.cs +++ b/TestHNSW/HNSWDemo/Tests/HistogramTest.cs @@ -1,5 +1,6 @@ using System; using System.Drawing; +using System.IO; using System.Linq; using ZeroLevel.HNSW; @@ -10,12 +11,23 @@ namespace HNSWDemo.Tests { private static int Count = 3000; private static int Dimensionality = 128; - private static int Width = 3000; - private static int Height = 3000; + private static int Width = 2440; + private static int Height = 1920; public void Run() { - var vectors = VectorUtils.RandomVectors(Dimensionality, Count); + Create(Dimensionality, @"D:\hist"); + // Process.Start("explorer", $"D:\\hist{Dimensionality.ToString("D3")}.jpg"); + + /* for (int i = 12; i < 512; i++) + { + Create(i, @"D:\hist"); + }*/ + } + + private void Create(int dim, string output) + { + var vectors = VectorUtils.RandomVectors(dim, Count); var world = SmallWorld.CreateWorld(NSWOptions.Create(8, 16, 200, 200, Metrics.L2Euclidean)); world.AddItems(vectors); @@ -29,7 +41,7 @@ namespace HNSWDemo.Tests var max = histogram.Bounds[threshold]; var R = (max + min) / 2; - DrawHistogram(histogram, @"D:\hist.jpg"); + DrawHistogram(histogram, Path.Combine(output, $"hist{dim.ToString("D3")}.jpg")); } static void DrawHistogram(Histogram histogram, string filename) diff --git a/TestHNSW/HNSWDemo/Tests/LALTest.cs b/TestHNSW/HNSWDemo/Tests/LALTest.cs new file mode 100644 index 0000000..5db13da --- /dev/null +++ b/TestHNSW/HNSWDemo/Tests/LALTest.cs @@ -0,0 +1,101 @@ +using HNSWDemo.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ZeroLevel.HNSW; + +namespace HNSWDemo.Tests +{ + internal class LALTest + : ITest + { + private const int count = 5000; + private const int testCount = 100; + private const int dimensionality = 128; + + public void Run() + { + var moda = 3; + var persons = Person.GenerateRandom(dimensionality, count); + var samples = new Dictionary>(); + var options = NSWOptions.Create(6, 8, 100, 100, Metrics.Cosine); + + foreach (var p in persons) + { + var c = (int)Math.Abs(p.Item2.Number.GetHashCode() % moda); + if (samples.ContainsKey(c) == false) samples.Add(c, new List<(float[], Person)>()); + samples[c].Add(p); + } + + + var worlds = new SplittedLALGraph(); + var mappers = new HNSWMappers(l => (int)Math.Abs(l.GetHashCode() % moda)); + + var worlds_dict = new Dictionary>(); + var maps_dict = new Dictionary>(); + + foreach (var p in samples) + { + var c = p.Key; + if (worlds_dict.ContainsKey(c) == false) + { + worlds_dict.Add(c, new SmallWorld(options)); + } + if (maps_dict.ContainsKey(c) == false) + { + maps_dict.Add(c, new HNSWMap()); + } + var w = worlds_dict[c]; + var m = maps_dict[c]; + var ids = w.AddItems(p.Value.Select(i => i.Item1)); + + for (int i = 0; i < ids.Length; i++) + { + m.Append(p.Value[i].Item2.Number, ids[i]); + } + } + + var name = Guid.NewGuid().ToString(); + foreach (var p in samples) + { + var c = p.Key; + var w = worlds_dict[c]; + var m = maps_dict[c]; + + using (var s = File.Create(name)) + { + w.Serialize(s); + } + using (var s = File.OpenRead(name)) + { + var l = LALGraph.FromHNSWGraph(s); + worlds.Append(l, c); + } + File.Delete(name); + mappers.Append(m, c); + } + + var entries = new long[10]; + for (int i = 0; i < entries.Length; i++) + { + entries[i] = persons[DefaultRandomGenerator.Instance.Next(0, persons.Count - 1)].Item2.Number; + } + + var contexts = mappers.CreateContext(null, entries); + var result = worlds.KNearest(10, contexts); + + Console.WriteLine("Entries:"); + foreach (var n in entries) + { + Console.WriteLine($"\t{n}"); + } + + Console.WriteLine("Extensions:"); + foreach (var n in mappers.ConvertIdsToFeatures(result)) + { + Console.WriteLine($"\t[{n}]"); + } + } + } +} diff --git a/TestPipeLine/Consumer/Program.cs b/TestPipeLine/Consumer/Program.cs index 937ae33..2f018f5 100644 --- a/TestPipeLine/Consumer/Program.cs +++ b/TestPipeLine/Consumer/Program.cs @@ -7,6 +7,15 @@ namespace Consumer { static void Main(string[] args) { + IConfiguration conf = Configuration.Create(); + conf.Append("ServiceName", "Test consumer"); + conf.Append("ServiceKey", "test.consumer"); + conf.Append("ServiceType", "Destination"); + conf.Append("ServiceGroup", "Test"); + conf.Append("Version", "1.0.0.1"); + conf.Append("discovery", "127.0.0.1:5012"); + Configuration.Save(conf); + Bootstrap.Startup(args) .EnableConsoleLog(LogLevel.FullStandart) .UseDiscovery() diff --git a/TestPipeLine/Processor/Program.cs b/TestPipeLine/Processor/Program.cs index 100d790..5b516f5 100644 --- a/TestPipeLine/Processor/Program.cs +++ b/TestPipeLine/Processor/Program.cs @@ -7,6 +7,15 @@ namespace Processor { static void Main(string[] args) { + IConfiguration conf = Configuration.Create(); + conf.Append("ServiceName", "Test processor"); + conf.Append("ServiceKey", "test.processor"); + conf.Append("ServiceType", "Core"); + conf.Append("ServiceGroup", "Test"); + conf.Append("Version", "1.0.0.1"); + conf.Append("discovery", "127.0.0.1:5012"); + Configuration.Save(conf); + Bootstrap.Startup(args) .EnableConsoleLog(LogLevel.FullStandart) .UseDiscovery() diff --git a/TestPipeLine/Source/Program.cs b/TestPipeLine/Source/Program.cs index 4ce61f2..6ee7307 100644 --- a/TestPipeLine/Source/Program.cs +++ b/TestPipeLine/Source/Program.cs @@ -6,6 +6,15 @@ namespace Source { static void Main(string[] args) { + IConfiguration conf = Configuration.Create(); + conf.Append("ServiceName", "Test source"); + conf.Append("ServiceKey", "test.source"); + conf.Append("ServiceType", "Sources"); + conf.Append("ServiceGroup", "Test"); + conf.Append("Version", "1.0.0.1"); + conf.Append("discovery", "127.0.0.1:5012"); + Configuration.Save(conf); + Bootstrap.Startup(args) .EnableConsoleLog(ZeroLevel.Logging.LogLevel.FullStandart) .UseDiscovery() diff --git a/TestPipeLine/Watcher/Program.cs b/TestPipeLine/Watcher/Program.cs index 0c9ff32..df3a6e9 100644 --- a/TestPipeLine/Watcher/Program.cs +++ b/TestPipeLine/Watcher/Program.cs @@ -6,6 +6,15 @@ namespace Watcher { static void Main(string[] args) { + IConfiguration conf = Configuration.Create(); + conf.Append("ServiceName", "Watcher"); + conf.Append("ServiceKey", "test.watcher"); + conf.Append("ServiceType", "System"); + conf.Append("ServiceGroup", "Test"); + conf.Append("Version", "1.0.0.1"); + conf.Append("discovery", "127.0.0.1:5012"); + Configuration.Save(conf); + Bootstrap.Startup(args) .UseDiscovery() .Run() diff --git a/ZeroLevel.HNSW/HNSWMap.cs b/ZeroLevel.HNSW/Services/HNSWMap.cs similarity index 87% rename from ZeroLevel.HNSW/HNSWMap.cs rename to ZeroLevel.HNSW/Services/HNSWMap.cs index 1d3141d..1574e6d 100644 --- a/ZeroLevel.HNSW/HNSWMap.cs +++ b/ZeroLevel.HNSW/Services/HNSWMap.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using ZeroLevel.Services.Serialization; namespace ZeroLevel.HNSW @@ -12,6 +13,7 @@ namespace ZeroLevel.HNSW private Dictionary _map; private Dictionary _reverse_map; + public int this[TFeature feature] => _map.GetValueOrDefault(feature); public HNSWMap(int capacity = -1) { if (capacity > 0) @@ -27,6 +29,14 @@ namespace ZeroLevel.HNSW } } + public HNSWMap(Stream stream) + { + using (var reader = new MemoryStreamReader(stream)) + { + Deserialize(reader); + } + } + public void Append(TFeature feature, int vectorId) { _map[feature] = vectorId; diff --git a/ZeroLevel.HNSW/Services/HNSWMappers.cs b/ZeroLevel.HNSW/Services/HNSWMappers.cs new file mode 100644 index 0000000..859f638 --- /dev/null +++ b/ZeroLevel.HNSW/Services/HNSWMappers.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace ZeroLevel.HNSW +{ + public class HNSWMappers + { + private readonly IDictionary> _mappers = new Dictionary>(); + private readonly Func _bucketFunction; + public HNSWMappers(Func bucketFunction) + { + _bucketFunction = bucketFunction; + } + + public void Append(HNSWMap map, int c) + { + _mappers.Add(c, map); + } + + public IEnumerable ConvertIdsToFeatures(IEnumerable ids) + { + foreach (var map in _mappers) + { + foreach (var feature in map.Value.ConvertIdsToFeatures(ids)) + { + yield return feature; + } + } + } + + public IDictionary CreateContext(IEnumerable activeNodes, IEnumerable entryPoints) + { + var actives = new Dictionary>(); + var entries = new Dictionary>(); + if (activeNodes != null) + { + foreach (var node in activeNodes) + { + var c = _bucketFunction(node); + if (_mappers.ContainsKey(c)) + { + if (actives.ContainsKey(c) == false) + { + actives.Add(c, new List()); + } + actives[c].Add(_mappers[c][node]); + } + } + } + if (entryPoints != null) + { + foreach (var entryPoint in entryPoints) + { + var c = _bucketFunction(entryPoint); + if (_mappers.ContainsKey(c)) + { + if (entries.ContainsKey(c) == false) + { + entries.Add(c, new List()); + } + entries[c].Add(_mappers[c][entryPoint]); + } + } + } + var result = new Dictionary(); + foreach (var pair in _mappers) + { + var active = actives.GetValueOrDefault(pair.Key); + var entry = entries.GetValueOrDefault(pair.Key); + result.Add(pair.Key, new SearchContext().SetActiveNodes(active).SetEntryPointsNodes(entry)); + } + return result; + } + } +} diff --git a/ZeroLevel.HNSW/Services/LAL/LALGraph.cs b/ZeroLevel.HNSW/Services/LAL/LALGraph.cs new file mode 100644 index 0000000..eeb2d5f --- /dev/null +++ b/ZeroLevel.HNSW/Services/LAL/LALGraph.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.HNSW +{ + public class LALGraph + { + private readonly LALLinks _links = new LALLinks(); + + private LALGraph() { } + public static LALGraph FromLALGraph(Stream stream) + { + var l = new LALGraph(); + l.Deserialize(stream); + return l; + } + + public static LALGraph FromHNSWGraph(Stream stream) + { + var l = new LALGraph(); + l.DeserializeFromHNSW(stream); + return l; + } + + public IEnumerable KNearest(int k, SearchContext context) + { + var v = new VisitedBitSet(_links.Count, 1); + var C = new Queue(); + var W = new List(); + var entryPoints = context.EntryPoints; + + do + { + foreach (var ep in entryPoints) + { + var neighboursIds = _links.FindNeighbors(ep); + for (int i = 0; i < neighboursIds.Length; ++i) + { + C.Enqueue(neighboursIds[i]); + } + v.Add(ep); + } + // run bfs + while (C.Count > 0) + { + // get next candidate to check and expand + var toExpand = C.Dequeue(); + if (context.IsActiveNode(toExpand)) + { + if (W.Count < k) + { + W.Add(toExpand); + if (W.Count > k) + { + var loser_id = DefaultRandomGenerator.Instance.Next(0, W.Count - 1); + W.RemoveAt(loser_id); + } + } + } + } + entryPoints = W.Select(id => id).ToList(); + } + while (W.Count < k && entryPoints.Any()); + C.Clear(); + v.Clear(); + return W; + } + + public void Deserialize(Stream stream) + { + using (var reader = new MemoryStreamReader(stream)) + { + _links.Deserialize(reader); // deserialize only base layer and skip another + } + } + + public void DeserializeFromHNSW(Stream stream) + { + using (var reader = new MemoryStreamReader(stream)) + { + reader.ReadInt32(); // EntryPoint + reader.ReadInt32(); // MaxLayer + + int count = reader.ReadInt32(); // Vectors count + for (int i = 0; i < count; i++) + { + reader.ReadCompatible(); // Vector + } + + reader.ReadInt32(); // countLayers + _links.Deserialize(reader); // deserialize only base layer and skip another + } + } + + public void Serialize(Stream stream) + { + using (var writer = new MemoryStreamWriter(stream)) + { + _links.Serialize(writer); + } + } + } +} diff --git a/ZeroLevel.HNSW/Services/LAL/LALLinks.cs b/ZeroLevel.HNSW/Services/LAL/LALLinks.cs new file mode 100644 index 0000000..dd9bdce --- /dev/null +++ b/ZeroLevel.HNSW/Services/LAL/LALLinks.cs @@ -0,0 +1,79 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.HNSW +{ + internal class LALLinks + { + private ConcurrentDictionary _set = new ConcurrentDictionary(); + internal IDictionary Links => _set; + + private readonly int[] _empty = new int[0]; + internal int Count => _set.Count; + + public LALLinks() + { + } + + internal IEnumerable<(int, int)> FindLinksForId(int id) + { + if (_set.ContainsKey(id)) + { + return _set[id].Select(v => (id, v)); + } + return Enumerable.Empty<(int, int)>(); + } + + internal int[] FindNeighbors(int id) + { + if (_set.ContainsKey(id)) + { + return _set[id]; + } + return _empty; + } + + internal IEnumerable<(int, int)> Items() + { + return _set + .SelectMany(pair => _set[pair.Key] + .Select(v => (pair.Key, v))); + } + + public void Dispose() + { + _set.Clear(); + _set = null; + } + public void Serialize(IBinaryWriter writer) + { + writer.WriteInt32(_set.Count); + foreach (var record in _set) + { + writer.WriteInt32(record.Key); + writer.WriteCollection(record.Value); + } + } + + public void Deserialize(IBinaryReader reader) + { + _set.Clear(); + _set = null; + var count = reader.ReadInt32(); + _set = new ConcurrentDictionary(1, count); + + for (int i = 0; i < count; i++) + { + var id = reader.ReadInt32(); + var links_count = reader.ReadInt32(); + _set[id] = new int[links_count]; + for (int l = 0; l < links_count; l++) + { + _set[id][l] = reader.ReadInt32(); + } + } + } + } +} diff --git a/ZeroLevel.HNSW/Services/LAL/SplittedLALGraph.cs b/ZeroLevel.HNSW/Services/LAL/SplittedLALGraph.cs new file mode 100644 index 0000000..d46f441 --- /dev/null +++ b/ZeroLevel.HNSW/Services/LAL/SplittedLALGraph.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace ZeroLevel.HNSW +{ + public class SplittedLALGraph + { + private readonly IDictionary _graphs = new Dictionary(); + + public void Append(LALGraph graph, int c) + { + _graphs.Add(c, graph); + } + + public IEnumerable KNearest(int k, IDictionary contexts) + { + var partial_k = 1 + (k / _graphs.Count); + var result = new List(); + foreach (var graph in _graphs) + { + var context = contexts[graph.Key]; + if (context.EntryPoints != null) + { + result.AddRange(graph.Value.KNearest(partial_k, context)); + } + } + return result; + } + } +} diff --git a/ZeroLevel.HNSW/Services/LinksSet.cs b/ZeroLevel.HNSW/Services/LinksSet.cs index 8870d1e..f1175a8 100644 --- a/ZeroLevel.HNSW/Services/LinksSet.cs +++ b/ZeroLevel.HNSW/Services/LinksSet.cs @@ -70,24 +70,15 @@ namespace ZeroLevel.HNSW _set.Clear(); _set = null; } - - private const int HALF_LONG_BITS = 32; public void Serialize(IBinaryWriter writer) { - writer.WriteBoolean(false); // true - set with weights - var count = _set.Sum(pair => pair.Value.Count); - writer.WriteInt32(count); + writer.WriteInt32(_set.Count); foreach (var record in _set) { - var id = record.Key; - foreach (var r in record.Value) - { - var key = (((long)(id)) << HALF_LONG_BITS) + r; - writer.WriteLong(key); - } + writer.WriteInt32(record.Key); + writer.WriteCollection(record.Value); } } - public void Deserialize(IBinaryReader reader) { if (reader.ReadBoolean() != false) @@ -100,16 +91,13 @@ namespace ZeroLevel.HNSW _set = new ConcurrentDictionary>(); for (int i = 0; i < count; i++) { - var key = reader.ReadLong(); - - var id1 = (int)(key >> HALF_LONG_BITS); - var id2 = (int)(key - (((long)id1) << HALF_LONG_BITS)); - - if (!_set.ContainsKey(id1)) + var id = reader.ReadInt32(); + var links_count = reader.ReadInt32(); + _set[id] = new HashSet(links_count); + for (var l = 0; l < links_count; l++) { - _set[id1] = new HashSet(); + _set[id].Add(reader.ReadInt32()); } - _set[id1].Add(id2); } } } diff --git a/ZeroLevel.HNSW/SmallWorldFactory.cs b/ZeroLevel.HNSW/SmallWorldFactory.cs index 204bb68..4ec5a65 100644 --- a/ZeroLevel.HNSW/SmallWorldFactory.cs +++ b/ZeroLevel.HNSW/SmallWorldFactory.cs @@ -4,7 +4,9 @@ namespace ZeroLevel.HNSW { public static class SmallWorld { - public static SmallWorld CreateWorld(NSWOptions options) => new SmallWorld(options); - public static SmallWorld CreateWorldFrom(NSWOptions options, Stream stream) => new SmallWorld(options, stream); + public static SmallWorld CreateWorld(NSWOptions options) + => new SmallWorld(options); + public static SmallWorld CreateWorldFrom(NSWOptions options, Stream stream) + => new SmallWorld(options, stream); } } diff --git a/ZeroLevel.HNSW/ZeroLevel.HNSW.csproj b/ZeroLevel.HNSW/ZeroLevel.HNSW.csproj index 083e579..b7c936a 100644 --- a/ZeroLevel.HNSW/ZeroLevel.HNSW.csproj +++ b/ZeroLevel.HNSW/ZeroLevel.HNSW.csproj @@ -3,8 +3,41 @@ net6.0 AnyCPU;x64 + x64 + full + 1.0.0.1 + ogoun + Ogoun + Copyright Ogoun 2022 + https://github.com/ogoun/Zero/wiki + zero.png + https://github.com/ogoun/Zero + git + + False + + + + False + + + + True + + + + True + + + + + True + \ + + + diff --git a/ZeroLevel/ZeroLevel.csproj b/ZeroLevel/ZeroLevel.csproj index 5759e8a..81b79ed 100644 --- a/ZeroLevel/ZeroLevel.csproj +++ b/ZeroLevel/ZeroLevel.csproj @@ -13,12 +13,12 @@ https://github.com/ogoun/Zero - GitHub + git 3.3.5.7 3.3.5.7 AnyCPU;x64;x86 zero.png - none + full none zero.ico