From f8a65821546d33a25b913e8f0b67a46871ab7d01 Mon Sep 17 00:00:00 2001 From: hyperion51 Date: Fri, 27 Mar 2020 01:53:03 +0000 Subject: [PATCH] Server Initialization and Serverside Galaxy/Zone Delivery Made the master server connect to the database and now it's listening for and responding correctly to GalaxyRequestMessages and ZoneRequestMessages, generating the zone contents if they do not yet exist. --- .../.idea/contentModel.xml | 1 + Assembly-CSharp.csproj | 1 + Assets/LoginScreen.cs | 4 +- Assets/Materials/Galaxy.mat | 77 +++++ Assets/Materials/Galaxy.mat.meta | 8 + .../{Zone Background.mat => Zone.mat} | 0 ...Zone Background.mat.meta => Zone.mat.meta} | 0 Assets/Scenes/Start.unity | 43 --- Assets/Scripts/CultClient.cs | 45 ++- Assets/Scripts/Editor/SerializerTestWindow.cs | 4 +- Assets/Scripts/ServerShared/DatabaseCache.cs | 5 +- Assets/Scripts/ServerShared/GameContext.cs | 31 +- Assets/Scripts/ServerShared/GlobalData.cs | 7 +- .../ServerShared/ItemBehaviors/Afterburner.cs | 1 + .../ServerShared/ItemBehaviors/Cannon.cs | 1 + .../ServerShared/ItemBehaviors/Launcher.cs | 1 + .../ServerShared/ItemBehaviors/Reactor.cs | 1 + .../ServerShared/ItemBehaviors/Sensor.cs | 1 + Assets/Scripts/ServerShared/ItemData.cs | 7 + Assets/Scripts/ServerShared/Messages.cs | 32 ++ Assets/Scripts/ServerShared/ZoneData.cs | 2 +- Assets/Shaders/GalaxyMap.shader | 29 +- Assets/StrategyGameManager.cs | 63 ++++ Assets/StrategyGameManager.cs.meta | 11 + Economy.Server/Economy.Server.csproj | 3 + Economy.Server/Program.cs | 225 +++++++++++++- Economy.Server/Server.cs | 4 +- .../netcoreapp3.1/Economy.Server.assets.cache | Bin 25739 -> 30208 bytes ...onomy.Server.csprojAssemblyReference.cache | Bin 92495 -> 4002 bytes .../obj/Economy.Server.csproj.nuget.cache | 2 +- .../Economy.Server.csproj.nuget.dgspec.json | 12 + Economy.Server/obj/project.assets.json | 278 +++++++++++++++++- Economy.Server/obj/project.packagespec.json | 12 + README.md | 49 ++- 34 files changed, 845 insertions(+), 115 deletions(-) create mode 100644 Assets/Materials/Galaxy.mat create mode 100644 Assets/Materials/Galaxy.mat.meta rename Assets/Materials/{Zone Background.mat => Zone.mat} (100%) rename Assets/Materials/{Zone Background.mat.meta => Zone.mat.meta} (100%) create mode 100644 Assets/StrategyGameManager.cs create mode 100644 Assets/StrategyGameManager.cs.meta diff --git a/.idea/.idea.Aetheria-Economy/.idea/contentModel.xml b/.idea/.idea.Aetheria-Economy/.idea/contentModel.xml index 887137473..05f167ade 100644 --- a/.idea/.idea.Aetheria-Economy/.idea/contentModel.xml +++ b/.idea/.idea.Aetheria-Economy/.idea/contentModel.xml @@ -1108,6 +1108,7 @@ + diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index 5ff8b9be7..ffe74edc7 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -397,6 +397,7 @@ + diff --git a/Assets/LoginScreen.cs b/Assets/LoginScreen.cs index 43b8904e0..e171b93d9 100644 --- a/Assets/LoginScreen.cs +++ b/Assets/LoginScreen.cs @@ -18,10 +18,10 @@ public class LoginScreen : MonoBehaviour void Start() { - Error.text = ""; + Error.text = " "; LoginButton.CurrentState = FlatButtonState.Selected; CultClient.AddMessageListener(success => SceneManager.LoadScene("Main")); - CultClient.AddMessageListener(error => Error.text = error.Error); + CultClient.OnError += s => Error.text = s; RegisterButton.OnClick += _ => { if (_registering) diff --git a/Assets/Materials/Galaxy.mat b/Assets/Materials/Galaxy.mat new file mode 100644 index 000000000..62a47cce6 --- /dev/null +++ b/Assets/Materials/Galaxy.mat @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Galaxy Background + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Materials/Galaxy.mat.meta b/Assets/Materials/Galaxy.mat.meta new file mode 100644 index 000000000..db3e8faed --- /dev/null +++ b/Assets/Materials/Galaxy.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57000f7bd59643b4f9a5731dea6468ef +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Materials/Zone Background.mat b/Assets/Materials/Zone.mat similarity index 100% rename from Assets/Materials/Zone Background.mat rename to Assets/Materials/Zone.mat diff --git a/Assets/Materials/Zone Background.mat.meta b/Assets/Materials/Zone.mat.meta similarity index 100% rename from Assets/Materials/Zone Background.mat.meta rename to Assets/Materials/Zone.mat.meta diff --git a/Assets/Scenes/Start.unity b/Assets/Scenes/Start.unity index 7ee2da43b..0af5ee36e 100644 --- a/Assets/Scenes/Start.unity +++ b/Assets/Scenes/Start.unity @@ -196,49 +196,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 313576382} m_CullTransparentMesh: 0 ---- !u!1 &518090073 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 518090075} - - component: {fileID: 518090074} - m_Layer: 0 - m_Name: NetworkManager - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &518090074 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 518090073} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a4689a973ca91db4c94ad7946b6cdf48, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!4 &518090075 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 518090073} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 533.1099, y: 377.69528, z: 10.5} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 3 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &519420028 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/CultClient.cs b/Assets/Scripts/CultClient.cs index ddbcee318..2e0fa6574 100644 --- a/Assets/Scripts/CultClient.cs +++ b/Assets/Scripts/CultClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Text; using LiteNetLib; @@ -33,9 +34,9 @@ public static class CultClient { Logger($"Sending message {MessagePackSerializer.SerializeToJson(m as Message)}"); _peer.Send(m as Message); } - else Logger("Cannot send message, client is not verified!"); + else OnError?.Invoke("Cannot send, client is not verified!"); } - else Logger("Cannot send message, client is not connected!"); + else OnError?.Invoke("Cannot send, client is not connected!"); } public static void ClearMessageListeners() @@ -52,25 +53,37 @@ public static class CultClient { public static void Login(string auth, string password) { - if (8 < password.Length && password.Length < 32) - Send(new LoginMessage - { - Auth = auth, - Password = _hasher.ComputeHash(Encoding.UTF8.GetBytes(password)) - }); - else Logger("Invalid Password"); + if (auth.Length>0) + { + if (8 <= password.Length && password.Length < 32) + Send(new LoginMessage + { + Auth = auth, + Password = _hasher.ComputeHash(Encoding.UTF8.GetBytes(password)) + }); + else OnError?.Invoke("Password Invalid: needs to be at least 8 characters"); + } + else OnError?.Invoke("Email/Username Empty"); } public static void Register(string email, string username, string password) { - if (8 < password.Length && password.Length < 32) - Send(new RegisterMessage + if (email.Length > 0) + { + if (username.Length > 0) { - Email = email, - Name = username, - Password = _hasher.ComputeHash(Encoding.UTF8.GetBytes(password)) - }); - else Logger("Invalid Password"); + if (8 <= password.Length && password.Length < 32) + Send(new RegisterMessage + { + Email = email, + Name = username, + Password = _hasher.ComputeHash(Encoding.UTF8.GetBytes(password)) + }); + else OnError?.Invoke("Password Invalid: needs to be at least 8 characters"); + } + else OnError?.Invoke("Username Empty"); + } + else OnError?.Invoke("Email Empty"); } public static void Connect(string host = "localhost", int port = 3075) diff --git a/Assets/Scripts/Editor/SerializerTestWindow.cs b/Assets/Scripts/Editor/SerializerTestWindow.cs index 5af73f71f..1d08eef81 100644 --- a/Assets/Scripts/Editor/SerializerTestWindow.cs +++ b/Assets/Scripts/Editor/SerializerTestWindow.cs @@ -52,6 +52,8 @@ public class SerializerTestWindow : EditorWindow } }; + Converter.Serializer.Converters.Add(new MathJsonConverter()); + // Set extensions to default resolver. var resolver = CompositeResolver.Create( MathResolver.Instance, @@ -61,8 +63,6 @@ public class SerializerTestWindow : EditorWindow var options = MessagePackSerializerOptions.Standard.WithResolver(resolver); MessagePackSerializer.DefaultOptions = options; - Converter.Serializer.Converters.Add(new MathJsonConverter()); - if (GUILayout.Button("Print MsgPack JSON")) Debug.Log(MessagePackSerializer.SerializeToJson(obj, options)); diff --git a/Assets/Scripts/ServerShared/DatabaseCache.cs b/Assets/Scripts/ServerShared/DatabaseCache.cs index ddf02ea32..b89b12724 100644 --- a/Assets/Scripts/ServerShared/DatabaseCache.cs +++ b/Assets/Scripts/ServerShared/DatabaseCache.cs @@ -76,7 +76,7 @@ public class DatabaseCache return _types[typeof(T)].Values.Cast(); } - public void Delete(DatabaseEntry entry) + public void Delete(DatabaseEntry entry, bool remote = false) { _entries.Remove(entry.ID); foreach (var type in _types.Values) @@ -84,6 +84,7 @@ public class DatabaseCache type.Remove(entry.ID); } - OnDataDeleteLocal?.Invoke(entry); + if (!remote) + OnDataDeleteLocal?.Invoke(entry); } } \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/GameContext.cs b/Assets/Scripts/ServerShared/GameContext.cs index d5de8ca15..fd5db14ea 100644 --- a/Assets/Scripts/ServerShared/GameContext.cs +++ b/Assets/Scripts/ServerShared/GameContext.cs @@ -6,11 +6,22 @@ using static Unity.Mathematics.math; public class GameContext { - private DatabaseCache _databaseCache; + private DatabaseCache _cache; - public GlobalData GlobalData => _databaseCache.GetAll().FirstOrDefault(); + public GlobalData GlobalData => _cache.GetAll().FirstOrDefault(); private readonly Dictionary Tier = new Dictionary(); + + public GameContext(DatabaseCache cache) + { + _cache = cache; + var globalData = _cache.GetAll().FirstOrDefault(); + if (globalData == null) + { + globalData = new GlobalData(); + _cache.Add(globalData); + } + } public float PlanetRadius(float mass) { @@ -21,29 +32,29 @@ public class GameContext { if (Tier.ContainsKey(itemData)) return Tier[itemData]; - Tier[itemData] = itemData.Ingredients.Keys.Max(ci => _databaseCache.Get(ci) is CraftedItemData craftableIngredient ? ItemTier(craftableIngredient) : 0); + Tier[itemData] = itemData.Ingredients.Keys.Max(ci => _cache.Get(ci) is CraftedItemData craftableIngredient ? ItemTier(craftableIngredient) : 0); return Tier[itemData]; } public ItemData GetData(ItemInstance item) { - return _databaseCache.Get(item.Data); + return _cache.Get(item.Data); } public SimpleCommodityData GetData(SimpleCommodity item) { - return _databaseCache.Get(item.Data); + return _cache.Get(item.Data); } public CraftedItemData GetData(CraftedItemInstance item) { - return _databaseCache.Get(item.Data); + return _cache.Get(item.Data); } public EquippableItemData GetData(Gear gear) { - return _databaseCache.Get(gear.Data); + return _cache.Get(gear.Data); } public float GetMass(ItemInstance item) @@ -128,7 +139,7 @@ public class GameContext public SimpleCommodity CreateInstance(Guid data, int count) { - var item = _databaseCache.Get(data); + var item = _cache.Get(data); if (item != null) return new SimpleCommodity { @@ -142,7 +153,7 @@ public class GameContext public CraftedItemInstance CreateInstance(Guid data, float quality) { - var item = _databaseCache.Get(data); + var item = _cache.Get(data); if (item == null) { throw new InvalidOperationException("Attempted to create crafted item instance using missing or incorrect item id"); @@ -150,7 +161,7 @@ public class GameContext var ingredients = item.Ingredients.SelectMany(ci => { - var ingredient = _databaseCache.Get(ci.Key); + var ingredient = _cache.Get(ci.Key); return ingredient is SimpleCommodityData ? (IEnumerable) new[] {CreateInstance(ci.Key, ci.Value)} : Enumerable.Range(0, ci.Value).Select(i => CreateInstance(ci.Key, quality)); diff --git a/Assets/Scripts/ServerShared/GlobalData.cs b/Assets/Scripts/ServerShared/GlobalData.cs index 37a936600..55f26783c 100644 --- a/Assets/Scripts/ServerShared/GlobalData.cs +++ b/Assets/Scripts/ServerShared/GlobalData.cs @@ -3,18 +3,19 @@ using System.Collections.Generic; using MessagePack; using Newtonsoft.Json; +[RethinkTable("Galaxy")] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] public class GlobalData : DatabaseEntry { [InspectableField] [JsonProperty("targetPersistenceDuration")] [Key(1)] - public float TargetPersistenceDuration; + public float TargetPersistenceDuration = 3; [InspectableField] [JsonProperty("heatRadiationPower")] [Key(2)] - public float HeatRadiationPower; + public float HeatRadiationPower = 1; [InspectableField] [JsonProperty("heatRadiationMultiplier")] [Key(3)] - public float HeatRadiationMultiplier; + public float HeatRadiationMultiplier = 1; [InspectableField] [JsonProperty("radiusPower")] [Key(4)] public float RadiusPower = 1.75f; diff --git a/Assets/Scripts/ServerShared/ItemBehaviors/Afterburner.cs b/Assets/Scripts/ServerShared/ItemBehaviors/Afterburner.cs index c60bf92a7..bab7ea30b 100644 --- a/Assets/Scripts/ServerShared/ItemBehaviors/Afterburner.cs +++ b/Assets/Scripts/ServerShared/ItemBehaviors/Afterburner.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using MessagePack; using Newtonsoft.Json; +[RethinkTable("Items")] [InspectableField] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] diff --git a/Assets/Scripts/ServerShared/ItemBehaviors/Cannon.cs b/Assets/Scripts/ServerShared/ItemBehaviors/Cannon.cs index 397aeefb6..75838417d 100644 --- a/Assets/Scripts/ServerShared/ItemBehaviors/Cannon.cs +++ b/Assets/Scripts/ServerShared/ItemBehaviors/Cannon.cs @@ -1,6 +1,7 @@ using MessagePack; using Newtonsoft.Json; +[RethinkTable("Items")] [InspectableField] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] diff --git a/Assets/Scripts/ServerShared/ItemBehaviors/Launcher.cs b/Assets/Scripts/ServerShared/ItemBehaviors/Launcher.cs index aa9f19277..bdc1d9b06 100644 --- a/Assets/Scripts/ServerShared/ItemBehaviors/Launcher.cs +++ b/Assets/Scripts/ServerShared/ItemBehaviors/Launcher.cs @@ -4,6 +4,7 @@ using MessagePack; using Newtonsoft.Json; using Unity.Mathematics; +[RethinkTable("Items")] [InspectableField] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] diff --git a/Assets/Scripts/ServerShared/ItemBehaviors/Reactor.cs b/Assets/Scripts/ServerShared/ItemBehaviors/Reactor.cs index cfd54b4dc..63cc7530c 100644 --- a/Assets/Scripts/ServerShared/ItemBehaviors/Reactor.cs +++ b/Assets/Scripts/ServerShared/ItemBehaviors/Reactor.cs @@ -1,6 +1,7 @@ using MessagePack; using Newtonsoft.Json; +[RethinkTable("Items")] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] public class ReactorBehaviorData : IItemBehaviorData diff --git a/Assets/Scripts/ServerShared/ItemBehaviors/Sensor.cs b/Assets/Scripts/ServerShared/ItemBehaviors/Sensor.cs index 42e0ccbbe..989a76446 100644 --- a/Assets/Scripts/ServerShared/ItemBehaviors/Sensor.cs +++ b/Assets/Scripts/ServerShared/ItemBehaviors/Sensor.cs @@ -2,6 +2,7 @@ using MessagePack; using Newtonsoft.Json; +[RethinkTable("Items")] [InspectableField] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] diff --git a/Assets/Scripts/ServerShared/ItemData.cs b/Assets/Scripts/ServerShared/ItemData.cs index 14cf39f0e..c59b7f1e9 100644 --- a/Assets/Scripts/ServerShared/ItemData.cs +++ b/Assets/Scripts/ServerShared/ItemData.cs @@ -36,6 +36,7 @@ public abstract class ItemData : DatabaseEntry, INamedEntry } } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] public class SimpleCommodityData : ItemData {} @@ -54,6 +55,7 @@ public abstract class CraftedItemData : ItemData public Dictionary Ingredients = new Dictionary(); } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] public class CompoundCommodityData : CraftedItemData {} @@ -123,6 +125,7 @@ public abstract class EquippableItemData : CraftedItemData } } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] @@ -133,6 +136,7 @@ public class GearData : EquippableItemData [IgnoreMember] public override HardpointType HardpointType => Hardpoint; } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] @@ -165,6 +169,7 @@ public class HullData : EquippableItemData [IgnoreMember] public override HardpointType HardpointType => HardpointType.Hull; } +[RethinkTable("Items")] [InspectableField] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] @@ -174,6 +179,7 @@ public class HardpointData public HardpointType Type; } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] @@ -188,6 +194,7 @@ public class ShieldData : EquippableItemData [IgnoreMember] public override HardpointType HardpointType => HardpointType.Shield; } +[RethinkTable("Items")] [Inspectable] [MessagePackObject] [JsonObject(MemberSerialization.OptIn)] diff --git a/Assets/Scripts/ServerShared/Messages.cs b/Assets/Scripts/ServerShared/Messages.cs index 93bb11b8e..e2ace967c 100644 --- a/Assets/Scripts/ServerShared/Messages.cs +++ b/Assets/Scripts/ServerShared/Messages.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using LiteNetLib; using MessagePack; +using Unity.Mathematics; [Union(0,typeof(PingMessage))] [Union(1,typeof(LoginMessage))] @@ -75,4 +76,35 @@ public class ChatBroadcastMessage : Message public class ChangeNameMessage : Message { [Key(0)] public string Name; +} + +[MessagePackObject] +public class GalaxyRequestMessage : Message { } + +[MessagePackObject] +public class GalaxyResponseMessage : Message +{ + [Key(0)] public GalaxyResponseZone[] Zones; +} + +[MessagePackObject] +public class GalaxyResponseZone +{ + [Key(0)] public Guid ZoneID; + [Key(1)] public Guid[] Links; + [Key(2)] public string Name; + [Key(3)] public float2 Position; +} + +[MessagePackObject] +public class ZoneRequestMessage : Message +{ + [Key(0)] public Guid ZoneID; +} + +[MessagePackObject] +public class ZoneResponseMessage : Message +{ + [Key(0)] public ZoneData Zone; + [Key(1)] public DatabaseEntry[] Contents; } \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/ZoneData.cs b/Assets/Scripts/ServerShared/ZoneData.cs index 37b6d7355..cb5133512 100644 --- a/Assets/Scripts/ServerShared/ZoneData.cs +++ b/Assets/Scripts/ServerShared/ZoneData.cs @@ -99,7 +99,7 @@ public class PlanetData : DatabaseEntry, INamedEntry public class OrbitData : DatabaseEntry { [JsonProperty("parent")] [Key(1)] - public Guid Parent; + public Guid? Parent; [JsonProperty("distance")] [Key(2)] public float Distance; diff --git a/Assets/Shaders/GalaxyMap.shader b/Assets/Shaders/GalaxyMap.shader index 69e2ad27f..7d3edc208 100644 --- a/Assets/Shaders/GalaxyMap.shader +++ b/Assets/Shaders/GalaxyMap.shader @@ -24,30 +24,14 @@ GlowAmount("GlowAmount", Float) = 0 GlowPower("GlowPower", Float) = 0 StarBoost("StarBoost", Float) = 0 - - // required for UI.Mask - _StencilComp ("Stencil Comparison", Float) = 8 - _Stencil ("Stencil ID", Float) = 0 - _StencilOp ("Stencil Operation", Float) = 0 - _StencilWriteMask ("Stencil Write Mask", Float) = 255 - _StencilReadMask ("Stencil Read Mask", Float) = 255 - _ColorMask ("Color Mask", Float) = 15 + GalaxyAmplitude("GalaxyAmplitude", Float) = 1 } SubShader { - Tags { "RenderType"="Opaque" } + Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" } LOD 100 - - // required for UI.Mask - Stencil - { - Ref [_Stencil] - Comp [_StencilComp] - Pass [_StencilOp] - ReadMask [_StencilReadMask] - WriteMask [_StencilWriteMask] - } - ColorMask [_ColorMask] + Blend One One + Cull Off Lighting Off ZWrite Off Pass { @@ -96,6 +80,7 @@ float GlowAmount; float GlowPower; float StarBoost; + float GalaxyAmplitude; v2f vert (appdata v) { @@ -130,11 +115,11 @@ float spokes = (sin(atan*Arms) + SpokeOffset) * SpokeScale; float noise = fBm(i.uv + NoisePosition.xx, 7); float shape = lerp(spokes - EdgeReduction * length(offset), 1, pow(circle + CoreBoostOffset, CoreBoostPower) * CoreBoost); - float gal = max(shape - noise * clamp(circle,0,1), 0); + float gal = max(shape - noise * clamp(circle,0,1), 0) * GalaxyAmplitude; float glow = (pow(circle,GlowPower) + GlowOffset) * GlowAmount; float stars = tex2D(_MainTex, i.uv2) * clamp(shape*StarBoost, 0, 1); fixed4 col = fixed4(gal*_CloudColor.rgb + glow*_GlowColor.rgb + stars.xxx, 1); - return col; + return col; } ENDCG } diff --git a/Assets/StrategyGameManager.cs b/Assets/StrategyGameManager.cs new file mode 100644 index 000000000..67e7e1ed6 --- /dev/null +++ b/Assets/StrategyGameManager.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +public class StrategyGameManager : MonoBehaviour +{ + public TabGroup PrimaryTabGroup; + public TabButton GalaxyTabButton; + public TabButton ZoneTabButton; + public Prototype GalaxyZonePrototype; + public Prototype GalaxyZoneLinkPrototype; + public float GalaxyScale; + public Prototype ZoneObjectPrototype; + + private DatabaseCache _cache; + private GameContext _context; + private Dictionary _galaxy; + private TabButton _currentTab; + private bool _galaxyPopulated; + + void Start() + { + PrimaryTabGroup.OnTabChange += button => + { + _currentTab = button; + if (button == GalaxyTabButton) + { + + } + }; + CultClient.AddMessageListener(galaxy => + { + _galaxy = galaxy.Zones.ToDictionary(z=>z.ZoneID); + if (_currentTab == GalaxyTabButton && !_galaxyPopulated) + { + _galaxyPopulated = true; + var linkedZones = new List(); + foreach (var zone in galaxy.Zones) + { + var instance = GalaxyZonePrototype.Instantiate(); + instance.position = float3(zone.Position,0) * GalaxyScale; + foreach (var linkedZone in zone.Links.Where(l=>linkedZones.Contains(l))) + { + var link = GalaxyZoneLinkPrototype.Instantiate(); + var diff = _galaxy[linkedZone].Position - zone.Position; + link.rotation = Quaternion.Euler(0,0,atan2(diff.y, diff.x) * Mathf.Rad2Deg); + link.localScale = new Vector3(length(diff) * GalaxyScale, 1, 1); + } + } + } + }); + CultClient.Send(new GalaxyRequestMessage()); + } + + void Update() + { + + } +} diff --git a/Assets/StrategyGameManager.cs.meta b/Assets/StrategyGameManager.cs.meta new file mode 100644 index 000000000..4d97486fa --- /dev/null +++ b/Assets/StrategyGameManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c1faf0a46b9cf24498cd5ec93c34c71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Economy.Server/Economy.Server.csproj b/Economy.Server/Economy.Server.csproj index f26636769..f96ca8953 100644 --- a/Economy.Server/Economy.Server.csproj +++ b/Economy.Server/Economy.Server.csproj @@ -8,6 +8,9 @@ + + + diff --git a/Economy.Server/Program.cs b/Economy.Server/Program.cs index 031ceab7a..d1d686aff 100644 --- a/Economy.Server/Program.cs +++ b/Economy.Server/Program.cs @@ -1,18 +1,241 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; using System.Threading.Tasks; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RethinkDb.Driver; +using RethinkDb.Driver.Net; +using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace ChatApp.Server { class Program { + private static readonly RethinkDB R = RethinkDB.R; + private static ILogger _logger; + static async Task Main(string[] args) { - var connection = RethinkDb.Driver.RethinkDB.R.Connection().Hostname("asgard.gamecult.games") + var serilogger = new LoggerConfiguration() + .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + var loggerFactory = LoggerFactory.Create(builder => + { + builder + // .AddFilter("Microsoft", LogLevel.Warning) + // .AddFilter("System", LogLevel.Warning) + // .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug) + .AddSerilog(serilogger) + .AddConsole(); + }); + _logger = loggerFactory.CreateLogger(); + + // Register extensions to Json.NET Serialization + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = new List + { + new MathJsonConverter(), + Converter.DateTimeConverter, + Converter.BinaryConverter, + Converter.GroupingConverter, + Converter.PocoExprConverter + } + }; + Converter.Serializer.Converters.Add(new MathJsonConverter()); + + // Register extensions to MessagePack Serialization + var resolver = CompositeResolver.Create( + MathResolver.Instance, + NativeGuidResolver.Instance, + StandardResolver.Instance + ); + var options = MessagePackSerializerOptions.Standard.WithResolver(resolver); + MessagePackSerializer.DefaultOptions = options; + + var connection = R.Connection().Hostname("asgard.gamecult.games") .Port(RethinkDBConstants.DefaultPort).Timeout(60).Connect(); + var cache = new DatabaseCache(); + + // When entries are changed locally, push the changes to RethinkDB + cache.OnDataUpdateLocal += async entry => + { + var table = entry.GetType().GetCustomAttribute()?.TableName ?? "Other"; + var result = await R.Db("Aetheria").Table(table).Update(entry).RunAsync(connection); + _logger.Log(LogLevel.Information, $"Uploaded entry to RethinkDB: {entry.ID} result: {result}"); + }; + + cache.OnDataInsertLocal += async entry => + { + var table = entry.GetType().GetCustomAttribute()?.TableName ?? "Other"; + var result = await R.Db("Aetheria").Table(table).Insert(entry).RunAsync(connection); + _logger.Log(LogLevel.Information, $"Inserted entry to RethinkDB: {entry.ID} result: {result}"); + }; + + cache.OnDataDeleteLocal += async entry => + { + var table = entry.GetType().GetCustomAttribute()?.TableName ?? "Other"; + var result = await R.Db("Aetheria").Table(table).Get(entry.ID).Delete().RunAsync(connection); + _logger.Log(LogLevel.Information, $"Deleted entry from RethinkDB: {entry.ID} result: {result}"); + }; + + // Get data from RethinkDB + GetTable("Items", connection, cache); + GetTable("Galaxy", connection, cache); + GetTable("Users", connection, cache); + + // Subscribe to changes from RethinkDB + SubscribeTable("Items", connection, cache); + SubscribeTable("Galaxy", connection, cache); + SubscribeTable("Users", connection, cache); + + var server = new MasterServer(cache) + { + Logger = _logger + }; + server.Start(); + + var context = new GameContext(cache); + + server.AddMessageListener(galaxyRequest => galaxyRequest.Peer.Send( + new GalaxyResponseMessage + { + Zones = cache.GetAll().Select(zd=> + new GalaxyResponseZone + { + Name = zd.Name, + Position = zd.Position, + ZoneID = zd.ID, + Links = zd.Wormholes.ToArray() + }).ToArray() + })); + + server.AddMessageListener(zoneRequest => + { + var zone = cache.Get(zoneRequest.ZoneID); + + // Zone has not been populated, generate the contents now! + if (!zone.Planets.Any()) + { + var planets = ZoneGenerator.GenerateEntities(context, zone, 10000, 2000); + + // Create collections to map between zone generator output and database entries + var orbitMap = new Dictionary(); + var orbitInverseMap = new Dictionary(); + + // Create orbit database entries + var orbitData = planets.Select(planet => + { + var data = new OrbitData() + { + ID = Guid.NewGuid(), + Distance = planet.Distance, + Period = planet.Period, + Phase = planet.Phase, + Zone = zone.ID + }; + orbitMap[planet] = data; + orbitInverseMap[data] = planet; + return data; + }).ToArray(); + + // Link OrbitData parents to database GUIDs + foreach (var data in orbitData) + data.Parent = orbitInverseMap[data].Parent != null + ? orbitMap[orbitInverseMap[data].Parent].ID + : (Guid?) null; + + foreach (var data in orbitData) cache.Add(data); + + var planetData = planets.Select(planet => + { + var planetDatum = new PlanetData + { + Mass = planet.Mass, + ID = Guid.NewGuid(), + Orbit = orbitMap[planet].ID, + Zone = zone.ID + }; + planetDatum.Name = planetDatum.ID.ToString().Substring(0, 8); + return planetDatum; + }).ToArray(); + + foreach (var data in planetData) cache.Add(data); + + zone.Planets = planetData.Select(pd => pd.ID).ToList(); + zone.Orbits = orbitData.Select(od => od.ID).ToList(); + cache.Add(zone); + } + zoneRequest.Peer.Send( + new ZoneResponseMessage + { + Zone = zone, + Contents = zone.Orbits.Select(id=>cache.Get(id)) + .Concat(zone.Planets.Select(id=>cache.Get(id))) + .Concat(zone.Stations.Select(id=>cache.Get(id))).ToArray() + }); + }); + + while (true) + { + Thread.Sleep(100); + } + } + + private static void SubscribeTable(string table, Connection connection, DatabaseCache cache) + { + Task.Run(async () => + { + var result = await R.Db("Aetheria").Table(table).Changes() + .RunChangesAsync(connection); + while (await result.MoveNextAsync()) + { + var change = result.Current; + if (change.OldValue == null) + { + _logger.Log(LogLevel.Information, + $"Received change from RethinkDB {table} table (Entry Created): {change.NewValue.GetType()} {(change.NewValue as INamedEntry)?.EntryName ?? ""}:{change.NewValue.ID}"); + cache.Add(change.NewValue, true); + } + else if (change.NewValue == null) + { + _logger.Log(LogLevel.Information, + $"Received change from RethinkDB {table} table (Entry Deleted): {change.OldValue.GetType()} {(change.OldValue as INamedEntry)?.EntryName ?? ""}:{change.OldValue.ID}"); + cache.Delete(change.NewValue, true); + } + else + { + _logger.Log(LogLevel.Information, + $"Received change from RethinkDB {table} table: {change.NewValue.GetType()} {(change.NewValue as INamedEntry)?.EntryName ?? ""}:{change.NewValue.ID}"); + cache.Add(change.NewValue, true); + } + + } + }); + } + + private static void GetTable(string table, Connection connection, DatabaseCache cache) + { + Task.Run(async () => + { + var result = await R.Db("Aetheria").Table(table).RunCursorAsync(connection); + while (await result.MoveNextAsync()) + { + var entry = result.Current; + _logger.Log(LogLevel.Information, + $"Received {table} entry from RethinkDB: {entry.GetType()} {(entry as INamedEntry)?.EntryName ?? ""}:{entry.ID}"); + cache.Add(entry, true); + } + }); } } } diff --git a/Economy.Server/Server.cs b/Economy.Server/Server.cs index 3b510ab21..af10bb9ae 100644 --- a/Economy.Server/Server.cs +++ b/Economy.Server/Server.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; public class MasterServer { - public ILogger Logger = null; + public ILogger Logger = null; private const string EmailPattern = @"^([0-9a-zA-Z]" + //Start with a digit or alphabetical @@ -160,7 +160,7 @@ using Microsoft.Extensions.Logging; if (!Argon2.Verify(userData.Password, login.Password)) { - peer.Send(new ErrorMessage {Error = "Password Wrong"}); + peer.Send(new ErrorMessage {Error = "Password Incorrect"}); return; } diff --git a/Economy.Server/obj/Debug/netcoreapp3.1/Economy.Server.assets.cache b/Economy.Server/obj/Debug/netcoreapp3.1/Economy.Server.assets.cache index a0fa245c933653de38b38398d0f961f4247589b4..55aa64da81d2d5cdcf72b97dcf36edb08a22eb31 100644 GIT binary patch delta 5686 zcmb7Idr(x@8Fx`+VTE;JVcF#&J7qz6EW5nq@=`=VMUa4sK~QU&UB{#IJMvJaxN~{>yqxwH}{_3 z_xpb5yXTyH?sxX3Yr4UI=%kv;ijt6V-eY1-!}*Zuq5_YxeR zoZ0kX*S-CPo7dzmW|H{j^R<^&$0yLSw1zPD+NTTjA z(4f|y?!KN*51k608&oRpR~cNC6mrOs!b$;b(QFggQrRZ50S{sWeu+-$bLoC&klD#n zD?8fzdz-x-J>8zQUQc%)tq7?qWZB1fnT#``&)L@58ORFwrLp<;l+JDRltwp0Ub8w? z-VEV&(q6-AyGwZ8d_|U-!V4T(x@pQJuOo}^&_gyGSUGIy0XSH|=kuu94W8bP&Yt$h zOlO{x?gT^z)eQsWHo9~hz?dk2Ex{3!8?&4)XJ&ev=Y@2)D|31#-3>60MAT%O(|jIM z;x3Q^CI~4MuptHDNFkpN6gm=y#hrZ}-Ou$oS9WxI8gsY{m)wQRm6uCb(;`bnPR=@+ zRn9Em(JW*!bX3HKj*8jP5pZ+_yoH}6nQl)irF*7CDi1wGgW-wvFmwxrMkLaNyhgeo zZKHv_c$z$3Z!hBrD3-H9v4RbXz@b=9%Z$hDbNCLdNH(zMvVjF0tVpVvQAnGohtYqe zNLn{}mSY~Hz7dt&Mg(v*=BkCOaK12y^+&<3d&VA`uQ;Ng4|!nVJix z(3BV(3&c`WjNXd3)Hm^Z5xJO>B34Pf#zyuD>7BWV4($?5SsiCZtJ8AMjN?eOxbhL@{HPrR&$8&u=ENk;E;->tFvlpwyoaYr(${q6S%@`<*6^X z_N$mL2xdPI^?+lI%DYy0fy+94m6uYqmp4?Wr@OVMt)si$IlQ}K7nK+2?U*nsND6WI zZUXC6(gQ+z9hqW#t?O0Z4Z^#gdQ4e#J+{)iQN`FK7#ro)csWI=tFqDcWZO{gl)vkADRgim z?^+G|@KGNzCXGu}Tn6#k1W2o^?^@AX6P+pU4LB`IQpJc?f=9Y2`8hVCV&Meg8$oou_7=5gr@KT7G5h0uLumG_VG3CI8 za5k;94$=A4-RZ<$u+3~>V@9>uFEjo!m86%_l?k=3Z6bnW)+SG46kK>Goxs zmF8t>vyU_TiIQ%TSetm6^wecY)lBCECz{VS+|r^H7Qqhauj131gjYCb>`E|n)ayWR z&WtL0YT?I%m~|wg)!|9T_sMhfPF8c#Qwzi~R9&^ zZi6&tS^AWOPk-fdb*b_MkGC*+L9#G@g=`oxl%*Ikt#Vas#JX7oGi$dTcQNx|m<7tET;B76;05e|`Lj{YqWjq+3ig%Pn?P z#QtJ_OwNd0N-s>&Eus6&M~&={e~B06T4|&!avOxhj6o6~> z|1+M$Q_>aoB3gM5uJS#iudw;+|6N{7g|x?Fpw%-MN zJi1-FwCH1%$|phvxIdL&v45F-PPrZ%OjWT#^v3KANnLqg7iz%$sojuk+~K-=mb97K zzTIaK%Z*q?j`(k_ zWRBN3m>5U@Rh2yraT>g#JqsQNM4H9HZ|(vo{=phBD3{BQ1Ub2~FLgs#T<(?XkDqK5|SgU9Qf%5m?>DV?gge5>sQ8~_6`<9WD? wd)hRD@ikfCN<+5U;4?q*;vVB2#jGgB_qd80deHD<06(dFxozlN)2)gB1HgYzH2?qr delta 3650 zcmai0Yfw~W7-msmT?AcZfx~@qQ3R6P0v%vMR6s7vMZ^#U5tRrxQ8d%>hNfoTNFFN{ zZ;&_AyhL6yQM7dGho(+7T_zp>)Qr}s|N3Dwz3=)hvvWEvJM+$-?|Gj0Iq!El-}klu zsoD0eCceD5AXn$+=C=I4r{&7U-XFr}ZJz28z3Tq`l}~P63yImQ>)l=OxW6;jQ0V4{ zZxYs>JGp8E4r|l#(Bg#`-ZSvM&V(ADJ=o`MLV$0RF_z5SDdH%GQ^ZqvQZVjC5r+f5 z%Z#tk6RRdtuquIqRgAN0BK{h^2&;XyVFqfkB8h?(i4?40d?>M%OGNIfmqr-m`3i9;`3k!yif3zekvNA-LjAH*@DO15_ z2{scxh7FQM!Db8Ag1KW;@T;L1wR#hZ0$QQ>Gie&IUY`y_U=l9)nQ$zqPRdi{=ZpM2 zgi7sFfr6bT*aDoFRvT%i>|;?x|2%TVf@PdXt_b>KD-H!Gp~Jv6#2_@JAi9(o9>FpS z&MK$i5oDaR7;nJy(c^I@Bv-1Smh*T|r#7oA&>vcc>d+FYQlXe36qTgtlByK!Ou<%R zN!TLeYl5w&f39VgU>WCHsyoAbv0u`LrBj0iwG{mN8VVLLzSv%@wN#6L!V8VF3E_er zmUZ;Z8I0R4;Tzd2)l6N-c`)g(ABJ_oCOzHU;~pU>WCL@bXvwatR2= zqeOo+%=GN*8TVKNOH2TAvb{W58i8G&2CS*mp?*ShpElOup$SAzWgyBE+BFk!FQG?T zM6ysiqHD#Q}b@LQF zNZq5VSVM{)yQ1E+0m5+gcgSg{y`tZ;=i*?NNmaFxRCV@s z(Up@2n?;-SzXt&+)n;H59T2uWEwTI?@}MA%n#*Rw9mhkMeFd}@kHAxX@SwBu$at4B zcug_x_?Fb1w-CQo96TfOa7vlwJwlv!jjhyXC&pblZ6kgKH2HZrJ2lgyJfyY@7sg$= z>>$3A=EaC>m0l>%TPmj!!u2?I$DQ=d^)Sv(jH~sm16FI6I0nmQ$``_@b6J3-zkFN~Fy$Mjy|#JDS`eVwK_lu3bztO!QCou&hI}YvKe>v{=;}4 z+%njUQsLt2E}>RKH!YnkoGDo#_bBT4Sk%F|t2&O*67JF*#hfXD#-l@MkHc7cLgp(2 z_t^2VoSIWCpppAtp|Rpv0v)2Znhcbx;uS8*Z=L__eP`lFhn+mBce@* zM+^Qy#OpWh_q(9J$;~5%;YAB4X@}FSjujU{Ul?uUG-f<%4My3V;J&N2LU;Hz8c|g@ R3eQ@-VQmcU%Wl2l`7a`=G%ElA diff --git a/Economy.Server/obj/Debug/netcoreapp3.1/Economy.Server.csprojAssemblyReference.cache b/Economy.Server/obj/Debug/netcoreapp3.1/Economy.Server.csprojAssemblyReference.cache index dfd99bfc03a1c2b18f10e53746918fddf1506efb..e943e6d7d03f36f7ac6f8582ba836ddc5503d432 100644 GIT binary patch literal 4002 zcmeHKO>7%Q6rQ!c_O6rGftFB002i7HNV51RCQg+I1&h#@G|)ILsBv(|yJKhYde_=r zyQHcFQiTweDyWnzqDO8OQY5%Q1+9WOAtcm7dq^+*rV@e*6$u4+&vu-jCXhf5jl@{{ zW@r6o=Dj!H`(}m+gpfa>V2!2G1>(g6T(wNw%sR@GC9Z4A2(|OJk}xgS5j@N+o0~>o zIHrV^kP_+$CQ7r8dP1Y6ll?Qw@@bcR;TZV(<6f2-;R1^=K) zwE?O+rsb>o(*Wis%tDb`%#v{2BtOnP@X_`jrh19lgt&<)Hb~7iJD#;PZcx46=la3; z6VH3V40y;nFkTknMaJno_-U1X=w(Bi=L`sLg4&D-LKAjGKfFE=gNZ$|cETq+7n9_iFrxicJ!bVrs?7Xb_Ar+aFl_h7Ym#R7SKKE-eKY4IfB zQxk?5!zKGsG@xh%A!4)rA7R!$Fb^?|w!2_z9(eLDYIrnOw7oV$&=xJH%q&;dfYo%l z1R_fv%STR*19mi}-?mvHqfZRc0;`7r@hPAnGx1-4d;d$R0`4u)_WQkCu_|{#WTT;I zxQ7Vn0vb<(M?akX=yFv#fFJ$dCae}KqpKC=tDj$zPh6@h-%(Y*bD47EHux}9Po%}^ zjMJ|zo4dPdOpQGr?umA>P`pP(FV+p2-zD*5I}s;RLIq>C@lCW@3>BDIr_<>Ph|SzD?^y3|OvI~JjF7K_tx2u73d1tDTCZ|;AJeq(5~ ziiw9HsC6>h1r50k#cmKw0q`}fcO3vb6xve&1-+0{3-ge)NsT5R(W)KcFQlu`I$F&<{Gu#qoJ$v%{^Ow z9POL>?2FUq3rE_{rG601hnu!ve*3~yuw{DS;=z-FPrq&HoKyR{F1+*l2RkP3X&#t0!5tnQ|3yuu5Lxp=R*O)cT9BRS64sWvqa0;$o7lekt093%*r~m)} literal 92495 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 tv7plEq|$$L&M94ymhON*= 1.0.0", "Isopoh.Cryptography.Argon2 >= 1.1.3", "MessagePack >= 2.1.90", + "Microsoft.Extensions.Logging.Console >= 5.0.0-preview.1.20120.4", + "Serilog.Extensions.Logging >= 3.0.2-dev-10272", + "Serilog.Sinks.File >= 5.0.0-dev-00864", "System.Reactive.Linq >= 4.3.2" ] }, @@ -3974,6 +4232,18 @@ "target": "Package", "version": "[2.1.90, )" }, + "Microsoft.Extensions.Logging.Console": { + "target": "Package", + "version": "[5.0.0-preview.1.20120.4, )" + }, + "Serilog.Extensions.Logging": { + "target": "Package", + "version": "[3.0.2-dev-10272, )" + }, + "Serilog.Sinks.File": { + "target": "Package", + "version": "[5.0.0-dev-00864, )" + }, "System.Reactive.Linq": { "target": "Package", "version": "[4.3.2, )" diff --git a/Economy.Server/obj/project.packagespec.json b/Economy.Server/obj/project.packagespec.json index 597d74d36..1d1c00eef 100644 --- a/Economy.Server/obj/project.packagespec.json +++ b/Economy.Server/obj/project.packagespec.json @@ -41,6 +41,18 @@ "target": "Package", "version": "[2.1.90, )" }, + "Microsoft.Extensions.Logging.Console": { + "target": "Package", + "version": "[5.0.0-preview.1.20120.4, )" + }, + "Serilog.Extensions.Logging": { + "target": "Package", + "version": "[3.0.2-dev-10272, )" + }, + "Serilog.Sinks.File": { + "target": "Package", + "version": "[5.0.0-dev-00864, )" + }, "System.Reactive.Linq": { "target": "Package", "version": "[4.3.2, )" diff --git a/README.md b/README.md index 190593645..2a30bc59b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,44 @@ -# Aetheria -This repository is the home of Aetheria, which could be described as a sci-fi hybrid ARPG/RTS MMO about a group of corporations colonizing a hostile alien galaxy, with a satirical narrative critiquing late-stage capitalism. +This repository is the home of Aetheria, a sci-fi hybrid ARPG/RTS MMO about a group of corporations colonizing a hostile alien galaxy, with a satirical narrative critiquing late-stage capitalism. + + + +## Table of Contents + +[toc] + +## Aetheria + +### Game Design The ARPG game design document is available [here](https://docs.google.com/document/d/1iULu1WsbuQoUM3c87XkGseb1P-8R5xlruoiyg03TsSE/edit?usp=sharing), while the RTS gameplay is documented [here](https://docs.google.com/document/d/1U3uGFqQboAiFJ_Y-nUOGpyixbXUHRbc5DiCuB59GM4w/edit?usp=sharing). The goal is to essentially create two games which both take place in the same persistent universe, allowing players with vastly different preferences to struggle together for the survival of mankind. Each instance of the game lasts until the inevitable destruction of the entire population at the hands of aliens, after which the universe resets. Each loop is designed to last up to a couple of months, during which the hostility of the aliens steadily increases until the players are unable to hold back the tide. -Previously I have built prototypes of the ARPG gameplay, [here's a video of the most recent one](https://www.youtube.com/watch?v=PNwVGtvefCg). While it included stations, AI opponents, multiple ships and a complex loadout system which simulates heat transfer between all of the ship's hardpoints with temperature affecting the performance of each item differently, the world was rather static and empty. +### Previous Work -As a result, the current focus is on the economy system, and building a client-server architecture for the networked simulation of a persistent universe. The goal is to create an RTS client, allowing players to take the role of a corporation, where they can define roles for their population, gather resources, build infrastructure, research new technology and produce items in order to make as much money as possible. +The concept for Aetheria goes back many years, during which I have steadily acquired my current skill with the primary objective of becoming competent enough to realize my vision. Previously I have built prototypes of the ARPG gameplay, [here's a video of the most recent one](https://www.youtube.com/watch?v=PNwVGtvefCg). While it included stations, AI opponents, multiple ships and a complex loadout system which simulates heat transfer between all of the ship's hardpoints with temperature affecting the performance of each item differently, the world was rather static and empty. + +### Current Work + +As a result of lessons learned, the current focus is on the economy system, and building a client-server architecture for the networked simulation of a persistent universe. The goal is to create an RTS client, allowing players to take the role of a corporation, where they can define roles for their population, gather resources, build infrastructure, research new technology and produce items in order to make as much money as possible. Once we have built a persistent universe with a dynamic economy, we will begin rebuilding the ARPG client allowing the player to take control of a single ship and engage in fast-paced combat, questing and trading. ## Architecture -There are two solutions in this repository. One is a Unity project containing the desktop client for Aetheria, and the other is a .NET Core server application intended to run on Linux cloud servers. There is some code shared between them, located at [Assets/Scripts/ServerShared](Assets/Scripts/ServerShared), defining a common protocol and data serialization format. Client-Server communication is implemented using [LiteNetLib](https://github.com/RevenantX/LiteNetLib), a reliable UDP transport library which we use to transmit [MessagePack](https://github.com/neuecc/MessagePack-CSharp) over the wire. +There are two solutions in this repository. One is a Unity project containing the desktop client for Aetheria, and the other is a .NET Core application intended to run on Linux cloud servers. There is some code shared between them, located at [Assets/Scripts/ServerShared](Assets/Scripts/ServerShared), defining a common protocol and data serialization format. Client-Server communication is implemented using [LiteNetLib](https://github.com/RevenantX/LiteNetLib), a reliable UDP transport library which we use to transmit [MessagePack](https://github.com/neuecc/MessagePack-CSharp) over the wire. Aetheria uses [RethinkDB](https://rethinkdb.com/) for data persistence. To make this possible, all persistent data is marked with attributes for both MessagePack and [JSON.Net](https://www.newtonsoft.com/json) serialization. During operation, the client does not communicate with the database server directly, only the game server does that; the game server caches data relevant to the game and sends it to the clients. -## Database Editor Tools +## Contributing + +### Getting the Files + +In order to checkout the project, you need a git client (Github's zip download will not work!). You also need to have installed [Git LFS (Large File Storage)](https://git-lfs.github.com/). This is necessary because assets in gamedev projects can get rather large, and Git is essentially a text versioning system that does not by itself support that use case well. + +### Choosing a Task + +We are organizing according to an [Agile development](https://en.wikipedia.org/wiki/Agile_software_development) schedule, with the progress of each sprint being tracked on its own board in the [Github Projects tab](https://github.com/rwvens/Aetheria-Economy/projects). If you wish to take on a task from the board, please contact us to become an official contributor so that the task can be assigned to you directly. Some issues are not on the sprint schedule, those are ideal for developers who want to jump in but are shy about joining. + +### Database Editor Tools In order to facilitate the creation and maintenance of game data, there is a Unity editor utility which communicates directly with RethinkDB. You can access the tools by selecting Window/Aetheria Database Tools in Unity's menu. This will cause two windows to appear, the Database List View and the Database Inspector. @@ -27,6 +50,20 @@ At the top of the list view there is a text field where you can enter the URL of You can unfold the categories of items in the list view to see what items exist. If you select an item, the Database Inspector will populate with all of the available fields of that item. Any changes you make in the Inspector will automatically be pushed to RethinkDB. If you've connected to the production database, this will update the stats of in-game items in real-time! +### Galaxy Editor + +When you select a galaxy asset in the Unity scene hierarchy, a custom editor opens in the inspector which enables the procedural generation of a new galaxy. There you can find some variables pertaining to the galaxy as a whole, such as the number and twist of the spiral arms. + +#### Map Layer Data + +Below that is an editor for map layer data which allows the creation of a density map defining the value of some variable as it varies over the space containing the galaxy, which can be previewed at the top of the inspector. By default the star density map layer is displayed, which controls the distribution of stars. Any number of map layers can be created, defining variables such as the radius of zones and the presence of life and resources to be mined. + +#### Star Tools + +After the map layer data section is a foldout containing tools which allow you to generate stars according to the star density map. Stars are placed by accumulating density while walking over a space-filling Hilbert curve, maintaining some minimum distance between stars. This isn't as good as a proper sampling algorithm like Poisson disk sampling or Mitchell’s best-candidate algorithm, but it gets the job done ([please feel free to contribute a better sampling algorithm!](https://github.com/rwvens/Aetheria-Economy/issues/15)). + +After generating stars, you can generate the links between them, which performs a Delaunay Tessellation, and then remove some proportion of links until the desired sparsity is reached. The algorithm for filtering star links is also not ideal, [there's an issue for fixing that, too!](https://github.com/rwvens/Aetheria-Economy/issues/25) + ## Contact Us If you want to chat, please join [our Discord server](https://discord.gg/trbteNj). \ No newline at end of file