From 4c9eea1c02eac0f3411b25037f05352cbc762bda Mon Sep 17 00:00:00 2001 From: Daniele Marotta Date: Thu, 21 May 2026 15:52:18 +0200 Subject: [PATCH] riversong code showcase --- README.md | 24 + Source/Engine/Collections/IMultiDictionary.cs | 35 ++ .../Engine/Collections/ListMultiDictionary.cs | 22 + Source/Engine/Collections/MultiDictionary.cs | 137 +++++ .../Attributes/DisableDiscoveryAttribute.cs | 9 + .../Attributes/GameSystemGroupAttribute.cs | 15 + .../Core/Attributes/InjectServiceAttribute.cs | 15 + .../Core/Attributes/ServiceAttribute.cs | 15 + .../Core/Attributes/SortingAttributes.cs | 57 ++ Source/Engine/Core/EngineRunner.cs | 155 +++++ Source/Engine/Core/EngineUpdateFilter.cs | 17 + Source/Engine/Core/GameSystem.cs | 14 + Source/Engine/Core/GameSystemGroup.cs | 83 +++ .../Core/Groups/DefaultGameSystemGroup.cs | 6 + .../Core/Groups/EarlyGameSystemGroup.cs | 9 + .../Engine/Core/Groups/LateGameSystemGroup.cs | 9 + .../Engine/Core/Groups/RootGameSystemGroup.cs | 7 + Source/Engine/Core/IEngine.cs | 13 + Source/Engine/Core/IGameSystem.cs | 7 + Source/Engine/Core/IInitializable.cs | 9 + Source/Engine/Core/IServiceLocator.cs | 13 + Source/Engine/Core/IServiceProvider.cs | 7 + Source/Engine/Core/IUpdatable.cs | 7 + Source/Engine/Core/IUpdateFilter.cs | 7 + Source/Engine/Core/ServiceLocator.cs | 53 ++ .../Engine/Core/ServiceLocatorExtensions.cs | 15 + Source/Engine/Core/SystemSorter.cs | 93 +++ Source/Engine/GameData/GameDataAsset.cs | 9 + Source/Engine/GameData/GameDatabase.cs | 61 ++ Source/Engine/GameData/GameDatabaseSystem.cs | 37 ++ Source/Engine/GameData/IGameDataRuntimeId.cs | 7 + Source/Engine/GameData/IGameDatabase.cs | 13 + Source/Engine/Helpers/AsyncBudget.cs | 32 ++ Source/Engine/Helpers/TopologicalSort.cs | 63 +++ Source/Riversong/Config/AppLinks.cs | 7 + Source/Riversong/Config/BuildVersionAsset.cs | 17 + Source/Riversong/Config/GameConfig.cs | 492 ++++++++++++++++ Source/Riversong/Config/IScene.cs | 30 + Source/Riversong/Config/SceneFolders.cs | 21 + .../Riversong/Config/UnityObjectInjector.cs | 64 +++ .../FinalizeInitializationSystemGroup.cs | 8 + .../GameInitializationCompletedSignal.cs | 6 + .../Game/AppLifecycle/GameStartedSignal.cs | 6 + ...NotifyGameInitializationCompletedSystem.cs | 29 + .../Game/AssetsLoading/PreLoadAssetsSystem.cs | 71 +++ .../Riversong/Game/Audio/AudioSystemGroup.cs | 8 + .../Game/Audio/BackgroundMusicSystem.cs | 82 +++ Source/Riversong/Game/Audio/ISoundPlayer.cs | 34 ++ .../Game/Audio/PlaySoundOnEventSystem.cs | 120 ++++ .../Riversong/Game/Audio/SoundPlayerSystem.cs | 184 ++++++ Source/Riversong/Game/Audio/SystemSoundId.cs | 21 + .../Game/Audio/SystemSoundLibrary.cs | 13 + Source/Riversong/Game/Camera/CameraSystem.cs | 145 +++++ .../Game/Camera/ICameraProperties.cs | 7 + .../Riversong/Game/Collections/NativeGrid.cs | 65 +++ .../Game/Collections/SpatialLookup.cs | 147 +++++ .../AnalyticsInitializationSystem.cs | 27 + .../Analytics/AnalyticsServiceFactory.cs | 14 + .../Analytics/AnalyticsSessionState.cs | 86 +++ .../Analytics/DemoAnalyticsSystem.cs | 89 +++ ...dingConstructionCompletedAnalyticsEvent.cs | 21 + .../Events/DemoCompletedAnalyticsEvent.cs | 36 ++ .../Events/HouseUpgradedAnalyticsEvent.cs | 31 + .../Events/SessionHeartbeatAnalyticsEvent.cs | 36 ++ .../Events/SessionStartedAnalyticsEvent.cs | 16 + .../Analytics/IAnalyticsService.cs | 19 + .../Analytics/NoOpAnalyticsService.cs | 32 ++ .../Analytics/UnityAnalyticsService.cs | 101 ++++ .../CommonServices/CommonServicesSystem.cs | 58 ++ .../CommonServicesSystemGroup.cs | 8 + .../Game/CommonServices/Entities/Entity.cs | 14 + .../Entities/EntityCache/EntityCache.cs | 81 +++ .../EntityCache/EntityCacheExtensions.cs | 67 +++ .../Entities/EntityCache/EntityCacheKeys.cs | 39 ++ .../Entities/EntityCache/EntityCacheSystem.cs | 71 +++ .../Entities/EntityCache/IEntityCache.cs | 16 + .../Entities/EntityCollection.cs | 90 +++ .../Entities/EntityCollectionCallbacks.cs | 21 + .../Entities/EntityCollectionExtensions.cs | 23 + .../Game/CommonServices/Entities/IEntity.cs | 7 + .../Entities/IEntityCollection.cs | 24 + .../Entities/IEntityCollectionCallbacks.cs | 18 + .../CommonServices/Pooling/IPoolingService.cs | 11 + .../CommonServices/Pooling/PooledObject.cs | 9 + .../CommonServices/Pooling/PoolingService.cs | 20 + .../Pooling/PoolingServiceExtensions.cs | 88 +++ .../Pooling/PoolsInitializationSystem.cs | 31 + .../RestoreTemporaryMaterialsSystem.cs | 21 + .../Game/CommonServices/Signals/ISignalBus.cs | 13 + .../Game/CommonServices/Signals/SignalBus.cs | 31 + .../TileMath/DirectionVectors.cs | 27 + .../CommonServices/TileMath/Directions.cs | 134 +++++ .../CommonServices/TileMath/ITileSpace.cs | 18 + .../Game/CommonServices/TileMath/TileMath.cs | 136 +++++ .../Game/CommonServices/TileMath/TileRange.cs | 88 +++ .../Game/CommonServices/TileMath/TileRect.cs | 71 +++ .../Game/CommonServices/TileMath/TileSpace.cs | 38 ++ .../Game/DebugCommands/DebugCommandsSystem.cs | 44 ++ .../Game/DebugCommands/DebugSystemGroup.cs | 8 + .../Game/DebugCommands/DrawGizmosSystem.cs | 61 ++ .../Game/DebugCommands/IDrawGizmos.cs | 7 + .../Game/Demo/DemoCompletedSignal.cs | 12 + Source/Riversong/Game/Demo/DemoSystem.cs | 41 ++ .../Game/EditTools/BuildTool/BuildTool.cs | 126 +++++ .../EditTools/BuildTool/BuildToolPreview.cs | 126 +++++ .../BuildTool/BuildToolPreviewManager.cs | 52 ++ .../EditTools/BuildTool/BuildToolValidator.cs | 30 + .../GameObjectsHighlightingSystem.cs | 191 +++++++ .../BuildTool/HideOnBuildingPreview.cs | 8 + .../BuildTool/IBuildToolPreviewManager.cs | 7 + .../CollectDeletedGameObjectsSignal.cs | 21 + ...ollectEditToolRelevantGameObjectsSignal.cs | 15 + .../Game/EditTools/DeleteTool/DeleteTool.cs | 57 ++ .../DeleteTool/DoDeleteToolSignal.cs | 12 + .../EditTools/DeletedGameObjectsFilter.cs | 18 + Source/Riversong/Game/EditTools/DragTool.cs | 96 ++++ Source/Riversong/Game/EditTools/EditTool.cs | 46 ++ .../Riversong/Game/EditTools/EditingState.cs | 30 + .../Game/EditTools/EditingStateGameSystem.cs | 151 +++++ .../Game/EditTools/IEditingService.cs | 15 + Source/Riversong/Game/EditTools/RoadTool.cs | 50 ++ .../Validation/EditToolValidationResult.cs | 13 + .../Validation/EditToolValidatorSystem.cs | 56 ++ .../Validation/IEditToolValidatorService.cs | 9 + .../Game/GameSpeed/GameSpeedSystem.cs | 64 +++ Source/Riversong/Game/GameSpeed/IGameSpeed.cs | 9 + .../Game/Helpers/GameObjectLayers.cs | 21 + .../Game/Input/CancelActionSystem.cs | 59 ++ .../Riversong/Game/Input/CancelActionType.cs | 11 + Source/Riversong/Game/Input/CancelActions.cs | 15 + Source/Riversong/Game/Input/ICancelAction.cs | 9 + .../Riversong/Game/Input/IPointerService.cs | 13 + Source/Riversong/Game/Input/PointerSystem.cs | 82 +++ .../Onboarding/OnboardingEventCompleted.cs | 12 + .../Game/Onboarding/OnboardingEvents.cs | 20 + .../Game/Onboarding/OnboardingMessages.cs | 21 + .../Game/Onboarding/OnboardingState.cs | 7 + .../Game/Onboarding/OnboardingSystem.cs | 78 +++ .../Game/Rendering/AoERenderingSystem.cs | 75 +++ .../Rendering/GlobalShaderParametersSystem.cs | 43 ++ .../Game/Rendering/IAoERenderingService.cs | 7 + .../Rendering/MaterialReplacementCache.cs | 132 +++++ .../RenderingInitializationGameSystem.cs | 16 + .../Game/Rendering/ShaderProperties.cs | 19 + .../Game/Rendering/WorldRenderingState.cs | 19 + Source/Riversong/Game/UI/DayNightUITheme.cs | 292 ++++++++++ .../Game/UI/DayNightUIThemeSystem.cs | 228 ++++++++ .../Riversong/Game/UI/Framework/IUIModel.cs | 10 + Source/Riversong/Game/UI/Framework/IUIRoot.cs | 19 + .../Game/UI/Framework/UIControllerSystem.cs | 54 ++ .../UI/Framework/UIInitializationSystem.cs | 66 +++ Source/Riversong/Game/UI/Framework/UIModel.cs | 33 ++ Source/Riversong/Game/UI/Framework/UIRoot.cs | 83 +++ .../Riversong/Game/UI/Framework/UIService.cs | 41 ++ .../Game/UI/Framework/UISystemGroup.cs | 8 + .../Game/UI/Framework/UITemplateLibrary.cs | 60 ++ Source/Riversong/Game/UI/Framework/UIView.cs | 85 +++ .../Game/UI/Framework/UIViewAttribute.cs | 15 + .../UI/Framework/UIVisibilityAnimation.cs | 112 ++++ .../Game/UI/Helpers/DraggableManipulator.cs | 66 +++ .../Game/UI/Helpers/TextFormatHelper.cs | 66 +++ .../UI/Helpers/VisualElementExtensions.cs | 40 ++ .../BuildMenu/BuildMenuBuildingModel.cs | 47 ++ .../Panels/BuildMenu/BuildMenuButtonUIView.cs | 89 +++ ...dMenuButtonUnlockAnimationStartedSignal.cs | 6 + .../UI/Panels/BuildMenu/BuildMenuModel.cs | 17 + .../BuildMenu/BuildMenuTooltipUIView.cs | 64 +++ .../Panels/BuildMenu/BuildMenuUIController.cs | 217 +++++++ .../UI/Panels/BuildMenu/BuildMenuUIView.cs | 141 +++++ .../BuildingPanel/BuildingPanelModel.cs | 64 +++ .../BuildingPanelUIController.cs | 109 ++++ .../BuildingPanel/BuildingPanelUIView.cs | 57 ++ .../HouseBuildingPanelSectionPresenter.cs | 92 +++ .../Panels/BuildingPanel/HousePanelModel.cs | 27 + .../BuildingPanel/HousePanelNeedModel.cs | 43 ++ .../BuildingPanel/HousePanelNeedUIView.cs | 47 ++ .../Panels/BuildingPanel/HousePanelUIView.cs | 116 ++++ .../HousePanelUpgradeMaterialModel.cs | 42 ++ .../HousePanelUpgradeMaterialUIView.cs | 37 ++ .../IBuildingPanelSectionPresenter.cs | 11 + .../StorageBuildingPanelSectionPresenter.cs | 95 ++++ .../Panels/BuildingPanel/StoragePanelModel.cs | 9 + .../BuildingPanel/StoragePanelProductModel.cs | 77 +++ .../StoragePanelProductUIView.cs | 89 +++ .../BuildingPanel/StoragePanelUIView.cs | 27 + .../BuildingPlacementTooltipController.cs | 81 +++ .../BuildingPlacementTooltipUIView.cs | 31 + .../DebugPanelUIControllerSystem.cs | 277 +++++++++ .../UI/Panels/Demo/DemoPanelUIController.cs | 45 ++ .../Game/UI/Panels/Demo/DemoPanelUIView.cs | 64 +++ .../UI/Panels/MainToolbar/MainToolbarModel.cs | 29 + .../MainToolbar/MainToolbarUIController.cs | 71 +++ .../Panels/MainToolbar/MainToolbarUIView.cs | 74 +++ .../Onboarding/OnboardingPanelUIController.cs | 88 +++ .../Onboarding/OnboardingPanelUIView.cs | 92 +++ .../PausePopup/PausePopupUIController.cs | 62 ++ .../UI/Panels/PausePopup/PausePopupUIView.cs | 43 ++ .../PopulationPanel/PopulationPanelModel.cs | 32 ++ .../PopulationPanelUIController.cs | 36 ++ .../PopulationPanel/PopulationPanelUIView.cs | 113 ++++ .../RuntimeTooltipUIController.cs | 54 ++ .../RuntimeTooltip/RuntimeTooltipUIView.cs | 52 ++ .../SpeedControlsPanelModel.cs | 16 + .../SpeedControlsPanelUIController.cs | 44 ++ .../SpeedControlsPanelUIView.cs | 77 +++ .../StorageTooltipController.cs | 125 +++++ .../StorageTooltip/StorageTooltipUIView.cs | 36 ++ .../UI/Panels/TimePanel/TimePanelModel.cs | 19 + .../Panels/TimePanel/TimePanelUIController.cs | 49 ++ .../UI/Panels/TimePanel/TimePanelUIView.cs | 45 ++ .../TitleScreen/TitleScreenUIController.cs | 68 +++ .../Panels/TitleScreen/TitleScreenUIView.cs | 118 ++++ .../WorldUI/BuildingBadgeUIController.cs | 125 +++++ .../UI/Panels/WorldUI/BuildingBadgeUIView.cs | 6 + .../Game/UI/Panels/WorldUI/IWorldUIService.cs | 14 + .../Panels/WorldUI/WorldUITrackingSystem.cs | 71 +++ Source/Riversong/Game/UI/UIState.cs | 7 + .../Riversong/Game/Unlocks/IUnlocksService.cs | 9 + .../Riversong/Game/Unlocks/UnlockCondition.cs | 22 + .../Game/Unlocks/UnlockConditionType.cs | 11 + .../Game/Unlocks/UnlockDefinition.cs | 22 + Source/Riversong/Game/Unlocks/UnlockType.cs | 9 + .../Game/Unlocks/UnlockUnlockedSignal.cs | 12 + .../Game/Unlocks/UnlocksManagerSystem.cs | 155 +++++ Source/Riversong/Game/Unlocks/UnlocksState.cs | 25 + Source/Riversong/Game/Vfx/AutoDestroyVfx.cs | 40 ++ .../Riversong/Game/Vfx/DustVfxProperties.cs | 18 + .../Riversong/Game/Vfx/IProjectileManager.cs | 9 + .../Game/Vfx/ProjectileManagerSystem.cs | 61 ++ Source/Riversong/Game/World/Agents/Agent.cs | 65 +++ .../Game/World/Agents/AgentAnimation.cs | 14 + .../Agents/AgentAnimationEventHandler.cs | 21 + .../Game/World/Agents/AgentAnimationSystem.cs | 68 +++ .../AgentCarriedProductVisualization.cs | 55 ++ .../World/Agents/AgentCommonLogicSystem.cs | 179 ++++++ .../Game/World/Agents/AgentDefinition.cs | 19 + .../Riversong/Game/World/Agents/AgentJob.cs | 17 + .../Game/World/Agents/AgentJobState.cs | 34 ++ .../Game/World/Agents/AgentLifecycleState.cs | 11 + .../Game/World/Agents/AgentManagerSystem.cs | 217 +++++++ .../Game/World/Agents/AgentSourceState.cs | 15 + .../World/Agents/AgentSpawnCooldownSystem.cs | 40 ++ .../World/Agents/AgentStateMachineStep.cs | 20 + .../Game/World/Agents/AgentVisualization.cs | 57 ++ .../Game/World/Agents/AgentsCleanUpSystem.cs | 52 ++ .../World/Agents/AgentsSpawnTickSystem.cs | 28 + .../World/Agents/ConstructionAgentSystem.cs | 114 ++++ .../Agents/Critters/CritterDefinition.cs | 24 + .../Game/World/Agents/Critters/CritterHerd.cs | 24 + .../Critters/CritterHerdMoveCenterSystem.cs | 37 ++ .../Agents/Critters/CritterSpawnSystem.cs | 80 +++ .../World/Agents/Critters/CritterState.cs | 11 + ...pawnProductStackAfterCritterDeathSystem.cs | 48 ++ .../Agents/Critters/UnlockCrittersSystem.cs | 37 ++ .../Agents/Critters/WorldCritterHerdsState.cs | 10 + .../Agents/DeSpawnAgentsAtNightSystem.cs | 52 ++ .../Game/World/Agents/FarmingAgentSystem.cs | 40 ++ .../Riversong/Game/World/Agents/FetchType.cs | 3 + .../Game/World/Agents/HarvesterAgentSystem.cs | 47 ++ .../Game/World/Agents/HouseAgentSystem.cs | 47 ++ .../Game/World/Agents/HunterBehaviorSystem.cs | 78 +++ .../Game/World/Agents/HunterSpawnSystem.cs | 45 ++ .../Game/World/Agents/IAgentCommonLogic.cs | 35 ++ .../Game/World/Agents/IAgentFactory.cs | 15 + .../Game/World/Agents/IAgentSourceEntity.cs | 9 + .../Agents/IAgentVisualizationCollection.cs | 7 + .../Game/World/Agents/Intents/AgentIntent.cs | 183 ++++++ .../World/Agents/Intents/AgentIntentType.cs | 47 ++ .../Intents/FireProjectileExecutionLogic.cs | 26 + .../Intents/FollowPathIntentExecutionLogic.cs | 41 ++ .../HarvestFertileTileIntentExecutionLogic.cs | 42 ++ .../HarvestResourceIntentExecutionLogic.cs | 31 + .../Agents/Intents/IIntentLogicExecutor.cs | 7 + .../Agents/Intents/IntentExecutionLogic.cs | 23 + .../Agents/Intents/IntentExecutionResult.cs | 11 + .../Agents/Intents/IntentExecutionState.cs | 11 + .../Agents/Intents/IntentExecutionSystem.cs | 108 ++++ .../Game/World/Agents/Intents/IntentQueue.cs | 156 ++++++ .../Intents/LookAtIntentExecutionLogic.cs | 63 +++ .../Intents/MakeLiveIntentExecutionLogic.cs | 50 ++ ...veToHarvestPositionIntentExecutionLogic.cs | 58 ++ .../Agents/Intents/PathQueryFailedSignal.cs | 12 + .../PathfindingIntentExecutionLogic.cs | 82 +++ .../PlayAnimationIntentExecutionLogic.cs | 52 ++ .../Intents/PutProductIntentExecutionLogic.cs | 44 ++ ...ertileTileLockStateIntentExecutionLogic.cs | 61 ++ .../TakeProductIntentExecutionLogic.cs | 43 ++ .../WaitForeverIntentExecutionLogic.cs | 10 + .../WaitSecondsIntentExecutionLogic.cs | 10 + .../Agents/Intents/WanderingExecutionLogic.cs | 74 +++ .../Agents/ProducerDeliveryAgentSystem.cs | 39 ++ .../Game/World/Agents/ProviderAgentSystem.cs | 36 ++ .../World/Agents/ResetAgentVelocitySystem.cs | 21 + .../Agents/ResetAgentsSpawnTickSystem.cs | 19 + ...geBuildingFetchProductStacksAgentSystem.cs | 63 +++ ...torageBuildingRequestProductAgentSystem.cs | 63 +++ .../SystemGroups/AgentSpawnSystemsGroup.cs | 9 + .../Agents/SystemGroups/AgentsSystemGroup.cs | 10 + .../DayOnlyAgentSpawnSystemsGroup.cs | 18 + .../SystemGroups/DefaultAgentsSystemGroup.cs | 9 + .../SystemGroups/EarlyAgentsSystemGroup.cs | 8 + .../SystemGroups/LateAgentsSystemGroup.cs | 9 + .../Game/World/Agents/WorldAgentsState.cs | 9 + .../Riversong/Game/World/BlockMap/BlockMap.cs | 37 ++ .../World/BlockMap/BlockMapUpdateSystem.cs | 79 +++ .../Game/World/BlockMap/BlockReason.cs | 32 ++ .../Game/World/Buildings/Building.cs | 61 ++ .../Game/World/Buildings/BuildingAoESystem.cs | 55 ++ .../World/Buildings/BuildingCreatedSignal.cs | 12 + .../World/Buildings/BuildingDefinition.cs | 115 ++++ .../World/Buildings/BuildingDeletedSignal.cs | 15 + .../World/Buildings/BuildingManagerSystem.cs | 323 +++++++++++ .../Buildings/BuildingSelectionSystem.cs | 110 ++++ .../World/Buildings/BuildingSleepState.cs | 13 + .../ConstructionCompletedSignal.cs | 12 + .../ConstructionSites/ConstructionSite.cs | 34 ++ .../ConstructionSiteDeletedSignal.cs | 12 + .../ConstructionSiteManager.cs | 171 ++++++ .../World/Buildings/DeleteBuildingOptions.cs | 9 + .../Game/World/Buildings/IBuildingFactory.cs | 7 + .../Game/World/Buildings/IBuildingShape.cs | 7 + .../World/Buildings/IBuildingSpatialQuery.cs | 21 + .../World/Buildings/IDeleteBuildingService.cs | 7 + .../SelectedBuildingChangedSignal.cs | 15 + .../Game/World/Buildings/TentRemovalSystem.cs | 44 ++ .../Visualizations/BuildingDeleteAnimation.cs | 102 ++++ .../BuildingDeleteAnimationCompletedSignal.cs | 12 + .../BuildingPlacementAnimation.cs | 123 ++++ ...ildingPlacementAnimationCompletedSignal.cs | 18 + ...BuildingPlacementAnimationStartedSignal.cs | 18 + .../BuildingStorageVisualization.cs | 75 +++ .../BuildingStorageVisualizationSystem.cs | 96 ++++ .../BuildingUpgradeAnimation.cs | 71 +++ .../Visualizations/BuildingVisualization.cs | 15 + .../BuildingVisualizationCreatedSignal.cs | 15 + .../BuildingVisualizationManager.cs | 163 ++++++ .../IBuildingVisualizationCollection.cs | 9 + .../Visualizations/ProducerAnimation.cs | 9 + .../Visualizations/ProducerAnimationSystem.cs | 32 ++ .../WindmillProducerAnimation.cs | 35 ++ .../Game/World/EntityIdMap/EntityIdMap.cs | 47 ++ .../EntityIdMap/EntityIdMapUpdateSystem.cs | 99 ++++ .../World/EntityIdMap/EntityIdMapValue.cs | 11 + .../Game/World/FertilityMap/FertilityMap.cs | 12 + .../World/FertilityMap/FertilityMapValue.cs | 17 + .../FertilityMap/UpdateFertilityMapSystem.cs | 92 +++ .../Pathfinding/FailedPathCacheSystem.cs | 58 ++ .../World/Pathfinding/IFailedPathCache.cs | 11 + .../Game/World/Pathfinding/IPathfinder.cs | 9 + .../Game/World/Pathfinding/PathQuery.cs | 21 + .../Game/World/Pathfinding/PathQueryResult.cs | 13 + .../Game/World/Pathfinding/PathSearchType.cs | 13 + .../World/Pathfinding/PathTraversalRules.cs | 22 + .../World/Pathfinding/PathfindingSystem.cs | 530 ++++++++++++++++++ .../Game/World/Pathfinding/TilePath.cs | 29 + .../Population/BuildingUpgradedSignal.cs | 12 + .../HouseTierCountTrackingSystem.cs | 85 +++ .../Population/PopulationChangedSignal.cs | 12 + .../Game/World/Population/PopulationNeed.cs | 21 + .../Population/PopulationNeedAuthoring.cs | 20 + .../World/Population/PopulationNeedType.cs | 7 + .../World/Population/PopulationNeedsState.cs | 21 + .../World/Population/PopulationNeedsSystem.cs | 126 +++++ .../Population/PopulationUpdateSystem.cs | 89 +++ .../Game/World/Population/TierUpgradeState.cs | 13 + .../World/Population/TierUpgradeSystem.cs | 93 +++ .../World/Population/WorldPopulationState.cs | 19 + .../Production/BuildingProductionState.cs | 15 + .../World/Production/EconomySystemGroup.cs | 8 + .../Game/World/Production/IProductCatalog.cs | 13 + .../World/Production/IProductStackFactory.cs | 15 + .../IProductStackVisualizationCollection.cs | 9 + .../Game/World/Production/LaborTier.cs | 15 + .../World/Production/LaborUpdateSystem.cs | 51 ++ .../Game/World/Production/ProducerState.cs | 9 + .../Game/World/Production/ProductAmount.cs | 36 ++ .../World/Production/ProductCatalogSystem.cs | 45 ++ .../World/Production/ProductDefinition.cs | 19 + .../Game/World/Production/ProductStack.cs | 27 + .../World/Production/ProductStackManager.cs | 216 +++++++ .../World/Production/ProductStacksState.cs | 113 ++++ .../Production/ProductionRequirements.cs | 14 + .../ProductionRequirementsGameSystem.cs | 33 ++ .../Production/ProductionTickGameSystem.cs | 66 +++ .../Riversong/Game/World/Production/Recipe.cs | 15 + .../Game/World/Production/RecipeDefinition.cs | 15 + .../World/Production/RestedWorkersSystem.cs | 154 +++++ .../Storage/IProductStorageCommonLogic.cs | 11 + .../Storage/IProductStorageEntity.cs | 9 + .../Storage/IProductStorageManager.cs | 9 + .../Production/Storage/ProductStorage.cs | 185 ++++++ .../Storage/ProductStorageManager.cs | 80 +++ .../Storage/ProductStoragePolicyState.cs | 82 +++ .../World/Production/WorldProductionState.cs | 17 + .../IDestroyRawResourceAnimation.cs | 9 + .../IRawResourceVisualizationCollection.cs | 9 + .../RawResourceHarvestedSignal.cs | 12 + .../RawResourceRenderingSystem.cs | 128 +++++ .../RawResources/RawResourcesRemovalSystem.cs | 63 +++ .../RawResources/RawResourcesRemovedSignal.cs | 24 + .../World/RawResources/RawResourcesState.cs | 90 +++ .../Game/World/RawResources/ResourceNode.cs | 21 + .../RawResources/ResourceNodeDefinition.cs | 37 ++ .../ResourceNodeHarvestingSystem.cs | 59 ++ .../World/RawResources/TreeFallAnimation.cs | 71 +++ .../Game/World/Roads/IRoadFactory.cs | 9 + .../Game/World/Roads/RoadManagerSystem.cs | 118 ++++ .../Riversong/Game/World/Roads/RoadNetwork.cs | 60 ++ .../Game/World/Roads/RoadTileUpdatedSignal.cs | 17 + .../World/Roads/RoadVisualizationManager.cs | 100 ++++ .../Game/World/Terrain/GrassLodGameSystem.cs | 49 ++ .../Game/World/Terrain/TerrainChunk.cs | 31 + .../World/Terrain/TerrainShaderDebugGUI.cs | 52 ++ .../Terrain/TerrainShaderParametersSystem.cs | 228 ++++++++ .../Terrain/TileHighlight/TileHighlight.cs | 61 ++ .../TileHighlight/TileHighlightSystem.cs | 73 +++ .../TileHighlight/TileHighlightType.cs | 11 + .../World/Time/DayNightCycleBlendUtility.cs | 5 + .../World/Time/DayNightCycleLightingSystem.cs | 133 +++++ .../Game/World/Time/DayNightCycleStep.cs | 13 + .../Time/DayNightCycleStepChangedSignal.cs | 12 + .../Game/World/Time/DayStartedSignal.cs | 6 + .../Game/World/Time/EndOfMonthSignal.cs | 6 + .../Game/World/Time/EndOfWeekSignal.cs | 6 + .../Game/World/Time/EndOfYearSignal.cs | 6 + .../Game/World/Time/NightStartedSignal.cs | 6 + .../Game/World/Time/WorldTimeState.cs | 21 + .../Game/World/Time/WorldTimeSystem.cs | 110 ++++ Source/Riversong/Game/World/WaterMap.cs | 22 + Source/Riversong/Game/World/World.cs | 68 +++ Source/Riversong/Game/World/WorldHeightmap.cs | 12 + .../ChunkGenerationJobState.cs | 24 + .../ChunkGenerators/ChunkMeshGenerator.cs | 124 ++++ .../CropsChunkMeshGenerator.cs | 70 +++ .../ChunkGenerators/CropsGrassTileMask.cs | 14 + .../ChunkGenerators/GenerateGrassChunkJob.cs | 151 +++++ .../GenerateTerrainChunkJob.cs | 158 ++++++ .../GrassChunkMeshGenerator.cs | 78 +++ .../GrassChunkMeshGeneratorBase.cs | 72 +++ .../WorldGen/ChunkGenerators/GrassTileMask.cs | 16 + .../ChunkGenerators/IGrassTileMask.cs | 7 + .../TerrainChunkMeshGenerator.cs | 68 +++ .../IOnWorldGenerationCompletedCallback.cs | 9 + .../Game/WorldGen/IWorldGeneratorOperation.cs | 9 + .../CritterHerdsGeneratorOperation.cs | 36 ++ .../FreeProductStacksGeneratorOperation.cs | 54 ++ .../MapDataInitializationOperation.cs | 125 +++++ .../StoneResourcesGeneratorOperation.cs | 127 +++++ .../Operations/TerrainGeneratorOperation.cs | 83 +++ ...TerrainMaterialsInitializationOperation.cs | 21 + .../TreeResourcesGeneratorOperation.cs | 86 +++ .../RequiresWorldReadyForUpdateAttribute.cs | 9 + .../Game/WorldGen/WorldCreationSystem.cs | 38 ++ .../Game/WorldGen/WorldGenSystemGroup.cs | 9 + .../WorldGenerationCompletedSignal.cs | 14 + .../WorldGenerationOperationsSystem.cs | 64 +++ .../Game/WorldGen/WorldReadySignal.cs | 6 + .../Game/WorldGen/WorldReadyUpdateFilter.cs | 58 ++ .../Tools/BuildVersionPreprocessor.cs | 50 ++ Source/Riversong/Tools/IconMaker.cs | 48 ++ Source/Riversong/Tools/MeshBaker.cs | 204 +++++++ Source/Riversong/Tools/WaterFlowMapBaker.cs | 91 +++ 462 files changed, 23406 insertions(+) create mode 100644 README.md create mode 100644 Source/Engine/Collections/IMultiDictionary.cs create mode 100644 Source/Engine/Collections/ListMultiDictionary.cs create mode 100644 Source/Engine/Collections/MultiDictionary.cs create mode 100644 Source/Engine/Core/Attributes/DisableDiscoveryAttribute.cs create mode 100644 Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs create mode 100644 Source/Engine/Core/Attributes/InjectServiceAttribute.cs create mode 100644 Source/Engine/Core/Attributes/ServiceAttribute.cs create mode 100644 Source/Engine/Core/Attributes/SortingAttributes.cs create mode 100644 Source/Engine/Core/EngineRunner.cs create mode 100644 Source/Engine/Core/EngineUpdateFilter.cs create mode 100644 Source/Engine/Core/GameSystem.cs create mode 100644 Source/Engine/Core/GameSystemGroup.cs create mode 100644 Source/Engine/Core/Groups/DefaultGameSystemGroup.cs create mode 100644 Source/Engine/Core/Groups/EarlyGameSystemGroup.cs create mode 100644 Source/Engine/Core/Groups/LateGameSystemGroup.cs create mode 100644 Source/Engine/Core/Groups/RootGameSystemGroup.cs create mode 100644 Source/Engine/Core/IEngine.cs create mode 100644 Source/Engine/Core/IGameSystem.cs create mode 100644 Source/Engine/Core/IInitializable.cs create mode 100644 Source/Engine/Core/IServiceLocator.cs create mode 100644 Source/Engine/Core/IServiceProvider.cs create mode 100644 Source/Engine/Core/IUpdatable.cs create mode 100644 Source/Engine/Core/IUpdateFilter.cs create mode 100644 Source/Engine/Core/ServiceLocator.cs create mode 100644 Source/Engine/Core/ServiceLocatorExtensions.cs create mode 100644 Source/Engine/Core/SystemSorter.cs create mode 100644 Source/Engine/GameData/GameDataAsset.cs create mode 100644 Source/Engine/GameData/GameDatabase.cs create mode 100644 Source/Engine/GameData/GameDatabaseSystem.cs create mode 100644 Source/Engine/GameData/IGameDataRuntimeId.cs create mode 100644 Source/Engine/GameData/IGameDatabase.cs create mode 100644 Source/Engine/Helpers/AsyncBudget.cs create mode 100644 Source/Engine/Helpers/TopologicalSort.cs create mode 100644 Source/Riversong/Config/AppLinks.cs create mode 100644 Source/Riversong/Config/BuildVersionAsset.cs create mode 100644 Source/Riversong/Config/GameConfig.cs create mode 100644 Source/Riversong/Config/IScene.cs create mode 100644 Source/Riversong/Config/SceneFolders.cs create mode 100644 Source/Riversong/Config/UnityObjectInjector.cs create mode 100644 Source/Riversong/Game/AppLifecycle/FinalizeInitializationSystemGroup.cs create mode 100644 Source/Riversong/Game/AppLifecycle/GameInitializationCompletedSignal.cs create mode 100644 Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs create mode 100644 Source/Riversong/Game/AppLifecycle/NotifyGameInitializationCompletedSystem.cs create mode 100644 Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs create mode 100644 Source/Riversong/Game/Audio/AudioSystemGroup.cs create mode 100644 Source/Riversong/Game/Audio/BackgroundMusicSystem.cs create mode 100644 Source/Riversong/Game/Audio/ISoundPlayer.cs create mode 100644 Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs create mode 100644 Source/Riversong/Game/Audio/SoundPlayerSystem.cs create mode 100644 Source/Riversong/Game/Audio/SystemSoundId.cs create mode 100644 Source/Riversong/Game/Audio/SystemSoundLibrary.cs create mode 100644 Source/Riversong/Game/Camera/CameraSystem.cs create mode 100644 Source/Riversong/Game/Camera/ICameraProperties.cs create mode 100644 Source/Riversong/Game/Collections/NativeGrid.cs create mode 100644 Source/Riversong/Game/Collections/SpatialLookup.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/AnalyticsInitializationSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/AnalyticsServiceFactory.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/AnalyticsSessionState.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/DemoAnalyticsSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/Events/BuildingConstructionCompletedAnalyticsEvent.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/Events/DemoCompletedAnalyticsEvent.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/Events/HouseUpgradedAnalyticsEvent.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/Events/SessionHeartbeatAnalyticsEvent.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/Events/SessionStartedAnalyticsEvent.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/IAnalyticsService.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/NoOpAnalyticsService.cs create mode 100644 Source/Riversong/Game/CommonServices/Analytics/UnityAnalyticsService.cs create mode 100644 Source/Riversong/Game/CommonServices/CommonServicesSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/CommonServicesSystemGroup.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/Entity.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCache.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheExtensions.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheKeys.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCache/IEntityCache.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCollection.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCollectionCallbacks.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/EntityCollectionExtensions.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/IEntity.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/IEntityCollection.cs create mode 100644 Source/Riversong/Game/CommonServices/Entities/IEntityCollectionCallbacks.cs create mode 100644 Source/Riversong/Game/CommonServices/Pooling/IPoolingService.cs create mode 100644 Source/Riversong/Game/CommonServices/Pooling/PooledObject.cs create mode 100644 Source/Riversong/Game/CommonServices/Pooling/PoolingService.cs create mode 100644 Source/Riversong/Game/CommonServices/Pooling/PoolingServiceExtensions.cs create mode 100644 Source/Riversong/Game/CommonServices/Pooling/PoolsInitializationSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/RestoreTemporaryMaterialsSystem.cs create mode 100644 Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs create mode 100644 Source/Riversong/Game/CommonServices/Signals/SignalBus.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/DirectionVectors.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/Directions.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/TileMath.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/TileRange.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/TileRect.cs create mode 100644 Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs create mode 100644 Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs create mode 100644 Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs create mode 100644 Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs create mode 100644 Source/Riversong/Game/DebugCommands/IDrawGizmos.cs create mode 100644 Source/Riversong/Game/Demo/DemoCompletedSignal.cs create mode 100644 Source/Riversong/Game/Demo/DemoSystem.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/BuildTool.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/BuildToolPreview.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/BuildToolPreviewManager.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/BuildToolValidator.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/GameObjectsHighlightingSystem.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/HideOnBuildingPreview.cs create mode 100644 Source/Riversong/Game/EditTools/BuildTool/IBuildToolPreviewManager.cs create mode 100644 Source/Riversong/Game/EditTools/CollectDeletedGameObjectsSignal.cs create mode 100644 Source/Riversong/Game/EditTools/CollectEditToolRelevantGameObjectsSignal.cs create mode 100644 Source/Riversong/Game/EditTools/DeleteTool/DeleteTool.cs create mode 100644 Source/Riversong/Game/EditTools/DeleteTool/DoDeleteToolSignal.cs create mode 100644 Source/Riversong/Game/EditTools/DeletedGameObjectsFilter.cs create mode 100644 Source/Riversong/Game/EditTools/DragTool.cs create mode 100644 Source/Riversong/Game/EditTools/EditTool.cs create mode 100644 Source/Riversong/Game/EditTools/EditingState.cs create mode 100644 Source/Riversong/Game/EditTools/EditingStateGameSystem.cs create mode 100644 Source/Riversong/Game/EditTools/IEditingService.cs create mode 100644 Source/Riversong/Game/EditTools/RoadTool.cs create mode 100644 Source/Riversong/Game/EditTools/Validation/EditToolValidationResult.cs create mode 100644 Source/Riversong/Game/EditTools/Validation/EditToolValidatorSystem.cs create mode 100644 Source/Riversong/Game/EditTools/Validation/IEditToolValidatorService.cs create mode 100644 Source/Riversong/Game/GameSpeed/GameSpeedSystem.cs create mode 100644 Source/Riversong/Game/GameSpeed/IGameSpeed.cs create mode 100644 Source/Riversong/Game/Helpers/GameObjectLayers.cs create mode 100644 Source/Riversong/Game/Input/CancelActionSystem.cs create mode 100644 Source/Riversong/Game/Input/CancelActionType.cs create mode 100644 Source/Riversong/Game/Input/CancelActions.cs create mode 100644 Source/Riversong/Game/Input/ICancelAction.cs create mode 100644 Source/Riversong/Game/Input/IPointerService.cs create mode 100644 Source/Riversong/Game/Input/PointerSystem.cs create mode 100644 Source/Riversong/Game/Onboarding/OnboardingEventCompleted.cs create mode 100644 Source/Riversong/Game/Onboarding/OnboardingEvents.cs create mode 100644 Source/Riversong/Game/Onboarding/OnboardingMessages.cs create mode 100644 Source/Riversong/Game/Onboarding/OnboardingState.cs create mode 100644 Source/Riversong/Game/Onboarding/OnboardingSystem.cs create mode 100644 Source/Riversong/Game/Rendering/AoERenderingSystem.cs create mode 100644 Source/Riversong/Game/Rendering/GlobalShaderParametersSystem.cs create mode 100644 Source/Riversong/Game/Rendering/IAoERenderingService.cs create mode 100644 Source/Riversong/Game/Rendering/MaterialReplacementCache.cs create mode 100644 Source/Riversong/Game/Rendering/RenderingInitializationGameSystem.cs create mode 100644 Source/Riversong/Game/Rendering/ShaderProperties.cs create mode 100644 Source/Riversong/Game/Rendering/WorldRenderingState.cs create mode 100644 Source/Riversong/Game/UI/DayNightUITheme.cs create mode 100644 Source/Riversong/Game/UI/DayNightUIThemeSystem.cs create mode 100644 Source/Riversong/Game/UI/Framework/IUIModel.cs create mode 100644 Source/Riversong/Game/UI/Framework/IUIRoot.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIControllerSystem.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIInitializationSystem.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIModel.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIRoot.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIService.cs create mode 100644 Source/Riversong/Game/UI/Framework/UISystemGroup.cs create mode 100644 Source/Riversong/Game/UI/Framework/UITemplateLibrary.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIView.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIViewAttribute.cs create mode 100644 Source/Riversong/Game/UI/Framework/UIVisibilityAnimation.cs create mode 100644 Source/Riversong/Game/UI/Helpers/DraggableManipulator.cs create mode 100644 Source/Riversong/Game/UI/Helpers/TextFormatHelper.cs create mode 100644 Source/Riversong/Game/UI/Helpers/VisualElementExtensions.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuBuildingModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuButtonUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuButtonUnlockAnimationStartedSignal.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuTooltipUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/BuildingPanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/BuildingPanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/BuildingPanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HouseBuildingPanelSectionPresenter.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelNeedModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelNeedUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelUpgradeMaterialModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/HousePanelUpgradeMaterialUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/IBuildingPanelSectionPresenter.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/StorageBuildingPanelSectionPresenter.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/StoragePanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/StoragePanelProductModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/StoragePanelProductUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPanel/StoragePanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPlacementTooltip/BuildingPlacementTooltipController.cs create mode 100644 Source/Riversong/Game/UI/Panels/BuildingPlacementTooltip/BuildingPlacementTooltipUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/DebugPanel/DebugPanelUIControllerSystem.cs create mode 100644 Source/Riversong/Game/UI/Panels/Demo/DemoPanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/Demo/DemoPanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/MainToolbar/MainToolbarModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/MainToolbar/MainToolbarUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/MainToolbar/MainToolbarUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/Onboarding/OnboardingPanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/Onboarding/OnboardingPanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/PausePopup/PausePopupUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/PausePopup/PausePopupUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/PopulationPanel/PopulationPanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/PopulationPanel/PopulationPanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/PopulationPanel/PopulationPanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/RuntimeTooltip/RuntimeTooltipUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/RuntimeTooltip/RuntimeTooltipUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/SpeedControlsPanel/SpeedControlsPanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/SpeedControlsPanel/SpeedControlsPanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/SpeedControlsPanel/SpeedControlsPanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/StorageTooltip/StorageTooltipController.cs create mode 100644 Source/Riversong/Game/UI/Panels/StorageTooltip/StorageTooltipUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/TimePanel/TimePanelModel.cs create mode 100644 Source/Riversong/Game/UI/Panels/TimePanel/TimePanelUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/TimePanel/TimePanelUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/TitleScreen/TitleScreenUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/TitleScreen/TitleScreenUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/WorldUI/BuildingBadgeUIController.cs create mode 100644 Source/Riversong/Game/UI/Panels/WorldUI/BuildingBadgeUIView.cs create mode 100644 Source/Riversong/Game/UI/Panels/WorldUI/IWorldUIService.cs create mode 100644 Source/Riversong/Game/UI/Panels/WorldUI/WorldUITrackingSystem.cs create mode 100644 Source/Riversong/Game/UI/UIState.cs create mode 100644 Source/Riversong/Game/Unlocks/IUnlocksService.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlockCondition.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlockConditionType.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlockDefinition.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlockType.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlockUnlockedSignal.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlocksManagerSystem.cs create mode 100644 Source/Riversong/Game/Unlocks/UnlocksState.cs create mode 100644 Source/Riversong/Game/Vfx/AutoDestroyVfx.cs create mode 100644 Source/Riversong/Game/Vfx/DustVfxProperties.cs create mode 100644 Source/Riversong/Game/Vfx/IProjectileManager.cs create mode 100644 Source/Riversong/Game/Vfx/ProjectileManagerSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Agent.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentAnimation.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentAnimationEventHandler.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentAnimationSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentCarriedProductVisualization.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentCommonLogicSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentDefinition.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentJob.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentJobState.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentLifecycleState.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentManagerSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentSourceState.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentSpawnCooldownSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentStateMachineStep.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentVisualization.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentsCleanUpSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/AgentsSpawnTickSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/ConstructionAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/CritterDefinition.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/CritterHerd.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/CritterHerdMoveCenterSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/CritterSpawnSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/CritterState.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/SpawnProductStackAfterCritterDeathSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/UnlockCrittersSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Critters/WorldCritterHerdsState.cs create mode 100644 Source/Riversong/Game/World/Agents/DeSpawnAgentsAtNightSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/FarmingAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/FetchType.cs create mode 100644 Source/Riversong/Game/World/Agents/HarvesterAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/HouseAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/HunterBehaviorSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/HunterSpawnSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/IAgentCommonLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/IAgentFactory.cs create mode 100644 Source/Riversong/Game/World/Agents/IAgentSourceEntity.cs create mode 100644 Source/Riversong/Game/World/Agents/IAgentVisualizationCollection.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/AgentIntent.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/AgentIntentType.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/FireProjectileExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/FollowPathIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/HarvestFertileTileIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/HarvestResourceIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IIntentLogicExecutor.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IntentExecutionResult.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IntentExecutionState.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IntentExecutionSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/IntentQueue.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/LookAtIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/MakeLiveIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/MoveToHarvestPositionIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/PathQueryFailedSignal.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/PathfindingIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/PlayAnimationIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/PutProductIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/SetFertileTileLockStateIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/TakeProductIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/WaitForeverIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/WaitSecondsIntentExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/Intents/WanderingExecutionLogic.cs create mode 100644 Source/Riversong/Game/World/Agents/ProducerDeliveryAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/ProviderAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/ResetAgentVelocitySystem.cs create mode 100644 Source/Riversong/Game/World/Agents/ResetAgentsSpawnTickSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/StorageBuildingFetchProductStacksAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/StorageBuildingRequestProductAgentSystem.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/AgentSpawnSystemsGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/AgentsSystemGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/DayOnlyAgentSpawnSystemsGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/DefaultAgentsSystemGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/EarlyAgentsSystemGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/SystemGroups/LateAgentsSystemGroup.cs create mode 100644 Source/Riversong/Game/World/Agents/WorldAgentsState.cs create mode 100644 Source/Riversong/Game/World/BlockMap/BlockMap.cs create mode 100644 Source/Riversong/Game/World/BlockMap/BlockMapUpdateSystem.cs create mode 100644 Source/Riversong/Game/World/BlockMap/BlockReason.cs create mode 100644 Source/Riversong/Game/World/Buildings/Building.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingAoESystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingCreatedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingDefinition.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingDeletedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingManagerSystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingSelectionSystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/BuildingSleepState.cs create mode 100644 Source/Riversong/Game/World/Buildings/ConstructionSites/ConstructionCompletedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/ConstructionSites/ConstructionSite.cs create mode 100644 Source/Riversong/Game/World/Buildings/ConstructionSites/ConstructionSiteDeletedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/ConstructionSites/ConstructionSiteManager.cs create mode 100644 Source/Riversong/Game/World/Buildings/DeleteBuildingOptions.cs create mode 100644 Source/Riversong/Game/World/Buildings/IBuildingFactory.cs create mode 100644 Source/Riversong/Game/World/Buildings/IBuildingShape.cs create mode 100644 Source/Riversong/Game/World/Buildings/IBuildingSpatialQuery.cs create mode 100644 Source/Riversong/Game/World/Buildings/IDeleteBuildingService.cs create mode 100644 Source/Riversong/Game/World/Buildings/SelectedBuildingChangedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/TentRemovalSystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingDeleteAnimation.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingDeleteAnimationCompletedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingPlacementAnimation.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingPlacementAnimationCompletedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingPlacementAnimationStartedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingStorageVisualization.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingStorageVisualizationSystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingUpgradeAnimation.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingVisualization.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingVisualizationCreatedSignal.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/BuildingVisualizationManager.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/IBuildingVisualizationCollection.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/ProducerAnimation.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/ProducerAnimationSystem.cs create mode 100644 Source/Riversong/Game/World/Buildings/Visualizations/WindmillProducerAnimation.cs create mode 100644 Source/Riversong/Game/World/EntityIdMap/EntityIdMap.cs create mode 100644 Source/Riversong/Game/World/EntityIdMap/EntityIdMapUpdateSystem.cs create mode 100644 Source/Riversong/Game/World/EntityIdMap/EntityIdMapValue.cs create mode 100644 Source/Riversong/Game/World/FertilityMap/FertilityMap.cs create mode 100644 Source/Riversong/Game/World/FertilityMap/FertilityMapValue.cs create mode 100644 Source/Riversong/Game/World/FertilityMap/UpdateFertilityMapSystem.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/FailedPathCacheSystem.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/IFailedPathCache.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/IPathfinder.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/PathQuery.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/PathQueryResult.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/PathSearchType.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/PathTraversalRules.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/PathfindingSystem.cs create mode 100644 Source/Riversong/Game/World/Pathfinding/TilePath.cs create mode 100644 Source/Riversong/Game/World/Population/BuildingUpgradedSignal.cs create mode 100644 Source/Riversong/Game/World/Population/HouseTierCountTrackingSystem.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationChangedSignal.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationNeed.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationNeedAuthoring.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationNeedType.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationNeedsState.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationNeedsSystem.cs create mode 100644 Source/Riversong/Game/World/Population/PopulationUpdateSystem.cs create mode 100644 Source/Riversong/Game/World/Population/TierUpgradeState.cs create mode 100644 Source/Riversong/Game/World/Population/TierUpgradeSystem.cs create mode 100644 Source/Riversong/Game/World/Population/WorldPopulationState.cs create mode 100644 Source/Riversong/Game/World/Production/BuildingProductionState.cs create mode 100644 Source/Riversong/Game/World/Production/EconomySystemGroup.cs create mode 100644 Source/Riversong/Game/World/Production/IProductCatalog.cs create mode 100644 Source/Riversong/Game/World/Production/IProductStackFactory.cs create mode 100644 Source/Riversong/Game/World/Production/IProductStackVisualizationCollection.cs create mode 100644 Source/Riversong/Game/World/Production/LaborTier.cs create mode 100644 Source/Riversong/Game/World/Production/LaborUpdateSystem.cs create mode 100644 Source/Riversong/Game/World/Production/ProducerState.cs create mode 100644 Source/Riversong/Game/World/Production/ProductAmount.cs create mode 100644 Source/Riversong/Game/World/Production/ProductCatalogSystem.cs create mode 100644 Source/Riversong/Game/World/Production/ProductDefinition.cs create mode 100644 Source/Riversong/Game/World/Production/ProductStack.cs create mode 100644 Source/Riversong/Game/World/Production/ProductStackManager.cs create mode 100644 Source/Riversong/Game/World/Production/ProductStacksState.cs create mode 100644 Source/Riversong/Game/World/Production/ProductionRequirements.cs create mode 100644 Source/Riversong/Game/World/Production/ProductionRequirementsGameSystem.cs create mode 100644 Source/Riversong/Game/World/Production/ProductionTickGameSystem.cs create mode 100644 Source/Riversong/Game/World/Production/Recipe.cs create mode 100644 Source/Riversong/Game/World/Production/RecipeDefinition.cs create mode 100644 Source/Riversong/Game/World/Production/RestedWorkersSystem.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/IProductStorageCommonLogic.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/IProductStorageEntity.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/IProductStorageManager.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/ProductStorage.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/ProductStorageManager.cs create mode 100644 Source/Riversong/Game/World/Production/Storage/ProductStoragePolicyState.cs create mode 100644 Source/Riversong/Game/World/Production/WorldProductionState.cs create mode 100644 Source/Riversong/Game/World/RawResources/IDestroyRawResourceAnimation.cs create mode 100644 Source/Riversong/Game/World/RawResources/IRawResourceVisualizationCollection.cs create mode 100644 Source/Riversong/Game/World/RawResources/RawResourceHarvestedSignal.cs create mode 100644 Source/Riversong/Game/World/RawResources/RawResourceRenderingSystem.cs create mode 100644 Source/Riversong/Game/World/RawResources/RawResourcesRemovalSystem.cs create mode 100644 Source/Riversong/Game/World/RawResources/RawResourcesRemovedSignal.cs create mode 100644 Source/Riversong/Game/World/RawResources/RawResourcesState.cs create mode 100644 Source/Riversong/Game/World/RawResources/ResourceNode.cs create mode 100644 Source/Riversong/Game/World/RawResources/ResourceNodeDefinition.cs create mode 100644 Source/Riversong/Game/World/RawResources/ResourceNodeHarvestingSystem.cs create mode 100644 Source/Riversong/Game/World/RawResources/TreeFallAnimation.cs create mode 100644 Source/Riversong/Game/World/Roads/IRoadFactory.cs create mode 100644 Source/Riversong/Game/World/Roads/RoadManagerSystem.cs create mode 100644 Source/Riversong/Game/World/Roads/RoadNetwork.cs create mode 100644 Source/Riversong/Game/World/Roads/RoadTileUpdatedSignal.cs create mode 100644 Source/Riversong/Game/World/Roads/RoadVisualizationManager.cs create mode 100644 Source/Riversong/Game/World/Terrain/GrassLodGameSystem.cs create mode 100644 Source/Riversong/Game/World/Terrain/TerrainChunk.cs create mode 100644 Source/Riversong/Game/World/Terrain/TerrainShaderDebugGUI.cs create mode 100644 Source/Riversong/Game/World/Terrain/TerrainShaderParametersSystem.cs create mode 100644 Source/Riversong/Game/World/Terrain/TileHighlight/TileHighlight.cs create mode 100644 Source/Riversong/Game/World/Terrain/TileHighlight/TileHighlightSystem.cs create mode 100644 Source/Riversong/Game/World/Terrain/TileHighlight/TileHighlightType.cs create mode 100644 Source/Riversong/Game/World/Time/DayNightCycleBlendUtility.cs create mode 100644 Source/Riversong/Game/World/Time/DayNightCycleLightingSystem.cs create mode 100644 Source/Riversong/Game/World/Time/DayNightCycleStep.cs create mode 100644 Source/Riversong/Game/World/Time/DayNightCycleStepChangedSignal.cs create mode 100644 Source/Riversong/Game/World/Time/DayStartedSignal.cs create mode 100644 Source/Riversong/Game/World/Time/EndOfMonthSignal.cs create mode 100644 Source/Riversong/Game/World/Time/EndOfWeekSignal.cs create mode 100644 Source/Riversong/Game/World/Time/EndOfYearSignal.cs create mode 100644 Source/Riversong/Game/World/Time/NightStartedSignal.cs create mode 100644 Source/Riversong/Game/World/Time/WorldTimeState.cs create mode 100644 Source/Riversong/Game/World/Time/WorldTimeSystem.cs create mode 100644 Source/Riversong/Game/World/WaterMap.cs create mode 100644 Source/Riversong/Game/World/World.cs create mode 100644 Source/Riversong/Game/World/WorldHeightmap.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/ChunkGenerationJobState.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/ChunkMeshGenerator.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/CropsChunkMeshGenerator.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/CropsGrassTileMask.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/GenerateGrassChunkJob.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/GenerateTerrainChunkJob.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/GrassChunkMeshGenerator.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/GrassChunkMeshGeneratorBase.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/GrassTileMask.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/IGrassTileMask.cs create mode 100644 Source/Riversong/Game/WorldGen/ChunkGenerators/TerrainChunkMeshGenerator.cs create mode 100644 Source/Riversong/Game/WorldGen/IOnWorldGenerationCompletedCallback.cs create mode 100644 Source/Riversong/Game/WorldGen/IWorldGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/CritterHerdsGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/FreeProductStacksGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/MapDataInitializationOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/StoneResourcesGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/TerrainGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/TerrainMaterialsInitializationOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/Operations/TreeResourcesGeneratorOperation.cs create mode 100644 Source/Riversong/Game/WorldGen/RequiresWorldReadyForUpdateAttribute.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldCreationSystem.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldGenSystemGroup.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldGenerationCompletedSignal.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldGenerationOperationsSystem.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldReadySignal.cs create mode 100644 Source/Riversong/Game/WorldGen/WorldReadyUpdateFilter.cs create mode 100644 Source/Riversong/Tools/BuildVersionPreprocessor.cs create mode 100644 Source/Riversong/Tools/IconMaker.cs create mode 100644 Source/Riversong/Tools/MeshBaker.cs create mode 100644 Source/Riversong/Tools/WaterFlowMapBaker.cs diff --git a/README.md b/README.md new file mode 100644 index 0000000..97911d7 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Riversong Code Showcase + +This repository is a client-facing code sample for **Riversong**, a **Unity 6** project. + +Its purpose is to provide a focused view of my engineering work, architecture, and coding style without including the full production project. + +## What Is Included + +- Selected C# source code only +- Gameplay, UI, systems, and supporting engine/framework code used in the project + +## What Is Not Included + +- Unity assets +- Scenes, prefabs, materials, and other content files +- Project settings and the full runnable Unity project + +This repository is intentionally trimmed down so clients can review the code clearly and without unrelated project files. + +## Project Page + +The public itch.io page for the project is here: + +[Riversong on itch.io](https://danis-workshop.itch.io/riversong) diff --git a/Source/Engine/Collections/IMultiDictionary.cs b/Source/Engine/Collections/IMultiDictionary.cs new file mode 100644 index 0000000..18fe48e --- /dev/null +++ b/Source/Engine/Collections/IMultiDictionary.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IMultiDictionary : IEnumerable> where C : ICollection + { + C this[K key] { get; } + + Dictionary.KeyCollection Keys { get; } + + int KeyCount { get; } + + int ValueCount { get; } + + int Count(K key); + + bool Add(K key, V value); + + bool Remove(K key, V value); + + bool Remove(V value); + + void Clear(K key); + + void Clear(); + + bool ContainsKey(K key); + + bool ContainsValue(K key, V value); + + bool ContainsValue(V value); + + bool TryGetValues(K key, out C values); + } +} \ No newline at end of file diff --git a/Source/Engine/Collections/ListMultiDictionary.cs b/Source/Engine/Collections/ListMultiDictionary.cs new file mode 100644 index 0000000..acb3752 --- /dev/null +++ b/Source/Engine/Collections/ListMultiDictionary.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class ListMultiDictionary : MultiDictionary> + { + public ListMultiDictionary(IEqualityComparer keyComparer = null) : base(keyComparer) + { + } + + protected override List CreateCollection() + { + return new List(); + } + + protected override bool AddToCollection(V value, List collection) + { + collection.Add(value); + return true; + } + } +} \ No newline at end of file diff --git a/Source/Engine/Collections/MultiDictionary.cs b/Source/Engine/Collections/MultiDictionary.cs new file mode 100644 index 0000000..f77bc9b --- /dev/null +++ b/Source/Engine/Collections/MultiDictionary.cs @@ -0,0 +1,137 @@ +using System.Collections; +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class MultiDictionary : IMultiDictionary where C : ICollection + { + private readonly Dictionary _collections; + + protected MultiDictionary(IEqualityComparer keyComparer = null) + { + _collections = keyComparer != null ? new Dictionary(keyComparer) : new Dictionary(); + } + + public C this[K key] => _collections[key]; + + public Dictionary.KeyCollection Keys => _collections.Keys; + + public int KeyCount => _collections.Count; + + public int ValueCount { get; private set; } + + protected abstract C CreateCollection(); + + protected abstract bool AddToCollection(V value, C collection); + + public int Count(K key) + { + return _collections.TryGetValue(key, out var collection) ? collection.Count : 0; + } + + public bool Add(K key, V value) + { + if (!_collections.TryGetValue(key, out var collection)) + { + collection = CreateCollection(); + _collections.Add(key, collection); + } + + if (AddToCollection(value, collection)) + { + ValueCount++; + return true; + } + + return false; + } + + public bool Remove(K key, V value) + { + if (_collections.TryGetValue(key, out var collection) && collection.Remove(value)) + { + ValueCount--; + if (collection.Count == 0) _collections.Remove(key); + return true; + } + return false; + } + + public bool Remove(V value) + { + var valueRemoved = false; + var removeCollection = false; + K collectionKeyToRemove = default; + foreach (var kvp in _collections) + { + var collection = kvp.Value; + if (collection.Remove(value)) + { + valueRemoved = true; + ValueCount--; + if (collection.Count == 0) + { + removeCollection = true; + collectionKeyToRemove = kvp.Key; + } + break; + } + } + if (removeCollection) _collections.Remove(collectionKeyToRemove); + return valueRemoved; + } + + public void Clear(K key) + { + if (_collections.TryGetValue(key, out var collection)) + { + ValueCount -= collection.Count; + collection.Clear(); + } + } + + public void Clear() + { + _collections.Clear(); + ValueCount = 0; + } + + public bool ContainsKey(K key) + { + return _collections.ContainsKey(key); + } + + public bool ContainsValue(K key, V value) + { + return _collections.TryGetValue(key, out var collection) && collection.Contains(value); + } + + public bool ContainsValue(V value) + { + foreach (var collection in _collections.Values) + if (collection.Contains(value)) + return true; + return false; + } + + public bool TryGetValues(K key, out C values) + { + return _collections.TryGetValue(key, out values); + } + + public IEnumerator> GetEnumerator() + { + foreach (var kvp in _collections) + { + var key = kvp.Key; + var collection = kvp.Value; + foreach (var value in collection) yield return new KeyValuePair(key, value); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Attributes/DisableDiscoveryAttribute.cs b/Source/Engine/Core/Attributes/DisableDiscoveryAttribute.cs new file mode 100644 index 0000000..d6e0878 --- /dev/null +++ b/Source/Engine/Core/Attributes/DisableDiscoveryAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [AttributeUsage(AttributeTargets.Class)] + public class DisableDiscoveryAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs b/Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs new file mode 100644 index 0000000..ea0fdf9 --- /dev/null +++ b/Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [AttributeUsage(AttributeTargets.Class)] + public class GameSystemGroupAttribute : Attribute + { + public GameSystemGroupAttribute(Type systemGroupType) + { + SystemGroupType = systemGroupType; + } + + public Type SystemGroupType { get; set; } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Attributes/InjectServiceAttribute.cs b/Source/Engine/Core/Attributes/InjectServiceAttribute.cs new file mode 100644 index 0000000..448dfb5 --- /dev/null +++ b/Source/Engine/Core/Attributes/InjectServiceAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [AttributeUsage(AttributeTargets.Field)] + public class InjectServiceAttribute : Attribute + { + public InjectServiceAttribute(Type serviceType = null) + { + ServiceType = serviceType; + } + + public Type ServiceType { get; set; } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Attributes/ServiceAttribute.cs b/Source/Engine/Core/Attributes/ServiceAttribute.cs new file mode 100644 index 0000000..71dc647 --- /dev/null +++ b/Source/Engine/Core/Attributes/ServiceAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class ServiceAttribute : Attribute + { + public ServiceAttribute(Type serviceType = null) + { + ServiceType = serviceType; + } + + public Type ServiceType { get; set; } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Attributes/SortingAttributes.cs b/Source/Engine/Core/Attributes/SortingAttributes.cs new file mode 100644 index 0000000..75a36f7 --- /dev/null +++ b/Source/Engine/Core/Attributes/SortingAttributes.cs @@ -0,0 +1,57 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class SortingAttribute : Attribute + { + protected SortingAttribute(Type systemType) + { + SystemType = systemType; + } + + public Type SystemType { get; set; } + } + + public class InitializeAfterAttribute : SortingAttribute + { + public InitializeAfterAttribute(Type systemType) : base(systemType) + { + } + } + + public class InitializeBeforeAttribute : SortingAttribute + { + public InitializeBeforeAttribute(Type systemType) : base(systemType) + { + } + } + + public class UpdateAfterAttribute : SortingAttribute + { + public UpdateAfterAttribute(Type systemType) : base(systemType) + { + } + } + + public class UpdateBeforeAttribute : SortingAttribute + { + public UpdateBeforeAttribute(Type systemType) : base(systemType) + { + } + } + + public class DisposeAfterAttribute : SortingAttribute + { + public DisposeAfterAttribute(Type systemType) : base(systemType) + { + } + } + + public class DisposeBeforeAttribute : SortingAttribute + { + public DisposeBeforeAttribute(Type systemType) : base(systemType) + { + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/EngineRunner.cs b/Source/Engine/Core/EngineRunner.cs new file mode 100644 index 0000000..0873fb4 --- /dev/null +++ b/Source/Engine/Core/EngineRunner.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EngineRunner : MonoBehaviour, IEngine + { + private ServiceLocator _serviceLocator; + + private RootGameSystemGroup _rootSystemGroup; + + private List _systemGroups = new(); + + private Dictionary _systemGroupsByType = new(); + + private EngineUpdateFilter _updateFilter = new(); + + public bool IsInitialized { get; private set; } + + public List Systems { get; } = new(); + + public void Start() + { + StartAsync().Forget(Debug.LogException); + } + + private async UniTask StartAsync() + { + Debug.Log("Engine started"); + + _serviceLocator = new ServiceLocator(); + + _serviceLocator.RegisterService(typeof(IEngine), this); + + var serviceProviders = GetComponentsInChildren(); + foreach (var serviceProvider in serviceProviders) serviceProvider.RegisterServices(_serviceLocator); + + await UniTask.NextFrame(); + + _rootSystemGroup = new RootGameSystemGroup(); + _systemGroups.Add(_rootSystemGroup); + _systemGroupsByType.Add(_rootSystemGroup.GetType(), _rootSystemGroup); + + Debug.Log("Systems discovery started"); + DiscoverSystems(); + Debug.Log($"Systems discovery completed. Discovered {_rootSystemGroup.Systems.Count} system groups and {Systems.Count} systems"); + + await UniTask.NextFrame(); + + foreach (var systemGroup in _systemGroups) _serviceLocator.Inject(systemGroup); + foreach (var system in Systems) _serviceLocator.Inject(system); + + await UniTask.NextFrame(); + + var types = Systems.Select(obj => obj.GetType()).Concat(_systemGroups.Select(obj => obj.GetType())).Distinct().ToList(); + SystemSorter.InitializeSorters(types); + + await UniTask.NextFrame(); + + Debug.Log("Systems initialization started"); + await InitializeAsync(); + Debug.Log("Systems initialization completed"); + + IsInitialized = true; + } + + private void DiscoverSystems() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assembly in assemblies) + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || !type.IsSubclassOf(typeof(GameSystemGroup)) || type.GetCustomAttribute() != null) continue; + + var systemGroup = (GameSystemGroup)Activator.CreateInstance(type); + systemGroup.UpdateFilter = _updateFilter; + + _systemGroups.Add(systemGroup); + _systemGroupsByType.Add(type, systemGroup); + } + + foreach (var systemGroup in _systemGroups) + { + if (systemGroup == _rootSystemGroup) continue; + + var parent = GetContainingSystemGroup(systemGroup.GetType(), typeof(RootGameSystemGroup)); + parent.Add(systemGroup); + } + + foreach (var assembly in assemblies) + foreach (var type in assembly.GetTypes()) + { + if (type.IsAbstract || !type.IsSubclassOf(typeof(GameSystem)) || type.GetCustomAttribute() != null) continue; + + var system = CreateSystem(type); + Systems.Add(system); + + var systemGroup = GetContainingSystemGroup(type, typeof(DefaultGameSystemGroup)); + systemGroup.Add(system); + } + } + + private GameSystem CreateSystem(Type systemType) + { + var system = (GameSystem)Activator.CreateInstance(systemType, _serviceLocator); + + var serviceAttributes = systemType.GetCustomAttributes(); + foreach (var serviceAttribute in serviceAttributes) + { + var serviceType = serviceAttribute?.ServiceType ?? systemType; + _serviceLocator.RegisterService(serviceType, system); + } + + if (system is IServiceProvider serviceProvider) serviceProvider.RegisterServices(_serviceLocator); + + return system; + } + + private GameSystemGroup GetContainingSystemGroup(Type type, Type defaultGroup) + { + var systemGroupAttribute = type.GetCustomAttribute(); + var systemGroupType = systemGroupAttribute?.SystemGroupType ?? defaultGroup; + return _systemGroupsByType[systemGroupType]; + } + + private async UniTask InitializeAsync() + { + await _rootSystemGroup.InitializeAsync(); + } + + private void Update() + { + if (!IsInitialized) return; + + _rootSystemGroup.Update(); + } + + private void OnDestroy() + { + Debug.Log("Systems disposing started"); + _rootSystemGroup.Dispose(); + Debug.Log("Systems disposing completed"); + } + + public void RegisterUpdateFilter(IUpdateFilter filter) + { + _updateFilter.Filters.Add(filter); + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/EngineUpdateFilter.cs b/Source/Engine/Core/EngineUpdateFilter.cs new file mode 100644 index 0000000..e8926c3 --- /dev/null +++ b/Source/Engine/Core/EngineUpdateFilter.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EngineUpdateFilter : IUpdateFilter + { + public List Filters { get; } = new(); + + public bool CanUpdate(IUpdatable updatable) + { + foreach (var updateFilter in Filters) + if (!updateFilter.CanUpdate(updatable)) + return false; + return true; + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/GameSystem.cs b/Source/Engine/Core/GameSystem.cs new file mode 100644 index 0000000..28021e0 --- /dev/null +++ b/Source/Engine/Core/GameSystem.cs @@ -0,0 +1,14 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class GameSystem : IGameSystem + { + protected GameSystem(IServiceLocator serviceLocator) + { + ServiceLocator = serviceLocator; + } + + public virtual string Name => GetType().Name; + + protected IServiceLocator ServiceLocator { get; } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/GameSystemGroup.cs b/Source/Engine/Core/GameSystemGroup.cs new file mode 100644 index 0000000..6bd3bf5 --- /dev/null +++ b/Source/Engine/Core/GameSystemGroup.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class GameSystemGroup : IGameSystem, IInitializable, IUpdatable, IDisposable + { + private List _initializables = new(); + + private List _updatables = new(); + + private List _disposables = new(); + + public virtual string Name => GetType().Name; + + public List Systems { get; } = new(); + + public IUpdateFilter UpdateFilter { get; set; } + + public void Add(IGameSystem system) + { + Systems.Add(system); + if (system is IInitializable initializable) _initializables.Add(initializable); + if (system is IUpdatable updatable) _updatables.Add(updatable); + if (system is IDisposable disposable) _disposables.Add(disposable); + } + + public virtual async UniTask InitializeAsync() + { + SystemSorter.InitializableSorter.Sort(_initializables); + SystemSorter.UpdatableSorter.Sort(_updatables); + SystemSorter.DisposableSorter.Sort(_disposables); + + var batches = SystemSorter.InitializableSorter.CreateExecutionBatches(_initializables); + foreach (var batch in batches) + { + var tasks = new UniTask[batch.Count]; + for (var i = 0; i < batch.Count; i++) tasks[i] = InitializeAndLogAsync(batch[i]); + + await UniTask.WhenAll(tasks); + } + } + + private static async UniTask InitializeAndLogAsync(IInitializable initializable) + { + var startTime = Time.unscaledTime; + + await initializable.InitializeAsync(); + + LogInitializationTime(initializable, startTime); + } + + private static void LogInitializationTime(IInitializable initializable, float startTime) + { + if (initializable is GameSystemGroup) return; + + var elapsed = Time.unscaledTime - startTime; + + var log = $"Initialized {((IGameSystem)initializable).Name} in {(int)(elapsed * 1000)} ms"; + if (elapsed > 0.3f) log = $"{log}"; + + Debug.Log(log); + } + + public virtual void Update() + { + foreach (var updatable in _updatables) + { + if (UpdateFilter != null && !UpdateFilter.CanUpdate(updatable)) continue; + + updatable.Update(); + } + } + + public virtual void Dispose() + { + foreach (var disposable in _disposables) disposable.Dispose(); + } + } +} diff --git a/Source/Engine/Core/Groups/DefaultGameSystemGroup.cs b/Source/Engine/Core/Groups/DefaultGameSystemGroup.cs new file mode 100644 index 0000000..62ca0ab --- /dev/null +++ b/Source/Engine/Core/Groups/DefaultGameSystemGroup.cs @@ -0,0 +1,6 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class DefaultGameSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Groups/EarlyGameSystemGroup.cs b/Source/Engine/Core/Groups/EarlyGameSystemGroup.cs new file mode 100644 index 0000000..0f088a3 --- /dev/null +++ b/Source/Engine/Core/Groups/EarlyGameSystemGroup.cs @@ -0,0 +1,9 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + [InitializeBefore(typeof(DefaultGameSystemGroup))] + [UpdateBefore(typeof(DefaultGameSystemGroup))] + [DisposeBefore(typeof(DefaultGameSystemGroup))] + public class EarlyGameSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Groups/LateGameSystemGroup.cs b/Source/Engine/Core/Groups/LateGameSystemGroup.cs new file mode 100644 index 0000000..c11bcf9 --- /dev/null +++ b/Source/Engine/Core/Groups/LateGameSystemGroup.cs @@ -0,0 +1,9 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + [InitializeAfter(typeof(DefaultGameSystemGroup))] + [UpdateAfter(typeof(DefaultGameSystemGroup))] + [DisposeAfter(typeof(DefaultGameSystemGroup))] + public class LateGameSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Engine/Core/Groups/RootGameSystemGroup.cs b/Source/Engine/Core/Groups/RootGameSystemGroup.cs new file mode 100644 index 0000000..dd998b8 --- /dev/null +++ b/Source/Engine/Core/Groups/RootGameSystemGroup.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + [DisableDiscovery] + public class RootGameSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IEngine.cs b/Source/Engine/Core/IEngine.cs new file mode 100644 index 0000000..e316a9c --- /dev/null +++ b/Source/Engine/Core/IEngine.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEngine + { + public bool IsInitialized { get; } + + public List Systems { get; } + + public void RegisterUpdateFilter(IUpdateFilter filter); + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IGameSystem.cs b/Source/Engine/Core/IGameSystem.cs new file mode 100644 index 0000000..69af766 --- /dev/null +++ b/Source/Engine/Core/IGameSystem.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IGameSystem + { + public string Name { get; } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IInitializable.cs b/Source/Engine/Core/IInitializable.cs new file mode 100644 index 0000000..dc78735 --- /dev/null +++ b/Source/Engine/Core/IInitializable.cs @@ -0,0 +1,9 @@ +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IInitializable + { + UniTask InitializeAsync(); + } +} diff --git a/Source/Engine/Core/IServiceLocator.cs b/Source/Engine/Core/IServiceLocator.cs new file mode 100644 index 0000000..4d8d7cf --- /dev/null +++ b/Source/Engine/Core/IServiceLocator.cs @@ -0,0 +1,13 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IServiceLocator + { + void RegisterService(Type serviceType, object service); + + object GetService(Type serviceType); + + void Inject(object target); + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IServiceProvider.cs b/Source/Engine/Core/IServiceProvider.cs new file mode 100644 index 0000000..6669585 --- /dev/null +++ b/Source/Engine/Core/IServiceProvider.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IServiceProvider + { + void RegisterServices(IServiceLocator serviceLocator); + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IUpdatable.cs b/Source/Engine/Core/IUpdatable.cs new file mode 100644 index 0000000..6f6da78 --- /dev/null +++ b/Source/Engine/Core/IUpdatable.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IUpdatable + { + void Update(); + } +} \ No newline at end of file diff --git a/Source/Engine/Core/IUpdateFilter.cs b/Source/Engine/Core/IUpdateFilter.cs new file mode 100644 index 0000000..8fccb91 --- /dev/null +++ b/Source/Engine/Core/IUpdateFilter.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IUpdateFilter + { + bool CanUpdate(IUpdatable updatable); + } +} \ No newline at end of file diff --git a/Source/Engine/Core/ServiceLocator.cs b/Source/Engine/Core/ServiceLocator.cs new file mode 100644 index 0000000..3ec2bbe --- /dev/null +++ b/Source/Engine/Core/ServiceLocator.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class ServiceLocator : IServiceLocator + { + private Dictionary _services = new(); + + public void RegisterService(Type serviceType, object service) + { + _services.Add(serviceType, service); + } + + public object GetService(Type serviceType) + { + _services.TryGetValue(serviceType, out var service); + return service; + } + + public void Inject(object target) + { + var type = target.GetType(); + + while (type != typeof(object)) + { + var bindingAttr = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + var fields = type.GetFields(bindingAttr); + + foreach (var field in fields) + { + var serviceAttribute = field.GetCustomAttribute(); + if (serviceAttribute == null) continue; + + var serviceType = serviceAttribute.ServiceType ?? field.FieldType; + + var service = GetService(serviceType); + if (service == null) + { + Debug.LogError($"Could not resolve service of type {serviceType} when injecting {type}"); + continue; + } + + field.SetValue(target, service); + } + + type = type.BaseType; + } + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/ServiceLocatorExtensions.cs b/Source/Engine/Core/ServiceLocatorExtensions.cs new file mode 100644 index 0000000..14e4173 --- /dev/null +++ b/Source/Engine/Core/ServiceLocatorExtensions.cs @@ -0,0 +1,15 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class ServiceLocatorExtensions + { + public static void RegisterService(this IServiceLocator serviceLocator, T service) + { + serviceLocator.RegisterService(typeof(T), service); + } + + public static T GetService(this IServiceLocator serviceLocator) + { + return (T)serviceLocator.GetService(typeof(T)); + } + } +} \ No newline at end of file diff --git a/Source/Engine/Core/SystemSorter.cs b/Source/Engine/Core/SystemSorter.cs new file mode 100644 index 0000000..76737d6 --- /dev/null +++ b/Source/Engine/Core/SystemSorter.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class SystemSorter + { + public static DependencySorter InitializableSorter { get; } = new(); + + public static DependencySorter UpdatableSorter { get; } = new(); + + public static DependencySorter DisposableSorter { get; } = new(); + + public static void InitializeSorters(List types) + { + InitializableSorter.Initialize(types); + UpdatableSorter.Initialize(types); + DisposableSorter.Initialize(types); + } + } + + public class DependencySorter where TBeforeAttr : SortingAttribute where TAfterAttr : SortingAttribute + { + private Dictionary _indexLookup = new(); + + private Dictionary> _dependencyLookup = new(); + + private Comparison _comparison; + + public DependencySorter() + { + _comparison = (lhs, rhs) => _indexLookup[lhs.GetType()].CompareTo(_indexLookup[rhs.GetType()]); + } + + public void Initialize(List types) + { + var dependencies = new ListMultiDictionary(); + foreach (var type in types) + { + var beforeAttributes = type.GetCustomAttributes(true); + foreach (var beforeAttribute in beforeAttributes) dependencies.Add(beforeAttribute.SystemType, type); + + var afterAttributes = type.GetCustomAttributes(true); + foreach (var afterAttribute in afterAttributes) dependencies.Add(type, afterAttribute.SystemType); + } + + _dependencyLookup = types.ToDictionary(type => type, type => dependencies.TryGetValues(type, out var d) ? d : new List()); + _indexLookup.Clear(); + + var sortedTypes = TopologicalSort.Default.Sort(types, t => dependencies.TryGetValues(t, out var d) ? d : null); + + var index = 0; + foreach (var type in sortedTypes) _indexLookup.Add(type, index++); + } + + public void Sort(List list) + { + list.Sort(_comparison); + } + + public List> CreateExecutionBatches(List list) + { + var batches = new List>(); + + if (list.Count == 0) return batches; + + var remainingTypes = new HashSet(list.Select(item => item.GetType())); + while (remainingTypes.Count > 0) + { + var batch = new List(); + foreach (var item in list) + { + var itemType = item.GetType(); + if (!remainingTypes.Contains(itemType)) continue; + + if (_dependencyLookup.TryGetValue(itemType, out var dependencies) && dependencies.Exists(remainingTypes.Contains)) continue; + + batch.Add(item); + } + + if (batch.Count == 0) throw new InvalidOperationException($"Unable to build execution batch for {typeof(TObj).Name}"); + + batches.Add(batch); + + foreach (var item in batch) remainingTypes.Remove(item.GetType()); + } + + return batches; + } + } +} \ No newline at end of file diff --git a/Source/Engine/GameData/GameDataAsset.cs b/Source/Engine/GameData/GameDataAsset.cs new file mode 100644 index 0000000..9286a77 --- /dev/null +++ b/Source/Engine/GameData/GameDataAsset.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class GameDataAsset : ScriptableObject, IGameDataRuntimeId + { + public int RuntimeId { get; set; } + } +} \ No newline at end of file diff --git a/Source/Engine/GameData/GameDatabase.cs b/Source/Engine/GameData/GameDatabase.cs new file mode 100644 index 0000000..2dcbb7d --- /dev/null +++ b/Source/Engine/GameData/GameDatabase.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class GameDatabase : IGameDatabase + { + private Dictionary _idLookup = new(); + + private Dictionary _typeLookup = new(); + + public void Add(int id, T asset) where T : class + { + _idLookup.Add(id, asset); + + InvokeAddToTypeLookupWithType(asset, asset.GetType()); + + if (asset is IGameDataRuntimeId runtimeId) runtimeId.RuntimeId = id; + } + + private void InvokeAddToTypeLookupWithType(object asset, Type type) + { + var bindingAttr = BindingFlags.Instance | BindingFlags.NonPublic; + var method = GetType().GetMethod(nameof(AddToTypeLookup), bindingAttr)!.MakeGenericMethod(type); + method.Invoke(this, new[] { asset }); + } + + private void AddToTypeLookup(T asset) + { + var type = typeof(T); + + if (!_typeLookup.TryGetValue(type, out var list)) + { + list = new List(); + _typeLookup.Add(type, list); + } + ((List)list).Add(asset); + + var nextType = type.BaseType; + if (nextType == typeof(object)) return; + + InvokeAddToTypeLookupWithType(asset, nextType); + } + + public T WithId(int id) where T : class + { + return _idLookup.TryGetValue(id, out var asset) ? (T)asset : null; + } + + public List OfType() where T : class + { + if (!_typeLookup.TryGetValue(typeof(T), out var assets)) + { + assets = new List(); + _typeLookup.Add(typeof(T), assets); + } + return (List)assets; + } + } +} \ No newline at end of file diff --git a/Source/Engine/GameData/GameDatabaseSystem.cs b/Source/Engine/GameData/GameDatabaseSystem.cs new file mode 100644 index 0000000..ec1e723 --- /dev/null +++ b/Source/Engine/GameData/GameDatabaseSystem.cs @@ -0,0 +1,37 @@ +using Cysharp.Threading.Tasks; +using UnityEngine.AddressableAssets; +using Object = UnityEngine.Object; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + public class GameDatabaseSystem : GameSystem, IInitializable, IServiceProvider + { + public const string GameDataAddressablesKey = "GameData"; + + private GameDatabase _gameDatabase; + + public GameDatabaseSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void RegisterServices(IServiceLocator serviceLocator) + { + _gameDatabase = new GameDatabase(); + ServiceLocator.RegisterService(_gameDatabase); + } + + public async UniTask InitializeAsync() + { + await Addressables.LoadAssetsAsync(GameDataAddressablesKey, OnAssetLoaded); + } + + private void OnAssetLoaded(Object asset) + { + var id = asset.GetInstanceID(); + id = unchecked(id + 0x40000000); + + _gameDatabase.Add(id, asset); + } + } +} diff --git a/Source/Engine/GameData/IGameDataRuntimeId.cs b/Source/Engine/GameData/IGameDataRuntimeId.cs new file mode 100644 index 0000000..1771f08 --- /dev/null +++ b/Source/Engine/GameData/IGameDataRuntimeId.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IGameDataRuntimeId + { + int RuntimeId { get; set; } + } +} \ No newline at end of file diff --git a/Source/Engine/GameData/IGameDatabase.cs b/Source/Engine/GameData/IGameDatabase.cs new file mode 100644 index 0000000..5fafc38 --- /dev/null +++ b/Source/Engine/GameData/IGameDatabase.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IGameDatabase + { + void Add(int id, T asset) where T : class; + + T WithId(int id) where T : class; + + List OfType() where T : class; + } +} \ No newline at end of file diff --git a/Source/Engine/Helpers/AsyncBudget.cs b/Source/Engine/Helpers/AsyncBudget.cs new file mode 100644 index 0000000..51c2921 --- /dev/null +++ b/Source/Engine/Helpers/AsyncBudget.cs @@ -0,0 +1,32 @@ +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class AsyncBudget + { + private int _max; + + private int _counter; + + public AsyncBudget(int max) + { + _max = max; + } + + public async UniTask TickAsync() + { + if (++_counter > _max) + { + _counter = 0; + await UniTask.NextFrame(); + } + } + + public void Reset(int max) + { + _max = max; + _counter = 0; + } + } +} diff --git a/Source/Engine/Helpers/TopologicalSort.cs b/Source/Engine/Helpers/TopologicalSort.cs new file mode 100644 index 0000000..10b2f16 --- /dev/null +++ b/Source/Engine/Helpers/TopologicalSort.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class TopologicalSort + { + private HashSet _currentDependencies; + + private HashSet _closed; + + public TopologicalSort(IEqualityComparer comparer) + { + _currentDependencies = new HashSet(comparer); + _closed = new HashSet(comparer); + } + + public TopologicalSort() : this(EqualityComparer.Default) + { + } + + public static TopologicalSort Default { get; } = new(); + + public IEnumerable Sort(IEnumerable source, Func> dependenciesGetter, ICollection sorted = null) + { + sorted ??= new List(); + + try + { + foreach (var item in source) Visit(item, dependenciesGetter, sorted); + } + finally + { + _currentDependencies.Clear(); + _closed.Clear(); + } + + return sorted; + } + + private void Visit(T item, Func> dependenciesGetter, ICollection sorted) + { + if (!_currentDependencies.Add(item)) + { + var ex = new StringBuilder(); + ex.AppendLine(item.ToString()); + foreach (var dependency in _currentDependencies) ex.AppendLine(dependency.ToString()); + + throw new InvalidOperationException(ex.ToString()); + } + + var dependencies = dependenciesGetter.Invoke(item); + if (dependencies != null) + foreach (var dependency in dependencies) + Visit(dependency, dependenciesGetter, sorted); + + _currentDependencies.Remove(item); + + if (_closed.Add(item)) sorted.Add(item); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Config/AppLinks.cs b/Source/Riversong/Config/AppLinks.cs new file mode 100644 index 0000000..26384e1 --- /dev/null +++ b/Source/Riversong/Config/AppLinks.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class AppLinks + { + public const string DemoFeedbackUrl = "https://forms.gle/vA5owahuh8AUMbf87"; + } +} diff --git a/Source/Riversong/Config/BuildVersionAsset.cs b/Source/Riversong/Config/BuildVersionAsset.cs new file mode 100644 index 0000000..033f515 --- /dev/null +++ b/Source/Riversong/Config/BuildVersionAsset.cs @@ -0,0 +1,17 @@ +using Sirenix.OdinInspector; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [CreateAssetMenu(fileName = "BuildVersion", menuName = "Riversong Code Showcase/Build Version")] + public class BuildVersionAsset : ScriptableObject + { + public string CurrentVersion; + + [ReadOnly] + public string LastBuildDate; + + [ReadOnly] + public int LastBuildCounter; + } +} diff --git a/Source/Riversong/Config/GameConfig.cs b/Source/Riversong/Config/GameConfig.cs new file mode 100644 index 0000000..11d7eeb --- /dev/null +++ b/Source/Riversong/Config/GameConfig.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections.Generic; +using Sirenix.OdinInspector; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.TextCore.Text; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [CreateAssetMenu(fileName = "GameConfig", menuName = "Riversong Code Showcase/Game Config")] + [Searchable] + public class GameConfig : ScriptableObject + { + [FoldoutGroup("General Settings")] + [InlineProperty] + [HideLabel] + public GeneralSettingsConfig GeneralSettings; + + [FoldoutGroup("Audio")] + [InlineProperty] + [HideLabel] + public AudioConfig Audio; + + [FoldoutGroup("Camera")] + [InlineProperty] + [HideLabel] + public CameraConfig Camera; + + [FoldoutGroup("World Generation")] + [InlineProperty] + [HideLabel] + public WorldGenConfig WorldGen; + + [FoldoutGroup("Time")] + [InlineProperty] + [HideLabel] + public TimeConfig Time; + + [FoldoutGroup("Terrain")] + [InlineProperty] + [HideLabel] + public TerrainConfig Terrain; + + [FoldoutGroup("Buildings")] + [InlineProperty] + [HideLabel] + public BuildingsConfig Buildings; + + [FoldoutGroup("Roads")] + [InlineProperty] + [HideLabel] + public RoadsConfig Roads; + + [FoldoutGroup("Agents")] + [InlineProperty] + [HideLabel] + public AgentsConfig Agents; + + [FoldoutGroup("Population")] + [InlineProperty] + [HideLabel] + public PopulationConfig Population; + + [FoldoutGroup("Economy")] + [InlineProperty] + [HideLabel] + public EconomyConfig Economy; + + [FoldoutGroup("Onboarding")] + [InlineProperty] + [HideLabel] + public OnboardingConfig Onboarding; + + [FoldoutGroup("UI")] + [InlineProperty] + [HideLabel] + public UIConfig UI; + + [FoldoutGroup("VFX")] + [InlineProperty] + [HideLabel] + public VfxConfig Vfx; + + [Serializable] + public class GeneralSettingsConfig + { + public float TileSize = 1; + + public int BaseElevation = 2; + + [TitleGroup("Demo Only")] + public int PopulationGoal = 50; + + [Range(0, 1)] + public float HappinessGoal = 1; + } + + [Serializable] + public class AudioConfig + { + public AssetReferenceT MainThemeClip; + + [Range(0, 1)] + public float MainThemeVolume = 1; + + public AssetReferenceT GameplayClip; + + [Range(0, 1)] + public float GameplayVolume = 1; + + public AssetReferenceT SystemSoundLibrary; + + public AssetReferenceGameObject AudioSourcePrefab; + + [TitleGroup("Spatial Audio")] + [LabelText("Horizontal Distance Range")] + public Vector2 SpatialAudioHorizontalDistanceRange = new(10, 20); + + [LabelText("Zoom Range")] + public Vector2 SpatialAudioZoomRange = new(0, 0.3f); + } + + [Serializable] + public class CameraConfig + { + [TitleGroup("Movement")] + public Vector2 MoveSpeed = new(10, 30); + + [TitleGroup("Rotation")] + public Vector2 MouseRotationSpeed = new(70, 70); + + public Vector2 KeyboardRotationSpeed = new(70, 70); + + public Vector2 PitchRange = new(30, 70); + + [TitleGroup("Zoom")] + public float ZoomSensitivity = 10; + + public float ZoomSpeed = 40; + + public Vector2 ZoomRange = new(10, 150); + } + + [Serializable] + public class WorldGenConfig + { + public int ChunkSize = 16; + + public int ChunkGenerationBatchCount = 16; + + public int ChunksPerThread = 16; + + public List> MapTextures = new(); + + public TreesConfig Trees = new(); + + public StoneConfig Stone = new(); + + public GrassConfig Grass = new(); + + public GrassConfig Crops = new(); + + public ProductStacksConfig ProductStacks = new(); + + [Serializable] + public class TreesConfig + { + public AssetReferenceT TreeDefinition; + + public float NoiseScale = 0.05f; + + [Range(0, 1)] + public float Coverage = 0.3f; + + public float Spacing = 2.5f; + + public Vector2 OffsetRange = new(0, 0.25f); + + [Range(0, 1)] + public float RandomTreeChance = 0.03f; + } + + [Serializable] + public class StoneConfig + { + public AssetReferenceT StoneDefinition; + + public Vector2Int Count = new(3, 5); + + public int Spacing = 10; + } + + [Serializable] + public class GrassConfig + { + public float NoiseScale = 0.1f; + + public float NoiseDiscardThreshold = 0.1f; + + public float BladeWidth = 0.05f; + + public float2 BladeHeightRange = new(0.3f, 1); + } + + [Serializable] + public class ProductStacksConfig + { + public List> EligibleProducts; + + [Range(0, 1)] + public float Chance = 0.05f; + + public int ProductAmount = 1; + } + } + + [Serializable] + public class TimeConfig + { + public float WeekDuration = 15; + + [TitleGroup("Day Night Cycle")] + public float DayDuration = 120; + + public float NightDuration = 30; + + public float DayToNightDuration = 5; + + public float NightToDayDuration = 5; + + public DayNightLightingConfig DayLighting; + + public DayNightLightingConfig NightLighting; + + [MinMaxSlider(0, 1, true)] + public Vector2 WarmTintRampUp; + + [MinMaxSlider(0, 1, true)] + public Vector2 WarmTintRampDown; + + [Serializable] + public class DayNightLightingConfig + { + public Color AmbientColor = Color.black; + + [Range(0, 1)] + public float ShadowStrength = 1; + } + } + + [Serializable] + public class TerrainConfig + { + [TitleGroup("Materials")] + public Material GroundMaterial; + + public Material CliffMaterial; + + public Material GrassMaterial; + + public Material CropsMaterial; + + [TitleGroup("Grass and Crops")] + [LabelText("Growth Rate")] + public float GrassGrowthRate = 0.2f; + + [LabelText("Grass LODs")] + public GrassLOD[] GrassLODs; + + [LabelText("Crops LODs")] + public GrassLOD[] CropsLODs; + + public float FertilityRegenarationRate = 0.05f; + + [TitleGroup("Wind")] + [InlineProperty] + [HideLabel] + public WindConfig Wind; + + [Serializable] + public class GrassLOD + { + public float Threshold = 1000; + + public int Density = 16; + } + + [Serializable] + public class WindConfig + { + public AssetReferenceT Map; + + public Vector3 BendDirection = Vector3.right; + + public float MapScale = 0.1f; + + public float Speed = 1; + } + } + + [Serializable] + public class BuildingsConfig + { + public Material ConstructionSiteMaterial; + + public int WeeksWithNeedsMetToUpgrade = 2; + } + + [Serializable] + public class RoadsConfig + { + public float PlaceTileInterval = 0.2f; + + public List> Tiles; + + public AssetReferenceT PlacementVfxPrefab; + } + + [Serializable] + public class AgentsConfig + { + public AssetReferenceT GenericAgent; + + public AssetReferenceT HunterAgent; + + public AssetReferenceT FarmerAgent; + + public float MoveSpeed = 5; + + public float RotationSpeed = 180; + } + + [Serializable] + public class PopulationConfig + { + public AssetReferenceT TentBuilding; + + [TitleGroup("Grace Period")] + [LabelText("Number of Weeks")] + public int GraceWeekCount = 3; + + [LabelText("Min Happiness during Grace")] + [Range(0, 1)] + public float GraceMinHappiness = 0.8f; + + [TitleGroup("Overall Happiness")] + [LabelText("Initial Value")] + [Range(0, 1)] + public float InitialeOverallHappiness = 0.8f; + + [LabelText("Change Rate")] + public float OverallHappinessChangeRate = 0.5f; + + [TitleGroup("Houses Happiness")] + [LabelText("Initial Value")] + [Range(0, 1)] + public float InitialHouseHappiness = 0.8f; + + [LabelText("Weight Ramp Up (Weeks)")] + public int HouseWeightRampUpWeekCount = 3; + + [TitleGroup("Growth Rate")] + [LabelText("Peak")] + public float GrowthRatePeakValue = 1; + + [LabelText("Curve")] + public AnimationCurve GrowthRateCurve = AnimationCurve.Linear(0, -1, 1, 1); + } + + [Serializable] + public class EconomyConfig + { + public float ProductionTickInterval = 0.1f; + + public float RestedWorkersEfficiencyModifier = 0.5f; + + public int RestedWorkersHouseMaxStepCount = 10; + + public float PopulationToLaborFactor = 1; + + public int PopulationPerMinLaborTierStep = 8; + + public List LaborTiers = new() + { + new LaborTierConfig + { + Threshold = 0, + EfficiencyModifier = -0.3f + }, + new LaborTierConfig + { + Threshold = 0.6f, + EfficiencyModifier = -0.15f + }, + new LaborTierConfig + { + Threshold = 0.85f, + EfficiencyModifier = -0.05f + }, + new LaborTierConfig + { + Threshold = 1, + EfficiencyModifier = 0 + } + }; + } + + [Serializable] + public class LaborTierConfig + { + [Range(0, 1)] + public float Threshold; + + public float EfficiencyModifier; + } + + [Serializable] + public class OnboardingConfig + { + public int PopulationMilestone = 20; + + public float MessageDuration = 30; + + public OnboardingMessages Messages; + } + + [Serializable] + public class UIConfig + { + public AssetReferenceT RootPrefab; + + public AssetReferenceT TemplateLibrary; + + public DayNightUITheme Theme; + + public AssetReferenceT DebugFont; + + public Material HighlightedGameObjectsMaterial; + + public Material DeletedGameObjectsMaterial; + + [TitleGroup("Tile Highlight")] + [InlineProperty] + [HideLabel] + public TileHighlightConfig TileHighlight; + + [TitleGroup("Build Tool")] + [InlineProperty] + [HideLabel] + public BuildToolConfig BuildTool; + + [Serializable] + public class TileHighlightConfig + { + public AssetReferenceT Prefab; + + public Color ValidColor = Color.cyan; + + public Color InvalidColor = Color.red; + + public Color DeletePreviewColor = Color.red; + } + + [Serializable] + public class BuildToolConfig + { + public float Height = 5; + + public float LerpFactor = 10; + + public float ImpulseScale = 1; + + public float MaxTilt = 5; + + public float MaxTiltAngle = 30; + + public float Elasticity = 25; + + [Range(0, 1)] + public float Damping = 0.9f; + } + } + + [Serializable] + public class VfxConfig + { + [LabelText("AoE Material")] + public Material AoEMaterial; + } + } +} diff --git a/Source/Riversong/Config/IScene.cs b/Source/Riversong/Config/IScene.cs new file mode 100644 index 0000000..3ecf5da --- /dev/null +++ b/Source/Riversong/Config/IScene.cs @@ -0,0 +1,30 @@ +using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IScene + { + Camera MainCamera { get; } + + CinemachineCamera CinemachineCamera { get; } + + SceneFolders SceneFolders { get; } + + UIDocument LoadingOverlay { get; } + + Transform LightRig { get; } + + Light MainLight { get; } + + Light NightLight { get; } + + Volume NightVolume { get; } + + Volume BloomVolume { get; } + + Volume WarmTintVolume { get; } + } +} diff --git a/Source/Riversong/Config/SceneFolders.cs b/Source/Riversong/Config/SceneFolders.cs new file mode 100644 index 0000000..3dc8de4 --- /dev/null +++ b/Source/Riversong/Config/SceneFolders.cs @@ -0,0 +1,21 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class SceneFolders : MonoBehaviour + { + public Transform AudioSources; + + public Transform TerrainChunks; + + public Transform RawResources; + + public Transform Buildings; + + public Transform RoadTiles; + + public Transform ProductStacks; + + public Transform Agents; + } +} \ No newline at end of file diff --git a/Source/Riversong/Config/UnityObjectInjector.cs b/Source/Riversong/Config/UnityObjectInjector.cs new file mode 100644 index 0000000..97f65e4 --- /dev/null +++ b/Source/Riversong/Config/UnityObjectInjector.cs @@ -0,0 +1,64 @@ +using Sirenix.OdinInspector; +using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class UnityObjectInjector : MonoBehaviour, IScene, IServiceProvider + { + [field: TitleGroup("Config")] + [field: SerializeField] + public GameConfig GameConfig { get; private set; } + + [field: SerializeField] public BuildVersionAsset BuildVersion { get; private set; } + + [field: TitleGroup("Scene")] + [field: SerializeField] + public Camera MainCamera { get; private set; } + + [field: TitleGroup("Scene")] + [field: SerializeField] + public CinemachineCamera CinemachineCamera { get; private set; } + + [field: TitleGroup("Scene")] + [field: SerializeField] + public SceneFolders SceneFolders { get; private set; } + + [field: TitleGroup("Scene")] + [field: SerializeField] + public UIDocument LoadingOverlay { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Transform LightRig { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Light MainLight { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Light NightLight { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Volume NightVolume { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Volume BloomVolume { get; private set; } + + [field: FoldoutGroup("Scene/Day Night Cycle")] + [field: SerializeField] + public Volume WarmTintVolume { get; private set; } + + public void RegisterServices(IServiceLocator serviceLocator) + { + serviceLocator.RegisterService(GameConfig); + serviceLocator.RegisterService(BuildVersion); + serviceLocator.RegisterService(this); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/AppLifecycle/FinalizeInitializationSystemGroup.cs b/Source/Riversong/Game/AppLifecycle/FinalizeInitializationSystemGroup.cs new file mode 100644 index 0000000..7b3e3c8 --- /dev/null +++ b/Source/Riversong/Game/AppLifecycle/FinalizeInitializationSystemGroup.cs @@ -0,0 +1,8 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [InitializeAfter(typeof(DebugSystemGroup))] + public class FinalizeInitializationSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/AppLifecycle/GameInitializationCompletedSignal.cs b/Source/Riversong/Game/AppLifecycle/GameInitializationCompletedSignal.cs new file mode 100644 index 0000000..22d4354 --- /dev/null +++ b/Source/Riversong/Game/AppLifecycle/GameInitializationCompletedSignal.cs @@ -0,0 +1,6 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct GameInitializationCompletedSignal + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs b/Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs new file mode 100644 index 0000000..844a745 --- /dev/null +++ b/Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs @@ -0,0 +1,6 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct GameStartedSignal + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/AppLifecycle/NotifyGameInitializationCompletedSystem.cs b/Source/Riversong/Game/AppLifecycle/NotifyGameInitializationCompletedSystem.cs new file mode 100644 index 0000000..66547d8 --- /dev/null +++ b/Source/Riversong/Game/AppLifecycle/NotifyGameInitializationCompletedSystem.cs @@ -0,0 +1,29 @@ +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(FinalizeInitializationSystemGroup))] + public class GameInitializationCompletedSignalSystem : GameSystem, IInitializable + { + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private IScene _scene; + + public GameInitializationCompletedSignalSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public async UniTask InitializeAsync() + { + await UniTask.NextFrame(); + + _signalBus.Raise(new GameInitializationCompletedSignal()); + + await UniTask.NextFrame(); + + _scene.LoadingOverlay.enabled = false; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs b/Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs new file mode 100644 index 0000000..5df5e7e --- /dev/null +++ b/Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using UnityEngine.AddressableAssets; +using Object = UnityEngine.Object; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + [InitializeAfter(typeof(GameDatabaseSystem))] + public class PreLoadAssetsSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private GameConfig _config; + + [InjectService] + private IGameDatabase _gameDatabase; + + private readonly HashSet _loadedReferences = new(); + + public PreLoadAssetsSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public async UniTask InitializeAsync() + { + var tasks = new List(); + + PreLoadAgents(tasks); + PreLoadProducts(tasks); + PreLoadBuildings(tasks); + + await UniTask.WhenAll(tasks); + } + + private void PreLoadAgents(List tasks) + { + Load(_config.Agents.GenericAgent, tasks); + Load(_config.Agents.HunterAgent, tasks); + Load(_config.Agents.FarmerAgent, tasks); + } + + private void PreLoadProducts(List tasks) + { + foreach (var product in _gameDatabase.OfType()) + { + Load(product.ProductStackVisualization, tasks); + Load(product.CarriedVisualization, tasks); + } + } + + private void PreLoadBuildings(List tasks) + { + Load(_config.Population.TentBuilding, tasks); + + foreach (var building in _gameDatabase.OfType()) Load(building.Visualization, tasks); + } + + private void Load(AssetReferenceT assetReference, List tasks) where T : Object + { + if (!assetReference.RuntimeKeyIsValid() || !_loadedReferences.Add(assetReference)) return; + + tasks.Add(assetReference.LoadAssetAsync().ToUniTask()); + } + + public void Dispose() + { + foreach (var assetReference in _loadedReferences) assetReference.ReleaseAsset(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Audio/AudioSystemGroup.cs b/Source/Riversong/Game/Audio/AudioSystemGroup.cs new file mode 100644 index 0000000..697cb26 --- /dev/null +++ b/Source/Riversong/Game/Audio/AudioSystemGroup.cs @@ -0,0 +1,8 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + public class AudioSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Audio/BackgroundMusicSystem.cs b/Source/Riversong/Game/Audio/BackgroundMusicSystem.cs new file mode 100644 index 0000000..7bbac26 --- /dev/null +++ b/Source/Riversong/Game/Audio/BackgroundMusicSystem.cs @@ -0,0 +1,82 @@ +using System; +using Cysharp.Threading.Tasks; +using PrimeTween; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(AudioSystemGroup))] + public class BackgroundMusicSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private GameConfig _config; + + [InjectService] + private IScene _scene; + + [InjectService] + private ISignalBus _signalBus; + + private AudioSource _audioSource; + + private AudioClip _mainThemeClip; + + private AudioClip _gameplayClip; + + public BackgroundMusicSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public async UniTask InitializeAsync() + { + _audioSource = new GameObject("BGM").AddComponent(); + _audioSource.transform.SetParent(_scene.SceneFolders.AudioSources); + _audioSource.playOnAwake = false; + _audioSource.loop = true; + + _mainThemeClip = await _config.Audio.MainThemeClip.LoadAssetAsync(); + _gameplayClip = await _config.Audio.GameplayClip.LoadAssetAsync(); + + _signalBus.Subscribe(OnGameInitializationCompleted); + _signalBus.Subscribe(OnGameStarted); + _signalBus.Subscribe(OnWorldReady); + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnGameInitializationCompleted); + _signalBus.Unsubscribe(OnGameStarted); + _signalBus.Unsubscribe(OnWorldReady); + } + + private void OnGameInitializationCompleted(GameInitializationCompletedSignal signal) + { + Play(_mainThemeClip, _config.Audio.MainThemeVolume); + } + + private void OnGameStarted(GameStartedSignal signal) + { + _ = FadeOutAsync(2); + } + + private async UniTask FadeOutAsync(float duration) + { + await Tween.Custom(_audioSource.volume, 0, duration, value => _audioSource.volume = value, Ease.Linear); + } + + private void OnWorldReady(WorldReadySignal signal) + { + Play(_gameplayClip, _config.Audio.GameplayVolume); + } + + private void Play(AudioClip clip, float volume) + { + _audioSource.Stop(); + + _audioSource.clip = clip; + _audioSource.volume = volume; + + _audioSource.Play(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Audio/ISoundPlayer.cs b/Source/Riversong/Game/Audio/ISoundPlayer.cs new file mode 100644 index 0000000..5a12587 --- /dev/null +++ b/Source/Riversong/Game/Audio/ISoundPlayer.cs @@ -0,0 +1,34 @@ +using Unity.Mathematics; +using UnityEngine.Audio; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface ISoundPlayer + { + void Play(AudioResource resource); + + void PlayAt(AudioResource resource, float3 position); + + void PlayAt(AudioResource resource, int2 tile); + + AudioResource GetSystemSound(SystemSoundId soundId); + } + + public static class SoundPlayerExtensions + { + public static void Play(this ISoundPlayer soundPlayer, SystemSoundId soundId) + { + soundPlayer.Play(soundPlayer.GetSystemSound(soundId)); + } + + public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, float3 position) + { + soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), position); + } + + public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, int2 tile) + { + soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), tile); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs b/Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs new file mode 100644 index 0000000..472db75 --- /dev/null +++ b/Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using Unity.Mathematics; +using Time = UnityEngine.Time; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class PlaySoundOnEventSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private ISoundPlayer _soundPlayer; + + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private IGameDatabase _gameDatabase; + + [InjectService] + private ITileSpace _tileSpace; + + private Dictionary _soundPlayedTimestamps = new(); + + public PlaySoundOnEventSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _signalBus.Subscribe(OnBuildingPlacementAnimationCompleted); + _signalBus.Subscribe(OnConstructionSiteDeleted); + _signalBus.Subscribe(OnBuildingDeleted); + _signalBus.Subscribe(OnBuildingUpgraded); + _signalBus.Subscribe(OnRawResourcesRemoved); + _signalBus.Subscribe(OnRoadTileUpdated); + _signalBus.Subscribe(OnBuildMenuButtonUnlockAnimationStarted); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnBuildingPlacementAnimationCompleted); + _signalBus.Unsubscribe(OnConstructionSiteDeleted); + _signalBus.Unsubscribe(OnBuildingDeleted); + _signalBus.Unsubscribe(OnBuildingUpgraded); + _signalBus.Unsubscribe(OnRawResourcesRemoved); + _signalBus.Unsubscribe(OnRoadTileUpdated); + _signalBus.Unsubscribe(OnBuildMenuButtonUnlockAnimationStarted); + } + + private bool UpdatePlayedTimestamp(int key) + { + var timestamp = _soundPlayedTimestamps.GetValueOrDefault(key); + + const float minInterval = 0.1f; + if (Time.unscaledTime - timestamp < minInterval) return false; + + _soundPlayedTimestamps[key] = Time.unscaledTime; + + return true; + } + + private void OnBuildingPlacementAnimationCompleted(BuildingPlacementAnimationCompletedSignal signal) + { + PlaySystemSound(SystemSoundId.BuildingPlaced); + } + + private void OnConstructionSiteDeleted(ConstructionSiteDeletedSignal signal) + { + PlaySystemSound(SystemSoundId.BuildingDeleted); + } + + private void OnBuildingDeleted(BuildingDeletedSignal signal) + { + if (signal.Options == DeleteBuildingOptions.Silent) return; + + PlaySystemSound(SystemSoundId.BuildingDeleted); + } + + private void OnBuildingUpgraded(BuildingUpgradedSignal signal) + { + if (!signal.Building.Definition.IsHouse || !UpdatePlayedTimestamp((int)SystemSoundId.HouseUpgraded)) return; + + var position = (float3)_tileSpace.GetRectWorldCenter(signal.Building.Rect); + _soundPlayer.PlayAt(SystemSoundId.HouseUpgraded, position); + } + + private void OnRawResourcesRemoved(RawResourcesRemovedSignal signal) + { + foreach (var resourceNode in signal.ResourceNodes) + { + var definition = _gameDatabase.WithId(resourceNode.DefinitionId); + + if (!UpdatePlayedTimestamp(definition.RuntimeId)) continue; + + if (signal.Reason == RawResourcesRemovedSignal.RemovalReason.Harvested) + _soundPlayer.PlayAt(definition.DeleteAudio, resourceNode.Tile); + else + _soundPlayer.Play(definition.DeleteAudio); + } + } + + private void OnRoadTileUpdated(RoadTileUpdatedSignal signal) + { + PlaySystemSound(signal.RoadTileAdded ? SystemSoundId.RoadTilePlaced : SystemSoundId.RoadTileDeleted); + } + + private void OnBuildMenuButtonUnlockAnimationStarted(BuildMenuButtonUnlockAnimationStartedSignal signal) + { + PlaySystemSound(SystemSoundId.UnlockNotification); + } + + private void PlaySystemSound(SystemSoundId soundId) + { + if (UpdatePlayedTimestamp((int)soundId)) _soundPlayer.Play(soundId); + } + } +} diff --git a/Source/Riversong/Game/Audio/SoundPlayerSystem.cs b/Source/Riversong/Game/Audio/SoundPlayerSystem.cs new file mode 100644 index 0000000..9c788d2 --- /dev/null +++ b/Source/Riversong/Game/Audio/SoundPlayerSystem.cs @@ -0,0 +1,184 @@ +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Audio; +using Object = UnityEngine.Object; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(ISoundPlayer))] + [GameSystemGroup(typeof(AudioSystemGroup))] + [InitializeAfter(typeof(BackgroundMusicSystem))] + public class SoundPlayerSystem : GameSystem, IInitializable, IUpdatable, ISoundPlayer, IDrawGizmos + { + private const int ChannelCount = 16; + + [InjectService] + private ITileSpace _tileSpace; + + [InjectService] + private IScene _scene; + + [InjectService] + private ICameraProperties _cameraProperties; + + [InjectService] + private GameConfig _config; + + private AudioSourcePool _poolNonSpatial; + + private AudioSourcePool _poolSpatial; + + private SystemSoundLibrary _systemSoundLibrary; + + public SoundPlayerSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public async UniTask InitializeAsync() + { + var systemSoundLibraryTask = _config.Audio.SystemSoundLibrary.LoadAssetAsync().ToUniTask(); + var audioSourcePrefabTask = _config.Audio.AudioSourcePrefab.LoadAssetAsync().ToUniTask(); + + _systemSoundLibrary = await systemSoundLibraryTask; + + var audioSourcePrefab = (await audioSourcePrefabTask).GetComponent(); + await InitializePoolAsync(audioSourcePrefab); + } + + private async UniTask InitializePoolAsync(AudioSource audioSourcePrefab) + { + _poolNonSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (2D)"); + await UniTask.NextFrame(); + + _poolSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (3D)"); + await UniTask.NextFrame(); + } + + private AudioSourcePool InitializePool(AudioSource audioSourcePrefab, string nameFormat) + { + var audioSources = new AudioSource[ChannelCount]; + + for (var i = 0; i < ChannelCount; i++) + { + var source = Object.Instantiate(audioSourcePrefab, _scene.SceneFolders.AudioSources); + source.name = string.Format(nameFormat, i); + + audioSources[i] = source; + } + + return new AudioSourcePool(audioSources); + } + + public void Update() + { + UpdateSpatialAudioSources(); + } + + private void UpdateSpatialAudioSources() + { + var cameraPosition = ((float3)_scene.MainCamera.transform.position).xz; + + var horizontalDistanceRange = _config.Audio.SpatialAudioHorizontalDistanceRange; + var zoomRange = _config.Audio.SpatialAudioZoomRange; + + foreach (var audioSource in _poolSpatial.AudioSources) + { +#if !UNITY_EDITOR + if (!audioSource.isPlaying) continue; +#endif + + var p = ((float3)audioSource.transform.position).xz; + + var horizontalDistance = math.distance(cameraPosition, p); + + var t = math.saturate(math.unlerp(horizontalDistanceRange.x, horizontalDistanceRange.y, horizontalDistance)); + var attenuation = 1 - t * t; + + var zoomFactor = 1 - math.unlerp(zoomRange.x, zoomRange.y, _cameraProperties.Zoom); + + audioSource.volume = attenuation * zoomFactor; + } + } + + public void Play(AudioResource resource) + { + var source = _poolNonSpatial.GetAudioSource(); + + source.resource = resource; + + source.Play(); + } + + public void PlayAt(AudioResource resource, float3 position) + { + var source = _poolSpatial.GetAudioSource(); + + source.resource = resource; + source.transform.position = position; + + source.Play(); + } + + public void PlayAt(AudioResource resource, int2 tile) + { + PlayAt(resource, _tileSpace.TileToWorld(tile)); + } + + public AudioResource GetSystemSound(SystemSoundId soundId) + { + return _systemSoundLibrary.Sounds.GetValueOrDefault(soundId); + } + + public void DrawGizmos(bool selected) + { +#if UNITY_EDITOR + const int width = 60; + const int height = 8; + + var camera = _scene.MainCamera; + + foreach (var audioSource in _poolSpatial.AudioSources) + { + var screenPoint = camera.WorldToScreenPoint(audioSource.transform.position); + if (screenPoint.z < 0) continue; + + var p = new Vector2(screenPoint.x - width * 0.5f, camera.pixelHeight - screenPoint.y); + + Handles.BeginGUI(); + + EditorGUI.DrawRect(new Rect(p.x, p.y, width, height), new Color(0, 0, 0, 0.6f)); + + var volume = audioSource.volume; + var color = Color.Lerp(Color.red, Color.green, volume); + EditorGUI.DrawRect(new Rect(p.x + 0.5f * (width * (1 - volume)), p.y, width * volume, height), color); + + Handles.EndGUI(); + } +#endif + } + + private class AudioSourcePool + { + private int _nextChannel; + + public AudioSourcePool(AudioSource[] audioSources) + { + AudioSources = audioSources; + } + + public AudioSource[] AudioSources { get; } + + public AudioSource GetAudioSource() + { + var source = AudioSources[_nextChannel]; + _nextChannel = (_nextChannel + 1) % ChannelCount; + return source; + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Audio/SystemSoundId.cs b/Source/Riversong/Game/Audio/SystemSoundId.cs new file mode 100644 index 0000000..1bdcb4f --- /dev/null +++ b/Source/Riversong/Game/Audio/SystemSoundId.cs @@ -0,0 +1,21 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum SystemSoundId + { + UIClick, + + BuildingPlaced, + + BuildingDeleted, + + HouseUpgraded, + + RoadTilePlaced, + + RoadTileDeleted, + + UnlockNotification, + + OnboardingMessage + } +} diff --git a/Source/Riversong/Game/Audio/SystemSoundLibrary.cs b/Source/Riversong/Game/Audio/SystemSoundLibrary.cs new file mode 100644 index 0000000..ebbc483 --- /dev/null +++ b/Source/Riversong/Game/Audio/SystemSoundLibrary.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; +using UnityEngine.Audio; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [CreateAssetMenu(fileName = "SystemSoundLibrary", menuName = "Riversong Code Showcase/System Sound Library")] + public class SystemSoundLibrary : SerializedScriptableObject + { + public Dictionary Sounds; + } +} diff --git a/Source/Riversong/Game/Camera/CameraSystem.cs b/Source/Riversong/Game/Camera/CameraSystem.cs new file mode 100644 index 0000000..67e33af --- /dev/null +++ b/Source/Riversong/Game/Camera/CameraSystem.cs @@ -0,0 +1,145 @@ +using Cysharp.Threading.Tasks; +using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(ICameraProperties))] + public class CameraSystem : GameSystem, IInitializable, IUpdatable, ICameraProperties + { + [InjectService] + private GameConfig _gameConfig; + + [InjectService] + private IScene _scene; + + private GameConfig.CameraConfig _cameraConfig; + + private CinemachineBrain _cinemachineBrain; + + private CinemachineFollow _cinemachineFollow; + + private Transform _target; + + private float _targetZoom; + + private Vector3? _terrainDragAnchor; + + public CameraSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public float Zoom { get; private set; } + + public UniTask InitializeAsync() + { + _cameraConfig = _gameConfig.Camera; + + _cinemachineBrain = _scene.MainCamera.GetComponent(); + + _cinemachineFollow = (CinemachineFollow)_scene.CinemachineCamera.GetCinemachineComponent(CinemachineCore.Stage.Body); + _target = _cinemachineFollow.FollowTarget; + + Zoom = _targetZoom = _cinemachineFollow.FollowOffset.magnitude; + + return UniTask.CompletedTask; + } + + public void Update() + { + var dt = Time.unscaledDeltaTime; + + UpdateZoom(dt); + UpdatePosition(dt); + UpdateRotation(dt); + + _cinemachineBrain.ManualUpdate(); + } + + private void UpdateZoom(float dt) + { + var zoomInput = -1 * Mouse.current.scroll.y.ReadValue(); + + _targetZoom += zoomInput * _cameraConfig.ZoomSensitivity; + _targetZoom = Mathf.Clamp(_targetZoom, _cameraConfig.ZoomRange.x, _cameraConfig.ZoomRange.y); + + Zoom = Mathf.Lerp(Zoom, _targetZoom, _cameraConfig.ZoomSpeed * dt); + + _cinemachineFollow.FollowOffset = _cinemachineFollow.FollowOffset.normalized * Zoom; + } + + private void UpdatePosition(float dt) + { + var moveInput = Vector3.zero; + + moveInput.x += Keyboard.current.dKey.isPressed ? 1 : 0; + moveInput.x += Keyboard.current.aKey.isPressed ? -1 : 0; + moveInput.z += Keyboard.current.wKey.isPressed ? 1 : 0; + moveInput.z += Keyboard.current.sKey.isPressed ? -1 : 0; + + var right = _target.right; + var forward = _target.forward; + forward.y = 0; + forward.Normalize(); + + var normalizedZoom = Mathf.InverseLerp(_cameraConfig.ZoomRange.x, _cameraConfig.ZoomRange.y, Zoom); + var moveSpeed = Mathf.Lerp(_cameraConfig.MoveSpeed.x, _cameraConfig.MoveSpeed.y, normalizedZoom); + var delta = moveSpeed * dt; + + _target.position += right * (moveInput.x * delta); + _target.position += forward * (moveInput.z * delta); + + UpdateDraggingTerrain(); + } + + private void UpdateDraggingTerrain() + { + if (!Mouse.current.middleButton.isPressed) + { + _terrainDragAnchor = null; + return; + } + + var plane = new Plane(Vector3.up, new Vector3(0, _gameConfig.GeneralSettings.BaseElevation, 0)); + var ray = _scene.MainCamera.ScreenPointToRay(Mouse.current.position.ReadValue()); + if (!plane.Raycast(ray, out var enter)) return; + + var hit = ray.GetPoint(enter); + if (_terrainDragAnchor == null) + { + _terrainDragAnchor = hit; + return; + } + + _target.position += _terrainDragAnchor.Value - hit; + } + + private void UpdateRotation(float dt) + { + var keyboard = Keyboard.current; + var rotation = _target.rotation.eulerAngles; + var keyboardRotationDelta = Vector2.zero; + + keyboardRotationDelta.x += keyboard.eKey.isPressed ? 1 : 0; + keyboardRotationDelta.x += keyboard.qKey.isPressed ? -1 : 0; + keyboardRotationDelta.y += keyboard.rKey.isPressed ? 1 : 0; + keyboardRotationDelta.y += keyboard.fKey.isPressed ? -1 : 0; + + rotation.y += keyboardRotationDelta.x * _cameraConfig.KeyboardRotationSpeed.x * dt; + rotation.x -= keyboardRotationDelta.y * _cameraConfig.KeyboardRotationSpeed.y * dt; + + if (keyboard.leftAltKey.isPressed) + { + var mouseDelta = Mouse.current.delta; + + rotation.x += mouseDelta.y.ReadValue() * _cameraConfig.MouseRotationSpeed.y * dt; + rotation.y += mouseDelta.x.ReadValue() * _cameraConfig.MouseRotationSpeed.x * dt; + } + + rotation.x = Mathf.Clamp(rotation.x, _cameraConfig.PitchRange.x, _cameraConfig.PitchRange.y); + + _target.forward = Quaternion.Euler(rotation) * Vector3.forward; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Camera/ICameraProperties.cs b/Source/Riversong/Game/Camera/ICameraProperties.cs new file mode 100644 index 0000000..f5c1099 --- /dev/null +++ b/Source/Riversong/Game/Camera/ICameraProperties.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface ICameraProperties + { + float Zoom { get; } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Collections/NativeGrid.cs b/Source/Riversong/Game/Collections/NativeGrid.cs new file mode 100644 index 0000000..ba765e1 --- /dev/null +++ b/Source/Riversong/Game/Collections/NativeGrid.cs @@ -0,0 +1,65 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class NativeGrid : IDisposable where T : struct + { + private NativeArray _data; + + public NativeGrid(int2 size, Allocator allocator) + { + Size = size; + _data = new NativeArray(Size.x * Size.y, allocator); + } + + public int2 Size { get; } + + protected int GetIndex(int2 point) + { + return math.mad(point.y, Size.x, point.x); + } + + public T GetValue(int index) + { + return _data[index]; + } + + public T GetValue(int2 point) + { + return GetValue(GetIndex(point)); + } + + public unsafe ref T GetValueRW(int index) + { + return ref UnsafeUtility.ArrayElementAsRef(_data.GetUnsafePtr(), index); + } + + public ref T GetValueRW(int2 point) + { + return ref GetValueRW(GetIndex(point)); + } + + public void SetValue(int index, T value) + { + _data[index] = value; + } + + public void SetValue(int2 point, T value) + { + SetValue(GetIndex(point), value); + } + + public NativeArray GetNativeArray() + { + return _data; + } + + public void Dispose() + { + _data.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Collections/SpatialLookup.cs b/Source/Riversong/Game/Collections/SpatialLookup.cs new file mode 100644 index 0000000..b795f35 --- /dev/null +++ b/Source/Riversong/Game/Collections/SpatialLookup.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class SpatialLookup + { + private static readonly Predicate NoopFilter = _ => true; + + private int _cellSize; + + private ListMultiDictionary _lookup = new(); + + public SpatialLookup(int cellSize) + { + _cellSize = cellSize; + } + + public void Add(T obj, TileRect rect, int key) + { + foreach (var cell in CellRange(rect)) _lookup.Add(cell, new Item(obj, rect, key)); + } + + public void Remove(TileRect rect, int key) + { + foreach (var cell in CellRange(rect)) _lookup.Remove(cell, new Item(key)); + } + + private TileRange CellRange(TileRect rect) + { + return TileRange.From(rect.Min / _cellSize, rect.Max / _cellSize); + } + + public void RemoveAll(TileRect rect, Predicate filter = null, List result = null) + { + filter ??= NoopFilter; + + using var closedScope = HashSetPool.Get(out var closed); + using var toRemoveScope = ListPool<(int2, Item)>.Get(out var toRemove); + + var cellMin = rect.Min / _cellSize; + var cellMax = rect.Max / _cellSize; + + int2 cell; + for (cell.x = cellMin.x; cell.x <= cellMax.x; cell.x++) + for (cell.y = cellMin.y; cell.y <= cellMax.y; cell.y++) + { + if (!_lookup.TryGetValues(cell, out var list)) continue; + + if (cell.x == cellMin.x || cell.x == cellMax.x || cell.y == cellMin.y || cell.y == cellMax.y) + { + foreach (var item in list) + { + if (!closed.Add(item.EqualityKey) || !rect.Intersects(item.Rect) || !filter.Invoke(item.Obj)) continue; + toRemove.Add((cell, item)); + result?.Add(item.Obj); + } + } + else + { + if (result != null) + foreach (var item in list) + if (closed.Add(item.EqualityKey) || !filter.Invoke(item.Obj)) + result.Add(item.Obj); + _lookup.Clear(cell); + } + } + + foreach (var (key, value) in toRemove) _lookup.Remove(key, value); + } + + public void Find(TileRect rect, List result) + { + using var closedScope = HashSetPool.Get(out var closed); + + var cellMin = rect.Min / _cellSize; + var cellMax = rect.Max / _cellSize; + + int2 cell; + for (cell.x = cellMin.x; cell.x <= cellMax.x; cell.x++) + for (cell.y = cellMin.y; cell.y <= cellMax.y; cell.y++) + { + if (!_lookup.TryGetValues(cell, out var list)) continue; + + if (cell.x == cellMin.x || cell.x == cellMax.x || cell.y == cellMin.y || cell.y == cellMax.y) + foreach (var item in list) + { + if (!rect.Intersects(item.Rect) || !closed.Add(item.EqualityKey)) continue; + result.Add(item.Obj); + } + else + foreach (var item in list) + if (closed.Add(item.EqualityKey)) + result.Add(item.Obj); + } + } + + public bool FindMax(TileRect rect, Func scoreFunc, out T max) + { + using var resultScope = ListPool.Get(out var result); + + Find(rect, result); + + max = default; + var maxScore = int.MinValue; + + foreach (var item in result) + { + var score = scoreFunc.Invoke(item); + if (score > maxScore) + { + maxScore = score; + max = item; + } + } + + return maxScore > int.MinValue; + } + + private struct Item : IEquatable + { + public T Obj; + + public TileRect Rect; + + public int EqualityKey; + + public Item(T obj, TileRect rect, int equalityKey) + { + Obj = obj; + Rect = rect; + EqualityKey = equalityKey; + } + + public Item(int equalityKey) : this(default, default, equalityKey) + { + } + + public bool Equals(Item other) + { + return EqualityKey == other.EqualityKey; + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/AnalyticsInitializationSystem.cs b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsInitializationSystem.cs new file mode 100644 index 0000000..90cc899 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsInitializationSystem.cs @@ -0,0 +1,27 @@ +using Cysharp.Threading.Tasks; +using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(CommonServicesSystemGroup))] + [InitializeAfter(typeof(CommonServicesSystem))] + public class AnalyticsInitializationSystem : GameSystem, IServiceProvider, IInitializable + { + private IAnalyticsService _analyticsService; + + public AnalyticsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void RegisterServices(IServiceLocator serviceLocator) + { + _analyticsService = AnalyticsServiceFactory.Create(); + serviceLocator.RegisterService(_analyticsService); + } + + public async UniTask InitializeAsync() + { + await _analyticsService.InitializeAsync(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/AnalyticsServiceFactory.cs b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsServiceFactory.cs new file mode 100644 index 0000000..a1e18ad --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsServiceFactory.cs @@ -0,0 +1,14 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class AnalyticsServiceFactory + { + public static IAnalyticsService Create() + { +#if UNITY_EDITOR && !ENABLE_EDITOR_ANALYTICS + return new NoOpAnalyticsService(); +#else + return new UnityAnalyticsService(); +#endif + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/AnalyticsSessionState.cs b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsSessionState.cs new file mode 100644 index 0000000..5b1915b --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/AnalyticsSessionState.cs @@ -0,0 +1,86 @@ +using System; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class AnalyticsSessionState + { + private SessionStatus _status; + + public string SessionId { get; private set; } + + public float StartRealtimeSeconds { get; private set; } + + public int HeartbeatIndex { get; private set; } + + public int TotalBuildingsPlaced { get; private set; } + + public bool Started => _status != SessionStatus.NotStarted; + + public bool DemoCompleted => _status == SessionStatus.DemoCompleted; + + public void Begin(float realtimeSeconds) + { + _status = SessionStatus.Started; + SessionId = Guid.NewGuid().ToString("N"); + StartRealtimeSeconds = realtimeSeconds; + HeartbeatIndex = 0; + TotalBuildingsPlaced = 0; + } + + public bool TryRecordBuildingConstruction(BuildingDefinition definition) + { + if (!Started || !definition) return false; + + TotalBuildingsPlaced++; + + return true; + } + + public bool CanRecordHouseUpgrade(Building building) + { + return Started && building.Definition.IsHouse; + } + + public bool TryAdvanceHeartbeat(float realtimeSeconds, float heartbeatIntervalSeconds, out int heartbeatIndex, out float playtimeSeconds) + { + heartbeatIndex = 0; + playtimeSeconds = 0; + + if (!Started || DemoCompleted) return false; + + var nextHeartbeatTime = StartRealtimeSeconds + (HeartbeatIndex + 1) * heartbeatIntervalSeconds; + if (realtimeSeconds < nextHeartbeatTime) return false; + + heartbeatIndex = ++HeartbeatIndex; + playtimeSeconds = GetPlaytimeSeconds(realtimeSeconds); + + return true; + } + + public bool TryMarkDemoCompleted() + { + if (!Started || DemoCompleted) return false; + + _status = SessionStatus.DemoCompleted; + + return true; + } + + public float GetPlaytimeSeconds(float realtimeSeconds) + { + if (!Started) return 0; + + return Mathf.Max(realtimeSeconds - StartRealtimeSeconds, 0); + } + + private enum SessionStatus + { + NotStarted, + + Started, + + DemoCompleted + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/DemoAnalyticsSystem.cs b/Source/Riversong/Game/CommonServices/Analytics/DemoAnalyticsSystem.cs new file mode 100644 index 0000000..2510eb5 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/DemoAnalyticsSystem.cs @@ -0,0 +1,89 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(CommonServicesSystemGroup))] + [InitializeAfter(typeof(AnalyticsInitializationSystem))] + public class DemoAnalyticsSystem : GameSystem, IInitializable, IDisposable, IUpdatable + { + private const float HeartbeatIntervalSeconds = 300; + + [InjectService] + private IAnalyticsService _analyticsService; + + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private World _world; + + private AnalyticsSessionState _sessionState = new(); + + public DemoAnalyticsSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _signalBus.Subscribe(OnWorldReady); + _signalBus.Subscribe(OnBuildingCreated); + _signalBus.Subscribe(OnBuildingUpgraded); + _signalBus.Subscribe(OnDemoCompleted); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnWorldReady); + _signalBus.Unsubscribe(OnBuildingCreated); + _signalBus.Unsubscribe(OnBuildingUpgraded); + _signalBus.Unsubscribe(OnDemoCompleted); + } + + public void Update() + { + var realtimeSeconds = Time.realtimeSinceStartup; + + while (_sessionState.TryAdvanceHeartbeat(realtimeSeconds, HeartbeatIntervalSeconds, out var heartbeatIndex, out var playtimeSeconds)) + _analyticsService.RecordHeartbeat(_sessionState.SessionId, heartbeatIndex, playtimeSeconds, _sessionState.TotalBuildingsPlaced, _world.PopulationState.Population); + } + + private void OnWorldReady(WorldReadySignal signal) + { + _sessionState.Begin(Time.realtimeSinceStartup); + _analyticsService.RecordSessionStarted(_sessionState.SessionId); + } + + private void OnBuildingCreated(BuildingCreatedSignal signal) + { + var definition = signal.Building?.Definition; + if (!_sessionState.TryRecordBuildingConstruction(definition)) return; + + _analyticsService.RecordBuildingConstructionCompleted(_sessionState.SessionId, definition.name); + } + + private void OnBuildingUpgraded(BuildingUpgradedSignal signal) + { + var building = signal.Building; + if (!_sessionState.CanRecordHouseUpgrade(building)) return; + + _analyticsService.RecordHouseUpgraded(_sessionState.SessionId, building.Definition.name, building.TierIndex, building.TierIndex + 1); + } + + private void OnDemoCompleted(DemoCompletedSignal signal) + { + if (!_sessionState.TryMarkDemoCompleted()) return; + + var playtimeSeconds = _sessionState.GetPlaytimeSeconds(Time.realtimeSinceStartup); + _analyticsService.RecordDemoCompleted( + _sessionState.SessionId, + playtimeSeconds, + _world.TimeState.TotalWeeks, + _world.PopulationState.Population, + _world.PopulationState.Happiness); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/Events/BuildingConstructionCompletedAnalyticsEvent.cs b/Source/Riversong/Game/CommonServices/Analytics/Events/BuildingConstructionCompletedAnalyticsEvent.cs new file mode 100644 index 0000000..3399f78 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/Events/BuildingConstructionCompletedAnalyticsEvent.cs @@ -0,0 +1,21 @@ +using Unity.Services.Analytics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public sealed class BuildingConstructionCompletedAnalyticsEvent : Event + { + public BuildingConstructionCompletedAnalyticsEvent() : base("building_construction_completed") + { + } + + public string SessionId + { + set => SetParameter("session_id", value); + } + + public string BuildingName + { + set => SetParameter("building_name", value); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/Events/DemoCompletedAnalyticsEvent.cs b/Source/Riversong/Game/CommonServices/Analytics/Events/DemoCompletedAnalyticsEvent.cs new file mode 100644 index 0000000..826acda --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/Events/DemoCompletedAnalyticsEvent.cs @@ -0,0 +1,36 @@ +using Unity.Services.Analytics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public sealed class DemoCompletedAnalyticsEvent : Event + { + public DemoCompletedAnalyticsEvent() : base("demo_completed") + { + } + + public string SessionId + { + set => SetParameter("session_id", value); + } + + public float PlaytimeSeconds + { + set => SetParameter("playtime_seconds", value); + } + + public int TotalWeeks + { + set => SetParameter("total_weeks", value); + } + + public int Population + { + set => SetParameter("population", value); + } + + public float Happiness + { + set => SetParameter("happiness", value); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/Events/HouseUpgradedAnalyticsEvent.cs b/Source/Riversong/Game/CommonServices/Analytics/Events/HouseUpgradedAnalyticsEvent.cs new file mode 100644 index 0000000..7f574f2 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/Events/HouseUpgradedAnalyticsEvent.cs @@ -0,0 +1,31 @@ +using Unity.Services.Analytics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public sealed class HouseUpgradedAnalyticsEvent : Event + { + public HouseUpgradedAnalyticsEvent() : base("house_upgraded") + { + } + + public string SessionId + { + set => SetParameter("session_id", value); + } + + public string BuildingName + { + set => SetParameter("building_name", value); + } + + public int FromTier + { + set => SetParameter("from_tier", value); + } + + public int ToTier + { + set => SetParameter("to_tier", value); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/Events/SessionHeartbeatAnalyticsEvent.cs b/Source/Riversong/Game/CommonServices/Analytics/Events/SessionHeartbeatAnalyticsEvent.cs new file mode 100644 index 0000000..4fb9795 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/Events/SessionHeartbeatAnalyticsEvent.cs @@ -0,0 +1,36 @@ +using Unity.Services.Analytics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public sealed class SessionHeartbeatAnalyticsEvent : Event + { + public SessionHeartbeatAnalyticsEvent() : base("session_heartbeat") + { + } + + public string SessionId + { + set => SetParameter("session_id", value); + } + + public int HeartbeatIndex + { + set => SetParameter("heartbeat_index", value); + } + + public float PlaytimeSeconds + { + set => SetParameter("playtime_seconds", value); + } + + public int TotalBuildingsPlaced + { + set => SetParameter("total_buildings_placed", value); + } + + public int Population + { + set => SetParameter("population", value); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/Events/SessionStartedAnalyticsEvent.cs b/Source/Riversong/Game/CommonServices/Analytics/Events/SessionStartedAnalyticsEvent.cs new file mode 100644 index 0000000..ae7ae66 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/Events/SessionStartedAnalyticsEvent.cs @@ -0,0 +1,16 @@ +using Unity.Services.Analytics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public sealed class SessionStartedAnalyticsEvent : Event + { + public SessionStartedAnalyticsEvent() : base("session_started") + { + } + + public string SessionId + { + set => SetParameter("session_id", value); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/IAnalyticsService.cs b/Source/Riversong/Game/CommonServices/Analytics/IAnalyticsService.cs new file mode 100644 index 0000000..50055ec --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/IAnalyticsService.cs @@ -0,0 +1,19 @@ +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IAnalyticsService + { + UniTask InitializeAsync(); + + void RecordSessionStarted(string sessionId); + + void RecordBuildingConstructionCompleted(string sessionId, string buildingName); + + void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier); + + void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population); + + void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/NoOpAnalyticsService.cs b/Source/Riversong/Game/CommonServices/Analytics/NoOpAnalyticsService.cs new file mode 100644 index 0000000..52874b0 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/NoOpAnalyticsService.cs @@ -0,0 +1,32 @@ +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class NoOpAnalyticsService : IAnalyticsService + { + public UniTask InitializeAsync() + { + return UniTask.CompletedTask; + } + + public void RecordSessionStarted(string sessionId) + { + } + + public void RecordBuildingConstructionCompleted(string sessionId, string buildingName) + { + } + + public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier) + { + } + + public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population) + { + } + + public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness) + { + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Analytics/UnityAnalyticsService.cs b/Source/Riversong/Game/CommonServices/Analytics/UnityAnalyticsService.cs new file mode 100644 index 0000000..1740440 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Analytics/UnityAnalyticsService.cs @@ -0,0 +1,101 @@ +using System; +using Cysharp.Threading.Tasks; +using Unity.Services.Analytics; +using Unity.Services.Core; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class UnityAnalyticsService : IAnalyticsService + { + private bool _canRecord; + + public async UniTask InitializeAsync() + { + if (_canRecord) return; + + try + { + if (UnityServices.State == ServicesInitializationState.Uninitialized) await UnityServices.InitializeAsync(); + + AnalyticsService.Instance.StartDataCollection(); + + _canRecord = true; + } + catch (Exception exception) + { + Debug.LogError($"Failed to initialize analytics. Analytics will be disabled.\n{exception}"); + } + } + + public void RecordSessionStarted(string sessionId) + { + if (!_canRecord) return; + + var analyticsEvent = new SessionStartedAnalyticsEvent { SessionId = sessionId }; + + AnalyticsService.Instance.RecordEvent(analyticsEvent); + } + + public void RecordBuildingConstructionCompleted(string sessionId, string buildingName) + { + if (!_canRecord) return; + + var analyticsEvent = new BuildingConstructionCompletedAnalyticsEvent + { + SessionId = sessionId, + BuildingName = buildingName + }; + + AnalyticsService.Instance.RecordEvent(analyticsEvent); + } + + public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier) + { + if (!_canRecord) return; + + var analyticsEvent = new HouseUpgradedAnalyticsEvent + { + SessionId = sessionId, + BuildingName = buildingName, + FromTier = fromTier, + ToTier = toTier + }; + + AnalyticsService.Instance.RecordEvent(analyticsEvent); + } + + public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population) + { + if (!_canRecord) return; + + var analyticsEvent = new SessionHeartbeatAnalyticsEvent + { + SessionId = sessionId, + HeartbeatIndex = heartbeatIndex, + PlaytimeSeconds = playtimeSeconds, + TotalBuildingsPlaced = totalBuildingsPlaced, + Population = population + }; + + AnalyticsService.Instance.RecordEvent(analyticsEvent); + } + + public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness) + { + if (!_canRecord) return; + + var analyticsEvent = new DemoCompletedAnalyticsEvent + { + SessionId = sessionId, + PlaytimeSeconds = playtimeSeconds, + TotalWeeks = totalWeeks, + Population = population, + Happiness = Mathf.Clamp01(happiness) + }; + + AnalyticsService.Instance.RecordEvent(analyticsEvent); + AnalyticsService.Instance.Flush(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/CommonServicesSystem.cs b/Source/Riversong/Game/CommonServices/CommonServicesSystem.cs new file mode 100644 index 0000000..d9afed1 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/CommonServicesSystem.cs @@ -0,0 +1,58 @@ +using System; +using Cysharp.Threading.Tasks; +using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(CommonServicesSystemGroup))] + public class CommonServicesSystem : GameSystem, IServiceProvider, IInitializable, IDisposable + { + [InjectService] + private GameConfig _config; + + [InjectService] + private World _world; + + private SignalBus _signalBus; + + private TileSpace _tileSpace; + + private MaterialReplacementCache _materialReplacementCache; + + public CommonServicesSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void RegisterServices(IServiceLocator serviceLocator) + { + serviceLocator.RegisterService(new EntityCollection()); + serviceLocator.RegisterService(new EntityCache()); + + _signalBus = new SignalBus(); + serviceLocator.RegisterService(_signalBus); + + _tileSpace = new TileSpace(); + serviceLocator.RegisterService(_tileSpace); + + serviceLocator.RegisterService(new PoolingService()); + + _materialReplacementCache = new MaterialReplacementCache(); + serviceLocator.RegisterService(_materialReplacementCache); + } + + public UniTask InitializeAsync() + { + _tileSpace.TileSize = _config.GeneralSettings.TileSize; + _tileSpace.World = _world; + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus?.Dispose(); + _signalBus = null; + _materialReplacementCache = null; + } + } +} diff --git a/Source/Riversong/Game/CommonServices/CommonServicesSystemGroup.cs b/Source/Riversong/Game/CommonServices/CommonServicesSystemGroup.cs new file mode 100644 index 0000000..278feb5 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/CommonServicesSystemGroup.cs @@ -0,0 +1,8 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [InitializeBefore(typeof(EarlyGameSystemGroup))] + public class CommonServicesSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/Entity.cs b/Source/Riversong/Game/CommonServices/Entities/Entity.cs new file mode 100644 index 0000000..9e5f87c --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/Entity.cs @@ -0,0 +1,14 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class Entity + { + public const int InvalidId = 0; + + public int Id { get; private set; } + + public static T Create(int id) where T : Entity, new() + { + return new T { Id = id }; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCache.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCache.cs new file mode 100644 index 0000000..74553fd --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCache.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EntityCache : IEntityCache + { + private Dictionary _caches = new(); + + public void CreateCache(int key, Predicate filter) where T : Entity + { + if (!_caches.TryGetValue(key, out var cache)) + { + cache = new Cache(); + _caches.Add(key, cache); + } + ((Cache)cache).Filter = filter; + } + + public List Get(int key) where T : Entity + { + if (!_caches.TryGetValue(key, out var cache)) + { + cache = new Cache(); + _caches.Add(key, cache); + } + return ((Cache)cache).Entities; + } + + public void OnAdded(Entity entity) + { + foreach (var cache in _caches.Values) cache.TryAdd(entity); + } + + public void OnRemoved(Entity entity) + { + foreach (var cache in _caches.Values) cache.TryRemove(entity); + } + + private abstract class Cache + { + public void TryAdd(Entity entity) + { + if (FilterEntity(entity)) Add(entity); + } + + public void TryRemove(Entity entity) + { + if (FilterEntity(entity)) Remove(entity); + } + + protected abstract bool FilterEntity(Entity entity); + + protected abstract void Add(Entity entity); + + protected abstract void Remove(Entity entity); + } + + private class Cache : Cache where T : Entity + { + public Predicate Filter { get; set; } + + public List Entities { get; } = new(); + + protected override bool FilterEntity(Entity entity) + { + return entity is T typedEntity && Filter.Invoke(typedEntity); + } + + protected override void Add(Entity entity) + { + Entities.Add((T)entity); + } + + protected override void Remove(Entity entity) + { + Entities.Remove((T)entity); + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheExtensions.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheExtensions.cs new file mode 100644 index 0000000..e028db4 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheExtensions.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class EntityCacheExtensions + { + public static List GetHarvesterBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.HarvesterBuildings); + } + + public static List GetHunterBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.HunterBuildings); + } + + public static List GetFarmBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.FarmBuildings); + } + + public static List GetProducers(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.ProducerBuildings); + } + + public static List GetProviders(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.ProviderBuildings); + } + + public static List GetBuildingsWithWorkers(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.BuildingsWithWorkers); + } + + public static List GetHouses(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.HouseBuildings); + } + + public static List GetTentBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.TentBuildings); + } + + public static List GetStorageBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.StorageBuildings); + } + + public static List GetStorageRequestBuildings(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.StorageRequestBuildings); + } + + public static List GetHunterAgents(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.HunterAgents); + } + + public static List GetCritterAgents(this IEntityCache entityCache) + { + return entityCache.Get((int)EntityCacheKeys.CritterAgents); + } + } +} diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheKeys.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheKeys.cs new file mode 100644 index 0000000..844aac4 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheKeys.cs @@ -0,0 +1,39 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum EntityCacheKeys + { + Invalid, + + #region Buildings + + HarvesterBuildings, + + HunterBuildings, + + FarmBuildings, + + ProducerBuildings, + + ProviderBuildings, + + BuildingsWithWorkers, + + HouseBuildings, + + StorageBuildings, + + StorageRequestBuildings, + + TentBuildings, + + #endregion + + #region Agents + + HunterAgents, + + CritterAgents + + #endregion + } +} diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheSystem.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheSystem.cs new file mode 100644 index 0000000..d955978 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCache/EntityCacheSystem.cs @@ -0,0 +1,71 @@ +using System; +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + [InitializeAfter(typeof(PreLoadAssetsSystem))] + public class EntityCacheSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private IEntityCollection _entityCollection; + + [InjectService] + private IEntityCache _entityCache; + + [InjectService] + private GameConfig _config; + + public EntityCacheSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + CreateCaches(); + + foreach (var entity in _entityCollection.GetInternalEntityList(typeof(Entity))) _entityCache.OnAdded(entity); + + var callbacks = _entityCollection.On(); + callbacks.Added += _entityCache.OnAdded; + callbacks.Removed += _entityCache.OnRemoved; + + return UniTask.CompletedTask; + } + + private void CreateCaches() + { + _entityCache.CreateCache((int)EntityCacheKeys.HarvesterBuildings, b => b.Definition.HarvestedResource); + _entityCache.CreateCache((int)EntityCacheKeys.HunterBuildings, b => b.Definition.TargetCritter); + _entityCache.CreateCache((int)EntityCacheKeys.FarmBuildings, b => b.Definition.IsFarm); + _entityCache.CreateCache((int)EntityCacheKeys.ProducerBuildings, b => b.Definition.Recipe); + _entityCache.CreateCache((int)EntityCacheKeys.ProviderBuildings, b => b.Definition.ProvidedProducts.Count > 0); + _entityCache.CreateCache((int)EntityCacheKeys.BuildingsWithWorkers, b => b.Definition.WorkerCount > 0); + _entityCache.CreateCache((int)EntityCacheKeys.HouseBuildings, b => b.Definition.IsHouse); + _entityCache.CreateCache((int)EntityCacheKeys.StorageBuildings, b => b.Definition.IsStorage); + _entityCache.CreateCache((int)EntityCacheKeys.StorageRequestBuildings, b => b.Definition.IsStorage); + _entityCache.CreateCache((int)EntityCacheKeys.TentBuildings, b => b.Definition == _config.Population.TentBuilding.Asset); + _entityCache.CreateCache( + (int)EntityCacheKeys.HunterAgents, + a => + { + ref var jobState = ref a.GetJobStateRW(); + return jobState.Job == AgentJob.Hunter; + }); + _entityCache.CreateCache( + (int)EntityCacheKeys.CritterAgents, + a => + { + ref var critterState = ref a.GetCritterStateRW(); + return critterState.IsCritter; + }); + } + + public void Dispose() + { + var callbacks = _entityCollection.On(); + callbacks.Added -= _entityCache.OnAdded; + callbacks.Removed -= _entityCache.OnRemoved; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCache/IEntityCache.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCache/IEntityCache.cs new file mode 100644 index 0000000..c987d73 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCache/IEntityCache.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEntityCache + { + void CreateCache(int key, Predicate filter) where T : Entity; + + List Get(int key) where T : Entity; + + void OnAdded(Entity entity); + + void OnRemoved(Entity entity); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCollection.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCollection.cs new file mode 100644 index 0000000..c579358 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCollection.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EntityCollection : IEntityCollection + { + private static readonly List Empty = new(); + + private int _nextId = Entity.InvalidId + 1; + + private Dictionary _entitiesById = new(); + + private ListMultiDictionary _entitiesByType = new(); + + private Dictionary _callbacks = new(); + + public T Create() where T : Entity, new() + { + return Entity.Create(_nextId++); + } + + public void Add(Entity entity) + { + _entitiesById.Add(entity.Id, entity); + OnAdded(entity.GetType(), entity); + } + + private void OnAdded(Type type, Entity entity) + { + _entitiesByType.Add(type, entity); + + if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnAdded(entity); + + if (type == typeof(Entity)) return; + + OnAdded(type.BaseType, entity); + } + + public Entity Remove(int id) + { + if (!_entitiesById.Remove(id, out var entity)) return null; + + OnRemoved(entity.GetType(), entity); + + return entity; + } + + private void OnRemoved(Type type, Entity entity) + { + _entitiesByType.Remove(type, entity); + + if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnRemoved(entity); + + if (type == typeof(Entity)) return; + + OnRemoved(type.BaseType, entity); + } + + public bool Exists(int id) + { + return _entitiesById.ContainsKey(id); + } + + public Entity Get(int id) + { + return _entitiesById.TryGetValue(id, out var entity) ? entity : null; + } + + public Type GetEntityType(int id) + { + return Get(id)?.GetType(); + } + + public List GetInternalEntityList(Type type) + { + return _entitiesByType.TryGetValues(type, out var entityList) ? entityList : Empty; + } + + public IEntityCollectionCallbacks On() where T : Entity + { + if (!_callbacks.TryGetValue(typeof(T), out var callbacks)) + { + callbacks = new EntityCollectionCallbacks(); + _callbacks.Add(typeof(T), callbacks); + } + return (IEntityCollectionCallbacks)callbacks; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCollectionCallbacks.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCollectionCallbacks.cs new file mode 100644 index 0000000..431df82 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCollectionCallbacks.cs @@ -0,0 +1,21 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EntityCollectionCallbacks : IEntityCollectionCallbacks where T : Entity + { + public event Action Added; + + public event Action Removed; + + public void OnAdded(Entity entity) + { + Added?.Invoke((T)entity); + } + + public void OnRemoved(Entity entity) + { + Removed?.Invoke((T)entity); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/EntityCollectionExtensions.cs b/Source/Riversong/Game/CommonServices/Entities/EntityCollectionExtensions.cs new file mode 100644 index 0000000..4ebf3b8 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/EntityCollectionExtensions.cs @@ -0,0 +1,23 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class EntityCollectionExtensions + { + public static T Get(this IEntityCollection entityCollection, int id) where T : Entity + { + return (T)entityCollection.Get(id); + } + + public static T CreateAndAdd(this IEntityCollection entityCollection) where T : Entity, new() + { + var entity = entityCollection.Create(); + entityCollection.Add(entity); + return entity; + } + + public static bool TryGet(this IEntityCollection entityCollection, int id, out T entity) where T : Entity + { + entity = entityCollection.Get(id) as T; + return entity != null; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/IEntity.cs b/Source/Riversong/Game/CommonServices/Entities/IEntity.cs new file mode 100644 index 0000000..d70213d --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/IEntity.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEntity + { + int Id { get; } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/IEntityCollection.cs b/Source/Riversong/Game/CommonServices/Entities/IEntityCollection.cs new file mode 100644 index 0000000..34933d3 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/IEntityCollection.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEntityCollection + { + T Create() where T : Entity, new(); + + void Add(Entity entity); + + Entity Remove(int id); + + bool Exists(int id); + + Entity Get(int id); + + Type GetEntityType(int id); + + public List GetInternalEntityList(Type type); + + public IEntityCollectionCallbacks On() where T : Entity; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Entities/IEntityCollectionCallbacks.cs b/Source/Riversong/Game/CommonServices/Entities/IEntityCollectionCallbacks.cs new file mode 100644 index 0000000..d0c55a3 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Entities/IEntityCollectionCallbacks.cs @@ -0,0 +1,18 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEntityCollectionCallbacks + { + public void OnAdded(Entity entity); + + public void OnRemoved(Entity entity); + } + + public interface IEntityCollectionCallbacks : IEntityCollectionCallbacks + { + event Action Added; + + event Action Removed; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Pooling/IPoolingService.cs b/Source/Riversong/Game/CommonServices/Pooling/IPoolingService.cs new file mode 100644 index 0000000..2f70ec8 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Pooling/IPoolingService.cs @@ -0,0 +1,11 @@ +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IPoolingService + { + void AddPool(int key, IObjectPool pool) where T : class; + + IObjectPool GetPool(int key) where T : class; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Pooling/PooledObject.cs b/Source/Riversong/Game/CommonServices/Pooling/PooledObject.cs new file mode 100644 index 0000000..0b7f632 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Pooling/PooledObject.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class PooledObject : MonoBehaviour + { + public int PoolKey { get; set; } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Pooling/PoolingService.cs b/Source/Riversong/Game/CommonServices/Pooling/PoolingService.cs new file mode 100644 index 0000000..6353e41 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Pooling/PoolingService.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class PoolingService : IPoolingService + { + private Dictionary _poolLookup = new(); + + public void AddPool(int key, IObjectPool pool) where T : class + { + _poolLookup.Add(key, pool); + } + + public IObjectPool GetPool(int key) where T : class + { + return _poolLookup.TryGetValue(key, out var pool) ? (IObjectPool)pool : null; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Pooling/PoolingServiceExtensions.cs b/Source/Riversong/Game/CommonServices/Pooling/PoolingServiceExtensions.cs new file mode 100644 index 0000000..8e06780 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Pooling/PoolingServiceExtensions.cs @@ -0,0 +1,88 @@ +using UnityEngine; +using UnityEngine.Pool; +using Object = UnityEngine.Object; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class PoolingServiceExtensions + { + public static IObjectPool GetOrCreatePool(this IPoolingService poolingService, GameObject prefab, Transform folder = null) + { + var poolKey = prefab.GetInstanceID(); + + var pool = poolingService.GetPool(poolKey); + if (pool != null) return pool; + + pool = new ObjectPool( + () => + { + var go = Object.Instantiate(prefab, folder); + OnCreate(go, poolKey); + return go; + }, + OnGet, + go => OnRelease(go, folder)); + + poolingService.AddPool(poolKey, pool); + + return pool; + } + + public static IObjectPool GetOrCreatePool(this IPoolingService poolingService, T prefab, Transform folder = null) where T : Component + { + var poolKey = prefab.GetInstanceID(); + + var pool = poolingService.GetPool(poolKey); + if (pool != null) return pool; + + pool = new ObjectPool( + () => + { + var component = Object.Instantiate(prefab, folder); + OnCreate(component.gameObject, poolKey); + return component; + }, + component => OnGet(component.gameObject), + component => OnRelease(component.gameObject, folder)); + + poolingService.AddPool(poolKey, pool); + + return pool; + } + + private static void OnCreate(GameObject go, int poolKey) + { + go.AddComponent().PoolKey = poolKey; + go.SetActive(false); + } + + private static void OnGet(GameObject go) + { + go.transform.SetParent(null); + go.SetActive(true); + } + + private static void OnRelease(GameObject go, Transform folder) + { + go.SetActive(false); + go.transform.SetParent(folder); + } + + public static T Get(this IPoolingService poolingService, int poolKey) where T : class + { + return poolingService.GetPool(poolKey).Get(); + } + + public static void Release(this IPoolingService poolingService, GameObject go) + { + var poolKey = go.GetComponent().PoolKey; + poolingService.GetPool(poolKey).Release(go); + } + + public static void Release(this IPoolingService poolingService, T component) where T : Component + { + var poolKey = component.GetComponent().PoolKey; + poolingService.GetPool(poolKey).Release(component); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Pooling/PoolsInitializationSystem.cs b/Source/Riversong/Game/CommonServices/Pooling/PoolsInitializationSystem.cs new file mode 100644 index 0000000..71d8e94 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Pooling/PoolsInitializationSystem.cs @@ -0,0 +1,31 @@ +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + [InitializeAfter(typeof(PreLoadAssetsSystem))] + public class PoolsInitializationSystem : GameSystem, IInitializable + { + [InjectService] + private IPoolingService _poolingService; + + [InjectService] + private IGameDatabase _gameDatabase; + + public PoolsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + foreach (var product in _gameDatabase.OfType()) + { + _poolingService.GetOrCreatePool((GameObject)product.ProductStackVisualization.Asset); + _poolingService.GetOrCreatePool((GameObject)product.CarriedVisualization.Asset); + } + + return UniTask.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/RestoreTemporaryMaterialsSystem.cs b/Source/Riversong/Game/CommonServices/RestoreTemporaryMaterialsSystem.cs new file mode 100644 index 0000000..3c9eed9 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/RestoreTemporaryMaterialsSystem.cs @@ -0,0 +1,21 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(DefaultGameSystemGroup))] + [UpdateBefore(typeof(EditingStateGameSystem))] + [UpdateBefore(typeof(BuildingSelectionSystem))] + public class RestoreTemporaryMaterialsSystem : GameSystem, IUpdatable + { + [InjectService] + private MaterialReplacementCache _materialReplacementCache; + + public RestoreTemporaryMaterialsSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void Update() + { + _materialReplacementCache.RestoreMaterials(); + } + } +} diff --git a/Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs b/Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs new file mode 100644 index 0000000..d44d080 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs @@ -0,0 +1,13 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface ISignalBus + { + void Raise(T signal); + + void Subscribe(Action handler); + + void Unsubscribe(Action handler); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/Signals/SignalBus.cs b/Source/Riversong/Game/CommonServices/Signals/SignalBus.cs new file mode 100644 index 0000000..10e51dc --- /dev/null +++ b/Source/Riversong/Game/CommonServices/Signals/SignalBus.cs @@ -0,0 +1,31 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class SignalBus : ISignalBus, IDisposable + { + private readonly ListMultiDictionary _subscribers = new(); + + public void Dispose() + { + _subscribers.Clear(); + } + + public void Raise(T signal) + { + if (!_subscribers.TryGetValues(typeof(T), out var handlers)) return; + + foreach (var handler in handlers) ((Action)handler).Invoke(signal); + } + + public void Subscribe(Action handler) + { + _subscribers.Add(typeof(T), handler); + } + + public void Unsubscribe(Action handler) + { + _subscribers.Remove(typeof(T), handler); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/DirectionVectors.cs b/Source/Riversong/Game/CommonServices/TileMath/DirectionVectors.cs new file mode 100644 index 0000000..4b733a5 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/DirectionVectors.cs @@ -0,0 +1,27 @@ +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class DirectionVectors + { + public static readonly int2[] Directions4 = + { + new(1, 0), + new(-1, 0), + new(0, 1), + new(0, -1) + }; + + public static readonly int2[] Directions8 = + { + new(1, 0), + new(-1, 0), + new(0, 1), + new(0, -1), + new(1, 1), + new(1, -1), + new(-1, 1), + new(-1, -1) + }; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/Directions.cs b/Source/Riversong/Game/CommonServices/TileMath/Directions.cs new file mode 100644 index 0000000..971c8c9 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/Directions.cs @@ -0,0 +1,134 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum Directions + { + North, + + NorthWest, + + West, + + SouthWest, + + South, + + SouthEast, + + East, + + NorthEast + } + + public enum DirectionsMask4 + { + None = 0, + + North = 1 << 0, + + West = 1 << 1, + + South = 1 << 2, + + East = 1 << 3 + } + + [Flags] + public enum DirectionsMask8 + { + None = 0, + + North = 1 << 0, + + NorthWest = 1 << 1, + + West = 1 << 2, + + SouthWest = 1 << 3, + + South = 1 << 4, + + SouthEast = 1 << 5, + + East = 1 << 6, + + NorthEast = 1 << 7 + } + + public static class DirectionsExtensions + { + public static int2 ToVector(this Directions direction) + { + switch (direction) + { + case Directions.North: + return new int2(0, 1); + + case Directions.NorthWest: + return new int2(-1, 1); + + case Directions.West: + return new int2(-1, 0); + + case Directions.SouthWest: + return new int2(-1, -1); + + case Directions.South: + return new int2(0, -1); + + case Directions.SouthEast: + return new int2(1, -1); + + case Directions.East: + return new int2(1, 0); + + case Directions.NorthEast: + return new int2(1, 1); + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static Quaternion ToQuaternion(this Directions direction) + { + switch (direction) + { + case Directions.North: + return Quaternion.identity; + + case Directions.NorthWest: + return Quaternion.LookRotation(new Vector3(-1, 0, 1)); + + case Directions.West: + return Quaternion.LookRotation(Vector3.left); + + case Directions.SouthWest: + return Quaternion.LookRotation(new Vector3(-1, 0, -1)); + + case Directions.South: + return Quaternion.LookRotation(Vector3.back); + + case Directions.SouthEast: + return Quaternion.LookRotation(new Vector3(1, 0, -1)); + + case Directions.East: + return Quaternion.LookRotation(Vector3.right); + + case Directions.NorthEast: + return Quaternion.LookRotation(new Vector3(1, 0, 1)); + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static int2 Rotate(this Directions direction, int2 v) + { + return TileMath.Rotate(v, direction); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs b/Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs new file mode 100644 index 0000000..091ed0b --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs @@ -0,0 +1,18 @@ +using Unity.Mathematics; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface ITileSpace + { + float TileSize { get; } + + int2 WorldToTile(Vector3 position); + + Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f); + + int GetElevation(float y); + + Vector3 GetRectWorldCenter(in TileRect rect); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/TileMath.cs b/Source/Riversong/Game/CommonServices/TileMath/TileMath.cs new file mode 100644 index 0000000..a1e4505 --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/TileMath.cs @@ -0,0 +1,136 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class TileMath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int2 Rotate(int2 v, Directions direction) + { + switch (direction) + { + case Directions.North: + return new int2(v.x, v.y); + + case Directions.West: + return new int2(v.y, -v.x); + + case Directions.South: + return new int2(-v.x, -v.y); + + case Directions.East: + return new int2(-v.y, v.x); + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public static TileRect GetBuildingRect(int2 rectCenter, Directions direction, int width, int height) + { + int dx; + int dy; + + var sx = (width - 1) >> 1; + var sy = (height - 1) >> 1; + + if (width == height || direction == Directions.North) + { + dx = -sx; + dy = -sy; + } + else + { + switch (direction) + { + case Directions.West: + dx = sy - (height - 1); + dy = -sx; + (width, height) = (height, width); + break; + + case Directions.South: + dx = sx - (width - 1); + dy = sy - (height - 1); + break; + + case Directions.East: + dx = -sy; + dy = sx - (width - 1); + (width, height) = (height, width); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + return new TileRect(rectCenter + new int2(dx, dy), width, height); + } + + public static void WalkLine(int2 startTile, int2 endTile, Action tileAction) + { + var dx = endTile.x - startTile.x; + var dy = endTile.y - startTile.y; + if (math.abs(dx) >= math.abs(dy)) + endTile.y = startTile.y; + else + endTile.x = startTile.x; + + var tile = startTile; + var d = math.sign(endTile - startTile); + + while (true) + { + tileAction.Invoke(tile); + + if (math.all(tile == endTile)) break; + + tile += d; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int StepCount(int2 tile, int2 otherTile) + { + var delta = math.abs(tile - otherTile); + return math.max(delta.x, delta.y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int StepCount(int2 tile, in TileRect rect) + { + var left = math.max(rect.Min.x - tile.x, 0); + var right = math.max(tile.x - rect.Max.x, 0); + var dx = math.max(left, right); + + var bottom = math.max(rect.Min.y - tile.y, 0); + var top = math.max(tile.y - rect.Max.y, 0); + var dy = math.max(bottom, top); + + return math.max(dx, dy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int StepCount(in TileRect rect, int2 tile) + { + return StepCount(tile, rect); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int StepCount(in TileRect rect, in TileRect otherRect) + { + var left = math.max(otherRect.Min.x - rect.Max.x, 0); + var right = math.max(rect.Min.x - otherRect.Max.x, 0); + var dx = math.max(left, right); + + var bottom = math.max(otherRect.Min.y - rect.Max.y, 0); + var top = math.max(rect.Min.y - otherRect.Max.y, 0); + var dy = math.max(bottom, top); + + return math.max(dx, dy); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/TileRange.cs b/Source/Riversong/Game/CommonServices/TileMath/TileRange.cs new file mode 100644 index 0000000..767512a --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/TileRange.cs @@ -0,0 +1,88 @@ +using System.Collections; +using System.Collections.Generic; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct TileRange : IEnumerable + { + public int2 Min; + + public int2 Max; + + private TileRange(int2 min, int2 max) + { + Min = min; + Max = max; + } + + public static TileRange From(in TileRect rect) + { + return new TileRange(rect.Min, rect.Max); + } + + public static TileRange From(int2 min, int2 max) + { + return From(new TileRect(min, max)); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(Min, Max); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + public int2 Min; + + public int2 Max; + + public int2 Current { get; private set; } + + object IEnumerator.Current => Current; + + public Enumerator(int2 min, int2 max) : this() + { + Min = min; + Max = max; + Reset(); + } + + public bool MoveNext() + { + if (Current.x < Max.x) + { + Current = new int2(Current.x + 1, Current.y); + return true; + } + + if (Current.y < Max.y) + { + Current = new int2(Min.x, Current.y + 1); + return true; + } + + return false; + } + + public void Reset() + { + Current = new int2(Min.x - 1, Min.y); + } + + public void Dispose() + { + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/TileRect.cs b/Source/Riversong/Game/CommonServices/TileMath/TileRect.cs new file mode 100644 index 0000000..9be59dc --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/TileRect.cs @@ -0,0 +1,71 @@ +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct TileRect + { + public static readonly TileRect Empty = new(int2.zero, int2.zero); + + public static readonly TileRect Everything = new(int.MinValue, int.MaxValue); + + private int2 _min; + + private int2 _max; + + public int2 Min + { + readonly get => _min; + set + { + _min = value; + _max = math.max(_min, _max); + } + } + + public int2 Max + { + readonly get => _max; + set + { + _max = value; + _min = math.min(_min, _max); + } + } + + public int Width => _max.x - _min.x + 1; + + public int Height => _max.y - _min.y + 1; + + public int2 Center => (_min + _max) / 2; + + public TileRect(int2 min, int2 max) + { + _min = math.min(min, max); + _max = math.max(min, max); + } + + public TileRect(int2 min, int width, int height) : this(min, min + new int2(width - 1, height - 1)) + { + } + + public readonly bool Contains(int2 tile) + { + return tile.x >= Min.x && tile.x <= Max.x && tile.y >= Min.y && tile.y <= Max.y; + } + + public readonly bool Intersects(TileRect other) + { + return !(other.Max.x < Min.x || other.Min.x > Max.x || other.Max.y < Min.y || other.Min.y > Max.y); + } + + public readonly TileRect Inflate(int amount) + { + return amount == int.MaxValue ? Everything : new TileRect(Min - amount, Max + amount); + } + + public static TileRect OneTile(int2 tile) + { + return new TileRect(tile, 1, 1); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs b/Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs new file mode 100644 index 0000000..6a0208c --- /dev/null +++ b/Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs @@ -0,0 +1,38 @@ +using Unity.Mathematics; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class TileSpace : ITileSpace + { + public float TileSize { get; set; } + + public World World { get; set; } + + public int2 WorldToTile(Vector3 position) + { + var tileX = Mathf.FloorToInt(position.x / TileSize); + var tileY = Mathf.FloorToInt(position.z / TileSize); + return new int2(tileX, tileY); + } + + public Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f) + { + tileX = Mathf.Clamp01(tileX); + tileY = Mathf.Clamp01(tileY); + return new Vector3((tile.x + tileX) * TileSize, World.Heightmap.GetValue(tile), (tile.y + tileY) * TileSize); + } + + public int GetElevation(float y) + { + return Mathf.RoundToInt(y); + } + + public Vector3 GetRectWorldCenter(in TileRect rect) + { + var worldCenter = TileToWorld(rect.Min, 0, 0); + worldCenter += new Vector3(rect.Width, 0, rect.Height) * (0.5f * TileSize); + return worldCenter; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs b/Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs new file mode 100644 index 0000000..5ba1c2a --- /dev/null +++ b/Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs @@ -0,0 +1,44 @@ +using System; +using Cysharp.Threading.Tasks; +using QFSW.QC; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(DebugSystemGroup))] + public class DebugCommandsSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private IUnlocksService _unlocksService; + + [InjectService] + private ISignalBus _signalBus; + + public DebugCommandsSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + QuantumRegistry.RegisterObject(this); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + QuantumRegistry.DeregisterObject(this); + } + + [Command("unlock-all", MonoTargetType.Registry)] + private void UnlockAll() + { + _unlocksService.UnlockAll(); + } + + [Command("complete-demo", MonoTargetType.Registry)] + private void CompleteDemo(float gameTime = 15 * 60) + { + _signalBus.Raise(new DemoCompletedSignal(gameTime)); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs b/Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs new file mode 100644 index 0000000..e215001 --- /dev/null +++ b/Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs @@ -0,0 +1,8 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [InitializeAfter(typeof(UISystemGroup))] + public class DebugSystemGroup : GameSystemGroup + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs b/Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs new file mode 100644 index 0000000..f33a0a1 --- /dev/null +++ b/Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(DebugSystemGroup))] + public class DrawGizmosSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private IEngine _engine; + + private DrawGizmosSceneProxy _sceneProxy; + + private List _callbacks = new(); + + public DrawGizmosSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { +#if DEBUG + _sceneProxy = new GameObject(nameof(DrawGizmosSceneProxy)).AddComponent(); + _sceneProxy.DrawGizmos += OnDrawGizmos; +#endif + + foreach (var system in _engine.Systems) + if (system is IDrawGizmos callback) + _callbacks.Add(callback); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _sceneProxy.DrawGizmos -= OnDrawGizmos; + } + + private void OnDrawGizmos(bool selected) + { + foreach (var callback in _callbacks) callback.DrawGizmos(selected); + } + + private class DrawGizmosSceneProxy : MonoBehaviour + { + public event Action DrawGizmos; + + private void OnDrawGizmos() + { + DrawGizmos?.Invoke(false); + } + + private void OnDrawGizmosSelected() + { + DrawGizmos?.Invoke(true); + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/DebugCommands/IDrawGizmos.cs b/Source/Riversong/Game/DebugCommands/IDrawGizmos.cs new file mode 100644 index 0000000..e47a74e --- /dev/null +++ b/Source/Riversong/Game/DebugCommands/IDrawGizmos.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IDrawGizmos + { + void DrawGizmos(bool selected); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Demo/DemoCompletedSignal.cs b/Source/Riversong/Game/Demo/DemoCompletedSignal.cs new file mode 100644 index 0000000..1d40c91 --- /dev/null +++ b/Source/Riversong/Game/Demo/DemoCompletedSignal.cs @@ -0,0 +1,12 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct DemoCompletedSignal + { + public float GameTime; + + public DemoCompletedSignal(float gameTime) + { + GameTime = gameTime; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Demo/DemoSystem.cs b/Source/Riversong/Game/Demo/DemoSystem.cs new file mode 100644 index 0000000..aa4c321 --- /dev/null +++ b/Source/Riversong/Game/Demo/DemoSystem.cs @@ -0,0 +1,41 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [RequiresWorldReadyForUpdate] + public class DemoSystem : GameSystem, IUpdatable + { + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private GameConfig _config; + + [InjectService] + private World _world; + + private bool _demoCompleted; + + private float _gameTime; + + public DemoSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void Update() + { + if (_demoCompleted) return; + + _gameTime += Time.unscaledDeltaTime; + + var populationState = _world.PopulationState; + var generalSettings = _config.GeneralSettings; + + if (populationState.Population < generalSettings.PopulationGoal || populationState.Happiness < generalSettings.HappinessGoal) return; + + _demoCompleted = true; + + _signalBus.Raise(new DemoCompletedSignal(_gameTime)); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/BuildTool.cs b/Source/Riversong/Game/EditTools/BuildTool/BuildTool.cs new file mode 100644 index 0000000..226f882 --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/BuildTool.cs @@ -0,0 +1,126 @@ +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class BuildTool : EditTool + { + private IPointerService _pointerService; + + private ITileSpace _tileSpace; + + private IEditToolValidatorService _validator; + + private IBuildToolPreviewManager _previewManager; + + private Directions _buildingOrientation; + + private EditToolValidationResult _validationResult; + + public BuildTool(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public BuildingDefinition Building { get; set; } + + public BuildToolPreview Preview { get; private set; } + + public TileRect BuildingRect { get; private set; } + + public override UniTask InitializeAsync() + { + var config = ServiceLocator.GetService(); + + _pointerService = ServiceLocator.GetService(); + _tileSpace = ServiceLocator.GetService(); + _validator = ServiceLocator.GetService(); + _previewManager = ServiceLocator.GetService(); + Preview = new BuildToolPreview(config.UI.BuildTool, _tileSpace); + + return UniTask.CompletedTask; + } + + public override void OnEnabled() + { + base.OnEnabled(); + + if (!Building) + { + Debug.LogError("Building not set when activating Build Tool"); + return; + } + + Preview.PrepareForBuilding(Building); + + _buildingOrientation = Directions.North; + } + + public override void OnDisabled() + { + base.OnDisabled(); + + Preview.Release(); + } + + public override void Update() + { + base.Update(); + + var pointerOnTerrain = _pointerService.TryGetPositionOnTerrain(out var position); + + var keyboard = Keyboard.current; + if (keyboard.tKey.wasPressedThisFrame) _buildingOrientation = (Directions)(((int)_buildingOrientation + 6) % 8); + + var buildingCenter = _tileSpace.WorldToTile(position); + BuildingRect = TileMath.GetBuildingRect(buildingCenter, _buildingOrientation, Building.Width, Building.Height); + + _validationResult = ValidatePlacement(pointerOnTerrain); + var isValid = _validationResult == EditToolValidationResult.Success; + + AffectedTiles.Clear(); + if (pointerOnTerrain) + { + var highlightType = isValid ? TileHighlightType.ValidTile : TileHighlightType.InvalidTile; + foreach (var tile in TileRange.From(BuildingRect)) AffectedTiles.Add((tile, highlightType)); + } + + Preview.Update(isValid, position, _buildingOrientation); + + if (isValid && _pointerService.TryConsumeLeftClick()) Build(); + } + + private EditToolValidationResult ValidatePlacement(bool pointerOnTerrain) + { + if (!pointerOnTerrain) return EditToolValidationResult.BlockedTile; + + var commonValidation = _validator.DoCommonValidation(BuildingRect, BlockReason.CannotBuild); + if (commonValidation != EditToolValidationResult.Success) return commonValidation; + + return _validator.ValidateBuildingPlacementRules(BuildingRect, Building); + } + + public EditToolValidationResult GetLastValidationResult() + { + return _validationResult; + } + + private void Build() + { + _previewManager.PlayPlacementAnimationAndBuild(Preview, Building, BuildingRect, _buildingOrientation); + } + + public override void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect) + { + if (_validationResult != EditToolValidationResult.Success) + { + filter = DeletedGameObjectsFilter.None; + rect = TileRect.Empty; + return; + } + + filter = DeletedGameObjectsFilter.RawResources | DeletedGameObjectsFilter.ProductStacks; + rect = BuildingRect; + } + } +} diff --git a/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreview.cs b/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreview.cs new file mode 100644 index 0000000..fc3bc30 --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreview.cs @@ -0,0 +1,126 @@ +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Pool; +using Object = UnityEngine.Object; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class BuildToolPreview + { + private readonly GameConfig.UIConfig.BuildToolConfig _config; + + private readonly ITileSpace _tileSpace; + + private BuildingDefinition _building; + + private Vector3 _tilt; + + private Vector3 _tiltVelocity; + + private float _yaw; + + private float _yawVelocity; + + public BuildToolPreview(GameConfig.UIConfig.BuildToolConfig config, ITileSpace tileSpace) + { + _config = config; + _tileSpace = tileSpace; + } + + public GameObject PreviewObject { get; private set; } + + public bool IsVisible => PreviewObject && PreviewObject.activeSelf; + + public float3 Position => PreviewObject ? PreviewObject.transform.position : Vector3.zero; + + public void PrepareForBuilding(BuildingDefinition building) + { + _building = building; + } + + public void Release() + { + ClearPreviewObject(); + } + + public void Update(bool isValid, Vector3 pointer, Directions orientation) + { + if (!PreviewObject) + { + if (_building.Visualization.IsDone) + CreatePreviewObject(); + else + return; + } + + var snap = !PreviewObject.activeSelf && isValid; + if (snap) + { + _tilt = Vector3.zero; + _tiltVelocity = Vector3.zero; + } + + PreviewObject.SetActive(isValid); + if (!isValid) return; + + var rect = TileMath.GetBuildingRect(_tileSpace.WorldToTile(pointer), orientation, _building.Width, _building.Height); + var position = _tileSpace.GetRectWorldCenter(rect); + position.y += _config.Height; + + var dt = Time.unscaledDeltaTime; + + position = snap ? position : Vector3.Lerp(PreviewObject.transform.position, position, _config.LerpFactor * dt); + + var yaw = orientation.ToQuaternion().eulerAngles.y; + _yaw = snap ? yaw : Mathf.SmoothDampAngle(_yaw, yaw, ref _yawVelocity, 0.1f, Mathf.Infinity, dt); + var rotation = Quaternion.Euler(0, _yaw, 0); + if (!snap) rotation = SimulateSpring(position, dt) * rotation; + + PreviewObject.transform.SetPositionAndRotation(position, rotation); + } + + private Quaternion SimulateSpring(Vector3 targetPosition, float dt) + { + var positionDelta = targetPosition - PreviewObject.transform.position; + + _tilt -= positionDelta * _config.ImpulseScale; + _tilt = _tilt.normalized * Mathf.Min(_tilt.magnitude, _config.MaxTilt); + + _tiltVelocity -= _tilt * (_config.Elasticity * dt); + _tiltVelocity *= Mathf.Pow(1 - _config.Damping, dt); + + _tilt += _tiltVelocity * dt; + + if (_tilt.sqrMagnitude > 0.001f) + { + var axis = Vector3.Cross(Vector3.down, _tilt.normalized); + var angle = _tilt.magnitude / _config.MaxTilt * _config.MaxTiltAngle; + return Quaternion.AngleAxis(angle, axis); + } + + return Quaternion.identity; + } + + private void CreatePreviewObject() + { + PreviewObject = Object.Instantiate((GameObject)_building.Visualization.Asset); + PreviewObject.SetLayerRecursively(GameObjectLayers.IgnoreAoE); + + using var _ = ListPool.Get(out var hide); + PreviewObject.GetComponentsInChildren(hide); + foreach (var obj in hide) obj.gameObject.SetActive(false); + + // Start the preview in the disabled state, causes the position to be snapped when enabled + PreviewObject.SetActive(false); + } + + public void ClearPreviewObject() + { + if (PreviewObject) + { + Object.Destroy(PreviewObject); + PreviewObject = null; + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreviewManager.cs b/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreviewManager.cs new file mode 100644 index 0000000..fd6e3ca --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/BuildToolPreviewManager.cs @@ -0,0 +1,52 @@ +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(IBuildToolPreviewManager))] + public class BuildToolPreviewManager : GameSystem, IBuildToolPreviewManager + { + [InjectService] + private ITileSpace _tileSpace; + + [InjectService] + private ISignalBus _signalBus; + + public BuildToolPreviewManager(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void PlayPlacementAnimationAndBuild(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation) + { + _ = PlayPlacementAnimationAndBuildAsync(preview, definition, rect, orientation); + } + + private async UniTask PlayPlacementAnimationAndBuildAsync(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation) + { + _signalBus.Raise(new BuildingPlacementAnimationStartedSignal(definition, rect, orientation)); + + var animatedObject = Object.Instantiate(preview.PreviewObject); + + preview.ClearPreviewObject(); + + var placementAnimation = animatedObject.GetComponent(); + if (placementAnimation) + { + var finalPosition = _tileSpace.GetRectWorldCenter(rect); + var finalRotation = orientation.ToQuaternion(); + + await placementAnimation.PlayAsync(definition, animatedObject.transform.position, finalPosition, finalRotation); + } + else + { + Debug.LogError($"Prefab '{definition.name}' has no {nameof(BuildingPlacementAnimation)} component"); + } + + _signalBus.Raise(new BuildingPlacementAnimationCompletedSignal(definition, rect, orientation)); + + await UniTask.NextFrame(); + + Object.Destroy(animatedObject); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/BuildToolValidator.cs b/Source/Riversong/Game/EditTools/BuildTool/BuildToolValidator.cs new file mode 100644 index 0000000..6836b2f --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/BuildToolValidator.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class BuildToolValidator + { + private readonly World _world; + + private readonly int _baseElevation; + + public BuildToolValidator(World world, int baseElevation) + { + _world = world; + _baseElevation = baseElevation; + } + + public bool ValidatePlacement(List affectedTiles) + { + foreach (var tile in affectedTiles) + { + if (_world.Heightmap.GetValue(tile) != _baseElevation) return false; + + if (_world.BlockMap.IsBlocked(tile)) return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/GameObjectsHighlightingSystem.cs b/Source/Riversong/Game/EditTools/BuildTool/GameObjectsHighlightingSystem.cs new file mode 100644 index 0000000..1a98f11 --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/GameObjectsHighlightingSystem.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [UpdateAfter(typeof(EditingStateGameSystem))] + public class GameObjectsHighlightingSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private GameConfig _gameConfig; + + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private EditingState _editingState; + + [InjectService] + private UIState _uiState; + + [InjectService] + private ITileSpace _tileSpace; + + [InjectService] + private IEntityCache _entityCache; + + [InjectService] + private IBuildingSpatialQuery _buildingSpatialQuery; + + [InjectService] + private IProductStackVisualizationCollection _productStackVisualizationCollection; + + [InjectService] + private IRawResourceVisualizationCollection _rawResourceVisualizationCollection; + + [InjectService] + private IAgentVisualizationCollection _agentVisualizationCollection; + + [InjectService] + private IBuildingVisualizationCollection _buildingVisualizationCollection; + + [InjectService] + private World _world; + + public GameObjectsHighlightingSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _signalBus.Subscribe(OnCollectHighlightedGameObjects); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnCollectHighlightedGameObjects); + } + + private bool TryGetSourceBuilding(out BuildingDefinition building, out TileRect sourceRect) + { + var tool = _editingState.ActiveTool; + if (tool is BuildTool buildTool && buildTool.Preview.IsVisible) + { + building = buildTool.Building; + sourceRect = buildTool.BuildingRect; + return true; + } + + var selectedBuilding = _uiState.SelectedBuilding; + if (selectedBuilding != null) + { + building = selectedBuilding.Definition; + sourceRect = selectedBuilding.Rect; + return true; + } + + building = null; + sourceRect = TileRect.Empty; + return false; + } + + private void OnCollectHighlightedGameObjects(CollectHighlightedGameObjectsSignal signal) + { + if (!TryGetSourceBuilding(out var building, out var sourceRect)) return; + + CollectBuildings(signal.GameObjects, building, sourceRect); + CollectProductStacks(signal.GameObjects, building, sourceRect); + CollectResources(signal.GameObjects, building, sourceRect); + CollectCritters(signal.GameObjects, building, sourceRect); + } + + private void CollectBuildings(List gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect) + { + if (sourceBuilding.ProvidedProducts.Count > 0) + { + foreach (var house in _entityCache.GetHouses()) + { + if (TileMath.StepCount(sourceRect, house.Rect) > sourceBuilding.Range) continue; + AddHighlightedBuilding(gameObjects, house); + } + + if (sourceBuilding.FetchesProducts) + foreach (var storage in _entityCache.GetStorageBuildings()) + { + if (TileMath.StepCount(sourceRect, storage.Rect) > sourceBuilding.Range) continue; + AddHighlightedBuilding(gameObjects, storage); + } + } + + if (sourceBuilding.IsHouse) + { + using var providersScope = ListPool.Get(out var providers); + _buildingSpatialQuery.FindProvidersForHouse(sourceRect, providers); + + foreach (var provider in providers) AddHighlightedBuilding(gameObjects, provider); + } + + if (sourceBuilding.IsStorage) + { + using var providersScope = ListPool.Get(out var providers); + _buildingSpatialQuery.FindProvidersForStorage(sourceRect, providers); + + foreach (var provider in providers) AddHighlightedBuilding(gameObjects, provider); + } + } + + private void AddHighlightedBuilding(List gameObjects, Building building) + { + if (_buildingVisualizationCollection.TryGetVisualization(building.Id, out var visualization)) gameObjects.Add(visualization.gameObject); + } + + private void CollectProductStacks(List gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect) + { + if (!sourceBuilding.IsStorage) return; + + var rangeRect = sourceRect.Inflate(sourceBuilding.Range); + using var productStacksScope = ListPool.Get(out var productStacks); + _world.ProductStacks.Find(rangeRect, productStacks); + + foreach (var productStack in productStacks) + { + if (!_productStackVisualizationCollection.TryGetVisualization(productStack.Id, out var visualization)) continue; + + gameObjects.Add(visualization); + } + } + + private void CollectResources(List gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect) + { + if (!sourceBuilding.HarvestedResource) return; + + var rangeRect = sourceRect.Inflate(sourceBuilding.Range); + using var resourceIdsScope = ListPool<(int, bool)>.Get(out var resourceIds); + _world.RawResources.GetResourceNodes(rangeRect, _gameConfig.GeneralSettings.BaseElevation, resourceIds); + + foreach (var (id, _) in resourceIds) + { + _world.RawResources.TryGetResourceNode(id, out var resourceNode); + if (resourceNode.DefinitionId != sourceBuilding.HarvestedResource.RuntimeId) continue; + + if (!_rawResourceVisualizationCollection.TryGetVisualization(resourceNode.Id, out var visualization)) continue; + + gameObjects.Add(visualization); + } + } + + private void CollectCritters(List gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect) + { + if (!sourceBuilding.TargetCritter) return; + + var rangeRect = sourceRect.Inflate(sourceBuilding.Range); + foreach (var critter in _entityCache.GetCritterAgents()) + { + ref var critterState = ref critter.GetCritterStateRW(); + if (critterState.CritterDefinitionId != sourceBuilding.TargetCritter.RuntimeId) continue; + + var critterTile = _tileSpace.WorldToTile(critter.Position); + if (!rangeRect.Contains(critterTile)) continue; + + if (!_agentVisualizationCollection.TryGetVisualization(critter.Id, out var visualization)) continue; + + gameObjects.Add(visualization.gameObject); + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/HideOnBuildingPreview.cs b/Source/Riversong/Game/EditTools/BuildTool/HideOnBuildingPreview.cs new file mode 100644 index 0000000..c81d6fe --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/HideOnBuildingPreview.cs @@ -0,0 +1,8 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class HideOnBuildingPreview : MonoBehaviour + { + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/BuildTool/IBuildToolPreviewManager.cs b/Source/Riversong/Game/EditTools/BuildTool/IBuildToolPreviewManager.cs new file mode 100644 index 0000000..59ffa7d --- /dev/null +++ b/Source/Riversong/Game/EditTools/BuildTool/IBuildToolPreviewManager.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IBuildToolPreviewManager + { + void PlayPlacementAnimationAndBuild(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/CollectDeletedGameObjectsSignal.cs b/Source/Riversong/Game/EditTools/CollectDeletedGameObjectsSignal.cs new file mode 100644 index 0000000..43cf61c --- /dev/null +++ b/Source/Riversong/Game/EditTools/CollectDeletedGameObjectsSignal.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct CollectDeletedGameObjectsSignal + { + public DeletedGameObjectsFilter Filter; + + public TileRect Rect; + + public List GameObjects; + + public CollectDeletedGameObjectsSignal(DeletedGameObjectsFilter filter, TileRect rect, List gameObjects) + { + Filter = filter; + Rect = rect; + GameObjects = gameObjects; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/CollectEditToolRelevantGameObjectsSignal.cs b/Source/Riversong/Game/EditTools/CollectEditToolRelevantGameObjectsSignal.cs new file mode 100644 index 0000000..63aa096 --- /dev/null +++ b/Source/Riversong/Game/EditTools/CollectEditToolRelevantGameObjectsSignal.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct CollectHighlightedGameObjectsSignal + { + public List GameObjects; + + public CollectHighlightedGameObjectsSignal(List gameObjects) + { + GameObjects = gameObjects; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/DeleteTool/DeleteTool.cs b/Source/Riversong/Game/EditTools/DeleteTool/DeleteTool.cs new file mode 100644 index 0000000..268c8ae --- /dev/null +++ b/Source/Riversong/Game/EditTools/DeleteTool/DeleteTool.cs @@ -0,0 +1,57 @@ +using Cysharp.Threading.Tasks; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class DeleteTool : DragTool + { + private ISignalBus _signalBus; + + public DeleteTool(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + protected override DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.All; + + public override async UniTask InitializeAsync() + { + await base.InitializeAsync(); + + _signalBus = ServiceLocator.GetService(); + } + + protected override bool Validate(ref int2 startTile, ref int2 endTile) + { + return true; + } + + protected override void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile) + { + foreach (var tile in TileRange.From(startTile, endTile)) AffectedTiles.Add((tile, TileHighlightType.DeletePreview)); + } + + protected override void DoTool(int2 startTile, int2 endTile) + { + _ = DoToolAsync(new TileRect(startTile, endTile)); + } + + private async UniTask DoToolAsync(TileRect rect) + { + const int count = 10; + + var countX = (int)math.ceil((float)rect.Width / count); + var countY = (int)math.ceil((float)rect.Height / count); + + for (var x = 0; x < countX; x++) + for (var y = 0; y < countY; y++) + { + var r = new TileRect(rect.Min + new int2(x, y) * count, count, count); + r.Max = math.min(r.Max, rect.Max); + + _signalBus.Raise(new DoDeleteToolSignal(r)); + + await UniTask.NextFrame(); + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/DeleteTool/DoDeleteToolSignal.cs b/Source/Riversong/Game/EditTools/DeleteTool/DoDeleteToolSignal.cs new file mode 100644 index 0000000..c2a521b --- /dev/null +++ b/Source/Riversong/Game/EditTools/DeleteTool/DoDeleteToolSignal.cs @@ -0,0 +1,12 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct DoDeleteToolSignal + { + public TileRect Rect; + + public DoDeleteToolSignal(TileRect rect) + { + Rect = rect; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/DeletedGameObjectsFilter.cs b/Source/Riversong/Game/EditTools/DeletedGameObjectsFilter.cs new file mode 100644 index 0000000..819b399 --- /dev/null +++ b/Source/Riversong/Game/EditTools/DeletedGameObjectsFilter.cs @@ -0,0 +1,18 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Flags] + public enum DeletedGameObjectsFilter + { + None = 0, + + Buildings = 1, + + RawResources = 1 << 1, + + ProductStacks = 1 << 2, + + All = ~None + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/DragTool.cs b/Source/Riversong/Game/EditTools/DragTool.cs new file mode 100644 index 0000000..c1c9886 --- /dev/null +++ b/Source/Riversong/Game/EditTools/DragTool.cs @@ -0,0 +1,96 @@ +using Cysharp.Threading.Tasks; +using Unity.Mathematics; +using UnityEngine.InputSystem; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class DragTool : EditTool + { + private IPointerService _pointerService; + + private ITileSpace _tileSpace; + + private bool _isDragging; + + private int2 _startTile; + + private int2 _endTile; + + private bool _isValid; + + protected DragTool(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + protected virtual DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.None; + + public override async UniTask InitializeAsync() + { + await base.InitializeAsync(); + + _pointerService = ServiceLocator.GetService(); + _tileSpace = ServiceLocator.GetService(); + } + + public override void OnDisabled() + { + base.OnDisabled(); + + _isDragging = false; + } + + public override void Update() + { + base.Update(); + + AffectedTiles.Clear(); + + var lmb = Mouse.current.leftButton; + + var isPointerOnTerrain = _pointerService.TryGetPositionOnTerrain(out var position); + if (!isPointerOnTerrain) + { + _isDragging &= lmb.isPressed; + _isValid = false; + return; + } + + if (!_isDragging) + { + _startTile = _tileSpace.WorldToTile(position); + _endTile = _startTile; + } + _isDragging |= _pointerService.TryConsumeLeftClick(); + if (_isDragging && !_pointerService.IsPointerOverUI) _endTile = _tileSpace.WorldToTile(position); + + _isValid = Validate(ref _startTile, ref _endTile); + UpdateAffectedTiles(_isValid, _startTile, _endTile); + + if (lmb.wasReleasedThisFrame) + { + if (_isValid && !_pointerService.IsPointerOverUI) DoTool(_startTile, _endTile); + + _isDragging = false; + } + } + + protected abstract bool Validate(ref int2 startTile, ref int2 endTile); + + protected abstract void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile); + + protected abstract void DoTool(int2 startTile, int2 endTile); + + public override void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect) + { + if (!_isValid) + { + filter = DeletedGameObjectsFilter.None; + rect = TileRect.Empty; + return; + } + + filter = DeletedGameObjectsFilter; + rect = new TileRect(_startTile, _endTile); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/EditTool.cs b/Source/Riversong/Game/EditTools/EditTool.cs new file mode 100644 index 0000000..4759c4f --- /dev/null +++ b/Source/Riversong/Game/EditTools/EditTool.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Cysharp.Threading.Tasks; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public abstract class EditTool : IDisposable + { + protected EditTool(IServiceLocator serviceLocator) + { + ServiceLocator = serviceLocator; + } + + protected IServiceLocator ServiceLocator { get; } + + public List<(int2, TileHighlightType)> AffectedTiles { get; } = new(); + + public virtual UniTask InitializeAsync() + { + return UniTask.CompletedTask; + } + + public virtual void Dispose() + { + } + + public virtual void OnEnabled() + { + } + + public virtual void OnDisabled() + { + } + + public virtual void Update() + { + } + + public virtual void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect) + { + filter = DeletedGameObjectsFilter.None; + rect = TileRect.Empty; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/EditingState.cs b/Source/Riversong/Game/EditTools/EditingState.cs new file mode 100644 index 0000000..058acf6 --- /dev/null +++ b/Source/Riversong/Game/EditTools/EditingState.cs @@ -0,0 +1,30 @@ +using System; +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class EditingState : IDisposable + { + public BuildTool BuildTool { get; set; } + + public DeleteTool DeleteTool { get; set; } + + public RoadTool RoadTool { get; set; } + + public EditTool ActiveTool { get; set; } + + public async UniTask InitializeAsync() + { + await BuildTool.InitializeAsync(); + await DeleteTool.InitializeAsync(); + await RoadTool.InitializeAsync(); + } + + public void Dispose() + { + BuildTool.Dispose(); + DeleteTool.Dispose(); + RoadTool.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/EditingStateGameSystem.cs b/Source/Riversong/Game/EditTools/EditingStateGameSystem.cs new file mode 100644 index 0000000..98fa43b --- /dev/null +++ b/Source/Riversong/Game/EditTools/EditingStateGameSystem.cs @@ -0,0 +1,151 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.Pool; +using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [RequiresWorldReadyForUpdate] + public class EditingStateGameSystem : GameSystem, IServiceProvider, IInitializable, IDisposable, IUpdatable, IEditingService + { + [InjectService] + private ICancelAction _cancelAction; + + [InjectService] + private WorldRenderingState _renderingState; + + [InjectService] + private GameConfig _config; + + [InjectService] + private World _world; + + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private MaterialReplacementCache _materialReplacementCache; + + public EditingStateGameSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public EditingState EditingState { get; private set; } + + public event Action ActiveToolChanged; + + public void RegisterServices(IServiceLocator serviceLocator) + { + serviceLocator.RegisterService(this); + + EditingState = new EditingState(); + serviceLocator.RegisterService(EditingState); + } + + public async UniTask InitializeAsync() + { + EditingState.BuildTool = new BuildTool(ServiceLocator); + EditingState.DeleteTool = new DeleteTool(ServiceLocator); + EditingState.RoadTool = new RoadTool(ServiceLocator); + await EditingState.InitializeAsync(); + + _cancelAction.AddHandler( + (int)CancelActions.CancelEditTool, + _ => + { + if (EditingState.ActiveTool == null) return false; + + DeactivateTool(); + + return true; + }); + } + + public void Dispose() + { + EditingState?.Dispose(); + EditingState = null; + } + + public void ActivateTool(EditTool tool) + { + EditingState.ActiveTool?.OnDisabled(); + EditingState.ActiveTool = tool; + EditingState.ActiveTool?.OnEnabled(); + + ActiveToolChanged?.Invoke(EditingState.ActiveTool); + } + + public void DeactivateTool() + { + ActivateTool(null); + } + + public void Update() + { + UpdateActiveTool(); + UpdateHighlightedGameObjects(); + } + + private void UpdateActiveTool() + { + if (EditingState.ActiveTool == null) return; + + EditingState.ActiveTool.Update(); + + UpdateAffectedTiles(); + UpdateDeletedGameObjects(); + } + + private void UpdateAffectedTiles() + { + foreach (var (tile, type) in EditingState.ActiveTool.AffectedTiles) + { + if (_world.BlockMap.IsBlocked(tile, BlockReason.InvalidElevation)) continue; + + Color32 color; + var config = _config.UI.TileHighlight; + switch (type) + { + case TileHighlightType.ValidTile: + color = config.ValidColor; + break; + + case TileHighlightType.InvalidTile: + color = config.InvalidColor; + break; + + case TileHighlightType.DeletePreview: + color = config.DeletePreviewColor; + break; + + default: + color = Color.magenta; + break; + } + + _renderingState.TileHighlight.AddTile(tile, color); + } + } + + private void UpdateDeletedGameObjects() + { + EditingState.ActiveTool.GetDeleteGameObjectsPreviewInfo(out var filter, out var rect); + if (filter == DeletedGameObjectsFilter.None) return; + + using var gameObjectsScope = ListPool.Get(out var gameObjects); + _signalBus.Raise(new CollectDeletedGameObjectsSignal(filter, rect, gameObjects)); + + _materialReplacementCache.ReplaceMaterials(gameObjects, _config.UI.DeletedGameObjectsMaterial); + } + + private void UpdateHighlightedGameObjects() + { + using var gameObjectsScope = ListPool.Get(out var gameObjects); + _signalBus.Raise(new CollectHighlightedGameObjectsSignal(gameObjects)); + + _materialReplacementCache.ReplaceMaterials(gameObjects, _config.UI.HighlightedGameObjectsMaterial); + } + } +} diff --git a/Source/Riversong/Game/EditTools/IEditingService.cs b/Source/Riversong/Game/EditTools/IEditingService.cs new file mode 100644 index 0000000..0e8568f --- /dev/null +++ b/Source/Riversong/Game/EditTools/IEditingService.cs @@ -0,0 +1,15 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEditingService + { + EditingState EditingState { get; } + + event Action ActiveToolChanged; + + void ActivateTool(EditTool tool); + + void DeactivateTool(); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/RoadTool.cs b/Source/Riversong/Game/EditTools/RoadTool.cs new file mode 100644 index 0000000..eadcf99 --- /dev/null +++ b/Source/Riversong/Game/EditTools/RoadTool.cs @@ -0,0 +1,50 @@ +using Cysharp.Threading.Tasks; +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class RoadTool : DragTool + { + private IEditToolValidatorService _validator; + + private IRoadFactory _roadFactory; + + public RoadTool(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + protected override DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.RawResources; + + public override async UniTask InitializeAsync() + { + await base.InitializeAsync(); + + _validator = ServiceLocator.GetService(); + _roadFactory = ServiceLocator.GetService(); + } + + protected override bool Validate(ref int2 startTile, ref int2 endTile) + { + var dx = endTile.x - startTile.x; + var dy = endTile.y - startTile.y; + + if (math.abs(dx) >= math.abs(dy)) + endTile = new int2(endTile.x, startTile.y); + else + endTile = new int2(startTile.x, endTile.y); + + return _validator.DoCommonValidation(new TileRect(startTile, endTile), BlockReason.CannotBuildRoad) == EditToolValidationResult.Success; + } + + protected override void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile) + { + var type = isValid ? TileHighlightType.ValidTile : TileHighlightType.InvalidTile; + foreach (var tile in TileRange.From(startTile, endTile)) AffectedTiles.Add((tile, type)); + } + + protected override void DoTool(int2 startTile, int2 endTile) + { + _roadFactory.CreateRoad(startTile, endTile); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/Validation/EditToolValidationResult.cs b/Source/Riversong/Game/EditTools/Validation/EditToolValidationResult.cs new file mode 100644 index 0000000..961d328 --- /dev/null +++ b/Source/Riversong/Game/EditTools/Validation/EditToolValidationResult.cs @@ -0,0 +1,13 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum EditToolValidationResult + { + Success, + + BlockedTile, + + CanOnlyBePlacedNearWater, + + CanOnlyBePlacedOnFertileGround + } +} diff --git a/Source/Riversong/Game/EditTools/Validation/EditToolValidatorSystem.cs b/Source/Riversong/Game/EditTools/Validation/EditToolValidatorSystem.cs new file mode 100644 index 0000000..ee7edf5 --- /dev/null +++ b/Source/Riversong/Game/EditTools/Validation/EditToolValidatorSystem.cs @@ -0,0 +1,56 @@ +using Unity.Mathematics; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(IEditToolValidatorService))] + public class EditToolValidatorSystem : GameSystem, IEditToolValidatorService + { + [InjectService] + private GameConfig _config; + + [InjectService] + private World _world; + + public EditToolValidatorSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public EditToolValidationResult DoCommonValidation(TileRect rect, BlockReason blockReason) + { + foreach (var tile in TileRange.From(rect)) + if (!DoCommonTileValidation(tile, blockReason)) + return EditToolValidationResult.BlockedTile; + + return EditToolValidationResult.Success; + } + + private bool DoCommonTileValidation(int2 tile, BlockReason blockReason) + { + return _world.Contains(tile) && !_world.BlockMap.IsBlocked(tile, blockReason); + } + + public EditToolValidationResult ValidateBuildingPlacementRules(TileRect rect, BuildingDefinition buildingDefinition) + { + if (buildingDefinition.NearWater) + { + var waterMap = _world.WaterMap; + foreach (var tile in TileRange.From(rect)) + if (!waterMap.IsNearWater(tile)) + return EditToolValidationResult.CanOnlyBePlacedNearWater; + } + + if (buildingDefinition.RequiresFertileTile) + { + foreach (var tile in TileRange.From(rect)) + { + var fertility = _world.Fertility.GetValue(tile); + if (fertility.MaxFertility > 0) return EditToolValidationResult.Success; + } + + return EditToolValidationResult.CanOnlyBePlacedOnFertileGround; + } + + return EditToolValidationResult.Success; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/EditTools/Validation/IEditToolValidatorService.cs b/Source/Riversong/Game/EditTools/Validation/IEditToolValidatorService.cs new file mode 100644 index 0000000..b7c109d --- /dev/null +++ b/Source/Riversong/Game/EditTools/Validation/IEditToolValidatorService.cs @@ -0,0 +1,9 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IEditToolValidatorService + { + EditToolValidationResult DoCommonValidation(TileRect rect, BlockReason blockReason); + + public EditToolValidationResult ValidateBuildingPlacementRules(TileRect rect, BuildingDefinition buildingDefinition); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/GameSpeed/GameSpeedSystem.cs b/Source/Riversong/Game/GameSpeed/GameSpeedSystem.cs new file mode 100644 index 0000000..5427734 --- /dev/null +++ b/Source/Riversong/Game/GameSpeed/GameSpeedSystem.cs @@ -0,0 +1,64 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(IGameSpeed))] + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + [RequiresWorldReadyForUpdate] + [UpdateBefore(typeof(WorldTimeSystem))] + public class GameSpeedSystem : GameSystem, IGameSpeed, IInitializable, IUpdatable, IDisposable + { + private const float NormalSpeed = 1; + + private const float FastSpeed = 2; + + private const float VeryFastSpeed = 4; + + public GameSpeedSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public int SpeedLevel { get; private set; } + + public UniTask InitializeAsync() + { + SetSpeedLevel(0); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + SetSpeedLevel(0); + } + + public void Update() + { + if (Keyboard.current.digit1Key.wasPressedThisFrame) + SetSpeedLevel(0); + else if (Keyboard.current.digit2Key.wasPressedThisFrame) + SetSpeedLevel(1); + else if (Keyboard.current.digit3Key.wasPressedThisFrame) SetSpeedLevel(2); + + Time.timeScale = GetTimeScale(); + } + + public void SetSpeedLevel(int speedLevel) + { + SpeedLevel = speedLevel; + } + + private float GetTimeScale() + { + return SpeedLevel switch + { + 1 => FastSpeed, + 2 => VeryFastSpeed, + _ => NormalSpeed + }; + } + } +} diff --git a/Source/Riversong/Game/GameSpeed/IGameSpeed.cs b/Source/Riversong/Game/GameSpeed/IGameSpeed.cs new file mode 100644 index 0000000..b1cb0d2 --- /dev/null +++ b/Source/Riversong/Game/GameSpeed/IGameSpeed.cs @@ -0,0 +1,9 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IGameSpeed + { + int SpeedLevel { get; } + + void SetSpeedLevel(int speedLevel); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Helpers/GameObjectLayers.cs b/Source/Riversong/Game/Helpers/GameObjectLayers.cs new file mode 100644 index 0000000..cbfbb53 --- /dev/null +++ b/Source/Riversong/Game/Helpers/GameObjectLayers.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class GameObjectLayers + { + public static readonly int Default = LayerMask.NameToLayer("Default"); + + public static readonly int Terrain = LayerMask.NameToLayer("Terrain"); + + public static readonly int IgnoreAoE = LayerMask.NameToLayer("Ignore AoE"); + + public static void SetLayerRecursively(this GameObject gameObject, int layer, bool includeInactive = false) where T : Component + { + using var componentsScope = ListPool.Get(out var components); + gameObject.GetComponentsInChildren(includeInactive, components); + foreach (var component in components) component.gameObject.layer = layer; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/CancelActionSystem.cs b/Source/Riversong/Game/Input/CancelActionSystem.cs new file mode 100644 index 0000000..b669cee --- /dev/null +++ b/Source/Riversong/Game/Input/CancelActionSystem.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using UnityEngine.InputSystem; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(ICancelAction))] + [GameSystemGroup(typeof(LateGameSystemGroup))] + public class CancelActionSystem : GameSystem, IUpdatable, ICancelAction + { + private readonly List<(int, Func)> _handlers = new(); + + private Comparison<(int, Func)> _handlerComparison; + + public CancelActionSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void Update() + { + if (!TryGetCancelActionType(out var cancelActionType)) return; + + foreach (var (_, handler) in _handlers) + if (handler.Invoke(cancelActionType)) + return; + } + + public void AddHandler(int priority, Func cancelAction) + { + _handlerComparison ??= (handler, otherHandler) => + { + var (handlerPriority, _) = handler; + var (otherHandlerPriority, _) = otherHandler; + return otherHandlerPriority.CompareTo(handlerPriority); + }; + + _handlers.Add((priority, cancelAction)); + _handlers.Sort(_handlerComparison); + } + + private static bool TryGetCancelActionType(out CancelActionType cancelActionType) + { + if (Keyboard.current.escapeKey.wasPressedThisFrame) + { + cancelActionType = CancelActionType.EscapeKey; + return true; + } + + if (Mouse.current.rightButton.wasPressedThisFrame) + { + cancelActionType = CancelActionType.RightMouseButton; + return true; + } + + cancelActionType = CancelActionType.None; + return false; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/CancelActionType.cs b/Source/Riversong/Game/Input/CancelActionType.cs new file mode 100644 index 0000000..3a50aec --- /dev/null +++ b/Source/Riversong/Game/Input/CancelActionType.cs @@ -0,0 +1,11 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum CancelActionType + { + None, + + EscapeKey, + + RightMouseButton + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/CancelActions.cs b/Source/Riversong/Game/Input/CancelActions.cs new file mode 100644 index 0000000..f2592fe --- /dev/null +++ b/Source/Riversong/Game/Input/CancelActions.cs @@ -0,0 +1,15 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public enum CancelActions + { + Invalid, + + PauseMenu, + + CloseBuildMenu, + + CancelEditTool, + + CancelSelection + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/ICancelAction.cs b/Source/Riversong/Game/Input/ICancelAction.cs new file mode 100644 index 0000000..c569f44 --- /dev/null +++ b/Source/Riversong/Game/Input/ICancelAction.cs @@ -0,0 +1,9 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface ICancelAction + { + void AddHandler(int priority, Func cancelAction); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/IPointerService.cs b/Source/Riversong/Game/Input/IPointerService.cs new file mode 100644 index 0000000..195995a --- /dev/null +++ b/Source/Riversong/Game/Input/IPointerService.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IPointerService + { + bool IsPointerOverUI { get; } + + bool TryGetPositionOnTerrain(out Vector3 position); + + bool TryConsumeLeftClick(int maxClickCount = 0); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Input/PointerSystem.cs b/Source/Riversong/Game/Input/PointerSystem.cs new file mode 100644 index 0000000..3a4988d --- /dev/null +++ b/Source/Riversong/Game/Input/PointerSystem.cs @@ -0,0 +1,82 @@ +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(IPointerService))] + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + public class PointerSystem : GameSystem, IUpdatable, IPointerService + { + [InjectService] + private IScene _scene; + + [InjectService] + private GameConfig _config; + + [InjectService] + private ITileSpace _tileSpace; + + [InjectService] + private World _world; + + [InjectService] + private UIService _uiService; + + private int _leftButtonClickCount; + + private Vector3? _positionOnTerrain; + + public PointerSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public bool IsPointerOverUI { get; private set; } + + public void Update() + { + _leftButtonClickCount = 0; + + UpdateIsPointerOverUIFlag(); + + UpdatePositionOnTerrain(); + } + + private void UpdateIsPointerOverUIFlag() + { + var panel = _uiService.UIRoot.RootVisualElement.panel; + + var position = Mouse.current.position.ReadValue(); + position.y = Screen.height - position.y; + position = RuntimePanelUtils.ScreenToPanel(panel, position); + + IsPointerOverUI = panel.Pick(position) != null; + } + + private void UpdatePositionOnTerrain() + { + _positionOnTerrain = null; + + var ray = _scene.MainCamera.ScreenPointToRay(Mouse.current.position.ReadValue()); + if (Physics.Raycast(ray, out var hit, float.PositiveInfinity)) + { + if (!Mathf.Approximately(Vector3.Dot(hit.normal, Vector3.up), 1)) return; + + if (_tileSpace.GetElevation(hit.point.y) != _config.GeneralSettings.BaseElevation) return; + + _positionOnTerrain = hit.point; + } + } + + public bool TryGetPositionOnTerrain(out Vector3 position) + { + position = _positionOnTerrain ?? Vector3.zero; + return _positionOnTerrain != null; + } + + public bool TryConsumeLeftClick(int maxClickCount = 0) + { + return !IsPointerOverUI && Mouse.current.leftButton.wasPressedThisFrame && _leftButtonClickCount++ <= maxClickCount; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Onboarding/OnboardingEventCompleted.cs b/Source/Riversong/Game/Onboarding/OnboardingEventCompleted.cs new file mode 100644 index 0000000..6a61c19 --- /dev/null +++ b/Source/Riversong/Game/Onboarding/OnboardingEventCompleted.cs @@ -0,0 +1,12 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public struct OnboardingEventCompleted + { + public OnboardingEvents Event; + + public OnboardingEventCompleted(OnboardingEvents @event) + { + Event = @event; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Onboarding/OnboardingEvents.cs b/Source/Riversong/Game/Onboarding/OnboardingEvents.cs new file mode 100644 index 0000000..6f55ead --- /dev/null +++ b/Source/Riversong/Game/Onboarding/OnboardingEvents.cs @@ -0,0 +1,20 @@ +using System; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Flags] + public enum OnboardingEvents + { + None = 0, + + FirstHousePlaced = 1 << 0, + + FirstHouseUpgrade = 1 << 1, + + FirstRoadFailure = 1 << 2, + + FirstPopulationMilestone = 1 << 3, + + FirstBuildingPlaced = 1 << 4 + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Onboarding/OnboardingMessages.cs b/Source/Riversong/Game/Onboarding/OnboardingMessages.cs new file mode 100644 index 0000000..db1c529 --- /dev/null +++ b/Source/Riversong/Game/Onboarding/OnboardingMessages.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [CreateAssetMenu(fileName = "OnboardingMessages", menuName = "Riversong Code Showcase/Onboarding Messages")] + public class OnboardingMessages : ScriptableObject + { + public List Entries = new(); + + [Serializable] + public class OnboardingMessageEntry + { + public OnboardingEvents Event; + + [TextArea(3, 8)] + public string Text; + } + } +} diff --git a/Source/Riversong/Game/Onboarding/OnboardingState.cs b/Source/Riversong/Game/Onboarding/OnboardingState.cs new file mode 100644 index 0000000..d8bd722 --- /dev/null +++ b/Source/Riversong/Game/Onboarding/OnboardingState.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class OnboardingState + { + public OnboardingEvents CompletedEvents { get; set; } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Onboarding/OnboardingSystem.cs b/Source/Riversong/Game/Onboarding/OnboardingSystem.cs new file mode 100644 index 0000000..503c086 --- /dev/null +++ b/Source/Riversong/Game/Onboarding/OnboardingSystem.cs @@ -0,0 +1,78 @@ +using System; +using Cysharp.Threading.Tasks; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class OnboardingSystem : GameSystem, IInitializable, IDisposable + { + [InjectService] + private ISignalBus _signalBus; + + [InjectService] + private World _world; + + [InjectService] + private GameConfig _config; + + public OnboardingSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _signalBus.Subscribe(OnBuildingCreated); + _signalBus.Subscribe(OnBuildingUpgraded); + _signalBus.Subscribe(OnPathQueryFailed); + _signalBus.Subscribe(OnPopulationChanged); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnBuildingCreated); + _signalBus.Unsubscribe(OnBuildingUpgraded); + _signalBus.Unsubscribe(OnPathQueryFailed); + _signalBus.Unsubscribe(OnPopulationChanged); + } + + private void OnBuildingCreated(BuildingCreatedSignal signal) + { + CompleteEvent(OnboardingEvents.FirstBuildingPlaced); + + if (!signal.Building.Definition.IsHouse) return; + + CompleteEvent(OnboardingEvents.FirstHousePlaced); + } + + private void OnBuildingUpgraded(BuildingUpgradedSignal signal) + { + CompleteEvent(OnboardingEvents.FirstHouseUpgrade); + } + + private void OnPathQueryFailed(PathQueryFailedSignal signal) + { + if ((signal.TraversalRules & PathTraversalRules.RoadsOnly) == 0) return; + + CompleteEvent(OnboardingEvents.FirstRoadFailure); + } + + private void OnPopulationChanged(PopulationChangedSignal signal) + { + if (signal.Population < _config.Onboarding.PopulationMilestone) return; + + CompleteEvent(OnboardingEvents.FirstPopulationMilestone); + } + + private void CompleteEvent(OnboardingEvents completedEvent) + { + var onboardingState = _world.OnboardingState; + + if ((onboardingState.CompletedEvents & completedEvent) != 0) return; + + onboardingState.CompletedEvents |= completedEvent; + + _signalBus.Raise(new OnboardingEventCompleted(completedEvent)); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Rendering/AoERenderingSystem.cs b/Source/Riversong/Game/Rendering/AoERenderingSystem.cs new file mode 100644 index 0000000..56ee0cb --- /dev/null +++ b/Source/Riversong/Game/Rendering/AoERenderingSystem.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Cysharp.Threading.Tasks; +using Unity.Mathematics; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [Service(typeof(IAoERenderingService))] + [GameSystemGroup(typeof(LateGameSystemGroup))] + public class AoERenderingSystem : GameSystem, IInitializable, IDisposable, IUpdatable, IAoERenderingService + { + private const int MaxAoECount = 16; + + private static readonly int AoESourcesPropertyID = Shader.PropertyToID("_AoE_Sources"); + + private static readonly int AoECountPropertyID = Shader.PropertyToID("_AoE_Count"); + + [InjectService] + private GameConfig _config; + + [InjectService] + private IBuildingVisualizationCollection _buildingVisualizationCollection; + + private ComputeBuffer _aoeBuffer; + + private List _data = new(MaxAoECount); + + public AoERenderingSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _aoeBuffer = new ComputeBuffer(MaxAoECount, sizeof(int) + sizeof(float) * 4, ComputeBufferType.Structured, ComputeBufferMode.Dynamic); + + return UniTask.CompletedTask; + } + + public void Dispose() + { + _aoeBuffer.Dispose(); + } + + public void Update() + { + _aoeBuffer.SetData(_data); + + var material = _config.Vfx.AoEMaterial; + material.SetBuffer(AoESourcesPropertyID, _aoeBuffer); + material.SetInt(AoECountPropertyID, _data.Count); + + _data.Clear(); + } + + public void Add(int layer, TileRect rect) + { + _data.Add( + new AoE + { + Layer = layer, + Rect = new float4(rect.Min, rect.Max) + }); + } + + [StructLayout(LayoutKind.Sequential)] + private struct AoE + { + public int Layer; + + public float4 Rect; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Rendering/GlobalShaderParametersSystem.cs b/Source/Riversong/Game/Rendering/GlobalShaderParametersSystem.cs new file mode 100644 index 0000000..bb56e49 --- /dev/null +++ b/Source/Riversong/Game/Rendering/GlobalShaderParametersSystem.cs @@ -0,0 +1,43 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(LateGameSystemGroup))] + public class GlobalShaderParametersSystem : GameSystem, IInitializable, IDisposable, IUpdatable + { + [InjectService] + private World _world; + + [InjectService] + private ISignalBus _signalBus; + + public GlobalShaderParametersSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _signalBus.Subscribe(OnWorldReady); + return UniTask.CompletedTask; + } + + public void Dispose() + { + _signalBus.Unsubscribe(OnWorldReady); + } + + public void Update() + { + var t = Time.unscaledTime; + Shader.SetGlobalVector(ShaderProperties.UnscaledTime, new Vector4(t / 20, t, t * 2, t * 3)); + } + + private void OnWorldReady(WorldReadySignal signal) + { + Shader.SetGlobalVector(ShaderProperties.WorldSize, new Vector4(_world.Size.x, _world.Size.y, 0, 0)); + Shader.SetGlobalVector(ShaderProperties.InverseWorldSize, new Vector2(1 / (float)_world.Size.x, 1 / (float)_world.Size.y)); + } + } +} diff --git a/Source/Riversong/Game/Rendering/IAoERenderingService.cs b/Source/Riversong/Game/Rendering/IAoERenderingService.cs new file mode 100644 index 0000000..2fa9527 --- /dev/null +++ b/Source/Riversong/Game/Rendering/IAoERenderingService.cs @@ -0,0 +1,7 @@ +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IAoERenderingService + { + void Add(int layer, TileRect rect); + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Rendering/MaterialReplacementCache.cs b/Source/Riversong/Game/Rendering/MaterialReplacementCache.cs new file mode 100644 index 0000000..9430cf2 --- /dev/null +++ b/Source/Riversong/Game/Rendering/MaterialReplacementCache.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Pool; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class MaterialReplacementCache + { + private static readonly int BaseMapPropertyId = Shader.PropertyToID("_BaseMap"); + + private static readonly int MainTexPropertyId = Shader.PropertyToID("_MainTex"); + + private static readonly int MainTexturePropertyId = Shader.PropertyToID("_Main_Texture"); + + private HashSet _gameObjects = new(); + + private List<(Renderer, Material, int, MaterialPropertyBlock)> _cache = new(); + + private List _propertyBlockPool = new(); + + private int _propertyBlockPoolIndex; + + private MaterialPropertyBlock _propertyBlock; + + public static void ReplaceRendererMaterials(Renderer renderer, Material material, MaterialPropertyBlock propertyBlock) + { + var materials = renderer.sharedMaterials; + + for (var i = 0; i < materials.Length; i++) + { + var originalMaterial = materials[i]; + materials[i] = material; + ReplaceTexture(renderer, originalMaterial, i, propertyBlock, null); + } + + renderer.sharedMaterials = materials; + } + + public void ReplaceMaterials(GameObject gameObject, Material material) + { + if (!_gameObjects.Add(gameObject.GetInstanceID())) return; + + using var renderersScope = ListPool.Get(out var renderers); + gameObject.GetComponentsInChildren(renderers); + + foreach (var renderer in renderers) ReplaceMaterials(renderer, material); + } + + public void ReplaceMaterials(List gameObjects, Material material) + { + if (gameObjects.Count == 0) return; + + using var renderersScope = ListPool.Get(out var renderers); + + foreach (var gameObject in gameObjects) + { + if (!_gameObjects.Add(gameObject.GetInstanceID())) continue; + + renderers.Clear(); + gameObject.GetComponentsInChildren(renderers); + + foreach (var renderer in renderers) ReplaceMaterials(renderer, material); + } + } + + public void RestoreMaterials() + { + foreach (var (renderer, material, i, propertyBlock) in _cache) + { + if (!renderer) continue; + + var materials = renderer.sharedMaterials; + materials[i] = material; + renderer.sharedMaterials = materials; + renderer.SetPropertyBlock(propertyBlock, i); + } + + _gameObjects.Clear(); + _cache.Clear(); + _propertyBlockPoolIndex = 0; + } + + private void ReplaceMaterials(Renderer renderer, Material material) + { + var materials = renderer.sharedMaterials; + + _propertyBlock ??= new MaterialPropertyBlock(); + for (var i = 0; i < materials.Length; i++) + { + var currentPropertyBlock = GetPooledPropertyBlock(); + renderer.GetPropertyBlock(currentPropertyBlock, i); + + _cache.Add((renderer, materials[i], i, currentPropertyBlock)); + + var originalMaterial = materials[i]; + materials[i] = material; + ReplaceTexture(renderer, originalMaterial, i, _propertyBlock, currentPropertyBlock); + } + + renderer.sharedMaterials = materials; + } + + private MaterialPropertyBlock GetPooledPropertyBlock() + { + if (_propertyBlockPoolIndex >= _propertyBlockPool.Count) _propertyBlockPool.Add(new MaterialPropertyBlock()); + + var propertyBlock = _propertyBlockPool[_propertyBlockPoolIndex++]; + propertyBlock.Clear(); + + return propertyBlock; + } + + private static void ReplaceTexture(Renderer renderer, Material material, int materialIndex, MaterialPropertyBlock propertyBlock, MaterialPropertyBlock sourcePropertyBlock) + { + var texture = GetMainTexture(material); + if (!texture) texture = sourcePropertyBlock?.GetTexture(MainTexturePropertyId); + if (!texture) return; + + propertyBlock.Clear(); + propertyBlock.SetTexture(MainTexturePropertyId, texture); + + renderer.SetPropertyBlock(propertyBlock, materialIndex); + } + + private static Texture GetMainTexture(Material material) + { + if (material.HasProperty(BaseMapPropertyId)) return material.GetTexture(BaseMapPropertyId); + if (material.HasProperty(MainTexPropertyId)) return material.GetTexture(MainTexPropertyId); + return material.HasProperty(MainTexturePropertyId) ? material.GetTexture(MainTexturePropertyId) : null; + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Rendering/RenderingInitializationGameSystem.cs b/Source/Riversong/Game/Rendering/RenderingInitializationGameSystem.cs new file mode 100644 index 0000000..69e293d --- /dev/null +++ b/Source/Riversong/Game/Rendering/RenderingInitializationGameSystem.cs @@ -0,0 +1,16 @@ + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(EarlyGameSystemGroup))] + public class RenderingInitializationGameSystem : GameSystem, IServiceProvider + { + public RenderingInitializationGameSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public void RegisterServices(IServiceLocator serviceLocator) + { + serviceLocator.RegisterService(new WorldRenderingState()); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/Rendering/ShaderProperties.cs b/Source/Riversong/Game/Rendering/ShaderProperties.cs new file mode 100644 index 0000000..6630e31 --- /dev/null +++ b/Source/Riversong/Game/Rendering/ShaderProperties.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public static class ShaderProperties + { + public static readonly int Color = Shader.PropertyToID("_Color"); + + public static readonly int Amount = Shader.PropertyToID("_Amount"); + + public static readonly int UnscaledTime = Shader.PropertyToID("_Unscaled_Time"); + + public static readonly int WorldSize = Shader.PropertyToID("_World_Size"); + + public static readonly int InverseWorldSize = Shader.PropertyToID("_Inverse_World_Size"); + + public static readonly int NightBlend = Shader.PropertyToID("_Night_Blend"); + } +} diff --git a/Source/Riversong/Game/Rendering/WorldRenderingState.cs b/Source/Riversong/Game/Rendering/WorldRenderingState.cs new file mode 100644 index 0000000..b79c6c7 --- /dev/null +++ b/Source/Riversong/Game/Rendering/WorldRenderingState.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public class WorldRenderingState + { + /// + /// All terrain chunks. Use to register new chunks with the rendering state. + /// + public List TerrainChunks { get; } = new(); + + public TileHighlight TileHighlight { get; } = new(); + + public void AddTerrainChunk(TerrainChunk chunk) + { + TerrainChunks.Add(chunk); + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/UI/DayNightUITheme.cs b/Source/Riversong/Game/UI/DayNightUITheme.cs new file mode 100644 index 0000000..e2ea5ca --- /dev/null +++ b/Source/Riversong/Game/UI/DayNightUITheme.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Sirenix.OdinInspector; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [CreateAssetMenu(fileName = "DayNightUITheme", menuName = "Riversong Code Showcase/Day Night UI Theme")] + public class DayNightUITheme : ScriptableObject + { + private const string CommonUssAssetPath = "_Project/GameAssets/UI/Common.uss"; + + private const string CommonUssEditorAssetPath = "Assets/_Project/GameAssets/UI/Common.uss"; + + private static readonly Regex RootColorVariableRegex = new(@"^(?--[a-z0-9-]+)\s*:\s*(?#[0-9A-Fa-f]{6,8})\s*;$", RegexOptions.Compiled); + + [TableList(AlwaysExpanded = true, ShowIndexLabels = true, HideToolbar = true)] + [ListDrawerSettings(DefaultExpandedState = true, DraggableItems = false, HideAddButton = true, HideRemoveButton = true, ShowFoldout = false)] + public List Properties = new(); + + internal IReadOnlyList OrderedProperties => Properties; + + private void OnEnable() + { + EnsureProperties(); + } + + private void OnValidate() + { + EnsureProperties(); + } + + private void EnsureProperties() + { + Properties ??= new List(); + + var definitions = GetThemePropertyDefinitions(); + var existingProperties = Properties; + var existingPropertiesByName = CreatePropertiesByName(existingProperties); + var definitionNames = CreateDefinitionNames(definitions); + + var orderedProperties = new List(definitions.Count); + + for (var i = 0; i < definitions.Count; i++) + { + var definition = definitions[i]; + var property = TryGetProperty(existingPropertiesByName, definition.Name) ?? TryGetSameIndexFallbackProperty(existingProperties, definitionNames, i); + + property ??= new ThemeColorProperty(definition.Name, Color.white, Color.white); + property.Name = definition.Name; + + orderedProperties.Add(property); + } + + Properties = orderedProperties; + } + + private static Dictionary CreatePropertiesByName(IEnumerable properties) + { + var propertiesByName = new Dictionary(StringComparer.Ordinal); + + foreach (var property in properties) + { + if (property == null || string.IsNullOrWhiteSpace(property.Name)) continue; + + propertiesByName.TryAdd(property.Name, property); + } + + return propertiesByName; + } + + private static HashSet CreateDefinitionNames(IEnumerable definitions) + { + var names = new HashSet(StringComparer.Ordinal); + + foreach (var definition in definitions) names.Add(definition.Name); + + return names; + } + + private static ThemeColorProperty TryGetProperty(Dictionary propertiesByName, string propertyName) + { + return propertiesByName.GetValueOrDefault(propertyName); + } + + private static ThemeColorProperty TryGetSameIndexFallbackProperty(IReadOnlyList existingProperties, HashSet definitionNames, int index) + { + if (index >= existingProperties.Count) return null; + + var property = existingProperties[index]; + if (property == null) return null; + + return string.IsNullOrWhiteSpace(property.Name) || !definitionNames.Contains(property.Name) ? property : null; + } + + private static List GetThemePropertyDefinitions() + { + return TryReadThemePropertyDefinitionsFromCommonUss(); + } + + private static List TryReadThemePropertyDefinitionsFromCommonUss() + { + var path = GetCommonUssFullPath(); + if (!File.Exists(path)) return new List(); + + var definitions = new List(); + var lines = File.ReadAllLines(path); + + var inRootBlock = false; + + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + + if (!inRootBlock) + { + if (line == ":root {") inRootBlock = true; + + continue; + } + + if (line == "}") break; + + var match = RootColorVariableRegex.Match(line); + if (!match.Success) continue; + + var name = match.Groups["name"].Value; + var dayColor = FromHex(match.Groups["value"].Value); + var nightColor = GetDefaultNightColor(name, dayColor); + + definitions.Add(new ThemePropertyDefinition(name, dayColor, nightColor)); + } + + return definitions; + } + + private static Color GetDefaultNightColor(string name, Color dayColor) + { + return name switch + { + "--wood-color" => FromHex("#424C54"), + _ => dayColor + }; + } + + private static Color FromHex(string hex) + { + if (ColorUtility.TryParseHtmlString(hex, out var color)) return color; + + throw new ArgumentException($"Invalid color value: {hex}", nameof(hex)); + } + + private static string GetCommonUssFullPath() + { + return Path.Combine(Application.dataPath, CommonUssAssetPath.Replace('/', Path.DirectorySeparatorChar)); + } + + private static string ToHex(Color color) + { + var color32 = (Color32)color; + + return color32.a == byte.MaxValue ? $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}" : $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}{color32.a:X2}"; + } + + private readonly struct ThemePropertyDefinition + { + public readonly string Name; + + public readonly Color DayColor; + + public readonly Color NightColor; + + public ThemePropertyDefinition(string name, Color dayColor, Color nightColor) + { + Name = name; + DayColor = dayColor; + NightColor = nightColor; + } + } + + [Serializable] + public class ThemeColorProperty + { + [ReadOnly] + public string Name; + + public Color DayColor = Color.white; + + public Color NightColor = Color.white; + + public ThemeColorProperty() + { + } + + public ThemeColorProperty(string name, Color dayColor, Color nightColor) + { + Name = name; + DayColor = dayColor; + NightColor = nightColor; + } + } + +#if UNITY_EDITOR + [HorizontalGroup("Sync")] + [Button("Copy Day Colors From USS", ButtonSizes.Large)] + [GUIColor("cyan")] + private void CopyDayColorsFromUss() + { + EnsureProperties(); + + var definitions = TryReadThemePropertyDefinitionsFromCommonUss(); + if (definitions.Count == 0) + { + Debug.LogWarning($"Could not read any theme properties from '{CommonUssEditorAssetPath}'.", this); + return; + } + + var propertiesByName = CreatePropertiesByName(Properties); + + foreach (var definition in definitions) + { + if (!propertiesByName.TryGetValue(definition.Name, out var property)) continue; + + property.DayColor = definition.DayColor; + } + + EditorUtility.SetDirty(this); + AssetDatabase.SaveAssetIfDirty(this); + } + + [HorizontalGroup("Sync")] + [Button("Apply Day Colors To USS", ButtonSizes.Large)] + [GUIColor("cyan")] + private void ApplyDayColorsToUss() + { + EnsureProperties(); + + var path = GetCommonUssFullPath(); + if (!File.Exists(path)) + { + Debug.LogWarning($"Could not find '{CommonUssEditorAssetPath}'.", this); + return; + } + + var propertiesByName = CreatePropertiesByName(Properties); + var lines = File.ReadAllLines(path); + var updatedAnyLine = false; + var inRootBlock = false; + + for (var i = 0; i < lines.Length; i++) + { + var rawLine = lines[i]; + var trimmedLine = rawLine.Trim(); + + if (!inRootBlock) + { + if (trimmedLine == ":root {") inRootBlock = true; + + continue; + } + + if (trimmedLine == "}") break; + + var match = RootColorVariableRegex.Match(trimmedLine); + if (!match.Success) continue; + + var propertyName = match.Groups["name"].Value; + if (!propertiesByName.TryGetValue(propertyName, out var property)) continue; + + var indentationLength = rawLine.Length - rawLine.TrimStart().Length; + var indentation = rawLine.Substring(0, indentationLength); + lines[i] = $"{indentation}{propertyName}: {ToHex(property.DayColor)};"; + updatedAnyLine = true; + } + + if (!updatedAnyLine) + { + Debug.LogWarning($"No matching ':root' theme color properties were updated in '{CommonUssEditorAssetPath}'.", this); + return; + } + + File.WriteAllLines(path, lines); + AssetDatabase.ImportAsset(CommonUssEditorAssetPath); + } +#endif + } +} diff --git a/Source/Riversong/Game/UI/DayNightUIThemeSystem.cs b/Source/Riversong/Game/UI/DayNightUIThemeSystem.cs new file mode 100644 index 0000000..1bc4b16 --- /dev/null +++ b/Source/Riversong/Game/UI/DayNightUIThemeSystem.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(UISystemGroup))] + [InitializeAfter(typeof(UIInitializationSystem))] + public class DayNightUIThemeSystem : GameSystem, IInitializable, IDisposable, IUpdatable + { + [InjectService] + private GameConfig _config; + + [InjectService] + private UIService _uiService; + + [InjectService] + private World _world; + + private VisualElement _root; + + private RuntimeThemeSheet _runtimeThemeSheet; + + private float _lastAppliedNightBlend = -1; + + private bool _refreshClassEnabled; + + public DayNightUIThemeSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + public UniTask InitializeAsync() + { + _root = _uiService.UIRoot.RootVisualElement; + _runtimeThemeSheet = RuntimeThemeSheet.TryCreate(_root, _config.UI.Theme); + + ApplyTheme(true); + + return UniTask.CompletedTask; + } + + public void Update() + { + ApplyTheme(); + } + + public void Dispose() + { + if (_root == null || _runtimeThemeSheet == null) return; + + _runtimeThemeSheet.Apply(0); + ToggleRefreshClass(); + _lastAppliedNightBlend = 0; + } + + private void ApplyTheme(bool force = false) + { + if (_root == null || _config.UI.Theme == null || _runtimeThemeSheet == null) return; + + var nightBlend = _world.TimeState.NightBlend; + if (!force && !Application.isEditor && Mathf.Approximately(nightBlend, _lastAppliedNightBlend)) return; + + _runtimeThemeSheet.Apply(nightBlend); + ToggleRefreshClass(); + + _lastAppliedNightBlend = nightBlend; + } + + private void ToggleRefreshClass() + { + _refreshClassEnabled = !_refreshClassEnabled; + _root.EnableInClassList(RuntimeThemeSheet.RefreshClassName, _refreshClassEnabled); + } + + private sealed class RuntimeThemeSheet + { + public const string RefreshClassName = "__day-night-theme-refresh"; + + private readonly StyleSheet _styleSheet; + + private readonly RuntimePropertyBinding[] _bindings; + + private RuntimeThemeSheet(StyleSheet styleSheet, RuntimePropertyBinding[] bindings) + { + _styleSheet = styleSheet; + _bindings = bindings; + } + + public static RuntimeThemeSheet TryCreate(VisualElement root, DayNightUITheme theme) + { + if (root == null || theme == null) return null; + + var styleSheets = StyleSheetReflection.GetStyleSheets(root); + if (styleSheets == null) return null; + + foreach (var styleSheet in styleSheets) + { + if (!StyleSheetReflection.TryCreateBindings(styleSheet, theme.OrderedProperties, out var bindings)) continue; + + return new RuntimeThemeSheet(styleSheet, bindings); + } + + return null; + } + + public void Apply(float nightBlend) + { + foreach (var binding in _bindings) binding.Apply(nightBlend); + + _styleSheet.contentHash = unchecked(_styleSheet.contentHash + 1); + } + } + + private sealed class RuntimePropertyBinding + { + private readonly DayNightUITheme.ThemeColorProperty _themeProperty; + + private readonly object _manipulator; + + private readonly object[] _setColorArguments = new object[2]; + + public RuntimePropertyBinding(DayNightUITheme.ThemeColorProperty themeProperty, object manipulator) + { + _themeProperty = themeProperty; + _manipulator = manipulator; + } + + public void Apply(float nightBlend) + { + _setColorArguments[0] = 0; + _setColorArguments[1] = Color.Lerp(_themeProperty.DayColor, _themeProperty.NightColor, nightBlend); + StyleSheetReflection.SetColor(_manipulator, _setColorArguments); + } + } + + private static class StyleSheetReflection + { + private static readonly FieldInfo VisualElementStyleSheetListField = typeof(VisualElement).GetField("styleSheetList", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo StyleSheetRulesField = typeof(StyleSheet).GetField("m_Rules", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly Type StyleRuleType = StyleSheetRulesField?.FieldType.GetElementType(); + + private static readonly MethodInfo StyleRuleGetPropertiesMethod = StyleRuleType?.GetMethod("get_properties", BindingFlags.Instance | BindingFlags.Public); + + private static readonly Type StylePropertyType = StyleRuleGetPropertiesMethod?.ReturnType.GetElementType(); + + private static readonly MethodInfo StylePropertyGetNameMethod = StylePropertyType?.GetMethod("get_name", BindingFlags.Instance | BindingFlags.Public); + + private static readonly MethodInfo StylePropertyGetManipulatorMethod = StylePropertyType?.GetMethod("GetManipulator", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly Type ManipulatorType = StylePropertyGetManipulatorMethod?.ReturnType; + + private static readonly MethodInfo ManipulatorSetColorMethod = ManipulatorType?.GetMethod( + "SetColor", + BindingFlags.Instance | BindingFlags.Public, + null, + new[] + { + typeof(int), + typeof(Color) + }, + null); + + public static List GetStyleSheets(VisualElement root) + { + return VisualElementStyleSheetListField?.GetValue(root) as List; + } + + public static void SetColor(object manipulator, object[] arguments) + { + ManipulatorSetColorMethod?.Invoke(manipulator, arguments); + } + + public static bool TryCreateBindings(StyleSheet styleSheet, IReadOnlyList themeProperties, out RuntimePropertyBinding[] bindings) + { + bindings = null; + if (styleSheet == null || themeProperties == null) return false; + if (StyleSheetRulesField == null || + StyleRuleGetPropertiesMethod == null || + StylePropertyGetNameMethod == null || + StylePropertyGetManipulatorMethod == null || + ManipulatorSetColorMethod == null) + return false; + + var propertiesByName = new Dictionary(themeProperties.Count, StringComparer.Ordinal); + var rules = StyleSheetRulesField.GetValue(styleSheet) as Array; + if (rules == null) return false; + + foreach (var rule in rules) + { + if (rule == null) continue; + + var styleProperties = StyleRuleGetPropertiesMethod.Invoke(rule, null) as Array; + if (styleProperties == null) continue; + + foreach (var styleProperty in styleProperties) + { + if (styleProperty == null) continue; + + var propertyName = StylePropertyGetNameMethod.Invoke(styleProperty, null) as string; + if (string.IsNullOrEmpty(propertyName)) continue; + if (propertiesByName.ContainsKey(propertyName)) continue; + + propertiesByName[propertyName] = styleProperty; + } + } + + var resolvedBindings = new RuntimePropertyBinding[themeProperties.Count]; + + for (var i = 0; i < themeProperties.Count; i++) + { + var themeProperty = themeProperties[i]; + if (!propertiesByName.TryGetValue(themeProperty.Name, out var styleProperty)) return false; + + var manipulator = StylePropertyGetManipulatorMethod.Invoke(styleProperty, new object[] { styleSheet }); + resolvedBindings[i] = new RuntimePropertyBinding(themeProperty, manipulator); + } + + bindings = resolvedBindings; + return true; + } + } + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/UI/Framework/IUIModel.cs b/Source/Riversong/Game/UI/Framework/IUIModel.cs new file mode 100644 index 0000000..943bd70 --- /dev/null +++ b/Source/Riversong/Game/UI/Framework/IUIModel.cs @@ -0,0 +1,10 @@ +using System; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IUIModel : INotifyBindablePropertyChanged + { + event Action Changed; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/UI/Framework/IUIRoot.cs b/Source/Riversong/Game/UI/Framework/IUIRoot.cs new file mode 100644 index 0000000..6ea2dbf --- /dev/null +++ b/Source/Riversong/Game/UI/Framework/IUIRoot.cs @@ -0,0 +1,19 @@ +using System; +using Cysharp.Threading.Tasks; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + public interface IUIRoot : IDisposable + { + VisualElement RootVisualElement { get; } + + event Action ElementClicked; + + UniTask Initialize(UIService uiService); + + void MakeDraggable(VisualElement target); + + T GetView() where T : UIView; + } +} \ No newline at end of file diff --git a/Source/Riversong/Game/UI/Framework/UIControllerSystem.cs b/Source/Riversong/Game/UI/Framework/UIControllerSystem.cs new file mode 100644 index 0000000..f85f21d --- /dev/null +++ b/Source/Riversong/Game/UI/Framework/UIControllerSystem.cs @@ -0,0 +1,54 @@ +using Cysharp.Threading.Tasks; +using UnityEngine.UIElements; + +namespace DanieleMarotta.RiversongCodeShowcase +{ + [GameSystemGroup(typeof(UISystemGroup))] + [InitializeAfter(typeof(UIInitializationSystem))] + public abstract class UIControllerSystem : GameSystem, IInitializable + { + protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + [field: InjectService] protected UIService UIService { get; } + + protected IUIRoot UIRoot => UIService.UIRoot; + + public virtual UniTask InitializeAsync() + { + return UniTask.CompletedTask; + } + } + + [GameSystemGroup(typeof(UISystemGroup))] + [InitializeAfter(typeof(UIInitializationSystem))] + public abstract class UIControllerSystem : UIControllerSystem where T : UIView + { + private Button _closeButton; + + protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator) + { + } + + protected abstract T View { get; } + + public override async UniTask InitializeAsync() + { + await base.InitializeAsync(); + + _closeButton = View.RootElement.Q