v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
502
source/game/CMakeLists.txt
Normal file
502
source/game/CMakeLists.txt
Normal file
|
@ -0,0 +1,502 @@
|
|||
INCLUDE_DIRECTORIES (
|
||||
${STAR_EXTERN_INCLUDES}
|
||||
${STAR_CORE_INCLUDES}
|
||||
${STAR_BASE_INCLUDES}
|
||||
${STAR_PLATFORM_INCLUDES}
|
||||
${STAR_GAME_INCLUDES}
|
||||
)
|
||||
|
||||
SET (star_game_HEADERS
|
||||
StarActorMovementController.hpp
|
||||
StarAiDatabase.hpp
|
||||
StarAiTypes.hpp
|
||||
StarAmbient.hpp
|
||||
StarAnimation.hpp
|
||||
StarArmorWearer.hpp
|
||||
StarBehaviorDatabase.hpp
|
||||
StarBehaviorState.hpp
|
||||
StarBiome.hpp
|
||||
StarBiomeDatabase.hpp
|
||||
StarBiomePlacement.hpp
|
||||
StarCelestialCoordinate.hpp
|
||||
StarCelestialDatabase.hpp
|
||||
StarCelestialGraphics.hpp
|
||||
StarCelestialParameters.hpp
|
||||
StarCelestialTypes.hpp
|
||||
StarChatAction.hpp
|
||||
StarChatProcessor.hpp
|
||||
StarChatTypes.hpp
|
||||
StarClientContext.hpp
|
||||
StarCodex.hpp
|
||||
StarCodexDatabase.hpp
|
||||
StarCollectionDatabase.hpp
|
||||
StarCollisionBlock.hpp
|
||||
StarCollisionGenerator.hpp
|
||||
StarCommandProcessor.hpp
|
||||
StarDamage.hpp
|
||||
StarDamageDatabase.hpp
|
||||
StarDamageManager.hpp
|
||||
StarDamageTypes.hpp
|
||||
StarDanceDatabase.hpp
|
||||
StarDrawable.hpp
|
||||
StarDungeonGenerator.hpp
|
||||
StarDungeonImagePart.hpp
|
||||
StarDungeonTMXPart.hpp
|
||||
StarEffectEmitter.hpp
|
||||
StarEffectSourceDatabase.hpp
|
||||
StarEmoteProcessor.hpp
|
||||
StarEntityFactory.hpp
|
||||
StarEntityMap.hpp
|
||||
StarEntityRendering.hpp
|
||||
StarEntityRenderingTypes.hpp
|
||||
StarEntitySplash.hpp
|
||||
StarFallingBlocksAgent.hpp
|
||||
StarForceRegions.hpp
|
||||
StarGameTimers.hpp
|
||||
StarGameTypes.hpp
|
||||
StarHumanoid.hpp
|
||||
StarImageMetadataDatabase.hpp
|
||||
StarInteractionTypes.hpp
|
||||
StarInterpolationTracker.hpp
|
||||
StarInventoryTypes.hpp
|
||||
StarItem.hpp
|
||||
StarItemBag.hpp
|
||||
StarItemDatabase.hpp
|
||||
StarItemDescriptor.hpp
|
||||
StarItemDrop.hpp
|
||||
StarItemRecipe.hpp
|
||||
StarLightSource.hpp
|
||||
StarLiquidsDatabase.hpp
|
||||
StarLiquidTypes.hpp
|
||||
StarMaterialDatabase.hpp
|
||||
StarMaterialRenderProfile.hpp
|
||||
StarMaterialTypes.hpp
|
||||
StarMicroDungeon.hpp
|
||||
StarMonster.hpp
|
||||
StarMonsterDatabase.hpp
|
||||
StarMovementController.hpp
|
||||
StarNameGenerator.hpp
|
||||
StarNetPackets.hpp
|
||||
StarNetPacketSocket.hpp
|
||||
StarNetworkedAnimator.hpp
|
||||
StarNpc.hpp
|
||||
StarNpcDatabase.hpp
|
||||
StarObject.hpp
|
||||
StarObjectDatabase.hpp
|
||||
StarParallax.hpp
|
||||
StarParticle.hpp
|
||||
StarParticleDatabase.hpp
|
||||
StarParticleManager.hpp
|
||||
StarPlant.hpp
|
||||
StarPlantDatabase.hpp
|
||||
StarPlantDrop.hpp
|
||||
StarPlatformerAStar.hpp
|
||||
StarPlatformerAStarTypes.hpp
|
||||
StarPlayer.hpp
|
||||
StarPlayerBlueprints.hpp
|
||||
StarPlayerCodexes.hpp
|
||||
StarPlayerCompanions.hpp
|
||||
StarPlayerDeployment.hpp
|
||||
StarPlayerFactory.hpp
|
||||
StarPlayerInventory.hpp
|
||||
StarPlayerLog.hpp
|
||||
StarPlayerStorage.hpp
|
||||
StarPlayerTech.hpp
|
||||
StarPlayerTypes.hpp
|
||||
StarPlayerUniverseMap.hpp
|
||||
StarProjectile.hpp
|
||||
StarProjectileDatabase.hpp
|
||||
StarQuestDescriptor.hpp
|
||||
StarQuestManager.hpp
|
||||
StarQuests.hpp
|
||||
StarQuestTemplateDatabase.hpp
|
||||
StarRadioMessageDatabase.hpp
|
||||
StarRoot.hpp
|
||||
StarRootLoader.hpp
|
||||
StarServerClientContext.hpp
|
||||
StarSky.hpp
|
||||
StarSkyParameters.hpp
|
||||
StarSkyRenderData.hpp
|
||||
StarSkyTypes.hpp
|
||||
StarSongbook.hpp
|
||||
StarSpawner.hpp
|
||||
StarSpawnTypeDatabase.hpp
|
||||
StarSpeciesDatabase.hpp
|
||||
StarStagehand.hpp
|
||||
StarStagehandDatabase.hpp
|
||||
StarStatCollection.hpp
|
||||
StarStatistics.hpp
|
||||
StarStatisticsDatabase.hpp
|
||||
StarStatSet.hpp
|
||||
StarStatusController.hpp
|
||||
StarStatusEffectDatabase.hpp
|
||||
StarStatusTypes.hpp
|
||||
StarStoredFunctions.hpp
|
||||
StarSystemWorld.hpp
|
||||
StarSystemWorldClient.hpp
|
||||
StarSystemWorldServer.hpp
|
||||
StarSystemWorldServerThread.hpp
|
||||
StarTeamClient.hpp
|
||||
StarTeamManager.hpp
|
||||
StarTechController.hpp
|
||||
StarTechDatabase.hpp
|
||||
StarTenantDatabase.hpp
|
||||
StarTerrainDatabase.hpp
|
||||
StarTileDamage.hpp
|
||||
StarTileModification.hpp
|
||||
StarTileSectorArray.hpp
|
||||
StarTilesetDatabase.hpp
|
||||
StarToolUser.hpp
|
||||
StarTreasure.hpp
|
||||
StarUniverseClient.hpp
|
||||
StarUniverseConnection.hpp
|
||||
StarUniverseServer.hpp
|
||||
StarUniverseSettings.hpp
|
||||
StarVehicle.hpp
|
||||
StarVehicleDatabase.hpp
|
||||
StarVersioningDatabase.hpp
|
||||
StarWarping.hpp
|
||||
StarWeather.hpp
|
||||
StarWeatherTypes.hpp
|
||||
StarWireProcessor.hpp
|
||||
StarWiring.hpp
|
||||
StarWorldClient.hpp
|
||||
StarWorldClientState.hpp
|
||||
StarWorldGeneration.hpp
|
||||
StarWorldImpl.hpp
|
||||
StarWorldLayout.hpp
|
||||
StarWorldParameters.hpp
|
||||
StarWorldRenderData.hpp
|
||||
StarWorldServer.hpp
|
||||
StarWorldServerThread.hpp
|
||||
StarWorldStorage.hpp
|
||||
StarWorldStructure.hpp
|
||||
StarWorldTemplate.hpp
|
||||
StarWorldTiles.hpp
|
||||
|
||||
interfaces/StarAnchorableEntity.hpp
|
||||
interfaces/StarActivatableItem.hpp
|
||||
interfaces/StarAggressiveEntity.hpp
|
||||
interfaces/StarBeamItem.hpp
|
||||
interfaces/StarChattyEntity.hpp
|
||||
interfaces/StarContainerEntity.hpp
|
||||
interfaces/StarDamageBarEntity.hpp
|
||||
interfaces/StarDurabilityItem.hpp
|
||||
interfaces/StarEffectSourceItem.hpp
|
||||
interfaces/StarEmoteEntity.hpp
|
||||
interfaces/StarEntity.hpp
|
||||
interfaces/StarFireableItem.hpp
|
||||
interfaces/StarInteractiveEntity.hpp
|
||||
interfaces/StarLoungingEntities.hpp
|
||||
interfaces/StarNametagEntity.hpp
|
||||
interfaces/StarNonRotatedDrawablesItem.hpp
|
||||
interfaces/StarPhysicsEntity.hpp
|
||||
interfaces/StarPreviewTileTool.hpp
|
||||
interfaces/StarPointableItem.hpp
|
||||
interfaces/StarPortraitEntity.hpp
|
||||
interfaces/StarPreviewableItem.hpp
|
||||
interfaces/StarScriptedEntity.hpp
|
||||
interfaces/StarStatusEffectEntity.hpp
|
||||
interfaces/StarStatusEffectItem.hpp
|
||||
interfaces/StarSwingableItem.hpp
|
||||
interfaces/StarTileEntity.hpp
|
||||
interfaces/StarToolUserEntity.hpp
|
||||
interfaces/StarToolUserItem.hpp
|
||||
interfaces/StarWarpTargetEntity.hpp
|
||||
interfaces/StarWireEntity.hpp
|
||||
interfaces/StarWorld.hpp
|
||||
|
||||
items/StarActiveItem.hpp
|
||||
items/StarArmors.hpp
|
||||
items/StarAugmentItem.hpp
|
||||
items/StarBlueprintItem.hpp
|
||||
items/StarCodexItem.hpp
|
||||
items/StarCurrency.hpp
|
||||
items/StarConsumableItem.hpp
|
||||
items/StarInspectionTool.hpp
|
||||
items/StarInstrumentItem.hpp
|
||||
items/StarLiquidItem.hpp
|
||||
items/StarMaterialItem.hpp
|
||||
items/StarObjectItem.hpp
|
||||
items/StarThrownItem.hpp
|
||||
items/StarTools.hpp
|
||||
items/StarUnlockItem.hpp
|
||||
|
||||
objects/StarContainerObject.hpp
|
||||
objects/StarFarmableObject.hpp
|
||||
objects/StarLoungeableObject.hpp
|
||||
objects/StarPhysicsObject.hpp
|
||||
objects/StarTeleporterObject.hpp
|
||||
|
||||
scripting/StarCelestialLuaBindings.hpp
|
||||
scripting/StarBehaviorLuaBindings.hpp
|
||||
scripting/StarConfigLuaBindings.hpp
|
||||
scripting/StarEntityLuaBindings.hpp
|
||||
scripting/StarFireableItemLuaBindings.hpp
|
||||
scripting/StarItemLuaBindings.hpp
|
||||
scripting/StarLuaActorMovementComponent.hpp
|
||||
scripting/StarLuaAnimationComponent.hpp
|
||||
scripting/StarLuaGameConverters.hpp
|
||||
scripting/StarLuaComponents.hpp
|
||||
scripting/StarLuaRoot.hpp
|
||||
scripting/StarMovementControllerLuaBindings.hpp
|
||||
scripting/StarNetworkedAnimatorLuaBindings.hpp
|
||||
scripting/StarPlayerLuaBindings.hpp
|
||||
scripting/StarRootLuaBindings.hpp
|
||||
scripting/StarScriptedAnimatorLuaBindings.hpp
|
||||
scripting/StarStatusControllerLuaBindings.hpp
|
||||
scripting/StarWorldLuaBindings.hpp
|
||||
scripting/StarUniverseServerLuaBindings.hpp
|
||||
scripting/StarUtilityLuaBindings.hpp
|
||||
|
||||
terrain/StarCacheSelector.hpp
|
||||
terrain/StarConstantSelector.hpp
|
||||
terrain/StarDisplacementSelector.hpp
|
||||
terrain/StarFlatSurfaceSelector.hpp
|
||||
terrain/StarIslandSurfaceSelector.hpp
|
||||
terrain/StarKarstCave.hpp
|
||||
terrain/StarMaxSelector.hpp
|
||||
terrain/StarMinMaxSelector.hpp
|
||||
terrain/StarMixSelector.hpp
|
||||
terrain/StarPerlinSelector.hpp
|
||||
terrain/StarRidgeBlocksSelector.hpp
|
||||
terrain/StarRotateSelector.hpp
|
||||
terrain/StarWormCave.hpp
|
||||
)
|
||||
|
||||
SET (star_game_SOURCES
|
||||
StarActorMovementController.cpp
|
||||
StarAiDatabase.cpp
|
||||
StarAiTypes.cpp
|
||||
StarAmbient.cpp
|
||||
StarAnimation.cpp
|
||||
StarArmorWearer.cpp
|
||||
StarBehaviorDatabase.cpp
|
||||
StarBehaviorState.cpp
|
||||
StarBiome.cpp
|
||||
StarBiomeDatabase.cpp
|
||||
StarBiomePlacement.cpp
|
||||
StarCelestialCoordinate.cpp
|
||||
StarCelestialDatabase.cpp
|
||||
StarCelestialGraphics.cpp
|
||||
StarCelestialParameters.cpp
|
||||
StarCelestialTypes.cpp
|
||||
StarChatAction.cpp
|
||||
StarChatProcessor.cpp
|
||||
StarChatTypes.cpp
|
||||
StarClientContext.cpp
|
||||
StarCodex.cpp
|
||||
StarCodexDatabase.cpp
|
||||
StarCollectionDatabase.cpp
|
||||
StarCollisionBlock.cpp
|
||||
StarCollisionGenerator.cpp
|
||||
StarCommandProcessor.cpp
|
||||
StarDamage.cpp
|
||||
StarDamageDatabase.cpp
|
||||
StarDamageManager.cpp
|
||||
StarDamageTypes.cpp
|
||||
StarDanceDatabase.cpp
|
||||
StarDrawable.cpp
|
||||
StarDungeonGenerator.cpp
|
||||
StarDungeonImagePart.cpp
|
||||
StarDungeonTMXPart.cpp
|
||||
StarEffectEmitter.cpp
|
||||
StarEffectSourceDatabase.cpp
|
||||
StarEmoteProcessor.cpp
|
||||
StarEntityFactory.cpp
|
||||
StarEntityMap.cpp
|
||||
StarEntityRendering.cpp
|
||||
StarEntityRenderingTypes.cpp
|
||||
StarEntitySplash.cpp
|
||||
StarFallingBlocksAgent.cpp
|
||||
StarForceRegions.cpp
|
||||
StarGameTimers.cpp
|
||||
StarGameTypes.cpp
|
||||
StarHumanoid.cpp
|
||||
StarImageMetadataDatabase.cpp
|
||||
StarInteractionTypes.cpp
|
||||
StarInterpolationTracker.cpp
|
||||
StarInventoryTypes.cpp
|
||||
StarItem.cpp
|
||||
StarItemBag.cpp
|
||||
StarItemDatabase.cpp
|
||||
StarItemDescriptor.cpp
|
||||
StarItemDrop.cpp
|
||||
StarItemRecipe.cpp
|
||||
StarLightSource.cpp
|
||||
StarLiquidsDatabase.cpp
|
||||
StarLiquidTypes.cpp
|
||||
StarMaterialDatabase.cpp
|
||||
StarMaterialRenderProfile.cpp
|
||||
StarMicroDungeon.cpp
|
||||
StarMonster.cpp
|
||||
StarMonsterDatabase.cpp
|
||||
StarMovementController.cpp
|
||||
StarNameGenerator.cpp
|
||||
StarNetPackets.cpp
|
||||
StarNetPacketSocket.cpp
|
||||
StarNetworkedAnimator.cpp
|
||||
StarNpc.cpp
|
||||
StarNpcDatabase.cpp
|
||||
StarObject.cpp
|
||||
StarObjectDatabase.cpp
|
||||
StarParallax.cpp
|
||||
StarParticle.cpp
|
||||
StarParticleDatabase.cpp
|
||||
StarParticleManager.cpp
|
||||
StarPlant.cpp
|
||||
StarPlantDatabase.cpp
|
||||
StarPlantDrop.cpp
|
||||
StarPlatformerAStar.cpp
|
||||
StarPlatformerAStarTypes.cpp
|
||||
StarPlayer.cpp
|
||||
StarPlayerBlueprints.cpp
|
||||
StarPlayerCodexes.cpp
|
||||
StarPlayerCompanions.cpp
|
||||
StarPlayerDeployment.cpp
|
||||
StarPlayerFactory.cpp
|
||||
StarPlayerInventory.cpp
|
||||
StarPlayerLog.cpp
|
||||
StarPlayerStorage.cpp
|
||||
StarPlayerTech.cpp
|
||||
StarPlayerTypes.cpp
|
||||
StarPlayerUniverseMap.cpp
|
||||
StarProjectile.cpp
|
||||
StarProjectileDatabase.cpp
|
||||
StarQuestDescriptor.cpp
|
||||
StarQuestManager.cpp
|
||||
StarQuests.cpp
|
||||
StarQuestTemplateDatabase.cpp
|
||||
StarRadioMessageDatabase.cpp
|
||||
StarRoot.cpp
|
||||
StarRootLoader.cpp
|
||||
StarServerClientContext.cpp
|
||||
StarSky.cpp
|
||||
StarSkyParameters.cpp
|
||||
StarSkyRenderData.cpp
|
||||
StarSkyTypes.cpp
|
||||
StarSongbook.cpp
|
||||
StarSpawner.cpp
|
||||
StarSpawnTypeDatabase.cpp
|
||||
StarSpeciesDatabase.cpp
|
||||
StarStagehand.cpp
|
||||
StarStagehandDatabase.cpp
|
||||
StarStatCollection.cpp
|
||||
StarStatistics.cpp
|
||||
StarStatisticsDatabase.cpp
|
||||
StarStatSet.cpp
|
||||
StarStatusController.cpp
|
||||
StarStatusEffectDatabase.cpp
|
||||
StarStatusTypes.cpp
|
||||
StarStoredFunctions.cpp
|
||||
StarSystemWorld.cpp
|
||||
StarSystemWorldClient.cpp
|
||||
StarSystemWorldServer.cpp
|
||||
StarSystemWorldServerThread.cpp
|
||||
StarTeamClient.cpp
|
||||
StarTeamManager.cpp
|
||||
StarTechController.cpp
|
||||
StarTechDatabase.cpp
|
||||
StarTenantDatabase.cpp
|
||||
StarTerrainDatabase.cpp
|
||||
StarTileDamage.cpp
|
||||
StarTileModification.cpp
|
||||
StarTilesetDatabase.cpp
|
||||
StarToolUser.cpp
|
||||
StarTreasure.cpp
|
||||
StarUniverseClient.cpp
|
||||
StarUniverseConnection.cpp
|
||||
StarUniverseServer.cpp
|
||||
StarUniverseSettings.cpp
|
||||
StarVehicle.cpp
|
||||
StarVehicleDatabase.cpp
|
||||
StarVersioningDatabase.cpp
|
||||
StarWarping.cpp
|
||||
StarWeather.cpp
|
||||
StarWeatherTypes.cpp
|
||||
StarWireProcessor.cpp
|
||||
StarWiring.cpp
|
||||
StarWorldClient.cpp
|
||||
StarWorldClientState.cpp
|
||||
StarWorldGeneration.cpp
|
||||
StarWorldLayout.cpp
|
||||
StarWorldParameters.cpp
|
||||
StarWorldServer.cpp
|
||||
StarWorldServerThread.cpp
|
||||
StarWorldStorage.cpp
|
||||
StarWorldStructure.cpp
|
||||
StarWorldTemplate.cpp
|
||||
StarWorldTiles.cpp
|
||||
|
||||
interfaces/StarAnchorableEntity.cpp
|
||||
interfaces/StarBeamItem.cpp
|
||||
interfaces/StarContainerEntity.cpp
|
||||
interfaces/StarDamageBarEntity.cpp
|
||||
interfaces/StarEntity.cpp
|
||||
interfaces/StarFireableItem.cpp
|
||||
interfaces/StarInteractiveEntity.cpp
|
||||
interfaces/StarLoungingEntities.cpp
|
||||
interfaces/StarPhysicsEntity.cpp
|
||||
interfaces/StarPointableItem.cpp
|
||||
interfaces/StarSwingableItem.cpp
|
||||
interfaces/StarTileEntity.cpp
|
||||
interfaces/StarToolUserItem.cpp
|
||||
interfaces/StarWorld.cpp
|
||||
|
||||
items/StarActiveItem.cpp
|
||||
items/StarArmors.cpp
|
||||
items/StarAugmentItem.cpp
|
||||
items/StarBlueprintItem.cpp
|
||||
items/StarCodexItem.cpp
|
||||
items/StarCurrency.cpp
|
||||
items/StarConsumableItem.cpp
|
||||
items/StarInspectionTool.cpp
|
||||
items/StarInstrumentItem.cpp
|
||||
items/StarLiquidItem.cpp
|
||||
items/StarMaterialItem.cpp
|
||||
items/StarObjectItem.cpp
|
||||
items/StarThrownItem.cpp
|
||||
items/StarTools.cpp
|
||||
items/StarUnlockItem.cpp
|
||||
|
||||
objects/StarContainerObject.cpp
|
||||
objects/StarFarmableObject.cpp
|
||||
objects/StarLoungeableObject.cpp
|
||||
objects/StarPhysicsObject.cpp
|
||||
objects/StarTeleporterObject.cpp
|
||||
|
||||
scripting/StarCelestialLuaBindings.cpp
|
||||
scripting/StarBehaviorLuaBindings.cpp
|
||||
scripting/StarConfigLuaBindings.cpp
|
||||
scripting/StarEntityLuaBindings.cpp
|
||||
scripting/StarFireableItemLuaBindings.cpp
|
||||
scripting/StarItemLuaBindings.cpp
|
||||
scripting/StarLuaComponents.cpp
|
||||
scripting/StarLuaGameConverters.cpp
|
||||
scripting/StarLuaRoot.cpp
|
||||
scripting/StarMovementControllerLuaBindings.cpp
|
||||
scripting/StarNetworkedAnimatorLuaBindings.cpp
|
||||
scripting/StarPlayerLuaBindings.cpp
|
||||
scripting/StarRootLuaBindings.cpp
|
||||
scripting/StarScriptedAnimatorLuaBindings.cpp
|
||||
scripting/StarStatusControllerLuaBindings.cpp
|
||||
scripting/StarWorldLuaBindings.cpp
|
||||
scripting/StarUniverseServerLuaBindings.cpp
|
||||
scripting/StarUtilityLuaBindings.cpp
|
||||
|
||||
terrain/StarCacheSelector.cpp
|
||||
terrain/StarConstantSelector.cpp
|
||||
terrain/StarDisplacementSelector.cpp
|
||||
terrain/StarFlatSurfaceSelector.cpp
|
||||
terrain/StarIslandSurfaceSelector.cpp
|
||||
terrain/StarKarstCave.cpp
|
||||
terrain/StarMaxSelector.cpp
|
||||
terrain/StarMinMaxSelector.cpp
|
||||
terrain/StarMixSelector.cpp
|
||||
terrain/StarPerlinSelector.cpp
|
||||
terrain/StarRidgeBlocksSelector.cpp
|
||||
terrain/StarRotateSelector.cpp
|
||||
terrain/StarWormCave.cpp
|
||||
)
|
||||
|
||||
ADD_LIBRARY (star_game OBJECT ${star_game_SOURCES} ${star_game_HEADERS})
|
1473
source/game/StarActorMovementController.cpp
Normal file
1473
source/game/StarActorMovementController.cpp
Normal file
File diff suppressed because it is too large
Load diff
363
source/game/StarActorMovementController.hpp
Normal file
363
source/game/StarActorMovementController.hpp
Normal file
|
@ -0,0 +1,363 @@
|
|||
#ifndef STAR_ACTOR_MOVEMENT_CONTROLLER_HPP
|
||||
#define STAR_ACTOR_MOVEMENT_CONTROLLER_HPP
|
||||
|
||||
#include "StarMovementController.hpp"
|
||||
#include "StarPlatformerAStarTypes.hpp"
|
||||
#include "StarAnchorableEntity.hpp"
|
||||
#include "StarGameTimers.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(ActorMovementControllerException, MovementControllerException);
|
||||
|
||||
STAR_CLASS(ActorMovementController);
|
||||
STAR_CLASS(PathController);
|
||||
|
||||
struct ActorJumpProfile {
|
||||
ActorJumpProfile();
|
||||
ActorJumpProfile(Json const& config);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
ActorJumpProfile merge(ActorJumpProfile const& rhs) const;
|
||||
|
||||
Maybe<float> jumpSpeed;
|
||||
Maybe<float> jumpControlForce;
|
||||
Maybe<float> jumpInitialPercentage;
|
||||
|
||||
// If this is greater than 0.0, jump hold time is limited by this factor.
|
||||
Maybe<float> jumpHoldTime;
|
||||
// If this is greater than 0.0, then the total jump time for *all jumps in a
|
||||
// multi jump set* is limited by this factor.
|
||||
Maybe<float> jumpTotalHoldTime;
|
||||
|
||||
Maybe<bool> multiJump;
|
||||
Maybe<float> reJumpDelay;
|
||||
Maybe<bool> autoJump;
|
||||
Maybe<bool> collisionCancelled;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ActorJumpProfile& movementParameters);
|
||||
DataStream& operator<<(DataStream& ds, ActorJumpProfile const& movementParameters);
|
||||
|
||||
// A not-quite superset of MovementParameters, with some fields from
|
||||
// MovementParameters ignored because they make no sense, and other fields
|
||||
// expanded out to different cases based on Actor specific things.
|
||||
struct ActorMovementParameters {
|
||||
// Load sensible defaults from a config file.
|
||||
static ActorMovementParameters sensibleDefaults();
|
||||
|
||||
// Construct parameters from config with only those specified in the config
|
||||
// set, if any.
|
||||
explicit ActorMovementParameters(Json const& config = Json());
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
// Merge the given set of movement parameters on top of this one, with any
|
||||
// set parameters in rhs overwriting the ones in this set.
|
||||
ActorMovementParameters merge(ActorMovementParameters const& rhs) const;
|
||||
|
||||
Maybe<float> mass;
|
||||
Maybe<float> gravityMultiplier;
|
||||
Maybe<float> liquidBuoyancy;
|
||||
Maybe<float> airBuoyancy;
|
||||
Maybe<float> bounceFactor;
|
||||
Maybe<bool> stopOnFirstBounce;
|
||||
Maybe<bool> enableSurfaceSlopeCorrection;
|
||||
Maybe<float> slopeSlidingFactor;
|
||||
Maybe<float> maxMovementPerStep;
|
||||
Maybe<float> maximumCorrection;
|
||||
Maybe<float> speedLimit;
|
||||
|
||||
Maybe<PolyF> standingPoly;
|
||||
Maybe<PolyF> crouchingPoly;
|
||||
|
||||
Maybe<bool> stickyCollision;
|
||||
Maybe<float> stickyForce;
|
||||
|
||||
Maybe<float> walkSpeed;
|
||||
Maybe<float> runSpeed;
|
||||
Maybe<float> flySpeed;
|
||||
|
||||
Maybe<float> airFriction;
|
||||
Maybe<float> liquidFriction;
|
||||
|
||||
Maybe<float> minimumLiquidPercentage;
|
||||
Maybe<float> liquidImpedance;
|
||||
|
||||
Maybe<float> normalGroundFriction;
|
||||
Maybe<float> ambulatingGroundFriction;
|
||||
|
||||
Maybe<float> groundForce;
|
||||
Maybe<float> airForce;
|
||||
Maybe<float> liquidForce;
|
||||
|
||||
ActorJumpProfile airJumpProfile;
|
||||
ActorJumpProfile liquidJumpProfile;
|
||||
|
||||
Maybe<float> fallStatusSpeedMin;
|
||||
Maybe<int> fallThroughSustainFrames;
|
||||
Maybe<float> maximumPlatformCorrection;
|
||||
Maybe<float> maximumPlatformCorrectionVelocityFactor;
|
||||
|
||||
Maybe<StringSet> physicsEffectCategories;
|
||||
|
||||
Maybe<float> groundMovementMinimumSustain;
|
||||
Maybe<float> groundMovementMaximumSustain;
|
||||
Maybe<float> groundMovementCheckDistance;
|
||||
|
||||
Maybe<bool> collisionEnabled;
|
||||
Maybe<bool> frictionEnabled;
|
||||
Maybe<bool> gravityEnabled;
|
||||
|
||||
Maybe<float> pathExploreRate;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ActorMovementParameters& movementParameters);
|
||||
DataStream& operator<<(DataStream& ds, ActorMovementParameters const& movementParameters);
|
||||
|
||||
// A set of normalized values that act as "modifiers" or "bonuses" to movement,
|
||||
// and can be combined sensibly. A modifier of 0.0 represents a 0% change, a
|
||||
// modifier of 0.2 represents a 20% increase, and a modifier of -0.2 represents
|
||||
// a 20% decrease. Also includes some flags that disable functionality
|
||||
// combined with logical OR.
|
||||
struct ActorMovementModifiers {
|
||||
explicit ActorMovementModifiers(Json const& config = Json());
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
// Combines each modifier value through addition.
|
||||
ActorMovementModifiers combine(ActorMovementModifiers const& rhs) const;
|
||||
|
||||
float groundMovementModifier;
|
||||
float liquidMovementModifier;
|
||||
float speedModifier;
|
||||
float airJumpModifier;
|
||||
float liquidJumpModifier;
|
||||
|
||||
bool runningSuppressed;
|
||||
bool jumpingSuppressed;
|
||||
// Suppresses left, right, down, crouch, jump, and fly controls
|
||||
bool movementSuppressed;
|
||||
bool facingSuppressed;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ActorMovementModifiers& movementModifiers);
|
||||
DataStream& operator<<(DataStream& ds, ActorMovementModifiers const& movementModifiers);
|
||||
|
||||
class ActorMovementController : public MovementController {
|
||||
public:
|
||||
// Constructs an ActorMovementController with parameters loaded from sensible
|
||||
// defaults, and the given parameters (if any) applied on top of them.
|
||||
explicit ActorMovementController(ActorMovementParameters const& parameters = ActorMovementParameters());
|
||||
|
||||
// Currently active parameters.
|
||||
ActorMovementParameters const& baseParameters() const;
|
||||
|
||||
// Apply any set parameters from the given set on top of the current set.
|
||||
void updateBaseParameters(ActorMovementParameters const& parameters);
|
||||
|
||||
// Reset the parameters from the sensible defaults, and apply the given
|
||||
// parameters (if any) on top of them.
|
||||
void resetBaseParameters(ActorMovementParameters const& parameters = ActorMovementParameters());
|
||||
|
||||
// Currently active modifiers.
|
||||
ActorMovementModifiers const& baseModifiers() const;
|
||||
|
||||
// Combine the given modifiers with the already active modifiers.
|
||||
void updateBaseModifiers(ActorMovementModifiers const& modifiers);
|
||||
|
||||
// Reset all modifiers to the given values
|
||||
void resetBaseModifiers(ActorMovementModifiers const& modifiers = ActorMovementModifiers());
|
||||
|
||||
// Stores and loads position, velocity, rotation, movingDirection,
|
||||
// facingDirection, and crouching
|
||||
Json storeState() const;
|
||||
void loadState(Json const& state);
|
||||
|
||||
// Optionaly anchor this ActorMovementController to the given
|
||||
// AnchorableEntity. position, rotation, and facing direction will be set
|
||||
// based on the entity anchor alone every tick, and on slaved
|
||||
// ActorMovementControllers it will be updated based on the actual slave-side
|
||||
// AnchorableEntity state.
|
||||
void setAnchorState(EntityAnchorState anchorState);
|
||||
void resetAnchorState();
|
||||
Maybe<EntityAnchorState> anchorState() const;
|
||||
EntityAnchorConstPtr entityAnchor() const;
|
||||
|
||||
// ActorMovementController position and rotation honor the entity anchor, if
|
||||
// an anchor is set.
|
||||
Vec2F position() const;
|
||||
float xPosition() const;
|
||||
float yPosition() const;
|
||||
float rotation() const;
|
||||
PolyF collisionBody() const;
|
||||
RectF localBoundBox() const;
|
||||
RectF collisionBoundBox() const;
|
||||
|
||||
bool walking() const;
|
||||
bool running() const;
|
||||
Direction movingDirection() const;
|
||||
Direction facingDirection() const;
|
||||
bool crouching() const;
|
||||
bool flying() const;
|
||||
bool falling() const;
|
||||
bool canJump() const;
|
||||
bool jumping() const;
|
||||
// Slightly different than onGround, in that this is sustained for a few
|
||||
// extra frames of movement before it becomes false.
|
||||
bool groundMovement() const;
|
||||
bool liquidMovement() const;
|
||||
bool pathfinding() const;
|
||||
|
||||
// Basic direct physics controls that can be called multiple times per
|
||||
// update and will be combined.
|
||||
void controlRotation(float rotationRate);
|
||||
void controlAcceleration(Vec2F const& acceleration);
|
||||
void controlForce(Vec2F const& force);
|
||||
void controlApproachVelocity(Vec2F const& targetVelocity, float maxControlForce);
|
||||
void controlApproachVelocityAlongAngle(float angle, float targetVelocity, float maxControlForce, bool positiveOnly = false);
|
||||
void controlApproachXVelocity(float targetXVelocity, float maxControlForce);
|
||||
void controlApproachYVelocity(float targetYVelocity, float maxControlForce);
|
||||
|
||||
// Apply ActorMovementParameters / ActorMovementModifiers only as long as
|
||||
// the controls are active. Can be called multiple times per update and
|
||||
// will be combined.
|
||||
void controlParameters(ActorMovementParameters const& parameters);
|
||||
void controlModifiers(ActorMovementModifiers const& modifiers);
|
||||
|
||||
// Higher level movement controls that use forces defined in the
|
||||
// ActorMovementParameters. Calling more than once per update will override
|
||||
// previous calls.
|
||||
void controlMove(Direction direction, bool run = true);
|
||||
void controlFace(Direction direction);
|
||||
void controlDown();
|
||||
void controlCrouch();
|
||||
void controlJump(bool jumpEvenIfUnable = false);
|
||||
void controlFly(Vec2F const& velocity);
|
||||
|
||||
Maybe<pair<Vec2F, bool>> pathMove(Vec2F const& pathPosition, bool run = false, Maybe<PlatformerAStar::Parameters> const& parameters = {});
|
||||
Maybe<pair<Vec2F, bool>> controlPathMove(Vec2F const& pathPosition, bool run = false, Maybe<PlatformerAStar::Parameters> const& parameters = {});
|
||||
|
||||
// Clears all control data.
|
||||
void clearControls();
|
||||
|
||||
// Integrates the ActorMovementController one WorldTimestep and applies all
|
||||
// the control data and clears it for the next step.
|
||||
void tickMaster();
|
||||
|
||||
void tickSlave();
|
||||
|
||||
private:
|
||||
struct ApproachVelocityCommand {
|
||||
Vec2F targetVelocity;
|
||||
float maxControlForce;
|
||||
};
|
||||
|
||||
struct ApproachVelocityAlongAngleCommand {
|
||||
float alongAngle;
|
||||
float targetVelocity;
|
||||
float maxControlForce;
|
||||
bool positiveOnly;
|
||||
};
|
||||
|
||||
void applyMCParameters(ActorMovementParameters const& parameters);
|
||||
void doSetAnchorState(Maybe<EntityAnchorState> anchorState);
|
||||
|
||||
ActorMovementParameters m_baseParameters;
|
||||
ActorMovementModifiers m_baseModifiers;
|
||||
|
||||
// State data
|
||||
|
||||
NetElementBool m_walking;
|
||||
NetElementBool m_running;
|
||||
NetElementEnum<Direction> m_movingDirection;
|
||||
NetElementEnum<Direction> m_facingDirection;
|
||||
NetElementBool m_crouching;
|
||||
NetElementBool m_flying;
|
||||
NetElementBool m_falling;
|
||||
NetElementBool m_canJump;
|
||||
NetElementBool m_jumping;
|
||||
NetElementBool m_groundMovement;
|
||||
NetElementBool m_liquidMovement;
|
||||
NetElementData<Maybe<EntityAnchorState>> m_anchorState;
|
||||
EntityAnchorConstPtr m_entityAnchor;
|
||||
|
||||
// Command data
|
||||
|
||||
float m_controlRotationRate;
|
||||
Vec2F m_controlAcceleration;
|
||||
Vec2F m_controlForce;
|
||||
List<ApproachVelocityCommand> m_controlApproachVelocities;
|
||||
List<ApproachVelocityAlongAngleCommand> m_controlApproachVelocityAlongAngles;
|
||||
|
||||
Maybe<Direction> m_controlMove;
|
||||
Maybe<Direction> m_controlFace;
|
||||
bool m_controlRun;
|
||||
bool m_controlCrouch;
|
||||
bool m_controlDown;
|
||||
bool m_controlJump;
|
||||
bool m_controlJumpAnyway;
|
||||
|
||||
Maybe<Vec2F> m_controlFly;
|
||||
|
||||
Maybe<pair<Vec2F, bool>> m_controlPathMove;
|
||||
Maybe<pair<Vec2F, bool>> m_pathMoveResult;
|
||||
PathControllerPtr m_pathController;
|
||||
|
||||
ActorMovementParameters m_controlParameters;
|
||||
ActorMovementModifiers m_controlModifiers;
|
||||
|
||||
// Internal state data
|
||||
|
||||
int m_fallThroughSustain;
|
||||
bool m_lastControlJump;
|
||||
bool m_lastControlDown;
|
||||
|
||||
GameTimer m_reJumpTimer;
|
||||
Maybe<GameTimer> m_jumpHoldTimer;
|
||||
GameTimer m_groundMovementSustainTimer;
|
||||
|
||||
// Target horizontal velocity for walking / running
|
||||
float m_targetHorizontalAmbulatingVelocity;
|
||||
};
|
||||
|
||||
class PathController {
|
||||
public:
|
||||
PathController(World* world);
|
||||
|
||||
PlatformerAStar::Parameters const& parameters();
|
||||
void setParameters(PlatformerAStar::Parameters const& parameters);
|
||||
void reset();
|
||||
bool pathfinding() const;
|
||||
Maybe<Vec2F> targetPosition() const;
|
||||
Maybe<Direction> facing() const;
|
||||
Maybe<PlatformerAStar::Action> curAction() const;
|
||||
|
||||
// return true for reaching goal, false for failing to find path, nothing for running
|
||||
Maybe<bool> findPath(ActorMovementController& movementController, Vec2F const& targetPosition);
|
||||
Maybe<bool> move(ActorMovementController& movementController, ActorMovementParameters const& parameters, ActorMovementModifiers const& modifiers, bool run, float dt);
|
||||
private:
|
||||
bool validateEdge(ActorMovementController& movementController, PlatformerAStar::Edge const& edge);
|
||||
bool movingCollision(ActorMovementController& movementController, PolyF const& collisionPoly);
|
||||
|
||||
private:
|
||||
bool onGround(ActorMovementController const& movementController, Vec2F const& position, CollisionSet const& collisionSet) const;
|
||||
|
||||
World* m_world;
|
||||
PlatformerAStar::Parameters m_parameters;
|
||||
|
||||
Maybe<Vec2F> m_startPosition;
|
||||
Maybe<Vec2F> m_targetPosition;
|
||||
PlatformerAStar::PathFinderPtr m_pathFinder;
|
||||
|
||||
Maybe<Direction> m_controlFace;
|
||||
|
||||
size_t m_edgeIndex;
|
||||
float m_edgeTimer;
|
||||
Maybe<PlatformerAStar::Path> m_path;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
124
source/game/StarAiDatabase.cpp
Normal file
124
source/game/StarAiDatabase.cpp
Normal file
|
@ -0,0 +1,124 @@
|
|||
#include "StarAiDatabase.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AiDatabase::AiDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto config = assets->json("/ai/ai.config");
|
||||
|
||||
auto missions = assets->scanExtension("aimission");
|
||||
assets->queueJsons(missions);
|
||||
|
||||
for (auto const& file : missions) {
|
||||
if (auto config = assets->json(file)) {
|
||||
auto mission = parseMission(config);
|
||||
m_missions[mission.missionName] = mission;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& speciesPair : config.get("species").iterateObject())
|
||||
m_speciesParameters[speciesPair.first] = parseSpeciesParameters(speciesPair.second);
|
||||
|
||||
for (auto const& p : config.get("shipStatus").iterateObject())
|
||||
m_shipStatus[lexicalCast<unsigned>(p.first)] = parseSpeech(p.second);
|
||||
|
||||
m_noMissionsSpeech = parseSpeech(config.get("noMissionsSpeech"));
|
||||
m_noCrewSpeech = parseSpeech(config.get("noCrewSpeech"));
|
||||
|
||||
m_animationConfig.charactersPerSecond = config.getFloat("charactersPerSecond");
|
||||
m_animationConfig.defaultAnimation = config.getString("defaultAnimation");
|
||||
m_animationConfig.staticAnimation = Animation("/ai/ai.config:staticAnimation");
|
||||
m_animationConfig.staticOpacity = config.getFloat("staticOpacity");
|
||||
m_animationConfig.scanlineAnimation = Animation("/ai/ai.config:scanlineAnimation");
|
||||
m_animationConfig.scanlineOpacity = config.getFloat("scanlineOpacity");
|
||||
|
||||
for (auto const& pair : config.get("aiAnimations").iterateObject())
|
||||
m_animationConfig.aiAnimations[pair.first] = Animation(pair.second, "/ai/");
|
||||
}
|
||||
|
||||
AiMission AiDatabase::mission(String const& missionName) const {
|
||||
return m_missions.get(missionName);
|
||||
}
|
||||
|
||||
AiSpeech AiDatabase::shipStatus(unsigned shipLevel) const {
|
||||
// Find the first open speech set at this ship level or below.
|
||||
auto i = m_shipStatus.upper_bound(shipLevel);
|
||||
if (i != m_shipStatus.begin() && (--i)->first <= shipLevel)
|
||||
return i->second;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
AiSpeech AiDatabase::noCrewSpeech() const {
|
||||
return m_noCrewSpeech;
|
||||
}
|
||||
|
||||
AiSpeech AiDatabase::noMissionsSpeech() const {
|
||||
return m_noMissionsSpeech;
|
||||
}
|
||||
|
||||
String AiDatabase::portraitImage(String const& species, String const& frame) const {
|
||||
return strf("/ai/%s:%s", m_speciesParameters.get(species).portraitFrames, frame);
|
||||
}
|
||||
|
||||
Animation AiDatabase::animation(String const& species, String const& animationName) const {
|
||||
auto faceAnimation = m_animationConfig.aiAnimations.get(animationName);
|
||||
faceAnimation.setTag("image", m_speciesParameters.get(species).aiFrames);
|
||||
return faceAnimation;
|
||||
}
|
||||
|
||||
Animation AiDatabase::staticAnimation(String const& species) const {
|
||||
auto staticAnimation = m_animationConfig.staticAnimation;
|
||||
staticAnimation.setTag("image", m_speciesParameters.get(species).staticFrames);
|
||||
staticAnimation.setColor(Color::rgbaf(1.0f, 1.0f, 1.0f, m_animationConfig.staticOpacity));
|
||||
return staticAnimation;
|
||||
}
|
||||
|
||||
Animation AiDatabase::scanlineAnimation() const {
|
||||
auto animation = m_animationConfig.scanlineAnimation;
|
||||
animation.setColor(Color::rgbaf(1.0f, 1.0f, 1.0f, m_animationConfig.scanlineOpacity));
|
||||
return animation;
|
||||
}
|
||||
|
||||
float AiDatabase::charactersPerSecond() const {
|
||||
return m_animationConfig.charactersPerSecond;
|
||||
}
|
||||
|
||||
String AiDatabase::defaultAnimation() const {
|
||||
return m_animationConfig.defaultAnimation;
|
||||
}
|
||||
|
||||
AiSpeech AiDatabase::parseSpeech(Json const& v) {
|
||||
return AiSpeech{v.getString("animation"), v.getString("text"), v.getFloat("speedModifier", 1.0f)};
|
||||
}
|
||||
|
||||
AiDatabase::AiSpeciesParameters AiDatabase::parseSpeciesParameters(Json const& v) {
|
||||
AiSpeciesParameters species;
|
||||
species.aiFrames = v.getString("aiFrames");
|
||||
species.portraitFrames = v.getString("portraitFrames");
|
||||
species.staticFrames = v.getString("staticFrames");
|
||||
return species;
|
||||
}
|
||||
|
||||
AiSpeciesMissionText AiDatabase::parseSpeciesMissionText(Json const& vm) {
|
||||
return AiSpeciesMissionText{
|
||||
vm.getString("buttonText"), vm.getString("repeatButtonText"), parseSpeech(vm.get("selectSpeech", {}))};
|
||||
}
|
||||
|
||||
AiMission AiDatabase::parseMission(Json const& vm) {
|
||||
AiMission mission;
|
||||
mission.missionName = vm.getString("missionName");
|
||||
mission.missionUniqueWorld = vm.getString("missionWorld");
|
||||
mission.warpAnimation = vm.optString("warpAnimation");
|
||||
mission.warpDeploy = vm.optBool("warpDeploy");
|
||||
mission.icon = AssetPath::relativeTo("/ai/", vm.getString("icon"));
|
||||
for (auto const& textPair : vm.get("speciesText").iterateObject())
|
||||
mission.speciesText[textPair.first] = parseSpeciesMissionText(textPair.second);
|
||||
return mission;
|
||||
}
|
||||
|
||||
}
|
62
source/game/StarAiDatabase.hpp
Normal file
62
source/game/StarAiDatabase.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#ifndef STAR_AI_DATABASE_HPP
|
||||
#define STAR_AI_DATABASE_HPP
|
||||
|
||||
#include "StarAiTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class AiDatabase {
|
||||
public:
|
||||
AiDatabase();
|
||||
|
||||
AiMission mission(String const& missionName) const;
|
||||
|
||||
AiSpeech shipStatus(unsigned shipLevel) const;
|
||||
AiSpeech noMissionsSpeech() const;
|
||||
AiSpeech noCrewSpeech() const;
|
||||
|
||||
String portraitImage(String const& species, String const& frame = "idle.0") const;
|
||||
Animation animation(String const& species, String const& animationName) const;
|
||||
Animation staticAnimation(String const& species) const;
|
||||
Animation scanlineAnimation() const;
|
||||
|
||||
float charactersPerSecond() const;
|
||||
String defaultAnimation() const;
|
||||
|
||||
private:
|
||||
struct AiAnimationConfig {
|
||||
StringMap<Animation> aiAnimations;
|
||||
String defaultAnimation;
|
||||
float charactersPerSecond;
|
||||
|
||||
Animation staticAnimation;
|
||||
float staticOpacity;
|
||||
|
||||
Animation scanlineAnimation;
|
||||
float scanlineOpacity;
|
||||
};
|
||||
|
||||
struct AiSpeciesParameters {
|
||||
String aiFrames;
|
||||
String portraitFrames;
|
||||
String staticFrames;
|
||||
};
|
||||
|
||||
static AiSpeech parseSpeech(Json const& v);
|
||||
static AiSpeciesParameters parseSpeciesParameters(Json const& vm);
|
||||
|
||||
static AiSpeciesMissionText parseSpeciesMissionText(Json const& vm);
|
||||
static AiMission parseMission(Json const& vm);
|
||||
|
||||
StringMap<AiMission> m_missions;
|
||||
StringMap<AiSpeciesParameters> m_speciesParameters;
|
||||
Map<unsigned, AiSpeech> m_shipStatus;
|
||||
AiSpeech m_noMissionsSpeech;
|
||||
AiSpeech m_noCrewSpeech;
|
||||
|
||||
AiAnimationConfig m_animationConfig;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
18
source/game/StarAiTypes.cpp
Normal file
18
source/game/StarAiTypes.cpp
Normal file
|
@ -0,0 +1,18 @@
|
|||
#include "StarAiTypes.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AiState::AiState() {}
|
||||
|
||||
AiState::AiState(Json const& v) {
|
||||
availableMissions.addAll(jsonToStringList(v.get("availableMissions", JsonArray())));
|
||||
completedMissions.addAll(jsonToStringList(v.get("completedMissions", JsonArray())));
|
||||
}
|
||||
|
||||
Json AiState::toJson() const {
|
||||
return JsonObject{{"availableMissions", jsonFromStringList(availableMissions.values())},
|
||||
{"completedMissions", jsonFromStringList(completedMissions.values())}};
|
||||
}
|
||||
|
||||
}
|
46
source/game/StarAiTypes.hpp
Normal file
46
source/game/StarAiTypes.hpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#ifndef STAR_AI_TYPES_HPP
|
||||
#define STAR_AI_TYPES_HPP
|
||||
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarAnimation.hpp"
|
||||
#include "StarQuestDescriptor.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(AiException, StarException);
|
||||
|
||||
struct AiSpeech {
|
||||
String animation;
|
||||
String text;
|
||||
float speedModifier;
|
||||
};
|
||||
|
||||
struct AiState {
|
||||
AiState();
|
||||
AiState(Json const& v);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
OrderedHashSet<String> availableMissions;
|
||||
OrderedHashSet<String> completedMissions;
|
||||
};
|
||||
|
||||
struct AiSpeciesMissionText {
|
||||
String buttonText;
|
||||
String repeatButtonText;
|
||||
AiSpeech selectSpeech;
|
||||
};
|
||||
|
||||
struct AiMission {
|
||||
String missionName;
|
||||
String missionUniqueWorld;
|
||||
Maybe<String> warpAnimation;
|
||||
Maybe<bool> warpDeploy;
|
||||
String icon;
|
||||
StringMap<AiSpeciesMissionText> speciesText;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
175
source/game/StarAmbient.cpp
Normal file
175
source/game/StarAmbient.cpp
Normal file
|
@ -0,0 +1,175 @@
|
|||
#include "StarAmbient.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarTime.hpp"
|
||||
#include "StarMixer.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AmbientTrackGroup::AmbientTrackGroup() {
|
||||
tracks = {};
|
||||
}
|
||||
|
||||
AmbientTrackGroup::AmbientTrackGroup(StringList tracks) : tracks(move(tracks)) {}
|
||||
|
||||
AmbientTrackGroup::AmbientTrackGroup(Json const& config, String const& directory) {
|
||||
for (auto track : jsonToStringList(config.get("tracks", JsonArray())))
|
||||
tracks.append(AssetPath::relativeTo(directory, track));
|
||||
}
|
||||
|
||||
Json AmbientTrackGroup::toJson() const {
|
||||
return JsonObject{{"tracks", jsonFromStringList(tracks)}};
|
||||
}
|
||||
|
||||
AmbientNoisesDescription::AmbientNoisesDescription() {}
|
||||
|
||||
AmbientNoisesDescription::AmbientNoisesDescription(AmbientTrackGroup day, AmbientTrackGroup night)
|
||||
: daySounds(move(day)), nightSounds(move(night)) {}
|
||||
|
||||
AmbientNoisesDescription::AmbientNoisesDescription(Json const& config, String const& directory) {
|
||||
if (auto day = config.opt("day"))
|
||||
daySounds = AmbientTrackGroup(*day, directory);
|
||||
if (auto night = config.opt("night"))
|
||||
nightSounds = AmbientTrackGroup(*night, directory);
|
||||
}
|
||||
|
||||
Json AmbientNoisesDescription::toJson() const {
|
||||
return JsonObject{{"day", daySounds.toJson()}, {"night", nightSounds.toJson()}};
|
||||
}
|
||||
|
||||
AmbientManager::~AmbientManager() {
|
||||
cancelAll();
|
||||
}
|
||||
|
||||
void AmbientManager::setTrackSwitchGrace(float grace) {
|
||||
m_trackSwitchGrace = grace;
|
||||
}
|
||||
|
||||
void AmbientManager::setTrackFadeInTime(float fadeInTime) {
|
||||
m_trackFadeInTime = fadeInTime;
|
||||
}
|
||||
|
||||
AudioInstancePtr AmbientManager::updateAmbient(AmbientNoisesDescriptionPtr current, bool dayTime) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
if (m_currentTrack) {
|
||||
if (m_currentTrack->finished())
|
||||
m_currentTrack = {};
|
||||
}
|
||||
StringList tracks;
|
||||
if (current) {
|
||||
if (dayTime)
|
||||
tracks = current->daySounds.tracks;
|
||||
else
|
||||
tracks = current->nightSounds.tracks;
|
||||
}
|
||||
if (m_currentTrack) {
|
||||
if (!tracks.contains(m_currentTrackName)) {
|
||||
if (m_trackSwitchGrace <= Time::monotonicTime() - m_trackGraceTimestamp) {
|
||||
m_currentTrack->stop(m_trackFadeInTime);
|
||||
m_currentTrack = {};
|
||||
}
|
||||
} else {
|
||||
m_trackGraceTimestamp = Time::monotonicTime();
|
||||
}
|
||||
}
|
||||
if (!m_currentTrack) {
|
||||
m_currentTrackName = "";
|
||||
if (tracks.size() > 0) {
|
||||
while ((m_recentTracks.size() / 2) >= tracks.size())
|
||||
m_recentTracks.removeFirst();
|
||||
while (true) {
|
||||
m_currentTrackName = Random::randValueFrom(tracks);
|
||||
if (m_currentTrackName.empty() || !m_recentTracks.contains(m_currentTrackName))
|
||||
break;
|
||||
m_recentTracks.removeFirst(); // reduce chance of collisions on collision
|
||||
}
|
||||
}
|
||||
if (!m_currentTrackName.empty()) {
|
||||
if (auto audio = assets->tryAudio(m_currentTrackName)) {
|
||||
m_recentTracks.append(m_currentTrackName);
|
||||
m_currentTrack = make_shared<AudioInstance>(*audio);
|
||||
m_currentTrack->setLoops(-1);
|
||||
// Slowly fade the music track in
|
||||
m_currentTrack->setVolume(0.0f);
|
||||
m_currentTrack->setVolume(m_volume, m_trackFadeInTime);
|
||||
m_delay = 0;
|
||||
m_duration = 0;
|
||||
m_volumeChanged = false;
|
||||
return m_currentTrack;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_volumeChanged) {
|
||||
if (m_delay > 0)
|
||||
m_delay -= WorldTimestep;
|
||||
else {
|
||||
m_volumeChanged = false;
|
||||
if (m_currentTrack) {
|
||||
m_currentTrack->setVolume(m_volume, m_duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
AudioInstancePtr AmbientManager::updateWeather(WeatherNoisesDescriptionPtr current) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
if (m_weatherTrack) {
|
||||
if (m_weatherTrack->finished())
|
||||
m_weatherTrack = {};
|
||||
}
|
||||
|
||||
StringList tracks;
|
||||
if (current)
|
||||
tracks = current->tracks;
|
||||
|
||||
if (m_weatherTrack) {
|
||||
if (!tracks.contains(m_weatherTrackName)) {
|
||||
m_weatherTrack->stop(10.0f);
|
||||
m_weatherTrack = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_weatherTrack) {
|
||||
m_weatherTrackName = Random::randValueFrom(tracks);
|
||||
if (!m_weatherTrackName.empty()) {
|
||||
if (auto audio = assets->tryAudio(m_weatherTrackName)) {
|
||||
m_weatherTrack = make_shared<AudioInstance>(*audio);
|
||||
m_weatherTrack->setLoops(-1);
|
||||
m_weatherTrack->setVolume(0.0f);
|
||||
m_weatherTrack->setVolume(1, m_trackFadeInTime);
|
||||
return m_weatherTrack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void AmbientManager::cancelAll() {
|
||||
if (m_weatherTrack) {
|
||||
m_weatherTrack->stop();
|
||||
m_weatherTrack = {};
|
||||
}
|
||||
if (m_currentTrack) {
|
||||
m_currentTrack->stop();
|
||||
m_currentTrack = {};
|
||||
}
|
||||
}
|
||||
|
||||
void AmbientManager::setVolume(float volume, float delay, float duration) {
|
||||
if (m_volume == volume)
|
||||
return;
|
||||
m_volume = volume;
|
||||
m_delay = delay;
|
||||
m_duration = duration;
|
||||
m_volumeChanged = true;
|
||||
}
|
||||
|
||||
}
|
71
source/game/StarAmbient.hpp
Normal file
71
source/game/StarAmbient.hpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#ifndef STAR_AMBIENT_HPP
|
||||
#define STAR_AMBIENT_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(AudioInstance);
|
||||
STAR_STRUCT(AmbientTrackGroup);
|
||||
STAR_STRUCT(AmbientNoisesDescription);
|
||||
STAR_CLASS(AmbientManager);
|
||||
|
||||
struct AmbientTrackGroup {
|
||||
AmbientTrackGroup();
|
||||
AmbientTrackGroup(StringList tracks);
|
||||
AmbientTrackGroup(Json const& config, String const& directory = "");
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
StringList tracks;
|
||||
};
|
||||
|
||||
// represents the ambient sounds data for a biome
|
||||
struct AmbientNoisesDescription {
|
||||
AmbientNoisesDescription();
|
||||
AmbientNoisesDescription(AmbientTrackGroup day, AmbientTrackGroup night);
|
||||
AmbientNoisesDescription(Json const& config, String const& directory = "");
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
AmbientTrackGroup daySounds;
|
||||
AmbientTrackGroup nightSounds;
|
||||
};
|
||||
|
||||
typedef AmbientTrackGroup WeatherNoisesDescription;
|
||||
typedef shared_ptr<WeatherNoisesDescription> WeatherNoisesDescriptionPtr;
|
||||
|
||||
// manages the running ambient sounds
|
||||
class AmbientManager {
|
||||
public:
|
||||
// Automatically calls cancelAll();
|
||||
~AmbientManager();
|
||||
|
||||
void setTrackSwitchGrace(float grace);
|
||||
void setTrackFadeInTime(float fadeInTime);
|
||||
|
||||
// Returns a new AudioInstance if a new ambient sound is to be started.
|
||||
AudioInstancePtr updateAmbient(AmbientNoisesDescriptionPtr current, bool dayTime = false);
|
||||
AudioInstancePtr updateWeather(WeatherNoisesDescriptionPtr current);
|
||||
void cancelAll();
|
||||
|
||||
void setVolume(float volume, float delay, float duration);
|
||||
|
||||
private:
|
||||
AudioInstancePtr m_currentTrack;
|
||||
AudioInstancePtr m_weatherTrack;
|
||||
String m_currentTrackName;
|
||||
String m_weatherTrackName;
|
||||
float m_trackFadeInTime = 0.0f;
|
||||
float m_trackSwitchGrace = 0.0f;
|
||||
double m_trackGraceTimestamp = 0;
|
||||
Deque<String> m_recentTracks;
|
||||
float m_volume = 1.0f;
|
||||
float m_delay = 0.0f;
|
||||
float m_duration = 0.0f;
|
||||
bool m_volumeChanged = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
121
source/game/StarAnimation.cpp
Normal file
121
source/game/StarAnimation.cpp
Normal file
|
@ -0,0 +1,121 @@
|
|||
#include "StarAnimation.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
Animation::Animation(Json config, String const& directory) {
|
||||
m_directory = directory;
|
||||
if (m_directory.empty()) {
|
||||
if (config.isType(Json::Type::String))
|
||||
m_directory = AssetPath::directory(config.toString());
|
||||
else
|
||||
m_directory = "/";
|
||||
}
|
||||
if (config.isNull())
|
||||
config = JsonObject();
|
||||
|
||||
config = Root::singleton().assets()->fetchJson(config);
|
||||
|
||||
m_mode = AnimationModeNames.getLeft(config.getString("mode", "endAndDisappear"));
|
||||
m_base = config.getString("frames", "");
|
||||
// If the base image has no index tag, then assume that the frame is appended
|
||||
// to the end.
|
||||
m_appendFrame = !m_base.contains("<index>");
|
||||
m_frameNumber = config.getInt("frameNumber", 1);
|
||||
m_animationCycle = config.getDouble("animationCycle", 1.0);
|
||||
m_animationTime = m_animationCycle * config.getDouble("loops", 1.0);
|
||||
m_angle = config.getFloat("angle", 0.0f);
|
||||
m_offset = jsonToVec2F(config.get("offset", JsonArray{0.0f, 0.0f}));
|
||||
m_centered = config.getBool("centered", true);
|
||||
m_processing = config.getString("processing", "");
|
||||
m_color = config.opt("color").apply(jsonToColor).value(Color::White);
|
||||
m_variantOffset = Random::randInt(config.getInt("variants", 1) - 1) * m_frameNumber;
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Animation::setAngle(float angle) {
|
||||
m_angle = angle;
|
||||
}
|
||||
|
||||
void Animation::setProcessing(String processing) {
|
||||
m_processing = move(processing);
|
||||
}
|
||||
|
||||
void Animation::addProcessing(String const& processing) {
|
||||
m_processing = String::joinWith("?", m_processing, processing);
|
||||
}
|
||||
|
||||
void Animation::setColor(Color color) {
|
||||
m_color = color;
|
||||
}
|
||||
|
||||
void Animation::setTag(String tagName, String tagValue) {
|
||||
m_tagValues[move(tagName)] = move(tagValue);
|
||||
}
|
||||
|
||||
void Animation::clearTags() {
|
||||
m_tagValues.clear();
|
||||
}
|
||||
|
||||
Drawable Animation::drawable(float pixelSize) const {
|
||||
if (m_base.empty() || m_frame < 0)
|
||||
return Drawable();
|
||||
|
||||
// Replace <index> with the frame index, if it is not found then add
|
||||
// :<index> to the end
|
||||
String baseFrame = AssetPath::relativeTo(m_directory, m_base).replaceTags(m_tagValues);
|
||||
if (m_appendFrame)
|
||||
baseFrame += ":" + toString(m_frame);
|
||||
|
||||
baseFrame = String::joinWith("?", baseFrame, m_processing);
|
||||
|
||||
Drawable drawable = Drawable::makeImage(move(baseFrame), pixelSize, m_centered, m_offset);
|
||||
drawable.rotate(m_angle);
|
||||
drawable.color = m_color;
|
||||
return drawable;
|
||||
}
|
||||
|
||||
void Animation::update(float dt) {
|
||||
if (m_completed)
|
||||
return;
|
||||
|
||||
float time_within_cycle = fmod(m_animationTimer, m_animationCycle);
|
||||
float time_per_frame = m_animationCycle / m_frameNumber;
|
||||
float frame_number = time_within_cycle / time_per_frame;
|
||||
m_frame = m_variantOffset + clamp<int>(frame_number, 0, m_frameNumber - 1);
|
||||
m_animationTimer += dt;
|
||||
|
||||
if (m_mode == Animation::LoopForever) {
|
||||
// to prevent floating point jitter and seizing after a very long time
|
||||
m_animationTimer = fmod(m_animationTimer, m_animationCycle);
|
||||
} else if (m_animationTimer >= m_timeToLive) {
|
||||
if (m_mode == Animation::EndAndDisappear)
|
||||
m_frame = -1;
|
||||
m_completed = true;
|
||||
}
|
||||
|
||||
m_tagValues["index"] = toString(m_frame);
|
||||
}
|
||||
|
||||
bool Animation::isComplete() const {
|
||||
return m_completed;
|
||||
}
|
||||
|
||||
void Animation::reset() {
|
||||
m_frame = 0;
|
||||
m_animationTimer = 0;
|
||||
m_timeToLive = m_animationTime;
|
||||
m_completed = false;
|
||||
m_tagValues["index"] = "0";
|
||||
}
|
||||
|
||||
EnumMap<Animation::AnimationMode> Animation::AnimationModeNames = {{Animation::AnimationMode::Stop, "Stop"},
|
||||
{Animation::AnimationMode::EndAndDisappear, "EndAndDisappear"},
|
||||
{Animation::AnimationMode::LoopForever, "LoopForever"}};
|
||||
}
|
60
source/game/StarAnimation.hpp
Normal file
60
source/game/StarAnimation.hpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#ifndef STAR_ANIMATION_HPP
|
||||
#define STAR_ANIMATION_HPP
|
||||
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Animation);
|
||||
|
||||
class Animation {
|
||||
public:
|
||||
// config can be either a path to a config or a literal config.
|
||||
Animation(Json config = {}, String const& directory = {});
|
||||
|
||||
void setAngle(float angle);
|
||||
|
||||
void setProcessing(String processing);
|
||||
void addProcessing(String const& processing);
|
||||
|
||||
void setColor(Color color);
|
||||
|
||||
void setTag(String tagName, String tagValue);
|
||||
void clearTags();
|
||||
|
||||
Drawable drawable(float pixelSize) const;
|
||||
|
||||
void update(float dt);
|
||||
|
||||
bool isComplete() const;
|
||||
void reset();
|
||||
|
||||
private:
|
||||
enum AnimationMode { Stop, EndAndDisappear, LoopForever };
|
||||
static EnumMap<AnimationMode> AnimationModeNames;
|
||||
|
||||
AnimationMode m_mode;
|
||||
String m_directory;
|
||||
String m_base;
|
||||
bool m_appendFrame;
|
||||
int m_frameNumber;
|
||||
float m_animationCycle;
|
||||
float m_animationTime;
|
||||
float m_angle;
|
||||
Vec2F m_offset;
|
||||
bool m_centered;
|
||||
String m_processing;
|
||||
Color m_color;
|
||||
int m_variantOffset;
|
||||
|
||||
StringMap<String> m_tagValues;
|
||||
int m_frame;
|
||||
float m_animationTimer;
|
||||
float m_timeToLive;
|
||||
bool m_completed;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
310
source/game/StarArmorWearer.cpp
Normal file
310
source/game/StarArmorWearer.cpp
Normal file
|
@ -0,0 +1,310 @@
|
|||
#include "StarArmorWearer.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
#include "StarArmors.hpp"
|
||||
#include "StarCasting.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
#include "StarLiquidItem.hpp"
|
||||
#include "StarMaterialItem.hpp"
|
||||
#include "StarObject.hpp"
|
||||
#include "StarTools.hpp"
|
||||
#include "StarActivatableItem.hpp"
|
||||
#include "StarObjectItem.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarObjectDatabase.hpp"
|
||||
#include "StarWorld.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ArmorWearer::ArmorWearer() {
|
||||
addNetElement(&m_headItemDataNetState);
|
||||
addNetElement(&m_chestItemDataNetState);
|
||||
addNetElement(&m_legsItemDataNetState);
|
||||
addNetElement(&m_backItemDataNetState);
|
||||
addNetElement(&m_headCosmeticItemDataNetState);
|
||||
addNetElement(&m_chestCosmeticItemDataNetState);
|
||||
addNetElement(&m_legsCosmeticItemDataNetState);
|
||||
addNetElement(&m_backCosmeticItemDataNetState);
|
||||
}
|
||||
|
||||
void ArmorWearer::setupHumanoidClothingDrawables(Humanoid& humanoid, bool forceNude) const {
|
||||
bool bodyHidden = false;
|
||||
if (m_headCosmeticItem && !forceNude) {
|
||||
humanoid.setHeadArmorFrameset(m_headCosmeticItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setHeadArmorDirectives(m_headCosmeticItem->directives());
|
||||
humanoid.setHelmetMaskDirectives(m_headCosmeticItem->maskDirectives());
|
||||
bodyHidden = bodyHidden || m_headCosmeticItem->hideBody();
|
||||
} else if (m_headItem && !forceNude) {
|
||||
humanoid.setHeadArmorFrameset(m_headItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setHeadArmorDirectives(m_headItem->directives());
|
||||
humanoid.setHelmetMaskDirectives(m_headItem->maskDirectives());
|
||||
bodyHidden = bodyHidden || m_headItem->hideBody();
|
||||
} else {
|
||||
humanoid.setHeadArmorFrameset("");
|
||||
humanoid.setHelmetMaskDirectives("");
|
||||
}
|
||||
|
||||
if (m_chestCosmeticItem && !forceNude) {
|
||||
humanoid.setBackSleeveFrameset(m_chestCosmeticItem->backSleeveFrameset(humanoid.identity().gender));
|
||||
humanoid.setFrontSleeveFrameset(m_chestCosmeticItem->frontSleeveFrameset(humanoid.identity().gender));
|
||||
humanoid.setChestArmorFrameset(m_chestCosmeticItem->bodyFrameset(humanoid.identity().gender));
|
||||
humanoid.setChestArmorDirectives(m_chestCosmeticItem->directives());
|
||||
bodyHidden = bodyHidden || m_chestCosmeticItem->hideBody();
|
||||
} else if (m_chestItem && !forceNude) {
|
||||
humanoid.setBackSleeveFrameset(m_chestItem->backSleeveFrameset(humanoid.identity().gender));
|
||||
humanoid.setFrontSleeveFrameset(m_chestItem->frontSleeveFrameset(humanoid.identity().gender));
|
||||
humanoid.setChestArmorFrameset(m_chestItem->bodyFrameset(humanoid.identity().gender));
|
||||
humanoid.setChestArmorDirectives(m_chestItem->directives());
|
||||
bodyHidden = bodyHidden || m_chestItem->hideBody();
|
||||
} else {
|
||||
humanoid.setBackSleeveFrameset("");
|
||||
humanoid.setFrontSleeveFrameset("");
|
||||
humanoid.setChestArmorFrameset("");
|
||||
}
|
||||
|
||||
if (m_legsCosmeticItem && !forceNude) {
|
||||
humanoid.setLegsArmorFrameset(m_legsCosmeticItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setLegsArmorDirectives(m_legsCosmeticItem->directives());
|
||||
bodyHidden = bodyHidden || m_legsCosmeticItem->hideBody();
|
||||
} else if (m_legsItem && !forceNude) {
|
||||
humanoid.setLegsArmorFrameset(m_legsItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setLegsArmorDirectives(m_legsItem->directives());
|
||||
bodyHidden = bodyHidden || m_legsItem->hideBody();
|
||||
} else {
|
||||
humanoid.setLegsArmorFrameset("");
|
||||
}
|
||||
|
||||
if (m_backCosmeticItem && !forceNude) {
|
||||
humanoid.setBackArmorFrameset(m_backCosmeticItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setBackArmorDirectives(m_backCosmeticItem->directives());
|
||||
bodyHidden = bodyHidden || m_backCosmeticItem->hideBody();
|
||||
} else if (m_backItem && !forceNude) {
|
||||
humanoid.setBackArmorFrameset(m_backItem->frameset(humanoid.identity().gender));
|
||||
humanoid.setBackArmorDirectives(m_backItem->directives());
|
||||
bodyHidden = bodyHidden || m_backItem->hideBody();
|
||||
} else {
|
||||
humanoid.setBackArmorFrameset("");
|
||||
}
|
||||
|
||||
humanoid.setBodyHidden(bodyHidden);
|
||||
}
|
||||
|
||||
void ArmorWearer::effects(EffectEmitter& effectEmitter) {
|
||||
if (auto item = as<EffectSourceItem>(m_headCosmeticItem))
|
||||
effectEmitter.addEffectSources("headArmor", item->effectSources());
|
||||
else if (auto item = as<EffectSourceItem>(m_headItem))
|
||||
effectEmitter.addEffectSources("headArmor", item->effectSources());
|
||||
|
||||
if (auto item = as<EffectSourceItem>(m_chestCosmeticItem))
|
||||
effectEmitter.addEffectSources("chestArmor", item->effectSources());
|
||||
else if (auto item = as<EffectSourceItem>(m_chestItem))
|
||||
effectEmitter.addEffectSources("chestArmor", item->effectSources());
|
||||
|
||||
if (auto item = as<EffectSourceItem>(m_legsCosmeticItem))
|
||||
effectEmitter.addEffectSources("legsArmor", item->effectSources());
|
||||
else if (auto item = as<EffectSourceItem>(m_legsItem))
|
||||
effectEmitter.addEffectSources("legsArmor", item->effectSources());
|
||||
|
||||
if (auto item = as<EffectSourceItem>(m_backCosmeticItem))
|
||||
effectEmitter.addEffectSources("backArmor", item->effectSources());
|
||||
else if (auto item = as<EffectSourceItem>(m_backItem))
|
||||
effectEmitter.addEffectSources("backArmor", item->effectSources());
|
||||
}
|
||||
|
||||
Json ArmorWearer::diskStore() const {
|
||||
JsonObject res;
|
||||
if (m_headItem)
|
||||
res["headItem"] = m_headItem->descriptor().diskStore();
|
||||
if (m_chestItem)
|
||||
res["chestItem"] = m_chestItem->descriptor().diskStore();
|
||||
if (m_legsItem)
|
||||
res["legsItem"] = m_legsItem->descriptor().diskStore();
|
||||
if (m_backItem)
|
||||
res["backItem"] = m_backItem->descriptor().diskStore();
|
||||
if (m_headCosmeticItem)
|
||||
res["headCosmeticItem"] = m_headCosmeticItem->descriptor().diskStore();
|
||||
if (m_chestCosmeticItem)
|
||||
res["chestCosmeticItem"] = m_chestCosmeticItem->descriptor().diskStore();
|
||||
if (m_legsCosmeticItem)
|
||||
res["legsCosmeticItem"] = m_legsCosmeticItem->descriptor().diskStore();
|
||||
if (m_backCosmeticItem)
|
||||
res["backCosmeticItem"] = m_backCosmeticItem->descriptor().diskStore();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void ArmorWearer::diskLoad(Json const& diskStore) {
|
||||
auto itemDb = Root::singleton().itemDatabase();
|
||||
m_headItem = as<HeadArmor>(itemDb->diskLoad(diskStore.get("headItem", {})));
|
||||
m_chestItem = as<ChestArmor>(itemDb->diskLoad(diskStore.get("chestItem", {})));
|
||||
m_legsItem = as<LegsArmor>(itemDb->diskLoad(diskStore.get("legsItem", {})));
|
||||
m_backItem = as<BackArmor>(itemDb->diskLoad(diskStore.get("backItem", {})));
|
||||
m_headCosmeticItem = as<HeadArmor>(itemDb->diskLoad(diskStore.get("headCosmeticItem", {})));
|
||||
m_chestCosmeticItem = as<ChestArmor>(itemDb->diskLoad(diskStore.get("chestCosmeticItem", {})));
|
||||
m_legsCosmeticItem = as<LegsArmor>(itemDb->diskLoad(diskStore.get("legsCosmeticItem", {})));
|
||||
m_backCosmeticItem = as<BackArmor>(itemDb->diskLoad(diskStore.get("backCosmeticItem", {})));
|
||||
}
|
||||
|
||||
List<PersistentStatusEffect> ArmorWearer::statusEffects() const {
|
||||
List<PersistentStatusEffect> statusEffects;
|
||||
auto addStatusFromItem = [&](ItemPtr const& item) {
|
||||
if (auto effectItem = as<StatusEffectItem>(item))
|
||||
statusEffects.appendAll(effectItem->statusEffects());
|
||||
};
|
||||
|
||||
addStatusFromItem(m_headItem);
|
||||
addStatusFromItem(m_chestItem);
|
||||
addStatusFromItem(m_legsItem);
|
||||
addStatusFromItem(m_backItem);
|
||||
|
||||
return statusEffects;
|
||||
}
|
||||
|
||||
void ArmorWearer::setHeadItem(HeadArmorPtr headItem) {
|
||||
m_headItem = headItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setHeadCosmeticItem(HeadArmorPtr headCosmeticItem) {
|
||||
m_headCosmeticItem = headCosmeticItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setChestCosmeticItem(ChestArmorPtr chestCosmeticItem) {
|
||||
m_chestCosmeticItem = chestCosmeticItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setChestItem(ChestArmorPtr chestItem) {
|
||||
m_chestItem = chestItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setLegsItem(LegsArmorPtr legsItem) {
|
||||
m_legsItem = legsItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setLegsCosmeticItem(LegsArmorPtr legsCosmeticItem) {
|
||||
m_legsCosmeticItem = legsCosmeticItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setBackItem(BackArmorPtr backItem) {
|
||||
m_backItem = backItem;
|
||||
}
|
||||
|
||||
void ArmorWearer::setBackCosmeticItem(BackArmorPtr backCosmeticItem) {
|
||||
m_backCosmeticItem = backCosmeticItem;
|
||||
}
|
||||
|
||||
HeadArmorPtr ArmorWearer::headItem() const {
|
||||
return m_headItem;
|
||||
}
|
||||
|
||||
HeadArmorPtr ArmorWearer::headCosmeticItem() const {
|
||||
return m_headCosmeticItem;
|
||||
}
|
||||
|
||||
ChestArmorPtr ArmorWearer::chestItem() const {
|
||||
return m_chestItem;
|
||||
}
|
||||
|
||||
ChestArmorPtr ArmorWearer::chestCosmeticItem() const {
|
||||
return m_chestCosmeticItem;
|
||||
}
|
||||
|
||||
LegsArmorPtr ArmorWearer::legsItem() const {
|
||||
return m_legsItem;
|
||||
}
|
||||
|
||||
LegsArmorPtr ArmorWearer::legsCosmeticItem() const {
|
||||
return m_legsCosmeticItem;
|
||||
}
|
||||
|
||||
BackArmorPtr ArmorWearer::backItem() const {
|
||||
return m_backItem;
|
||||
}
|
||||
|
||||
BackArmorPtr ArmorWearer::backCosmeticItem() const {
|
||||
return m_backCosmeticItem;
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::headItemDescriptor() const {
|
||||
if (m_headItem)
|
||||
return m_headItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::headCosmeticItemDescriptor() const {
|
||||
if (m_headCosmeticItem)
|
||||
return m_headCosmeticItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::chestItemDescriptor() const {
|
||||
if (m_chestItem)
|
||||
return m_chestItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::chestCosmeticItemDescriptor() const {
|
||||
if (m_chestCosmeticItem)
|
||||
return m_chestCosmeticItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::legsItemDescriptor() const {
|
||||
if (m_legsItem)
|
||||
return m_legsItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::legsCosmeticItemDescriptor() const {
|
||||
if (m_legsCosmeticItem)
|
||||
return m_legsCosmeticItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::backItemDescriptor() const {
|
||||
if (m_backItem)
|
||||
return m_backItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
ItemDescriptor ArmorWearer::backCosmeticItemDescriptor() const {
|
||||
if (m_backCosmeticItem)
|
||||
return m_backCosmeticItem->descriptor();
|
||||
return {};
|
||||
}
|
||||
|
||||
void ArmorWearer::netElementsNeedLoad(bool) {
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
|
||||
if (m_headItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_headItemDataNetState.get(), m_headItem);
|
||||
if (m_chestItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_chestItemDataNetState.get(), m_chestItem);
|
||||
if (m_legsItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_legsItemDataNetState.get(), m_legsItem);
|
||||
if (m_backItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_backItemDataNetState.get(), m_backItem);
|
||||
|
||||
if (m_headCosmeticItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_headCosmeticItemDataNetState.get(), m_headCosmeticItem);
|
||||
if (m_chestCosmeticItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_chestCosmeticItemDataNetState.get(), m_chestCosmeticItem);
|
||||
if (m_legsCosmeticItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_legsCosmeticItemDataNetState.get(), m_legsCosmeticItem);
|
||||
if (m_backCosmeticItemDataNetState.pullUpdated())
|
||||
itemDatabase->loadItem(m_backCosmeticItemDataNetState.get(), m_backCosmeticItem);
|
||||
}
|
||||
|
||||
void ArmorWearer::netElementsNeedStore() {
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
m_headItemDataNetState.set(itemSafeDescriptor(m_headItem));
|
||||
m_chestItemDataNetState.set(itemSafeDescriptor(m_chestItem));
|
||||
m_legsItemDataNetState.set(itemSafeDescriptor(m_legsItem));
|
||||
m_backItemDataNetState.set(itemSafeDescriptor(m_backItem));
|
||||
|
||||
m_headCosmeticItemDataNetState.set(itemSafeDescriptor(m_headCosmeticItem));
|
||||
m_chestCosmeticItemDataNetState.set(itemSafeDescriptor(m_chestCosmeticItem));
|
||||
m_legsCosmeticItemDataNetState.set(itemSafeDescriptor(m_legsCosmeticItem));
|
||||
m_backCosmeticItemDataNetState.set(itemSafeDescriptor(m_backCosmeticItem));
|
||||
}
|
||||
|
||||
}
|
89
source/game/StarArmorWearer.hpp
Normal file
89
source/game/StarArmorWearer.hpp
Normal file
|
@ -0,0 +1,89 @@
|
|||
#ifndef STAR_ARMOR_WEARER_HPP
|
||||
#define STAR_ARMOR_WEARER_HPP
|
||||
|
||||
#include "StarHumanoid.hpp"
|
||||
#include "StarNetElementSystem.hpp"
|
||||
#include "StarEffectEmitter.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarStatusTypes.hpp"
|
||||
#include "StarLightSource.hpp"
|
||||
#include "StarDamage.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ObjectItem);
|
||||
STAR_CLASS(HeadArmor);
|
||||
STAR_CLASS(ChestArmor);
|
||||
STAR_CLASS(LegsArmor);
|
||||
STAR_CLASS(BackArmor);
|
||||
STAR_CLASS(ToolUserEntity);
|
||||
STAR_CLASS(Item);
|
||||
STAR_CLASS(World);
|
||||
|
||||
STAR_CLASS(ArmorWearer);
|
||||
|
||||
class ArmorWearer : public NetElementSyncGroup {
|
||||
public:
|
||||
ArmorWearer();
|
||||
|
||||
void setupHumanoidClothingDrawables(Humanoid& humanoid, bool forceNude) const;
|
||||
void effects(EffectEmitter& effectEmitter);
|
||||
List<PersistentStatusEffect> statusEffects() const;
|
||||
|
||||
Json diskStore() const;
|
||||
void diskLoad(Json const& diskStore);
|
||||
|
||||
void setHeadItem(HeadArmorPtr headItem);
|
||||
void setHeadCosmeticItem(HeadArmorPtr headCosmeticItem);
|
||||
void setChestCosmeticItem(ChestArmorPtr chestCosmeticItem);
|
||||
void setChestItem(ChestArmorPtr chestItem);
|
||||
void setLegsItem(LegsArmorPtr legsItem);
|
||||
void setLegsCosmeticItem(LegsArmorPtr legsCosmeticItem);
|
||||
void setBackItem(BackArmorPtr backItem);
|
||||
void setBackCosmeticItem(BackArmorPtr backCosmeticItem);
|
||||
|
||||
HeadArmorPtr headItem() const;
|
||||
HeadArmorPtr headCosmeticItem() const;
|
||||
ChestArmorPtr chestItem() const;
|
||||
ChestArmorPtr chestCosmeticItem() const;
|
||||
LegsArmorPtr legsItem() const;
|
||||
LegsArmorPtr legsCosmeticItem() const;
|
||||
BackArmorPtr backItem() const;
|
||||
BackArmorPtr backCosmeticItem() const;
|
||||
|
||||
ItemDescriptor headItemDescriptor() const;
|
||||
ItemDescriptor headCosmeticItemDescriptor() const;
|
||||
ItemDescriptor chestItemDescriptor() const;
|
||||
ItemDescriptor chestCosmeticItemDescriptor() const;
|
||||
ItemDescriptor legsItemDescriptor() const;
|
||||
ItemDescriptor legsCosmeticItemDescriptor() const;
|
||||
ItemDescriptor backItemDescriptor() const;
|
||||
ItemDescriptor backCosmeticItemDescriptor() const;
|
||||
|
||||
private:
|
||||
void netElementsNeedLoad(bool full) override;
|
||||
void netElementsNeedStore() override;
|
||||
|
||||
HeadArmorPtr m_headItem;
|
||||
ChestArmorPtr m_chestItem;
|
||||
LegsArmorPtr m_legsItem;
|
||||
BackArmorPtr m_backItem;
|
||||
|
||||
HeadArmorPtr m_headCosmeticItem;
|
||||
ChestArmorPtr m_chestCosmeticItem;
|
||||
LegsArmorPtr m_legsCosmeticItem;
|
||||
BackArmorPtr m_backCosmeticItem;
|
||||
|
||||
NetElementData<ItemDescriptor> m_headItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_chestItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_legsItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_backItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_headCosmeticItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_chestCosmeticItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_legsCosmeticItemDataNetState;
|
||||
NetElementData<ItemDescriptor> m_backCosmeticItemDataNetState;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
298
source/game/StarBehaviorDatabase.cpp
Normal file
298
source/game/StarBehaviorDatabase.cpp
Normal file
|
@ -0,0 +1,298 @@
|
|||
#include "StarBehaviorDatabase.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<NodeParameterType> const NodeParameterTypeNames {
|
||||
{NodeParameterType::Json, "json"},
|
||||
{NodeParameterType::Entity, "entity"},
|
||||
{NodeParameterType::Position, "position"},
|
||||
{NodeParameterType::Vec2, "vec2"},
|
||||
{NodeParameterType::Number, "number"},
|
||||
{NodeParameterType::Bool, "bool"},
|
||||
{NodeParameterType::List, "list"},
|
||||
{NodeParameterType::Table, "table"},
|
||||
{NodeParameterType::String, "string"}
|
||||
};
|
||||
|
||||
NodeParameterValue nodeParameterValueFromJson(Json const& json) {
|
||||
if (auto key = json.optString("key"))
|
||||
return *key;
|
||||
else
|
||||
return json.get("value");
|
||||
}
|
||||
|
||||
Json jsonFromNodeParameter(NodeParameter const& parameter) {
|
||||
JsonObject json {
|
||||
{"type", NodeParameterTypeNames.getRight(parameter.first)}
|
||||
};
|
||||
if (auto key = parameter.second.maybe<String>())
|
||||
json.set("key", *key);
|
||||
else
|
||||
json.set("value", parameter.second.get<Json>());
|
||||
return json;
|
||||
}
|
||||
|
||||
NodeParameter jsonToNodeParameter(Json const& json) {
|
||||
NodeParameterType type = NodeParameterTypeNames.getLeft(json.getString("type"));
|
||||
if (auto key = json.optString("key"))
|
||||
return {type, *key};
|
||||
else
|
||||
return {type, json.opt("value").value(Json())};
|
||||
}
|
||||
|
||||
Json nodeOutputToJson(NodeOutput const& output) {
|
||||
return JsonObject {
|
||||
{"type", NodeParameterTypeNames.getRight(output.first)},
|
||||
{"key", jsonFromMaybe<String>(output.second.first, [](String const& s) { return Json(s); })},
|
||||
{"ephemeral", output.second.second}
|
||||
};
|
||||
}
|
||||
|
||||
NodeOutput jsonToNodeOutput(Json const& json) {
|
||||
return {
|
||||
NodeParameterTypeNames.getLeft(json.getString("type")),
|
||||
{jsonToMaybe<String>(json.get("key"), [](Json const& j) { return j.toString(); }), json.optBool("ephemeral").value(false)}
|
||||
};
|
||||
}
|
||||
|
||||
EnumMap<BehaviorNodeType> const BehaviorNodeTypeNames {
|
||||
{BehaviorNodeType::Action, "Action"},
|
||||
{BehaviorNodeType::Decorator, "Decorator"},
|
||||
{BehaviorNodeType::Composite, "Composite"},
|
||||
{BehaviorNodeType::Module, "Module"}
|
||||
};
|
||||
|
||||
EnumMap<CompositeType> const CompositeTypeNames {
|
||||
{CompositeType::Sequence, "Sequence"},
|
||||
{CompositeType::Selector, "Selector"},
|
||||
{CompositeType::Parallel, "Parallel"},
|
||||
{CompositeType::Dynamic, "Dynamic"},
|
||||
{CompositeType::Randomize, "Randomize"}
|
||||
};
|
||||
|
||||
void applyTreeParameters(StringMap<NodeParameter>& nodeParameters, StringMap<NodeParameterValue> const& treeParameters) {
|
||||
for (auto& p : nodeParameters) {
|
||||
NodeParameter& parameter = p.second;
|
||||
parameter.second = replaceBehaviorTag(parameter.second, treeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
NodeParameterValue replaceBehaviorTag(NodeParameterValue const& parameter, StringMap<NodeParameterValue> const& treeParameters) {
|
||||
Maybe<String> strVal = parameter.maybe<String>();
|
||||
if (!strVal && parameter.get<Json>().isType(Json::Type::String))
|
||||
strVal = parameter.get<Json>().toString();
|
||||
if (strVal) {
|
||||
String key = *strVal;
|
||||
if (key.beginsWith('<') && key.endsWith('>')) {
|
||||
String treeKey = key.substr(1, key.size() - 2);
|
||||
|
||||
if (auto replace = treeParameters.maybe(treeKey)) {
|
||||
return *replace;
|
||||
} else {
|
||||
throw StarException(strf("No parameter specified for tag '%s'", key));
|
||||
}
|
||||
}
|
||||
}
|
||||
return parameter;
|
||||
}
|
||||
|
||||
Maybe<String> replaceOutputBehaviorTag(Maybe<String> const& output, StringMap<NodeParameterValue> const& treeParameters) {
|
||||
if (auto out = output) {
|
||||
if (out->beginsWith('<') && out->endsWith('>')) {
|
||||
if (auto replace = treeParameters.maybe(out->substr(1, out->size() - 2))) {
|
||||
if (auto key = replace->maybe<String>())
|
||||
return *key;
|
||||
else if (replace->get<Json>().isType(Json::Type::String))
|
||||
return replace->get<Json>().toString();
|
||||
else
|
||||
return {};
|
||||
} else {
|
||||
throw StarException(strf("No parameter specified for tag '%s'", *out));
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// TODO: This is temporary until BehaviorState can handle valueType:value pairs
|
||||
void parseNodeParameters(JsonObject& parameters) {
|
||||
for (auto& p : parameters)
|
||||
p.second = p.second.opt("key").orMaybe(p.second.opt("value")).value(Json());
|
||||
}
|
||||
|
||||
ActionNode::ActionNode(String name, StringMap<NodeParameter> parameters, StringMap<NodeOutput> output)
|
||||
: name(move(name)), parameters(move(parameters)), output(move(output)) { }
|
||||
|
||||
DecoratorNode::DecoratorNode(String const& name, StringMap<NodeParameter> parameters, BehaviorNodeConstPtr child)
|
||||
: name(name), parameters(parameters), child(child) { }
|
||||
|
||||
SequenceNode::SequenceNode(List<BehaviorNodeConstPtr> children) : children(children) { }
|
||||
|
||||
SelectorNode::SelectorNode(List<BehaviorNodeConstPtr> children) : children(children) { }
|
||||
|
||||
ParallelNode::ParallelNode(StringMap<NodeParameter> parameters, List<BehaviorNodeConstPtr> children) : children(children) {
|
||||
int s = parameters.get("success").second.get<Json>().optInt().value(-1);
|
||||
succeed = s == -1 ? children.size() : s;
|
||||
|
||||
int f = parameters.get("fail").second.get<Json>().optInt().value(-1);
|
||||
fail = f == -1 ? children.size() : f;
|
||||
}
|
||||
|
||||
DynamicNode::DynamicNode(List<BehaviorNodeConstPtr> children) : children(children) { }
|
||||
|
||||
RandomizeNode::RandomizeNode(List<BehaviorNodeConstPtr> children) : children(children) { }
|
||||
|
||||
BehaviorTree::BehaviorTree(String const& name, StringSet scripts, JsonObject const& parameters)
|
||||
: name(name), scripts(scripts), parameters(parameters) { }
|
||||
|
||||
BehaviorDatabase::BehaviorDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
StringList nodeFiles = assets->scanExtension("nodes");
|
||||
assets->queueJsons(nodeFiles);
|
||||
for (String const& file : nodeFiles) {
|
||||
try {
|
||||
Json nodes = assets->json(file);
|
||||
for (auto node : nodes.toObject()) {
|
||||
StringMap<NodeParameter> parameters;
|
||||
for (auto p : node.second.getObject("properties", {}))
|
||||
parameters.set(p.first, jsonToNodeParameter(p.second));
|
||||
|
||||
m_nodeParameters.set(node.first, parameters);
|
||||
|
||||
StringMap<NodeOutput> output;
|
||||
for (auto p : node.second.getObject("output", {}))
|
||||
output.set(p.first, jsonToNodeOutput(p.second));
|
||||
|
||||
m_nodeOutput.set(node.first, output);
|
||||
}
|
||||
} catch (StarException const& e) {
|
||||
throw StarException(strf("Could not load nodes file \'%s\'", file), e);
|
||||
}
|
||||
}
|
||||
|
||||
auto behaviorFiles = assets->scanExtension("behavior");
|
||||
assets->queueJsons(behaviorFiles);
|
||||
for (auto const& file : behaviorFiles) {
|
||||
try {
|
||||
auto config = assets->json(file);
|
||||
auto name = config.getString("name");
|
||||
|
||||
if (m_configs.contains(name))
|
||||
throw StarException(strf("Duplicate behavior tree \'%s\'", name));
|
||||
|
||||
m_configs[name] = config;
|
||||
} catch (StarException const& e) {
|
||||
throw StarException(strf("Could not load behavior file \'%s\'", file), e);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto pair : m_configs) {
|
||||
if (!m_behaviors.contains(pair.first))
|
||||
loadTree(pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
BehaviorTreeConstPtr BehaviorDatabase::behaviorTree(String const& name) const {
|
||||
if (!m_behaviors.contains(name))
|
||||
throw StarException(strf("No such behavior tree \'%s\'", name));
|
||||
|
||||
return m_behaviors.get(name);
|
||||
}
|
||||
|
||||
BehaviorTreeConstPtr BehaviorDatabase::buildTree(Json const& config, StringMap<NodeParameterValue> const& overrides) const {
|
||||
StringSet scripts = jsonToStringSet(config.get("scripts", JsonArray()));
|
||||
auto tree = BehaviorTree(config.getString("name"), scripts, config.getObject("parameters", {}));
|
||||
|
||||
StringMap<NodeParameterValue> parameters;
|
||||
for (auto p : config.getObject("parameters", {}))
|
||||
parameters.set(p.first, p.second);
|
||||
for (auto p : overrides)
|
||||
parameters.set(p.first, p.second);
|
||||
BehaviorNodeConstPtr root = behaviorNode(config.get("root"), parameters, tree);
|
||||
tree.root = root;
|
||||
return make_shared<BehaviorTree>(move(tree));
|
||||
}
|
||||
|
||||
Json BehaviorDatabase::behaviorConfig(String const& name) const {
|
||||
if (!m_configs.contains(name))
|
||||
throw StarException(strf("No such behavior tree \'%s\'", name));
|
||||
|
||||
return m_configs.get(name);
|
||||
}
|
||||
|
||||
void BehaviorDatabase::loadTree(String const& name) {
|
||||
m_behaviors.set(name, buildTree(m_configs.get(name)));
|
||||
}
|
||||
|
||||
CompositeNode BehaviorDatabase::compositeNode(Json const& config, StringMap<NodeParameter> parameters, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const {
|
||||
List<BehaviorNodeConstPtr> children = config.getArray("children", {}).transformed([this,treeParameters,&tree](Json const& child) {
|
||||
return behaviorNode(child, treeParameters, tree);
|
||||
});
|
||||
|
||||
CompositeType type = CompositeTypeNames.getLeft(config.getString("name"));
|
||||
if (type == CompositeType::Sequence)
|
||||
return SequenceNode(children);
|
||||
else if (type == CompositeType::Selector)
|
||||
return SelectorNode(children);
|
||||
else if (type == CompositeType::Parallel)
|
||||
return ParallelNode(parameters, children);
|
||||
else if (type == CompositeType::Dynamic)
|
||||
return DynamicNode(children);
|
||||
else if (type == CompositeType::Randomize)
|
||||
return RandomizeNode(children);
|
||||
|
||||
// above statement needs to be exhaustive
|
||||
throw StarException(strf("Composite node type '%s' could not be created from JSON", CompositeTypeNames.getRight(type)));
|
||||
}
|
||||
|
||||
BehaviorNodeConstPtr BehaviorDatabase::behaviorNode(Json const& json, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const {
|
||||
BehaviorNodeType type = BehaviorNodeTypeNames.getLeft(json.getString("type"));
|
||||
|
||||
auto name = json.getString("name");
|
||||
auto parameterConfig = json.getObject("parameters", {});
|
||||
|
||||
if (type == BehaviorNodeType::Module) {
|
||||
// merge in module parameters to a copy of the treeParameters to propagate
|
||||
// tree parameters into the sub-tree, but allow modules to override
|
||||
auto moduleParameters = treeParameters;
|
||||
for (auto p : parameterConfig)
|
||||
moduleParameters.set(p.first, replaceBehaviorTag(nodeParameterValueFromJson(p.second), treeParameters));
|
||||
|
||||
BehaviorTree module = *buildTree(m_configs.get(name), moduleParameters);
|
||||
tree.scripts.addAll(module.scripts);
|
||||
tree.functions.addAll(module.functions);
|
||||
|
||||
return module.root;
|
||||
}
|
||||
|
||||
StringMap<NodeParameter> parameters = m_nodeParameters.get(name);
|
||||
for (auto& p : parameters)
|
||||
p.second.second = parameterConfig.maybe(p.first).apply(nodeParameterValueFromJson).value(p.second.second);
|
||||
applyTreeParameters(parameters, treeParameters);
|
||||
|
||||
if (type == BehaviorNodeType::Action) {
|
||||
tree.functions.add(name);
|
||||
|
||||
Json outputConfig = json.getObject("output", {});
|
||||
StringMap<NodeOutput> output = m_nodeOutput.get(name);
|
||||
for (auto& p : output)
|
||||
p.second.second.first = replaceOutputBehaviorTag(outputConfig.optString(p.first).orMaybe(p.second.second.first), treeParameters);
|
||||
|
||||
return make_shared<BehaviorNode>(ActionNode(name, parameters, output));
|
||||
} else if (type == BehaviorNodeType::Decorator) {
|
||||
tree.functions.add(name);
|
||||
BehaviorNodeConstPtr child = behaviorNode(json.get("child"), treeParameters, tree);
|
||||
return make_shared<BehaviorNode>(DecoratorNode(name, parameters, child));
|
||||
} else if (type == BehaviorNodeType::Composite) {
|
||||
return make_shared<BehaviorNode>(compositeNode(json, parameters, treeParameters, tree));
|
||||
}
|
||||
|
||||
// above statement must be exhaustive
|
||||
throw StarException(strf("Behavior node type '%s' could not be created from JSON", BehaviorNodeTypeNames.getRight(type)));
|
||||
}
|
||||
|
||||
}
|
155
source/game/StarBehaviorDatabase.hpp
Normal file
155
source/game/StarBehaviorDatabase.hpp
Normal file
|
@ -0,0 +1,155 @@
|
|||
#ifndef STAR_BEHAVIOR_DATABASE_HPP
|
||||
#define STAR_BEHAVIOR_DATABASE_HPP
|
||||
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(BehaviorDatabase);
|
||||
STAR_STRUCT(ActionNode);
|
||||
STAR_STRUCT(DecoratorNode);
|
||||
STAR_STRUCT(SequenceNode);
|
||||
STAR_STRUCT(SelectorNode);
|
||||
STAR_STRUCT(ParallelNode);
|
||||
STAR_STRUCT(DynamicNode);
|
||||
STAR_STRUCT(RandomizeNode);
|
||||
STAR_STRUCT(BehaviorTree);
|
||||
|
||||
typedef Variant<SequenceNode, SelectorNode, ParallelNode, DynamicNode, RandomizeNode> CompositeNode;
|
||||
|
||||
typedef Variant<ActionNode, DecoratorNode, CompositeNode, BehaviorTreeConstPtr> BehaviorNode;
|
||||
typedef std::shared_ptr<const BehaviorNode> BehaviorNodeConstPtr;
|
||||
|
||||
enum class NodeParameterType : uint8_t {
|
||||
Json,
|
||||
Entity,
|
||||
Position,
|
||||
Vec2,
|
||||
Number,
|
||||
Bool,
|
||||
List,
|
||||
Table,
|
||||
String
|
||||
};
|
||||
extern EnumMap<NodeParameterType> const NodeParameterTypeNames;
|
||||
|
||||
typedef Variant<String, Json> NodeParameterValue;
|
||||
typedef pair<NodeParameterType, NodeParameterValue> NodeParameter;
|
||||
typedef pair<NodeParameterType, pair<Maybe<String>, bool>> NodeOutput;
|
||||
|
||||
NodeParameterValue nodeParameterValueFromJson(Json const& json);
|
||||
|
||||
Json jsonFromNodeParameter(NodeParameter const& parameter);
|
||||
NodeParameter jsonToNodeParameter(Json const& json);
|
||||
|
||||
Json jsonFromNodeOutput(NodeOutput const& output);
|
||||
NodeOutput jsonToNodeOutput(Json const& json);
|
||||
|
||||
enum class BehaviorNodeType : uint16_t {
|
||||
Action,
|
||||
Decorator,
|
||||
Composite,
|
||||
Module
|
||||
};
|
||||
extern EnumMap<BehaviorNodeType> const BehaviorNodeTypeNames;
|
||||
|
||||
enum class CompositeType : uint16_t {
|
||||
Sequence,
|
||||
Selector,
|
||||
Parallel,
|
||||
Dynamic,
|
||||
Randomize
|
||||
};
|
||||
extern EnumMap<CompositeType> const CompositeTypeNames;
|
||||
|
||||
// replaces global tags in nodeParameters in place
|
||||
NodeParameterValue replaceBehaviorTag(NodeParameterValue const& parameter, StringMap<NodeParameterValue> const& treeParameters);
|
||||
Maybe<String> replaceOutputBehaviorTag(Maybe<String> const& output, StringMap<NodeParameterValue> const& treeParameters);
|
||||
void applyTreeParameters(StringMap<NodeParameter>& nodeParameters, StringMap<NodeParameterValue> const& treeParameters);
|
||||
|
||||
struct ActionNode {
|
||||
ActionNode(String name, StringMap<NodeParameter> parameters, StringMap<NodeOutput> output);
|
||||
|
||||
String name;
|
||||
StringMap<NodeParameter> parameters;
|
||||
StringMap<NodeOutput> output;
|
||||
};
|
||||
|
||||
struct DecoratorNode {
|
||||
DecoratorNode(String const& name, StringMap<NodeParameter> parameters, BehaviorNodeConstPtr child);
|
||||
|
||||
String name;
|
||||
StringMap<NodeParameter> parameters;
|
||||
BehaviorNodeConstPtr child;
|
||||
};
|
||||
|
||||
struct SequenceNode {
|
||||
SequenceNode(List<BehaviorNodeConstPtr> children);
|
||||
|
||||
List<BehaviorNodeConstPtr> children;
|
||||
};
|
||||
|
||||
struct SelectorNode {
|
||||
SelectorNode(List<BehaviorNodeConstPtr> children);
|
||||
|
||||
List<BehaviorNodeConstPtr> children;
|
||||
};
|
||||
|
||||
struct ParallelNode {
|
||||
ParallelNode(StringMap<NodeParameter>, List<BehaviorNodeConstPtr> children);
|
||||
|
||||
int succeed;
|
||||
int fail;
|
||||
List<BehaviorNodeConstPtr> children;
|
||||
};
|
||||
|
||||
struct DynamicNode {
|
||||
DynamicNode(List<BehaviorNodeConstPtr> children);
|
||||
|
||||
List<BehaviorNodeConstPtr> children;
|
||||
};
|
||||
|
||||
struct RandomizeNode {
|
||||
RandomizeNode(List<BehaviorNodeConstPtr> children);
|
||||
|
||||
List<BehaviorNodeConstPtr> children;
|
||||
};
|
||||
|
||||
struct BehaviorTree {
|
||||
BehaviorTree(String const& name, StringSet scripts, JsonObject const& parameters);
|
||||
|
||||
String name;
|
||||
StringSet scripts;
|
||||
StringSet functions;
|
||||
JsonObject parameters;
|
||||
|
||||
BehaviorNodeConstPtr root;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<const BehaviorNode> BehaviorNodeConstPtr;
|
||||
|
||||
class BehaviorDatabase {
|
||||
public:
|
||||
BehaviorDatabase();
|
||||
|
||||
BehaviorTreeConstPtr behaviorTree(String const& name) const;
|
||||
BehaviorTreeConstPtr buildTree(Json const& config, StringMap<NodeParameterValue> const& overrides = {}) const;
|
||||
Json behaviorConfig(String const& name) const;
|
||||
|
||||
private:
|
||||
StringMap<Json> m_configs;
|
||||
StringMap<BehaviorTreeConstPtr> m_behaviors;
|
||||
StringMap<StringMap<NodeParameter>> m_nodeParameters;
|
||||
StringMap<StringMap<NodeOutput>> m_nodeOutput;
|
||||
|
||||
void loadTree(String const& name);
|
||||
|
||||
// constructs node variants
|
||||
CompositeNode compositeNode(Json const& config, StringMap<NodeParameter> parameters, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const;
|
||||
BehaviorNodeConstPtr behaviorNode(Json const& json, StringMap<NodeParameterValue> const& treeParameters, BehaviorTree& tree) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
387
source/game/StarBehaviorState.cpp
Normal file
387
source/game/StarBehaviorState.cpp
Normal file
|
@ -0,0 +1,387 @@
|
|||
#include "StarBehaviorState.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarLuaGameConverters.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// node parameter types supported by the blackboard
|
||||
List<NodeParameterType> BlackboardTypes = {
|
||||
NodeParameterType::Json,
|
||||
NodeParameterType::Entity,
|
||||
NodeParameterType::Position,
|
||||
NodeParameterType::Vec2,
|
||||
NodeParameterType::Number,
|
||||
NodeParameterType::Bool,
|
||||
NodeParameterType::List,
|
||||
NodeParameterType::Table,
|
||||
NodeParameterType::String
|
||||
};
|
||||
|
||||
Blackboard::Blackboard(LuaTable luaContext) : m_luaContext(move(luaContext)) {
|
||||
for (auto type : BlackboardTypes) {
|
||||
m_board.set(type, {});
|
||||
m_input.set(type, {});
|
||||
}
|
||||
}
|
||||
|
||||
void Blackboard::set(NodeParameterType type, String const& key, LuaValue value) {
|
||||
if (value.is<LuaNilType>())
|
||||
m_board.get(type).remove(key);
|
||||
else
|
||||
m_board.get(type).set(key, value);
|
||||
|
||||
for (auto& input : m_input.get(type).maybe(key).value({})) {
|
||||
m_parameters.get(input.first).set(input.second, value);
|
||||
}
|
||||
|
||||
// dumb special case for setting number outputs to vec2 inputs
|
||||
if (type == NodeParameterType::Number) {
|
||||
for (pair<uint64_t, LuaTable>& input : m_vectorNumberInput.maybe(key).value()) {
|
||||
input.second.set(input.first, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LuaValue Blackboard::get(NodeParameterType type, String const& key) const {
|
||||
return m_board.get(type).maybe(key).value(LuaNil);
|
||||
}
|
||||
|
||||
LuaTable Blackboard::parameters(StringMap<NodeParameter> const& parameters, uint64_t nodeId) {
|
||||
if (auto table = m_parameters.maybe(nodeId))
|
||||
return *table;
|
||||
|
||||
LuaTable table = m_luaContext.engine().createTable();
|
||||
for (auto const& p : parameters) {
|
||||
if (auto key = p.second.second.maybe<String>()) {
|
||||
auto& typeInput = m_input.get(p.second.first);
|
||||
if (!typeInput.contains(*key))
|
||||
typeInput.add(*key, {});
|
||||
|
||||
typeInput.get(*key).append({nodeId, p.first});
|
||||
table.set(p.first, get(p.second.first, *key));
|
||||
} else {
|
||||
Json value = p.second.second.get<Json>();
|
||||
if (value.isNull())
|
||||
continue;
|
||||
|
||||
// dumb special case for allowing a vec2 of blackboard number keys
|
||||
if (p.second.first == NodeParameterType::Vec2) {
|
||||
if (value.type() != Json::Type::Array)
|
||||
throw StarException(strf("Vec2 parameter not of array type for key %s", p.first, value));
|
||||
JsonArray vector = value.toArray();
|
||||
LuaTable luaVector = m_luaContext.engine().createTable();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (vector[i].isType(Json::Type::String)) {
|
||||
auto key = vector[i].toString();
|
||||
if (!m_vectorNumberInput.contains(key))
|
||||
m_vectorNumberInput.add(key, {});
|
||||
|
||||
m_vectorNumberInput[key].append({i+1, luaVector});
|
||||
luaVector.set(i+1, get(NodeParameterType::Number, vector[i].toString()));
|
||||
} else {
|
||||
luaVector.set(i+1, m_luaContext.engine().luaFrom(vector[i]));
|
||||
}
|
||||
}
|
||||
table.set(p.first, luaVector);
|
||||
continue;
|
||||
}
|
||||
|
||||
table.set(p.first, value);
|
||||
}
|
||||
}
|
||||
|
||||
m_parameters.add(nodeId, table);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
void Blackboard::setOutput(ActionNode const& node, LuaTable const& output) {
|
||||
for (auto p : node.output) {
|
||||
auto out = p.second.second;
|
||||
if (auto boardKey = out.first) {
|
||||
set(p.second.first, *boardKey, output.get<LuaValue>(p.first));
|
||||
|
||||
if (out.second)
|
||||
m_ephemeral.add({p.second.first, *boardKey});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<pair<NodeParameterType, String>> Blackboard::takeEphemerals() {
|
||||
return take(m_ephemeral);
|
||||
}
|
||||
|
||||
void Blackboard::clearEphemerals(Set<pair<NodeParameterType, String>> ephemerals) {
|
||||
for (auto const& p : ephemerals) {
|
||||
if (!m_ephemeral.contains(p))
|
||||
set(p.first, p.second, LuaNil);
|
||||
}
|
||||
}
|
||||
|
||||
DecoratorState::DecoratorState(LuaThread thread) : thread(move(thread)) {
|
||||
child = make_shared<NodeState>();
|
||||
}
|
||||
|
||||
CompositeState::CompositeState(size_t childCount) : index() {
|
||||
for (size_t i = 0; i < childCount; i++)
|
||||
children.append(make_shared<NodeState>());
|
||||
}
|
||||
|
||||
CompositeState::CompositeState(size_t childCount, size_t begin) : CompositeState(childCount) {
|
||||
index = begin;
|
||||
}
|
||||
|
||||
BehaviorState::BehaviorState(BehaviorTreeConstPtr tree, LuaTable context, Maybe<BlackboardWeakPtr> blackboard) : m_tree(tree), m_luaContext(move(context)) {
|
||||
if (blackboard)
|
||||
m_board = *blackboard;
|
||||
else
|
||||
m_board = make_shared<Blackboard>(m_luaContext);
|
||||
|
||||
LuaFunction require = m_luaContext.get<LuaFunction>("require");
|
||||
for (auto script : m_tree->scripts)
|
||||
require.invoke(script);
|
||||
|
||||
for (String const& name : m_tree->functions)
|
||||
m_functions.set(name, m_luaContext.get<LuaFunction>(name));
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::run(float dt) {
|
||||
m_lastDt = dt;
|
||||
NodeStatus status;
|
||||
|
||||
auto ephemeral = m_board.maybe<BlackboardPtr>().apply([](auto const& b) { return b->takeEphemerals(); });
|
||||
|
||||
status = runNode(*m_tree->root, m_rootState);
|
||||
|
||||
if (ephemeral)
|
||||
m_board.get<BlackboardPtr>()->clearEphemerals(*ephemeral);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void BehaviorState::clear() {
|
||||
m_rootState.reset();
|
||||
}
|
||||
|
||||
BlackboardWeakPtr BehaviorState::blackboardPtr() {
|
||||
if (auto master = m_board.maybe<BlackboardPtr>())
|
||||
return weak_ptr<Blackboard>(*master);
|
||||
else
|
||||
return m_board.get<BlackboardWeakPtr>();
|
||||
}
|
||||
|
||||
BlackboardPtr BehaviorState::board() {
|
||||
if (auto master = m_board.maybe<BlackboardPtr>())
|
||||
return *master;
|
||||
else
|
||||
return m_board.get<BlackboardWeakPtr>().lock();
|
||||
}
|
||||
|
||||
LuaThread BehaviorState::nodeLuaThread(String const& funcName) {
|
||||
LuaThread thread = m_threads.maybeTakeLast().value(m_luaContext.engine().createThread());
|
||||
thread.pushFunction(m_functions.get(funcName));
|
||||
return thread;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runNode(BehaviorNode const& node, NodeState& state) {
|
||||
NodeStatus status = NodeStatus::Invalid;
|
||||
if (node.is<ActionNode>())
|
||||
status = runAction(node.get<ActionNode>(), state);
|
||||
else if(node.is<DecoratorNode>())
|
||||
status = runDecorator(node.get<DecoratorNode>(), state);
|
||||
else if(node.is<CompositeNode>())
|
||||
status = runComposite(node.get<CompositeNode>(), state);
|
||||
else
|
||||
StarException("Unidentified behavior node type");
|
||||
|
||||
if (status != NodeStatus::Running)
|
||||
state.reset();
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runAction(ActionNode const& node, NodeState& state) {
|
||||
uint64_t id = (uint64_t)&node;
|
||||
|
||||
auto result = ActionReturn(NodeStatus::Invalid, LuaNil);
|
||||
if (state.isNothing()) {
|
||||
LuaTable parameters = board()->parameters(node.parameters, id);
|
||||
LuaThread thread = nodeLuaThread(node.name);
|
||||
try {
|
||||
result = thread.resume<ActionReturn>(parameters, blackboardPtr(), id, m_lastDt).value(ActionReturn(NodeStatus::Invalid, LuaNil));
|
||||
} catch (LuaException e) {
|
||||
throw StarException(strf("Lua Exception caught running action node %s in behavior %s: %s", node.name, m_tree->name, outputException(e, false)));
|
||||
}
|
||||
|
||||
auto status = get<0>(result);
|
||||
if (status != NodeStatus::Success && status != NodeStatus::Failure)
|
||||
state.set(ActionState{thread});
|
||||
} else {
|
||||
LuaThread const& thread = state->get<ActionState>().thread;
|
||||
|
||||
try {
|
||||
result = thread.resume<ActionReturn>(m_lastDt).value(ActionReturn(NodeStatus::Invalid, LuaNil));
|
||||
} catch (LuaException e) {
|
||||
throw StarException(strf("Lua Exception caught resuming action node %s in behavior %s: %s", node.name, m_tree->name, outputException(e, false)));
|
||||
}
|
||||
|
||||
auto status = get<0>(result);
|
||||
if (status == NodeStatus::Success || status == NodeStatus::Failure)
|
||||
m_threads.append(thread);
|
||||
}
|
||||
|
||||
if (auto table = get<1>(result).maybe<LuaTable>())
|
||||
board()->setOutput(node, *table);
|
||||
|
||||
return get<0>(result);
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runDecorator(DecoratorNode const& node, NodeState& state) {
|
||||
uint64_t id = (uint64_t)&node;
|
||||
NodeStatus status = NodeStatus::Running;
|
||||
if (state.isNothing()) {
|
||||
auto parameters = board()->parameters(node.parameters, id);
|
||||
|
||||
LuaThread thread = nodeLuaThread(node.name);
|
||||
try {
|
||||
status = thread.resume<NodeStatus>(parameters, blackboardPtr(), id).value(NodeStatus::Invalid);
|
||||
} catch (LuaException e) {
|
||||
throw StarException(strf("Lua Exception caught initializing decorator node %s in behavior %s: %s", node.name, m_tree->name, outputException(e, false)));
|
||||
}
|
||||
if (status == NodeStatus::Success || status == NodeStatus::Failure)
|
||||
return status;
|
||||
|
||||
state.set(DecoratorState(thread));
|
||||
}
|
||||
|
||||
DecoratorState& decorator = state->get<DecoratorState>();
|
||||
// decorator runs its child on yield and is resumed with the child's status on success or failure
|
||||
while (status == NodeStatus::Running) {
|
||||
auto childStatus = runNode(*node.child, *decorator.child);
|
||||
if (childStatus == NodeStatus::Success || childStatus == NodeStatus::Failure) {
|
||||
try {
|
||||
status = decorator.thread.resume<NodeStatus>(childStatus).value(NodeStatus::Invalid);
|
||||
} catch (LuaException e) {
|
||||
throw StarException(strf("Lua Exception caught resuming decorator node %s in behavior %s: %s", node.name, m_tree->name, outputException(e, false)));
|
||||
}
|
||||
} else {
|
||||
return NodeStatus::Running;
|
||||
}
|
||||
}
|
||||
|
||||
m_threads.append(decorator.thread);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runComposite(CompositeNode const& node, NodeState& state) {
|
||||
NodeStatus status;
|
||||
if (node.is<SequenceNode>())
|
||||
status = runSequence(node.get<SequenceNode>(), state);
|
||||
else if (node.is<SelectorNode>())
|
||||
status = runSelector(node.get<SelectorNode>(), state);
|
||||
else if (node.is<ParallelNode>())
|
||||
status = runParallel(node.get<ParallelNode>(), state);
|
||||
else if (node.is<DynamicNode>())
|
||||
status = runDynamic(node.get<DynamicNode>(), state);
|
||||
else if (node.is<RandomizeNode>())
|
||||
status = runRandomize(node.get<RandomizeNode>(), state);
|
||||
else
|
||||
throw StarException(strf("Unable to run composite node type with variant type index %s", node.typeIndex()));
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runSequence(SequenceNode const& node, NodeState& state) {
|
||||
if (state.isNothing())
|
||||
state.set(CompositeState(node.children.size()));
|
||||
|
||||
CompositeState& composite = state->get<CompositeState>();
|
||||
while (composite.index < node.children.size()) {
|
||||
auto child = node.children.get(composite.index);
|
||||
NodeStatus childStatus = runNode(*child, *composite.children[composite.index]);
|
||||
|
||||
if (childStatus == NodeStatus::Failure || childStatus == NodeStatus::Running)
|
||||
return childStatus;
|
||||
else
|
||||
composite.index++;
|
||||
}
|
||||
|
||||
return NodeStatus::Success;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runSelector(SelectorNode const& node, NodeState& state) {
|
||||
if (state.isNothing())
|
||||
state.set(CompositeState(node.children.size()));
|
||||
|
||||
CompositeState& composite = state->get<CompositeState>();
|
||||
while (composite.index < node.children.size()) {
|
||||
NodeStatus childStatus = runNode(*node.children[composite.index], *composite.children[composite.index]);
|
||||
|
||||
if (childStatus == NodeStatus::Success || childStatus == NodeStatus::Running)
|
||||
return childStatus;
|
||||
else
|
||||
composite.index++;
|
||||
}
|
||||
|
||||
return NodeStatus::Failure;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runParallel(ParallelNode const& node, NodeState& state) {
|
||||
if (state.isNothing())
|
||||
state.set(CompositeState(node.children.size()));
|
||||
|
||||
CompositeState& composite = state->get<CompositeState>();
|
||||
|
||||
int failed = 0;
|
||||
int succeeded = 0;
|
||||
for (size_t i = 0; i < node.children.size(); i++) {
|
||||
NodeStatus status = runNode(*node.children[i], *composite.children[i]);
|
||||
if (status == NodeStatus::Success)
|
||||
succeeded++;
|
||||
else if (status == NodeStatus::Failure)
|
||||
failed++;
|
||||
|
||||
if (succeeded >= node.succeed || failed >= node.fail) {
|
||||
return succeeded >= node.succeed ? NodeStatus::Success : NodeStatus::Failure;
|
||||
}
|
||||
}
|
||||
|
||||
return NodeStatus::Running;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runDynamic(DynamicNode const& node, NodeState& state) {
|
||||
if (state.isNothing())
|
||||
state.set(CompositeState(node.children.size()));
|
||||
|
||||
CompositeState& composite = state->get<CompositeState>();
|
||||
for (size_t i = 0; i <= composite.index; i++) {
|
||||
auto child = node.children.get(i);
|
||||
auto status = runNode(*child, *composite.children.get(i));
|
||||
if (status == NodeStatus::Failure && i == composite.index)
|
||||
composite.index++;
|
||||
|
||||
if (i < composite.index && (status == NodeStatus::Success || status == NodeStatus::Running)) {
|
||||
composite.children[composite.index]->reset();
|
||||
composite.index = i;
|
||||
}
|
||||
|
||||
if (status == NodeStatus::Success || composite.index >= node.children.size()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return NodeStatus::Running;
|
||||
}
|
||||
|
||||
NodeStatus BehaviorState::runRandomize(RandomizeNode const& node, NodeState& state) {
|
||||
if (state.isNothing())
|
||||
state.set(CompositeState(node.children.size(), Random::randUInt(node.children.size() - 1)));
|
||||
|
||||
CompositeState& composite = state->get<CompositeState>();
|
||||
auto child = node.children.get(composite.index);
|
||||
NodeStatus status = runNode(*child, *composite.children[composite.index]);
|
||||
return status;
|
||||
}
|
||||
|
||||
}
|
119
source/game/StarBehaviorState.hpp
Normal file
119
source/game/StarBehaviorState.hpp
Normal file
|
@ -0,0 +1,119 @@
|
|||
#ifndef STAR_BEHAVIOR_STATE_HPP
|
||||
#define STAR_BEHAVIOR_STATE_HPP
|
||||
|
||||
#include "StarBehaviorDatabase.hpp"
|
||||
#include "StarLua.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Blackboard);
|
||||
STAR_CLASS(BehaviorState);
|
||||
STAR_STRUCT(ActionState);
|
||||
STAR_STRUCT(DecoratorState);
|
||||
STAR_STRUCT(CompositeState);
|
||||
|
||||
STAR_EXCEPTION(BehaviorException, StarException);
|
||||
|
||||
extern List<NodeParameterType> BlackboardTypes;
|
||||
|
||||
class Blackboard {
|
||||
public:
|
||||
Blackboard(LuaTable luaContext);
|
||||
|
||||
LuaValue get(NodeParameterType type, String const& key) const;
|
||||
void set(NodeParameterType type, String const& key, LuaValue value);
|
||||
|
||||
LuaTable parameters(StringMap<NodeParameter> const& nodeParameters, uint64_t nodeId);
|
||||
void setOutput(ActionNode const& node, LuaTable const& output);
|
||||
|
||||
// takes the set of currently held ephemeral values
|
||||
Set<pair<NodeParameterType, String>> takeEphemerals();
|
||||
|
||||
// clears any provided ephemerals that are not currently held
|
||||
void clearEphemerals(Set<pair<NodeParameterType, String>> ephemerals);
|
||||
private:
|
||||
LuaTable m_luaContext;
|
||||
|
||||
HashMap<uint64_t, LuaTable> m_parameters;
|
||||
HashMap<NodeParameterType, StringMap<LuaValue>> m_board;
|
||||
|
||||
HashMap<NodeParameterType, StringMap<List<pair<uint64_t, String>>>> m_input;
|
||||
StringMap<List<pair<uint64_t, LuaTable>>> m_vectorNumberInput;
|
||||
|
||||
Set<pair<NodeParameterType, String>> m_ephemeral;
|
||||
};
|
||||
|
||||
typedef Maybe<Variant<ActionState,DecoratorState,CompositeState>> NodeState;
|
||||
typedef shared_ptr<NodeState> NodeStatePtr;
|
||||
|
||||
typedef pair<LuaFunction, LuaThread> Coroutine;
|
||||
|
||||
enum class NodeStatus {
|
||||
Invalid,
|
||||
Success,
|
||||
Failure,
|
||||
Running
|
||||
};
|
||||
|
||||
typedef LuaTupleReturn<NodeStatus, LuaValue> ActionReturn;
|
||||
struct ActionState {
|
||||
LuaThread thread;
|
||||
};
|
||||
|
||||
struct DecoratorState {
|
||||
DecoratorState(LuaThread thread);
|
||||
LuaThread thread;
|
||||
NodeStatePtr child;
|
||||
};
|
||||
|
||||
struct CompositeState {
|
||||
CompositeState(size_t children);
|
||||
CompositeState(size_t children, size_t index);
|
||||
|
||||
size_t index;
|
||||
List<NodeStatePtr> children;
|
||||
};
|
||||
|
||||
class BehaviorState {
|
||||
public:
|
||||
BehaviorState(BehaviorTreeConstPtr tree, LuaTable context, Maybe<BlackboardWeakPtr> blackboard = {});
|
||||
|
||||
NodeStatus run(float dt);
|
||||
void clear();
|
||||
|
||||
BlackboardWeakPtr blackboardPtr();
|
||||
private:
|
||||
BlackboardPtr board();
|
||||
|
||||
LuaThread nodeLuaThread(String const& funcName);
|
||||
|
||||
NodeStatus runNode(BehaviorNode const& node, NodeState& state);
|
||||
|
||||
NodeStatus runAction(ActionNode const& node, NodeState& state);
|
||||
NodeStatus runDecorator(DecoratorNode const& node, NodeState& state);
|
||||
|
||||
NodeStatus runComposite(CompositeNode const& node, NodeState& state);
|
||||
NodeStatus runSequence(SequenceNode const& node, NodeState& state);
|
||||
NodeStatus runSelector(SelectorNode const& node, NodeState& state);
|
||||
NodeStatus runParallel(ParallelNode const& node, NodeState& state);
|
||||
NodeStatus runDynamic(DynamicNode const& node, NodeState& state);
|
||||
NodeStatus runRandomize(RandomizeNode const& node, NodeState& state);
|
||||
|
||||
BehaviorTreeConstPtr m_tree;
|
||||
NodeState m_rootState;
|
||||
|
||||
LuaTable m_luaContext;
|
||||
|
||||
// The blackboard can either be created and owned by this behavior,
|
||||
// or a blackboard from another behavior can be used
|
||||
Variant<BlackboardPtr, BlackboardWeakPtr> m_board;
|
||||
|
||||
// Keep threads here for recycling
|
||||
List<LuaThread> m_threads;
|
||||
StringMap<LuaFunction> m_functions;
|
||||
|
||||
float m_lastDt;
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
94
source/game/StarBiome.cpp
Normal file
94
source/game/StarBiome.cpp
Normal file
|
@ -0,0 +1,94 @@
|
|||
#include "StarBiome.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarParallax.hpp"
|
||||
#include "StarAmbient.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
BiomePlaceables::BiomePlaceables() {
|
||||
grassMod = NoModId;
|
||||
grassModDensity = 0.0f;
|
||||
ceilingGrassMod = NoModId;
|
||||
ceilingGrassModDensity = 0.0f;
|
||||
}
|
||||
|
||||
BiomePlaceables::BiomePlaceables(Json const& variant) {
|
||||
grassMod = variant.getInt("grassMod");
|
||||
grassModDensity = variant.getFloat("grassModDensity");
|
||||
ceilingGrassMod = variant.getInt("ceilingGrassMod");
|
||||
ceilingGrassModDensity = variant.getFloat("ceilingGrassModDensity");
|
||||
itemDistributions = variant.getArray("itemDistributions").transformed(construct<BiomeItemDistribution>());
|
||||
}
|
||||
|
||||
Json BiomePlaceables::toJson() const {
|
||||
return JsonObject{
|
||||
{"grassMod", grassMod},
|
||||
{"grassModDensity", grassModDensity},
|
||||
{"ceilingGrassMod", ceilingGrassMod},
|
||||
{"ceilingGrassModDensity", ceilingGrassModDensity},
|
||||
{"itemDistributions", itemDistributions.transformed(mem_fn(&BiomeItemDistribution::toJson))}
|
||||
};
|
||||
}
|
||||
|
||||
Maybe<TreeVariant> BiomePlaceables::firstTreeType() const {
|
||||
for (auto const& itemDistribution : itemDistributions) {
|
||||
for (auto const& biomeItem : itemDistribution.allItems()) {
|
||||
if (biomeItem.is<TreePair>())
|
||||
return biomeItem.get<TreePair>().first;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Biome::Biome() {
|
||||
mainBlock = EmptyMaterialId;
|
||||
hueShift = 0.0f;
|
||||
materialHueShift = MaterialHue();
|
||||
}
|
||||
|
||||
Biome::Biome(Json const& store) : Biome() {
|
||||
baseName = store.getString("baseName");
|
||||
description = store.getString("description");
|
||||
|
||||
mainBlock = store.getUInt("mainBlock");
|
||||
subBlocks = store.getArray("subBlocks").transformed([](Json const& v) -> MaterialId { return v.toUInt(); });
|
||||
ores =
|
||||
store.getArray("ores").transformed([](Json const& v) { return pair<ModId, float>(v.getUInt(0), v.getFloat(1)); });
|
||||
hueShift = store.getFloat("hueShift");
|
||||
materialHueShift = store.getUInt("materialHueShift");
|
||||
|
||||
surfacePlaceables = BiomePlaceables(store.get("surfacePlaceables"));
|
||||
undergroundPlaceables = BiomePlaceables(store.get("undergroundPlaceables"));
|
||||
|
||||
if (auto config = store.opt("spawnProfile"))
|
||||
spawnProfile = SpawnProfile(*config);
|
||||
|
||||
if (auto config = store.opt("parallax"))
|
||||
parallax = make_shared<Parallax>(*config);
|
||||
|
||||
if (auto config = store.opt("ambientNoises"))
|
||||
ambientNoises = make_shared<AmbientNoisesDescription>(*config);
|
||||
if (auto config = store.opt("musicTrack"))
|
||||
musicTrack = make_shared<AmbientNoisesDescription>(*config);
|
||||
}
|
||||
|
||||
Json Biome::toJson() const {
|
||||
return JsonObject{{"baseName", baseName},
|
||||
{"description", description},
|
||||
{"mainBlock", mainBlock},
|
||||
{"subBlocks", subBlocks.transformed(construct<Json>())},
|
||||
{"ores",
|
||||
ores.transformed([](pair<ModId, float> const& p) -> Json {
|
||||
return JsonArray{p.first, p.second};
|
||||
})},
|
||||
{"hueShift", hueShift},
|
||||
{"materialHueShift", materialHueShift},
|
||||
{"surfacePlaceables", surfacePlaceables.toJson()},
|
||||
{"undergroundPlaceables", undergroundPlaceables.toJson()},
|
||||
{"spawnProfile", spawnProfile.toJson()},
|
||||
{"parallax", parallax ? parallax->store() : Json()},
|
||||
{"ambientNoises", ambientNoises ? ambientNoises->toJson() : Json()},
|
||||
{"musicTrack", musicTrack ? musicTrack->toJson() : Json()}};
|
||||
}
|
||||
|
||||
}
|
62
source/game/StarBiome.hpp
Normal file
62
source/game/StarBiome.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#ifndef STAR_BIOME_HPP
|
||||
#define STAR_BIOME_HPP
|
||||
|
||||
#include "StarBiomePlacement.hpp"
|
||||
#include "StarSpawner.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_STRUCT(AmbientNoisesDescription);
|
||||
STAR_CLASS(Parallax);
|
||||
STAR_STRUCT(BiomePlaceables);
|
||||
STAR_STRUCT(Biome);
|
||||
|
||||
struct BiomePlaceables {
|
||||
BiomePlaceables();
|
||||
explicit BiomePlaceables(Json const& json);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
// If any of the item distributions contain trees, this returns the first
|
||||
// tree type.
|
||||
Maybe<TreeVariant> firstTreeType() const;
|
||||
|
||||
ModId grassMod;
|
||||
float grassModDensity;
|
||||
ModId ceilingGrassMod;
|
||||
float ceilingGrassModDensity;
|
||||
|
||||
List<BiomeItemDistribution> itemDistributions;
|
||||
};
|
||||
|
||||
struct Biome {
|
||||
Biome();
|
||||
explicit Biome(Json const& store);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
String baseName;
|
||||
String description;
|
||||
|
||||
MaterialId mainBlock;
|
||||
List<MaterialId> subBlocks;
|
||||
// Pairs the ore type with the commonality multiplier.
|
||||
List<pair<ModId, float>> ores;
|
||||
|
||||
float hueShift;
|
||||
MaterialHue materialHueShift;
|
||||
|
||||
BiomePlaceables surfacePlaceables;
|
||||
BiomePlaceables undergroundPlaceables;
|
||||
|
||||
SpawnProfile spawnProfile;
|
||||
|
||||
ParallaxPtr parallax;
|
||||
|
||||
AmbientNoisesDescriptionPtr ambientNoises;
|
||||
AmbientNoisesDescriptionPtr musicTrack;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
233
source/game/StarBiomeDatabase.cpp
Normal file
233
source/game/StarBiomeDatabase.cpp
Normal file
|
@ -0,0 +1,233 @@
|
|||
#include "StarBiomeDatabase.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarStoredFunctions.hpp"
|
||||
#include "StarParallax.hpp"
|
||||
#include "StarAmbient.hpp"
|
||||
#include "StarMaterialDatabase.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarSpawnTypeDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
BiomeDatabase::BiomeDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
// 'type' here is the extension of the file, and determines the selector type
|
||||
auto scanFiles = [=](String const& type, ConfigMap& map) {
|
||||
auto files = assets->scanExtension(type);
|
||||
assets->queueJsons(files);
|
||||
for (auto path : files) {
|
||||
auto parameters = assets->json(path);
|
||||
if (parameters.isNull())
|
||||
continue;
|
||||
|
||||
auto name = parameters.getString("name");
|
||||
if (map.contains(name))
|
||||
throw BiomeException(strf("Duplicate %s generator name '%s'", type, name));
|
||||
map[name] = {path, name, parameters};
|
||||
}
|
||||
};
|
||||
|
||||
scanFiles("biome", m_biomes);
|
||||
scanFiles("weather", m_weathers);
|
||||
}
|
||||
|
||||
StringList BiomeDatabase::biomeNames() const {
|
||||
return m_biomes.keys();
|
||||
}
|
||||
|
||||
float BiomeDatabase::biomeHueShift(String const& biomeName, uint64_t seed) const {
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
return pickHueShiftFromJson(config.parameters.get("hueShiftOptions", {}), seed, "BiomeHueShift");
|
||||
}
|
||||
|
||||
WeatherPool BiomeDatabase::biomeWeathers(String const& biomeName, uint64_t seed, float threatLevel) const {
|
||||
WeatherPool weatherPool;
|
||||
if (auto weatherList = binnedChoiceFromJson(m_biomes.get(biomeName).parameters.get("weather", JsonArray{}), threatLevel).optArray()) {
|
||||
auto weatherPoolPath = staticRandomFrom(*weatherList, seed, "WeatherPool");
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
auto weatherPoolConfig = assets->fetchJson(weatherPoolPath);
|
||||
weatherPool = jsonToWeightedPool<String>(weatherPoolConfig);
|
||||
}
|
||||
|
||||
return weatherPool;
|
||||
}
|
||||
|
||||
bool BiomeDatabase::biomeIsAirless(String const& biomeName) const {
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
return config.parameters.getBool("airless", false);
|
||||
}
|
||||
|
||||
SkyColoring BiomeDatabase::biomeSkyColoring(String const& biomeName, uint64_t seed) const {
|
||||
SkyColoring skyColoring;
|
||||
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
if (auto skyOptions = config.parameters.optArray("skyOptions")) {
|
||||
auto option = staticRandomFrom(*skyOptions, seed, "BiomeSkyOption");
|
||||
|
||||
skyColoring.mainColor = jsonToColor(option.get("mainColor"));
|
||||
|
||||
skyColoring.morningColors.first = jsonToColor(option.query("morningColors[0]"));
|
||||
skyColoring.morningColors.second = jsonToColor(option.query("morningColors[1]"));
|
||||
|
||||
skyColoring.dayColors.first = jsonToColor(option.query("dayColors[0]"));
|
||||
skyColoring.dayColors.second = jsonToColor(option.query("dayColors[1]"));
|
||||
|
||||
skyColoring.eveningColors.first = jsonToColor(option.query("eveningColors[0]"));
|
||||
skyColoring.eveningColors.second = jsonToColor(option.query("eveningColors[1]"));
|
||||
|
||||
skyColoring.nightColors.first = jsonToColor(option.query("nightColors[0]"));
|
||||
skyColoring.nightColors.second = jsonToColor(option.query("nightColors[1]"));
|
||||
|
||||
skyColoring.morningLightColor = jsonToColor(option.get("morningLightColor"));
|
||||
skyColoring.dayLightColor = jsonToColor(option.get("dayLightColor"));
|
||||
skyColoring.eveningLightColor = jsonToColor(option.get("eveningLightColor"));
|
||||
skyColoring.nightLightColor = jsonToColor(option.get("nightLightColor"));
|
||||
}
|
||||
|
||||
return skyColoring;
|
||||
}
|
||||
|
||||
String BiomeDatabase::biomeFriendlyName(String const& biomeName) const {
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
return config.parameters.getString("friendlyName");
|
||||
}
|
||||
|
||||
StringList BiomeDatabase::biomeStatusEffects(String const& biomeName) const {
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
return config.parameters.opt("statusEffects").apply(jsonToStringList).value();
|
||||
}
|
||||
|
||||
StringList BiomeDatabase::biomeOres(String const& biomeName, float threatLevel) const {
|
||||
StringList res;
|
||||
|
||||
auto const& config = m_biomes.get(biomeName);
|
||||
auto oreDistribution = config.parameters.get("ores", {});
|
||||
if (!oreDistribution.isNull()) {
|
||||
auto& root = Root::singleton();
|
||||
auto functionDatabase = root.functionDatabase();
|
||||
|
||||
auto oresList = functionDatabase->configFunction(oreDistribution)->get(threatLevel);
|
||||
for (Json v : oresList.iterateArray()) {
|
||||
if (v.getFloat(1) > 0)
|
||||
res.append(v.getString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
StringList BiomeDatabase::weatherNames() const {
|
||||
return m_weathers.keys();
|
||||
}
|
||||
|
||||
WeatherType BiomeDatabase::weatherType(String const& name) const {
|
||||
if (!m_weathers.contains(name))
|
||||
throw BiomeException(strf("No such weather type '%s'", name));
|
||||
|
||||
auto config = m_weathers.get(name);
|
||||
|
||||
try {
|
||||
return WeatherType(config.parameters, config.path);
|
||||
} catch (MapException const& e) {
|
||||
throw BiomeException(strf("Required key not found in weather config %s", config.path), e);
|
||||
}
|
||||
}
|
||||
|
||||
BiomePtr BiomeDatabase::createBiome(String const& biomeName, uint64_t seed, float verticalMidPoint, float threatLevel) const {
|
||||
if (!m_biomes.contains(biomeName))
|
||||
throw BiomeException(strf("No such biome '%s'", biomeName));
|
||||
|
||||
auto& root = Root::singleton();
|
||||
auto materialDatabase = root.materialDatabase();
|
||||
|
||||
try {
|
||||
RandomSource random(seed);
|
||||
auto config = m_biomes.get(biomeName);
|
||||
|
||||
auto biome = make_shared<Biome>();
|
||||
float mainHueShift = biomeHueShift(biomeName, seed);
|
||||
|
||||
biome->baseName = biomeName;
|
||||
biome->description = config.parameters.getString("description", "");
|
||||
|
||||
if (config.parameters.contains("mainBlock"))
|
||||
biome->mainBlock = materialDatabase->materialId(config.parameters.getString("mainBlock"));
|
||||
|
||||
for (Json v : config.parameters.getArray("subBlocks", {}))
|
||||
biome->subBlocks.append(materialDatabase->materialId(v.toString()));
|
||||
|
||||
biome->ores = readOres(config.parameters.get("ores", {}), threatLevel);
|
||||
|
||||
biome->surfacePlaceables = readBiomePlaceables(config.parameters.getObject("surfacePlaceables", {}), random.randu64(), mainHueShift);
|
||||
biome->undergroundPlaceables = readBiomePlaceables(config.parameters.getObject("undergroundPlaceables", {}), random.randu64(), mainHueShift);
|
||||
|
||||
biome->hueShift = mainHueShift;
|
||||
biome->materialHueShift = materialHueFromDegrees(biome->hueShift);
|
||||
|
||||
if (config.parameters.contains("parallax")) {
|
||||
auto parallaxFile = AssetPath::relativeTo(config.path, config.parameters.getString("parallax"));
|
||||
biome->parallax = make_shared<Parallax>(parallaxFile, seed, verticalMidPoint, mainHueShift, biome->surfacePlaceables.firstTreeType());
|
||||
}
|
||||
|
||||
if (config.parameters.contains("musicTrack"))
|
||||
biome->musicTrack = make_shared<AmbientNoisesDescription>(config.parameters.getObject("musicTrack"), config.path);
|
||||
|
||||
if (config.parameters.contains("ambientNoises"))
|
||||
biome->ambientNoises = make_shared<AmbientNoisesDescription>(config.parameters.getObject("ambientNoises"), config.path);
|
||||
|
||||
if (config.parameters.contains("spawnProfile"))
|
||||
biome->spawnProfile = constructSpawnProfile(config.parameters.getObject("spawnProfile"), seed);
|
||||
|
||||
return biome;
|
||||
} catch (std::exception const& cause) {
|
||||
throw BiomeException(strf("Failed to parse biome: '%s'", biomeName), cause);
|
||||
}
|
||||
}
|
||||
|
||||
float BiomeDatabase::pickHueShiftFromJson(Json source, uint64_t seed, String const& key) {
|
||||
if (source.isNull())
|
||||
return 0;
|
||||
auto options = jsonToFloatList(source);
|
||||
if (options.size() == 0)
|
||||
return 0;
|
||||
auto t = staticRandomU32(seed, key);
|
||||
return options.at(t % options.size());
|
||||
}
|
||||
|
||||
BiomePlaceables BiomeDatabase::readBiomePlaceables(Json const& config, uint64_t seed, float biomeHueShift) const {
|
||||
auto& root = Root::singleton();
|
||||
RandomSource rand(seed);
|
||||
BiomePlaceables placeables;
|
||||
if (config.contains("grassMod") && !config.getArray("grassMod").empty())
|
||||
placeables.grassMod = root.materialDatabase()->modId(rand.randFrom(config.getArray("grassMod")).toString());
|
||||
placeables.grassModDensity = config.getFloat("grassModDensity", 0);
|
||||
if (config.contains("ceilingGrassMod") && !config.getArray("ceilingGrassMod").empty())
|
||||
placeables.ceilingGrassMod = root.materialDatabase()->modId(rand.randFrom(config.getArray("ceilingGrassMod")).toString());
|
||||
placeables.ceilingGrassModDensity = config.getFloat("ceilingGrassModDensity", 0);
|
||||
|
||||
for (auto const& itemConfig : config.getArray("items", {}))
|
||||
placeables.itemDistributions.append(BiomeItemDistribution(itemConfig, rand.randu64(), biomeHueShift));
|
||||
|
||||
return placeables;
|
||||
}
|
||||
|
||||
List<pair<ModId, float>> BiomeDatabase::readOres(Json const& oreDistribution, float threatLevel) const {
|
||||
List<pair<ModId, float>> ores;
|
||||
if (!oreDistribution.isNull()) {
|
||||
auto& root = Root::singleton();
|
||||
auto functionDatabase = root.functionDatabase();
|
||||
auto materialDatabase = root.materialDatabase();
|
||||
|
||||
auto oresList = functionDatabase->configFunction(oreDistribution)->get(threatLevel);
|
||||
for (Json v : oresList.iterateArray()) {
|
||||
if (v.getFloat(1) > 0)
|
||||
ores.append({materialDatabase->modId(v.getString(0)), v.getFloat(1)});
|
||||
}
|
||||
}
|
||||
return ores;
|
||||
}
|
||||
|
||||
}
|
50
source/game/StarBiomeDatabase.hpp
Normal file
50
source/game/StarBiomeDatabase.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef STAR_BIOME_DATABASE_HPP
|
||||
#define STAR_BIOME_DATABASE_HPP
|
||||
|
||||
#include "StarBiome.hpp"
|
||||
#include "StarWeatherTypes.hpp"
|
||||
#include "StarSkyTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(BiomeDatabase);
|
||||
|
||||
class BiomeDatabase {
|
||||
public:
|
||||
BiomeDatabase();
|
||||
|
||||
StringList biomeNames() const;
|
||||
|
||||
float biomeHueShift(String const& biomeName, uint64_t seed) const;
|
||||
WeatherPool biomeWeathers(String const& biomeName, uint64_t seed, float threatLevel) const;
|
||||
bool biomeIsAirless(String const& biomeName) const;
|
||||
SkyColoring biomeSkyColoring(String const& biomeName, uint64_t seed) const;
|
||||
String biomeFriendlyName(String const& biomeName) const;
|
||||
StringList biomeStatusEffects(String const& biomeName) const;
|
||||
StringList biomeOres(String const& biomeName, float threatLevel) const;
|
||||
|
||||
StringList weatherNames() const;
|
||||
WeatherType weatherType(String const& weatherName) const;
|
||||
|
||||
BiomePtr createBiome(String const& biomeName, uint64_t seed, float verticalMidPoint, float threatLevel) const;
|
||||
|
||||
private:
|
||||
struct Config {
|
||||
String path;
|
||||
String name;
|
||||
Json parameters;
|
||||
};
|
||||
typedef StringMap<Config> ConfigMap;
|
||||
|
||||
static float pickHueShiftFromJson(Json source, uint64_t seed, String const& key);
|
||||
|
||||
BiomePlaceables readBiomePlaceables(Json const& config, uint64_t seed, float biomeHueShift) const;
|
||||
List<pair<ModId, float>> readOres(Json const& oreDistribution, float threatLevel) const;
|
||||
|
||||
ConfigMap m_biomes;
|
||||
ConfigMap m_weathers;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
298
source/game/StarBiomePlacement.cpp
Normal file
298
source/game/StarBiomePlacement.cpp
Normal file
|
@ -0,0 +1,298 @@
|
|||
#include "StarBiomePlacement.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
BiomeItem variantToBiomeItem(Json const& store) {
|
||||
auto type = store.get(0);
|
||||
if (type == "grass") {
|
||||
return GrassVariant(store.get(1));
|
||||
} else if (type == "bush") {
|
||||
return BushVariant(store.get(1));
|
||||
} else if (type == "treePair") {
|
||||
return TreePair(TreeVariant(store.get(1).get(0)), TreeVariant(store.get(1).get(1)));
|
||||
} else if (type == "objectPool") {
|
||||
return ObjectPool(store.getArray(1).transformed([](Json const& pair) {
|
||||
return make_pair(pair.getFloat(0), make_pair(pair.get(1).getString(0), pair.get(1).get(1)));
|
||||
}));
|
||||
} else if (type == "treasureBoxSet") {
|
||||
return TreasureBoxSet(store.getString(1));
|
||||
} else if (type == "microDungeon") {
|
||||
return MicroDungeonNames(jsonToStringSet(store.get(1)));
|
||||
} else {
|
||||
throw BiomeException(strf("Unrecognized biome item type '%s'", type));
|
||||
}
|
||||
}
|
||||
|
||||
Json variantFromBiomeItem(BiomeItem const& biomeItem) {
|
||||
if (auto grassVariant = biomeItem.ptr<GrassVariant>()) {
|
||||
return JsonArray{"grass", grassVariant->toJson()};
|
||||
} else if (auto bushVariant = biomeItem.ptr<BushVariant>()) {
|
||||
return JsonArray{"bush", bushVariant->toJson()};
|
||||
} else if (auto treePair = biomeItem.ptr<TreePair>()) {
|
||||
return JsonArray{"treePair", JsonArray{treePair->first.toJson(), treePair->second.toJson()}};
|
||||
} else if (auto objectPool = biomeItem.ptr<ObjectPool>()) {
|
||||
return JsonArray{"objectPool", transform<JsonArray>(objectPool->items(), [](pair<double, pair<String, Json>> const& p) {
|
||||
return JsonArray{p.first, JsonArray{p.second.first, p.second.second}};
|
||||
})};
|
||||
} else if (auto treasureBoxSet = biomeItem.ptr<TreasureBoxSet>()) {
|
||||
return JsonArray{"treasureBoxSet", String(*treasureBoxSet)};
|
||||
} else if (auto microDungeonNames = biomeItem.ptr<MicroDungeonNames>()) {
|
||||
return JsonArray{"microDungeon", jsonFromStringSet(*microDungeonNames)};
|
||||
} else {
|
||||
throw BiomeException(strf("Unrecognized biome item type"));
|
||||
}
|
||||
}
|
||||
|
||||
EnumMap<BiomePlacementMode> const BiomePlacementModeNames{
|
||||
{BiomePlacementMode::Floor, "floor"},
|
||||
{BiomePlacementMode::Ceiling, "ceiling"},
|
||||
{BiomePlacementMode::Background, "background"},
|
||||
{BiomePlacementMode::Ocean, "ocean"}
|
||||
};
|
||||
|
||||
BiomeItemPlacement::BiomeItemPlacement(BiomeItem item, Vec2I position, float priority)
|
||||
: item(move(item)), position(position), priority(priority) {}
|
||||
|
||||
bool BiomeItemPlacement::operator<(BiomeItemPlacement const& rhs) const {
|
||||
return priority < rhs.priority;
|
||||
}
|
||||
|
||||
Maybe<BiomeItem> BiomeItemDistribution::createItem(Json const& config, RandomSource& rand, float biomeHueShift) {
|
||||
auto& root = Root::singleton();
|
||||
|
||||
auto type = config.getString("type");
|
||||
if (type.equalsIgnoreCase("grass")) {
|
||||
auto grassList = jsonToStringList(config.get("grasses"));
|
||||
return BiomeItem{root.plantDatabase()->buildGrassVariant(rand.randFrom(grassList), biomeHueShift)};
|
||||
|
||||
} else if (type.equalsIgnoreCase("bush")) {
|
||||
auto bushList = config.getArray("bushes", {});
|
||||
auto bushSettings = rand.randValueFrom(bushList);
|
||||
|
||||
auto bushName = bushSettings.getString("name");
|
||||
auto bushMod = rand.randValueFrom(root.plantDatabase()->bushMods(bushName));
|
||||
float bushBaseHueShift = rand.randf(-1.0f, 1.0f) * bushSettings.getFloat("baseHueShiftMax");
|
||||
float bushModHueShift = rand.randf(-1.0f, 1.0f) * bushSettings.getFloat("modHueShiftMax");
|
||||
|
||||
return BiomeItem{root.plantDatabase()->buildBushVariant(bushName, bushBaseHueShift, bushMod, bushModHueShift)};
|
||||
|
||||
} else if (type.equalsIgnoreCase("tree")) {
|
||||
auto stemList = jsonToStringList(config.get("treeStemList", JsonArray()));
|
||||
auto foliageList = jsonToStringList(config.get("treeFoliageList", JsonArray()));
|
||||
|
||||
// Find matching pairs of stem / foliage (that have the same shape)
|
||||
List<pair<String, String>> matchingPairs;
|
||||
for (auto stem : stemList) {
|
||||
for (auto foliage : foliageList) {
|
||||
if (foliage.empty() || root.plantDatabase()->treeStemShape(stem) == root.plantDatabase()->treeFoliageShape(foliage))
|
||||
matchingPairs.append({stem, foliage});
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingPairs.empty() && !stemList.empty() && !foliageList.empty())
|
||||
Logger::warn("Specified stemList and foliageList, but no matching pairs found.");
|
||||
|
||||
auto chosenPair = rand.randValueFrom(matchingPairs);
|
||||
float treeStemHueShift = rand.randf(-1.0f, 1.0f) * config.getFloat("treeStemHueShiftMax", 0);
|
||||
float treeFoliageHueShift = rand.randf(-1.0f, 1.0f) * config.getFloat("treeFoliageHueShiftMax", 0);
|
||||
float treeAltFoliageHueShift = rand.randf(-1.0f, 1.0f) * config.getFloat("treeFoliageHueShiftMax", 0);
|
||||
|
||||
if (!chosenPair.first.empty()) {
|
||||
TreeVariant primaryTree;
|
||||
TreeVariant altTree;
|
||||
if (chosenPair.second.empty()) {
|
||||
// Foliage-less trees
|
||||
primaryTree = root.plantDatabase()->buildTreeVariant(chosenPair.first, treeStemHueShift);
|
||||
altTree = root.plantDatabase()->buildTreeVariant(chosenPair.first, treeStemHueShift);
|
||||
} else {
|
||||
primaryTree = root.plantDatabase()->buildTreeVariant(
|
||||
chosenPair.first, treeStemHueShift, chosenPair.second, treeFoliageHueShift);
|
||||
altTree = root.plantDatabase()->buildTreeVariant(
|
||||
chosenPair.first, treeStemHueShift, chosenPair.second, treeAltFoliageHueShift);
|
||||
}
|
||||
return BiomeItem{TreePair{primaryTree, altTree}};
|
||||
}
|
||||
|
||||
} else if (type.equalsIgnoreCase("object")) {
|
||||
Json objectPoolConfig = rand.randValueFrom(config.getArray("objectSets"));
|
||||
ObjectPool objectPool;
|
||||
|
||||
Json objectParameters = objectPoolConfig.get("parameters", JsonObject());
|
||||
for (auto const& pair : objectPoolConfig.getArray("pool")) {
|
||||
if (pair.size() != 2)
|
||||
throw BiomeException("Wrong size for objects weight / list pair in biome items");
|
||||
|
||||
objectPool.add(pair.getFloat(0), {pair.getString(1), objectParameters});
|
||||
}
|
||||
|
||||
return BiomeItem{objectPool};
|
||||
|
||||
} else if (type.equalsIgnoreCase("treasureBox")) {
|
||||
return BiomeItem{TreasureBoxSet(rand.randValueFrom(config.getArray("treasureBoxSets")).toString())};
|
||||
|
||||
} else if (type.equalsIgnoreCase("microdungeon")) {
|
||||
return BiomeItem{MicroDungeonNames(jsonToStringSet(config.get("microdungeons", JsonArray())))};
|
||||
|
||||
} else {
|
||||
throw BiomeException(strf("No such item type '%s' in item distribution", type));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
BiomeItemDistribution::BiomeItemDistribution() {
|
||||
m_mode = BiomePlacementMode::Floor;
|
||||
m_distribution = DistributionType::Random;
|
||||
m_modulus = 1;
|
||||
m_modulusOffset = 0;
|
||||
m_blockSeed = 0;
|
||||
m_blockProbability = 0.0f;
|
||||
m_priority = 0.0f;
|
||||
}
|
||||
|
||||
BiomeItemDistribution::BiomeItemDistribution(Json const& config, uint64_t seed, float biomeHueShift) {
|
||||
RandomSource rand(seed);
|
||||
|
||||
m_mode = BiomePlacementModeNames.getLeft(config.getString("mode", "floor"));
|
||||
|
||||
m_priority = config.getFloat("priority", 0.0f);
|
||||
int variants = config.getInt("variants", 1);
|
||||
|
||||
m_modulus = 1;
|
||||
m_modulusOffset = 0;
|
||||
m_blockSeed = 0;
|
||||
m_blockProbability = 0.0f;
|
||||
|
||||
// If distribution settings are string type, it should point to another asset
|
||||
// variant.
|
||||
auto distributionSettings = config.get("distribution", JsonObject());
|
||||
if (distributionSettings.type() == Json::Type::String) {
|
||||
auto assets = Root::singleton().assets();
|
||||
distributionSettings = assets->json(distributionSettings.toString());
|
||||
}
|
||||
|
||||
m_distribution = DistributionTypeNames.getLeft(distributionSettings.getString("type"));
|
||||
if (m_distribution == DistributionType::Random) {
|
||||
m_blockProbability = distributionSettings.getFloat("blockProbability");
|
||||
m_blockSeed = rand.randu64();
|
||||
for (int i = 0; i < variants; ++i) {
|
||||
if (auto item = createItem(config, rand, biomeHueShift))
|
||||
m_randomItems.append(item.take());
|
||||
}
|
||||
|
||||
} else if (m_distribution == DistributionType::Periodic) {
|
||||
unsigned octaves = distributionSettings.getInt("octaves", 1);
|
||||
float alpha = distributionSettings.getFloat("alpha", 2.0);
|
||||
float beta = distributionSettings.getFloat("beta", 2.0);
|
||||
|
||||
float modulusVariance = distributionSettings.getFloat("modulusVariance", 0.0);
|
||||
|
||||
// If density period / offset are not set, just offset a lot to get an even
|
||||
// distribution with no free spaces.
|
||||
float densityPeriod = distributionSettings.getFloat("densityPeriod", 10);
|
||||
float densityOffset = distributionSettings.getFloat("densityOffset", 2.0);
|
||||
|
||||
float typePeriod = distributionSettings.getFloat("typePeriod", 10);
|
||||
|
||||
m_modulus = distributionSettings.getInt("modulus", 1);
|
||||
m_modulusOffset = rand.randInt(-m_modulus, m_modulus);
|
||||
m_densityFunction = PerlinF(octaves, 1.0f / densityPeriod, 1.0, densityOffset, alpha, beta, rand.randu64());
|
||||
m_modulusDistortion = PerlinF(octaves, 1.0f / m_modulus, modulusVariance, modulusVariance * 2, alpha, beta, rand.randu64());
|
||||
|
||||
for (int i = 0; i < variants; ++i) {
|
||||
if (auto item = createItem(config, rand, biomeHueShift)) {
|
||||
PerlinF weight(octaves, 1.0f / typePeriod, 1.0, 0.0, alpha, beta, rand.randu64());
|
||||
m_weightedItems.append({item.take(), weight});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BiomeItemDistribution::BiomeItemDistribution(Json const& store) {
|
||||
m_mode = BiomePlacementModeNames.getLeft(store.getString("mode"));
|
||||
m_distribution = DistributionTypeNames.getLeft(store.getString("distribution"));
|
||||
m_priority = store.getFloat("priority");
|
||||
m_blockProbability = store.getFloat("blockProbability");
|
||||
m_blockSeed = store.getUInt("blockSeed");
|
||||
m_randomItems = store.getArray("randomItems").transformed(variantToBiomeItem);
|
||||
m_densityFunction = PerlinF(store.get("densityFunction"));
|
||||
m_modulusDistortion = PerlinF(store.get("modulusDistortion"));
|
||||
m_modulus = store.getInt("modulus");
|
||||
m_modulusOffset = store.getInt("modulusOffset");
|
||||
m_weightedItems = store.getArray("weightedItems") .transformed([](Json const& v) {
|
||||
return make_pair(variantToBiomeItem(v.get(0)), PerlinF(v.get(1)));
|
||||
});
|
||||
}
|
||||
|
||||
Json BiomeItemDistribution::toJson() const {
|
||||
return JsonObject{
|
||||
{"mode", BiomePlacementModeNames.getRight(m_mode)},
|
||||
{"distribution", DistributionTypeNames.getRight(m_distribution)},
|
||||
{"priority", m_priority},
|
||||
{"blockProbability", m_blockProbability},
|
||||
{"blockSeed", m_blockSeed},
|
||||
{"randomItems", m_randomItems.transformed(variantFromBiomeItem)},
|
||||
{"densityFunction", m_densityFunction.toJson()},
|
||||
{"modulusDistortion", m_modulusDistortion.toJson()},
|
||||
{"modulus", m_modulus},
|
||||
{"modulusOffset", m_modulusOffset},
|
||||
{"weightedItems", m_weightedItems.transformed([](pair<BiomeItem, PerlinF> const& p) -> Json {
|
||||
return JsonArray{variantFromBiomeItem(p.first), p.second.toJson()};
|
||||
})},
|
||||
};
|
||||
}
|
||||
|
||||
BiomePlacementMode BiomeItemDistribution::mode() const {
|
||||
return m_mode;
|
||||
}
|
||||
|
||||
List<BiomeItem> BiomeItemDistribution::allItems() const {
|
||||
if (m_distribution == DistributionType::Random) {
|
||||
return m_randomItems;
|
||||
} else if (m_distribution == DistributionType::Periodic) {
|
||||
List<BiomeItem> items;
|
||||
for (auto const& pair : m_weightedItems)
|
||||
items.append(pair.first);
|
||||
return items;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Maybe<BiomeItemPlacement> BiomeItemDistribution::itemToPlace(int x, int y) const {
|
||||
if (m_distribution == DistributionType::Random) {
|
||||
if (staticRandomFloat(x, y, m_blockSeed) <= m_blockProbability)
|
||||
return BiomeItemPlacement{staticRandomValueFrom(m_randomItems, x, y, m_blockSeed), Vec2I(x, y), m_priority};
|
||||
|
||||
} else if (m_distribution == DistributionType::Periodic) {
|
||||
BiomeItem const* biomeItem = nullptr;
|
||||
if (m_densityFunction.get(x, y) > 0) {
|
||||
if ((int)(x + m_modulusOffset + m_modulusDistortion.get(x, y)) % m_modulus == 0) {
|
||||
float maxWeight = lowest<float>();
|
||||
for (auto const& weightedItem : m_weightedItems) {
|
||||
float weight = weightedItem.second.get(x, y);
|
||||
if (weight > maxWeight) {
|
||||
maxWeight = weight;
|
||||
biomeItem = &weightedItem.first;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (biomeItem)
|
||||
return BiomeItemPlacement{*biomeItem, Vec2I(x, y), m_priority};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
EnumMap<BiomeItemDistribution::DistributionType> const BiomeItemDistribution::DistributionTypeNames{
|
||||
{DistributionType::Random, "random"},
|
||||
{DistributionType::Periodic, "periodic"}
|
||||
};
|
||||
|
||||
}
|
102
source/game/StarBiomePlacement.hpp
Normal file
102
source/game/StarBiomePlacement.hpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#ifndef STAR_BIOME_PLACEMENT_HPP
|
||||
#define STAR_BIOME_PLACEMENT_HPP
|
||||
|
||||
#include "StarPerlin.hpp"
|
||||
#include "StarWeightedPool.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
#include "StarPlant.hpp"
|
||||
#include "StarTreasure.hpp"
|
||||
#include "StarStrongTypedef.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(BiomeItemDistribution);
|
||||
|
||||
STAR_EXCEPTION(BiomeException, StarException);
|
||||
|
||||
typedef pair<TreeVariant, TreeVariant> TreePair;
|
||||
|
||||
// Weighted pairs of object name / parameters.
|
||||
typedef WeightedPool<pair<String, Json>> ObjectPool;
|
||||
|
||||
strong_typedef(String, TreasureBoxSet);
|
||||
strong_typedef(StringSet, MicroDungeonNames);
|
||||
|
||||
typedef Variant<GrassVariant, BushVariant, TreePair, ObjectPool, TreasureBoxSet, MicroDungeonNames> BiomeItem;
|
||||
BiomeItem variantToBiomeItem(Json const& store);
|
||||
Json variantFromBiomeItem(BiomeItem const& biomeItem);
|
||||
|
||||
enum class BiomePlacementArea { Surface, Underground };
|
||||
enum class BiomePlacementMode { Floor, Ceiling, Background, Ocean };
|
||||
extern EnumMap<BiomePlacementMode> const BiomePlacementModeNames;
|
||||
|
||||
struct BiomeItemPlacement {
|
||||
BiomeItemPlacement(BiomeItem item, Vec2I position, float priority);
|
||||
|
||||
// Orders by priority
|
||||
bool operator<(BiomeItemPlacement const& rhs) const;
|
||||
|
||||
BiomeItem item;
|
||||
Vec2I position;
|
||||
float priority;
|
||||
};
|
||||
|
||||
class BiomeItemDistribution {
|
||||
public:
|
||||
struct PeriodicWeightedItem {
|
||||
BiomeItem item;
|
||||
PerlinF weight;
|
||||
};
|
||||
|
||||
static Maybe<BiomeItem> createItem(Json const& itemSettings, RandomSource& rand, float biomeHueShift);
|
||||
|
||||
BiomeItemDistribution();
|
||||
BiomeItemDistribution(Json const& config, uint64_t seed, float biomeHueShift = 0.0f);
|
||||
BiomeItemDistribution(Json const& store);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
BiomePlacementMode mode() const;
|
||||
List<BiomeItem> allItems() const;
|
||||
|
||||
// Returns the best BiomeItem for this position out of the weighted item set,
|
||||
// if the density function specifies that an item should go in this position.
|
||||
Maybe<BiomeItemPlacement> itemToPlace(int x, int y) const;
|
||||
|
||||
private:
|
||||
enum class DistributionType {
|
||||
// Pure random distribution
|
||||
Random,
|
||||
// Uses perlin noise to morph a periodic function into a less predictable
|
||||
// periodic clumpy noise.
|
||||
Periodic
|
||||
};
|
||||
static EnumMap<DistributionType> const DistributionTypeNames;
|
||||
|
||||
BiomePlacementMode m_mode;
|
||||
DistributionType m_distribution;
|
||||
float m_priority;
|
||||
|
||||
// Used if the distribution type is Random
|
||||
|
||||
float m_blockProbability;
|
||||
uint64_t m_blockSeed;
|
||||
List<BiomeItem> m_randomItems;
|
||||
|
||||
// Used if the distribution type is Periodic
|
||||
|
||||
PerlinF m_densityFunction;
|
||||
PerlinF m_modulusDistortion;
|
||||
int m_modulus;
|
||||
int m_modulusOffset;
|
||||
// Pairs items with a periodic weight. Weight will vary over the space of
|
||||
// the distribution, If multiple items are present, this can be used to
|
||||
// select one of the items (with the highest weight) out of a list of items,
|
||||
// causing items to be grouped spatially in a way determined by the shape of
|
||||
// each weight function.
|
||||
List<pair<BiomeItem, PerlinF>> m_weightedItems;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
184
source/game/StarCelestialCoordinate.cpp
Normal file
184
source/game/StarCelestialCoordinate.cpp
Normal file
|
@ -0,0 +1,184 @@
|
|||
#include "StarCelestialCoordinate.hpp"
|
||||
#include "StarCelestialTypes.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarStaticRandom.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CelestialCoordinate::CelestialCoordinate() : m_planetaryOrbitNumber(0), m_satelliteOrbitNumber(0) {}
|
||||
|
||||
CelestialCoordinate::CelestialCoordinate(Vec3I location, int planetaryOrbitNumber, int satelliteOrbitNumber)
|
||||
: m_location(move(location)),
|
||||
m_planetaryOrbitNumber(planetaryOrbitNumber),
|
||||
m_satelliteOrbitNumber(satelliteOrbitNumber) {}
|
||||
|
||||
CelestialCoordinate::CelestialCoordinate(Json const& variant) : CelestialCoordinate() {
|
||||
if (variant.isType(Json::Type::String)) {
|
||||
String id = variant.toString();
|
||||
if (!id.empty() && !id.equalsIgnoreCase("null")) {
|
||||
try {
|
||||
auto plist = id.splitAny(" _:");
|
||||
|
||||
m_location[0] = lexicalCast<int>(plist.at(0));
|
||||
m_location[1] = lexicalCast<int>(plist.at(1));
|
||||
m_location[2] = lexicalCast<int>(plist.at(2));
|
||||
|
||||
if (plist.size() > 3)
|
||||
m_planetaryOrbitNumber = lexicalCast<int>(plist.at(3));
|
||||
if (plist.size() > 4)
|
||||
m_satelliteOrbitNumber = lexicalCast<int>(plist.at(4));
|
||||
|
||||
if (m_planetaryOrbitNumber <= 0)
|
||||
throw CelestialException(strf("Planetary body number out of range in '%s'", id));
|
||||
if (m_satelliteOrbitNumber < 0)
|
||||
throw CelestialException(strf("Satellite body number out of range in '%s'", id));
|
||||
} catch (StarException const& e) {
|
||||
throw CelestialException(strf("Error parsing CelestialCoordinate from '%s'", id), e);
|
||||
}
|
||||
}
|
||||
} else if (variant.isType(Json::Type::Object)) {
|
||||
m_location = jsonToVec3I(variant.get("location"));
|
||||
m_planetaryOrbitNumber = variant.getInt("planet", 0);
|
||||
m_satelliteOrbitNumber = variant.getInt("satellite", 0);
|
||||
} else if (!variant.isNull()) {
|
||||
throw CelestialException(
|
||||
strf("Improper variant type %s trying to convert to SystemCoordinate", variant.typeName()));
|
||||
}
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::isNull() const {
|
||||
return m_location == Vec3I() && m_planetaryOrbitNumber == 0 && m_satelliteOrbitNumber == 0;
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::isSystem() const {
|
||||
return !isNull() && m_planetaryOrbitNumber == 0;
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::isPlanetaryBody() const {
|
||||
return !isNull() && m_planetaryOrbitNumber != 0 && m_satelliteOrbitNumber == 0;
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::isSatelliteBody() const {
|
||||
return !isNull() && m_planetaryOrbitNumber != 0 && m_satelliteOrbitNumber != 0;
|
||||
}
|
||||
|
||||
Vec3I CelestialCoordinate::location() const {
|
||||
return m_location;
|
||||
}
|
||||
|
||||
CelestialCoordinate CelestialCoordinate::system() const {
|
||||
if (isNull())
|
||||
throw CelestialException("CelestialCoordinate::system() called on null coordinate");
|
||||
return CelestialCoordinate(m_location);
|
||||
}
|
||||
|
||||
CelestialCoordinate CelestialCoordinate::planet() const {
|
||||
if (isPlanetaryBody())
|
||||
return *this;
|
||||
if (isSatelliteBody())
|
||||
return CelestialCoordinate(m_location, m_planetaryOrbitNumber);
|
||||
throw CelestialException("CelestialCoordinate::planet() called on null or system coordinate type");
|
||||
}
|
||||
|
||||
int CelestialCoordinate::orbitNumber() const {
|
||||
if (isSatelliteBody())
|
||||
return m_satelliteOrbitNumber;
|
||||
if (isPlanetaryBody())
|
||||
return m_planetaryOrbitNumber;
|
||||
if (isSystem())
|
||||
return 0;
|
||||
throw CelestialException("CelestialCoordinate::orbitNumber() called on null coordinate");
|
||||
}
|
||||
|
||||
CelestialCoordinate CelestialCoordinate::parent() const {
|
||||
if (isSatelliteBody())
|
||||
return CelestialCoordinate(m_location, m_planetaryOrbitNumber);
|
||||
if (isPlanetaryBody())
|
||||
return CelestialCoordinate(m_location);
|
||||
throw CelestialException("CelestialCoordinate::parent() called on null or system coordinate");
|
||||
}
|
||||
|
||||
CelestialCoordinate CelestialCoordinate::child(int orbitNumber) const {
|
||||
if (isSystem())
|
||||
return CelestialCoordinate(m_location, orbitNumber);
|
||||
if (isPlanetaryBody())
|
||||
return CelestialCoordinate(m_location, m_planetaryOrbitNumber, orbitNumber);
|
||||
throw CelestialException("CelestialCoordinate::child called on null or satellite coordinate");
|
||||
}
|
||||
|
||||
Json CelestialCoordinate::toJson() const {
|
||||
if (isNull()) {
|
||||
return Json();
|
||||
} else {
|
||||
return JsonObject{{"location", jsonFromVec3I(m_location)},
|
||||
{"planet", m_planetaryOrbitNumber},
|
||||
{"satellite", m_satelliteOrbitNumber}};
|
||||
}
|
||||
}
|
||||
|
||||
String CelestialCoordinate::id() const {
|
||||
return toString(*this);
|
||||
}
|
||||
|
||||
double CelestialCoordinate::distance(CelestialCoordinate const& rhs) const {
|
||||
return Vec2D(m_location[0] - rhs.m_location[0], m_location[1] - rhs.m_location[1]).magnitude();
|
||||
}
|
||||
|
||||
String CelestialCoordinate::filename() const {
|
||||
return id().replace(":", "_");
|
||||
}
|
||||
|
||||
CelestialCoordinate::operator bool() const {
|
||||
return !isNull();
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::operator<(CelestialCoordinate const& rhs) const {
|
||||
return tie(m_location, m_planetaryOrbitNumber, m_satelliteOrbitNumber)
|
||||
< tie(rhs.m_location, rhs.m_planetaryOrbitNumber, rhs.m_satelliteOrbitNumber);
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::operator==(CelestialCoordinate const& rhs) const {
|
||||
return tie(m_location, m_planetaryOrbitNumber, m_satelliteOrbitNumber)
|
||||
== tie(rhs.m_location, rhs.m_planetaryOrbitNumber, rhs.m_satelliteOrbitNumber);
|
||||
}
|
||||
|
||||
bool CelestialCoordinate::operator!=(CelestialCoordinate const& rhs) const {
|
||||
return tie(m_location, m_planetaryOrbitNumber, m_satelliteOrbitNumber)
|
||||
!= tie(rhs.m_location, rhs.m_planetaryOrbitNumber, rhs.m_satelliteOrbitNumber);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, CelestialCoordinate const& coord) {
|
||||
if (coord.isNull()) {
|
||||
os << "null";
|
||||
} else {
|
||||
format(os, "%s:%s:%s", coord.m_location[0], coord.m_location[1], coord.m_location[2]);
|
||||
|
||||
if (coord.m_planetaryOrbitNumber) {
|
||||
format(os, ":%s", coord.m_planetaryOrbitNumber);
|
||||
if (coord.m_satelliteOrbitNumber)
|
||||
format(os, ":%s", coord.m_satelliteOrbitNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, CelestialCoordinate& coordinate) {
|
||||
ds.read(coordinate.m_location);
|
||||
ds.read(coordinate.m_planetaryOrbitNumber);
|
||||
ds.read(coordinate.m_satelliteOrbitNumber);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, CelestialCoordinate const& coordinate) {
|
||||
ds.write(coordinate.m_location);
|
||||
ds.write(coordinate.m_planetaryOrbitNumber);
|
||||
ds.write(coordinate.m_satelliteOrbitNumber);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
96
source/game/StarCelestialCoordinate.hpp
Normal file
96
source/game/StarCelestialCoordinate.hpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#ifndef STAR_CELESTIAL_COORDINATE_HPP
|
||||
#define STAR_CELESTIAL_COORDINATE_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarVector.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CelestialCoordinate);
|
||||
|
||||
STAR_EXCEPTION(CelestialException, StarException);
|
||||
|
||||
// Specifies coordinates to either a planetary system, a planetary body, or a
|
||||
// satellite around such a planetary body. The terms here are meant to be very
|
||||
// generic, a "planetary body" could be an asteroid field, or a ship, or
|
||||
// anything in orbit around the center of mass of a specific planetary system.
|
||||
// The terms are really simply meant as a hierarchy of orbits.
|
||||
//
|
||||
// No validity checking is done here, any coordinate to any body whether it
|
||||
// exists in a specific universe or not can be expressed. isNull() simply
|
||||
// checks whether the coordinate is the result of the empty constructor, not
|
||||
// whether the coordinate points to a valid object or not.
|
||||
class CelestialCoordinate {
|
||||
public:
|
||||
// Creates the null CelestialCoordinate
|
||||
CelestialCoordinate();
|
||||
CelestialCoordinate(Vec3I location, int planetaryOrbitNumber = 0, int satelliteOrbitNumber = 0);
|
||||
explicit CelestialCoordinate(Json const& json);
|
||||
|
||||
// Is this coordanate the null coordinate?
|
||||
bool isNull() const;
|
||||
|
||||
// Does this coordinate point to an entire planetary system?
|
||||
bool isSystem() const;
|
||||
// Is this world a body whose "designated gravity buddy" is the center of a
|
||||
// planetary system?
|
||||
bool isPlanetaryBody() const;
|
||||
// Is this world a body which orbits around a planetary body?
|
||||
bool isSatelliteBody() const;
|
||||
|
||||
Vec3I location() const;
|
||||
|
||||
// Returns just the system coordinate portion of this celestial coordinate.
|
||||
CelestialCoordinate system() const;
|
||||
|
||||
// Returns just the planet portion of this celestial coordinate, throws
|
||||
// exception if this is a system coordinate.
|
||||
CelestialCoordinate planet() const;
|
||||
|
||||
// Returns the orbit number for this body. Returns 0 for system coordinates.
|
||||
int orbitNumber() const;
|
||||
|
||||
// Returns the system for a planet or the planet for a satellite. If this is
|
||||
// a system coordinate, throws an exception.
|
||||
CelestialCoordinate parent() const;
|
||||
|
||||
// Returns a coordinate to a child object at the given orbit number. If the
|
||||
// orbit number is 0, returns *this, otherwise if this is a satellite throws
|
||||
// an exception.
|
||||
CelestialCoordinate child(int orbitNumber) const;
|
||||
|
||||
// Stores coordinate in json form that can be used to reconstruct it.
|
||||
Json toJson() const;
|
||||
|
||||
// Returns coordinate in a parseable String format.
|
||||
String id() const;
|
||||
|
||||
// Returns a fakey fake distance
|
||||
double distance(CelestialCoordinate const& rhs) const;
|
||||
|
||||
// Returns a slightly different string format than id(), which is still in an
|
||||
// accepted format, but more appropriate for filenames.
|
||||
String filename() const;
|
||||
|
||||
// Returns true if not null
|
||||
explicit operator bool() const;
|
||||
|
||||
bool operator<(CelestialCoordinate const& rhs) const;
|
||||
bool operator==(CelestialCoordinate const& rhs) const;
|
||||
bool operator!=(CelestialCoordinate const& rhs) const;
|
||||
|
||||
// Prints in the same format accepted by parser. Each coordinate is unique.
|
||||
friend std::ostream& operator<<(std::ostream& os, CelestialCoordinate const& coord);
|
||||
|
||||
friend DataStream& operator>>(DataStream& ds, CelestialCoordinate& coordinate);
|
||||
friend DataStream& operator<<(DataStream& ds, CelestialCoordinate const& coordinate);
|
||||
|
||||
private:
|
||||
Vec3I m_location;
|
||||
int m_planetaryOrbitNumber;
|
||||
int m_satelliteOrbitNumber;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
828
source/game/StarCelestialDatabase.cpp
Normal file
828
source/game/StarCelestialDatabase.cpp
Normal file
|
@ -0,0 +1,828 @@
|
|||
#include "StarCelestialDatabase.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarCasting.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarCompression.hpp"
|
||||
#include "StarFile.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarVersioningDatabase.hpp"
|
||||
#include "StarIterator.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CelestialDatabase::~CelestialDatabase() {}
|
||||
|
||||
RectI CelestialDatabase::xyRange() const {
|
||||
auto range = m_baseInformation.xyCoordRange;
|
||||
return RectI(range[0], range[0], range[1], range[1]);
|
||||
}
|
||||
|
||||
int CelestialDatabase::planetOrbitalLevels() const {
|
||||
return m_baseInformation.planetOrbitalLevels;
|
||||
}
|
||||
|
||||
int CelestialDatabase::satelliteOrbitalLevels() const {
|
||||
return m_baseInformation.satelliteOrbitalLevels;
|
||||
}
|
||||
|
||||
Vec2I CelestialDatabase::chunkIndexFor(CelestialCoordinate const& coordinate) const {
|
||||
return chunkIndexFor(coordinate.location().vec2());
|
||||
}
|
||||
|
||||
Vec2I CelestialDatabase::chunkIndexFor(Vec2I const& systemXY) const {
|
||||
return {(systemXY[0] - pmod(systemXY[0], m_baseInformation.chunkSize)) / m_baseInformation.chunkSize,
|
||||
(systemXY[1] - pmod(systemXY[1], m_baseInformation.chunkSize)) / m_baseInformation.chunkSize};
|
||||
}
|
||||
|
||||
List<Vec2I> CelestialDatabase::chunkIndexesFor(RectI const& region) const {
|
||||
if (region.isEmpty())
|
||||
return {};
|
||||
|
||||
List<Vec2I> chunkLocations;
|
||||
RectI chunkRegion(chunkIndexFor(region.min()), chunkIndexFor(region.max() - Vec2I(1, 1)));
|
||||
for (int x = chunkRegion.xMin(); x <= chunkRegion.xMax(); ++x) {
|
||||
for (int y = chunkRegion.yMin(); y <= chunkRegion.yMax(); ++y)
|
||||
chunkLocations.append({x, y});
|
||||
}
|
||||
return chunkLocations;
|
||||
}
|
||||
|
||||
RectI CelestialDatabase::chunkRegion(Vec2I const& chunkIndex) const {
|
||||
return RectI(chunkIndex * m_baseInformation.chunkSize, (chunkIndex + Vec2I(1, 1)) * m_baseInformation.chunkSize);
|
||||
}
|
||||
|
||||
CelestialMasterDatabase::CelestialMasterDatabase(Maybe<String> databaseFile) {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
auto config = assets->json("/celestial.config");
|
||||
|
||||
m_baseInformation.planetOrbitalLevels = config.getInt("planetOrbitalLevels");
|
||||
m_baseInformation.satelliteOrbitalLevels = config.getInt("satelliteOrbitalLevels");
|
||||
m_baseInformation.chunkSize = config.getInt("chunkSize");
|
||||
m_baseInformation.xyCoordRange = jsonToVec2I(config.get("xyCoordRange"));
|
||||
m_baseInformation.zCoordRange = jsonToVec2I(config.get("zCoordRange"));
|
||||
|
||||
m_generationInformation.systemProbability = config.getFloat("systemProbability");
|
||||
m_generationInformation.constellationProbability = config.getFloat("constellationProbability");
|
||||
m_generationInformation.constellationLineCountRange = jsonToVec2U(config.get("constellationLineCountRange"));
|
||||
m_generationInformation.constellationMaxTries = config.getUInt("constellationMaxTries");
|
||||
m_generationInformation.maximumConstellationLineLength = config.getFloat("maximumConstellationLineLength");
|
||||
m_generationInformation.minimumConstellationLineLength = config.getFloat("minimumConstellationLineLength");
|
||||
m_generationInformation.minimumConstellationMagnitude = config.getFloat("minimumConstellationMagnitude");
|
||||
m_generationInformation.minimumConstellationLineCloseness = config.getFloat("minimumConstellationLineCloseness");
|
||||
|
||||
// Copy construct into a Map<String, Json> in the parsing of the weighted
|
||||
// pools to make sure that each WeightedPool is predictably populated based
|
||||
// on key order.
|
||||
|
||||
for (auto const& systemPair : Map<String, Json>::from(config.getObject("systemTypes"))) {
|
||||
SystemType systemType;
|
||||
systemType.typeName = systemPair.first;
|
||||
systemType.baseParameters = systemPair.second.get("baseParameters");
|
||||
systemType.variationParameters = systemPair.second.getArray("variationParameters", JsonArray());
|
||||
for (auto const& orbitRegion : systemPair.second.getArray("orbitRegions", JsonArray())) {
|
||||
String regionName = orbitRegion.getString("regionName");
|
||||
Vec2I orbitRange = jsonToVec2I(orbitRegion.get("orbitRange"));
|
||||
float bodyProbability = orbitRegion.getFloat("bodyProbability");
|
||||
WeightedPool<String> regionPlanetaryTypes = jsonToWeightedPool<String>(orbitRegion.get("planetaryTypes"));
|
||||
WeightedPool<String> regionSatelliteTypes = jsonToWeightedPool<String>(orbitRegion.get("satelliteTypes"));
|
||||
systemType.orbitRegions.append({regionName, orbitRange, bodyProbability, regionPlanetaryTypes, regionSatelliteTypes});
|
||||
}
|
||||
m_generationInformation.systemTypes.add(systemPair.first, systemType);
|
||||
}
|
||||
|
||||
m_generationInformation.systemTypePerlin = PerlinD(config.getObject("systemTypePerlin"), staticRandomU64("SystemTypePerlin"));
|
||||
m_generationInformation.systemTypeBins = config.get("systemTypeBins");
|
||||
|
||||
for (auto const& planetaryPair : Map<String, Json>::from(config.getObject("planetaryTypes"))) {
|
||||
PlanetaryType planetaryType;
|
||||
planetaryType.typeName = planetaryPair.first;
|
||||
planetaryType.satelliteProbability = planetaryPair.second.getFloat("satelliteProbability");
|
||||
planetaryType.maxSatelliteCount =
|
||||
planetaryPair.second.getUInt("maxSatelliteCount", m_baseInformation.satelliteOrbitalLevels);
|
||||
planetaryType.baseParameters = planetaryPair.second.get("baseParameters");
|
||||
planetaryType.variationParameters = planetaryPair.second.getArray("variationParameters", JsonArray());
|
||||
planetaryType.orbitParameters = planetaryPair.second.getObject("orbitParameters", JsonObject());
|
||||
m_generationInformation.planetaryTypes[planetaryType.typeName] = planetaryType;
|
||||
}
|
||||
|
||||
for (auto const& satellitePair : Map<String, Json>::from(config.getObject("satelliteTypes"))) {
|
||||
SatelliteType satelliteType;
|
||||
satelliteType.typeName = satellitePair.first;
|
||||
satelliteType.baseParameters = satellitePair.second.get("baseParameters");
|
||||
satelliteType.variationParameters = satellitePair.second.getArray("variationParameters", JsonArray());
|
||||
satelliteType.orbitParameters = satellitePair.second.getObject("orbitParameters", JsonObject());
|
||||
m_generationInformation.satelliteTypes[satelliteType.typeName] = satelliteType;
|
||||
}
|
||||
|
||||
auto namesConfig = assets->json("/celestial/names.config");
|
||||
m_generationInformation.planetarySuffixes = jsonToStringList(namesConfig.get("planetarySuffixes"));
|
||||
m_generationInformation.satelliteSuffixes = jsonToStringList(namesConfig.get("satelliteSuffixes"));
|
||||
|
||||
for (auto const& list : namesConfig.get("systemPrefixNames").iterateArray())
|
||||
m_generationInformation.systemPrefixNames.add(list.getFloat(0), list.getString(1));
|
||||
|
||||
for (auto const& list : namesConfig.get("systemNames").iterateArray())
|
||||
m_generationInformation.systemNames.add(list.getFloat(0), list.getString(1));
|
||||
|
||||
for (auto const& list : namesConfig.get("systemSuffixNames").iterateArray())
|
||||
m_generationInformation.systemSuffixNames.add(list.getFloat(0), list.getString(1));
|
||||
|
||||
if (databaseFile) {
|
||||
m_database.setContentIdentifier("Celestial2");
|
||||
m_database.setIODevice(File::open(*databaseFile, IOMode::ReadWrite));
|
||||
m_database.open();
|
||||
if (m_database.contentIdentifier() != "Celestial2") {
|
||||
Logger::error("CelestialMasterDatabase database content identifier is not 'Celestial2', moving out of the way and recreating");
|
||||
m_database.close();
|
||||
File::rename(*databaseFile, strf("%s.%s.fail", *databaseFile, Time::millisecondsSinceEpoch()));
|
||||
m_database.setIODevice(File::open(*databaseFile, IOMode::ReadWrite));
|
||||
m_database.open();
|
||||
}
|
||||
m_database.setAutoCommit(false);
|
||||
}
|
||||
|
||||
m_commitInterval = config.getFloat("commitInterval");
|
||||
m_commitTimer.restart(m_commitInterval);
|
||||
}
|
||||
|
||||
CelestialBaseInformation CelestialMasterDatabase::baseInformation() const {
|
||||
return m_baseInformation;
|
||||
}
|
||||
|
||||
CelestialResponse CelestialMasterDatabase::respondToRequest(CelestialRequest const& request) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (auto chunkLocation = request.maybeLeft()) {
|
||||
auto chunk = getChunk(*chunkLocation);
|
||||
// System objects are sent by separate system requests.
|
||||
chunk.systemObjects.clear();
|
||||
return makeLeft(move(chunk));
|
||||
} else if (auto systemLocation = request.maybeRight()) {
|
||||
auto const& chunk = getChunk(chunkIndexFor(*systemLocation));
|
||||
CelestialSystemObjects systemObjects = {*systemLocation, chunk.systemObjects.get(*systemLocation)};
|
||||
return makeRight(move(systemObjects));
|
||||
} else {
|
||||
return CelestialResponse();
|
||||
}
|
||||
}
|
||||
|
||||
void CelestialMasterDatabase::cleanupAndCommit() {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
m_chunkCache.cleanup();
|
||||
if (m_database.isOpen() && m_commitTimer.timeUp()) {
|
||||
m_database.commit();
|
||||
m_commitTimer.restart(m_commitInterval);
|
||||
}
|
||||
}
|
||||
|
||||
bool CelestialMasterDatabase::coordinateValid(CelestialCoordinate const& coordinate) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (!coordinate)
|
||||
return false;
|
||||
|
||||
auto const& chunk = getChunk(chunkIndexFor(coordinate));
|
||||
|
||||
auto systemObjects = chunk.systemObjects.ptr(coordinate.location());
|
||||
if (!systemObjects)
|
||||
return false;
|
||||
|
||||
if (coordinate.isSystem())
|
||||
return true;
|
||||
|
||||
auto planet = systemObjects->ptr(coordinate.planet().orbitNumber());
|
||||
if (!planet)
|
||||
return false;
|
||||
|
||||
if (coordinate.isPlanetaryBody())
|
||||
return true;
|
||||
|
||||
return planet->satelliteParameters.contains(coordinate.orbitNumber());
|
||||
}
|
||||
|
||||
Maybe<CelestialCoordinate> CelestialMasterDatabase::findRandomWorld(unsigned tries, unsigned trySpatialRange,
|
||||
function<bool(CelestialCoordinate)> filter, Maybe<uint64_t> seed) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
RandomSource randSource;
|
||||
if (seed)
|
||||
randSource.init(*seed);
|
||||
for (unsigned i = 0; i < tries; ++i) {
|
||||
RectI range = xyRange();
|
||||
Vec2I randomLocation = Vec2I(randSource.randInt(range.xMin(), range.xMax()), randSource.randInt(range.yMin(), range.yMax()));
|
||||
for (auto system : scanSystems(RectI::withCenter(randomLocation, Vec2I::filled(trySpatialRange)))) {
|
||||
if (!hasChildren(system).value(false))
|
||||
continue;
|
||||
|
||||
auto world = randSource.randFrom(children(system));
|
||||
// This sucks, 50% of the time will try and return satellite, not really
|
||||
// balanced probability wise
|
||||
if (hasChildren(world).value(false) && randSource.randb())
|
||||
world = randSource.randFrom(children(world));
|
||||
|
||||
if (!filter || filter(world))
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Maybe<CelestialParameters> CelestialMasterDatabase::parameters(CelestialCoordinate const& coordinate) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (!coordinateValid(coordinate))
|
||||
throw CelestialException("CelestialMasterDatabase::parameters called on invalid coordinate");
|
||||
|
||||
auto const& chunk = getChunk(chunkIndexFor(coordinate));
|
||||
|
||||
if (coordinate.isSatelliteBody())
|
||||
return chunk.systemObjects.get(coordinate.location())
|
||||
.get(coordinate.parent().orbitNumber())
|
||||
.satelliteParameters.get(coordinate.orbitNumber());
|
||||
|
||||
if (coordinate.isPlanetaryBody())
|
||||
return chunk.systemObjects.get(coordinate.location()).get(coordinate.orbitNumber()).planetParameters;
|
||||
|
||||
return chunk.systemParameters.get(coordinate.location());
|
||||
}
|
||||
|
||||
Maybe<String> CelestialMasterDatabase::name(CelestialCoordinate const& coordinate) {
|
||||
return parameters(coordinate)->name();
|
||||
}
|
||||
|
||||
Maybe<bool> CelestialMasterDatabase::hasChildren(CelestialCoordinate const& coordinate) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (!coordinateValid(coordinate))
|
||||
throw CelestialException("CelestialMasterDatabase::hasChildren called on invalid coordinate");
|
||||
|
||||
auto const& systemObjects = getChunk(chunkIndexFor(coordinate)).systemObjects.get(coordinate.location());
|
||||
|
||||
if (coordinate.isSystem())
|
||||
return !systemObjects.empty();
|
||||
|
||||
if (coordinate.isPlanetaryBody())
|
||||
return !systemObjects.get(coordinate.orbitNumber()).satelliteParameters.empty();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<CelestialCoordinate> CelestialMasterDatabase::children(CelestialCoordinate const& coordinate) {
|
||||
return childOrbits(coordinate).transformed(bind(&CelestialCoordinate::child, coordinate, _1));
|
||||
}
|
||||
|
||||
List<int> CelestialMasterDatabase::childOrbits(CelestialCoordinate const& coordinate) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (!coordinateValid(coordinate))
|
||||
throw CelestialException("CelestialMasterDatabase::childOrbits called on invalid coordinate");
|
||||
|
||||
auto const& systemObjects = getChunk(chunkIndexFor(coordinate)).systemObjects.get(coordinate.location());
|
||||
|
||||
if (coordinate.isSystem())
|
||||
return systemObjects.keys();
|
||||
|
||||
if (coordinate.isPlanetaryBody())
|
||||
return systemObjects.get(coordinate.orbitNumber()).satelliteParameters.keys();
|
||||
|
||||
throw CelestialException("CelestialMasterDatabase::childOrbits called on improper type!");
|
||||
}
|
||||
|
||||
List<CelestialCoordinate> CelestialMasterDatabase::scanSystems(RectI const& region, Maybe<StringSet> const& includedTypes) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
List<CelestialCoordinate> systems;
|
||||
for (auto const& chunkLocation : chunkIndexesFor(region)) {
|
||||
auto const& chunkData = getChunk(chunkLocation);
|
||||
for (auto const& pair : chunkData.systemParameters) {
|
||||
Vec3I systemLocation = pair.first;
|
||||
if (region.contains(systemLocation.vec2())) {
|
||||
if (includedTypes) {
|
||||
String thisType = pair.second.getParameter("typeName", "").toString();
|
||||
if (!includedTypes->contains(thisType))
|
||||
continue;
|
||||
}
|
||||
systems.append(CelestialCoordinate(systemLocation));
|
||||
}
|
||||
}
|
||||
}
|
||||
return systems;
|
||||
}
|
||||
|
||||
List<pair<Vec2I, Vec2I>> CelestialMasterDatabase::scanConstellationLines(RectI const& region) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
List<pair<Vec2I, Vec2I>> lines;
|
||||
for (auto const& chunkLocation : chunkIndexesFor(region)) {
|
||||
auto const& chunkData = getChunk(chunkLocation);
|
||||
for (auto const& constellation : chunkData.constellations) {
|
||||
for (auto const& line : constellation) {
|
||||
if (region.intersects(Line2I(line.first, line.second)))
|
||||
lines.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool CelestialMasterDatabase::scanRegionFullyLoaded(RectI const&) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void CelestialMasterDatabase::updateParameters(CelestialCoordinate const& coordinate, CelestialParameters const& parameters) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (!coordinateValid(coordinate))
|
||||
throw CelestialException("CelestialMasterDatabase::updateParameters called on invalid coordinate");
|
||||
|
||||
auto chunkIndex = chunkIndexFor(coordinate);
|
||||
auto chunk = getChunk(chunkIndex);
|
||||
|
||||
bool updated = false;
|
||||
if (coordinate.isSatelliteBody()) {
|
||||
chunk.systemObjects.get(coordinate.location())
|
||||
.get(coordinate.parent().orbitNumber())
|
||||
.satelliteParameters.set(coordinate.orbitNumber(), parameters);
|
||||
updated = true;
|
||||
} else if (coordinate.isPlanetaryBody()) {
|
||||
chunk.systemObjects.get(coordinate.location()).get(coordinate.orbitNumber()).planetParameters = parameters;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated && m_database.isOpen()) {
|
||||
auto versioningDatabase = Root::singleton().versioningDatabase();
|
||||
auto versionedChunk = versioningDatabase->makeCurrentVersionedJson("CelestialChunk", chunk.toJson());
|
||||
m_database.insert(DataStreamBuffer::serialize(chunkIndex), compressData(DataStreamBuffer::serialize<VersionedJson>(versionedChunk)));
|
||||
|
||||
m_chunkCache.remove(chunkIndex);
|
||||
} else {
|
||||
updated = false;
|
||||
}
|
||||
|
||||
if (!updated)
|
||||
throw CelestialException("CelestialMasterDatabase::updateParameters failed; coordinate is not a valid planet or satellite, or celestial database was not open for writing");
|
||||
}
|
||||
|
||||
Maybe<CelestialOrbitRegion> CelestialMasterDatabase::orbitRegion(
|
||||
List<CelestialOrbitRegion> const& orbitRegions, int planetaryOrbitNumber) {
|
||||
for (auto const& region : orbitRegions) {
|
||||
if (planetaryOrbitNumber >= region.orbitRange[0] && planetaryOrbitNumber <= region.orbitRange[1])
|
||||
return region;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
CelestialChunk const& CelestialMasterDatabase::getChunk(Vec2I const& chunkIndex) {
|
||||
return m_chunkCache.get(chunkIndex, [this](Vec2I const& chunkIndex) -> CelestialChunk {
|
||||
auto versioningDatabase = Root::singleton().versioningDatabase();
|
||||
|
||||
if (m_database.isOpen()) {
|
||||
if (auto chunkData = m_database.find(DataStreamBuffer::serialize(chunkIndex))) {
|
||||
auto versionedChunk = DataStreamBuffer::deserialize<VersionedJson>(uncompressData(chunkData.take()));
|
||||
if (!versioningDatabase->versionedJsonCurrent(versionedChunk)) {
|
||||
versionedChunk = versioningDatabase->updateVersionedJson(versionedChunk);
|
||||
m_database.insert(DataStreamBuffer::serialize(chunkIndex),
|
||||
compressData(DataStreamBuffer::serialize<VersionedJson>(versionedChunk)));
|
||||
}
|
||||
return CelestialChunk(versionedChunk.content);
|
||||
}
|
||||
}
|
||||
|
||||
auto newChunk = produceChunk(chunkIndex);
|
||||
if (m_database.isOpen()) {
|
||||
auto versionedChunk = versioningDatabase->makeCurrentVersionedJson("CelestialChunk", newChunk.toJson());
|
||||
m_database.insert(DataStreamBuffer::serialize(chunkIndex),
|
||||
compressData(DataStreamBuffer::serialize<VersionedJson>(versionedChunk)));
|
||||
}
|
||||
|
||||
return newChunk;
|
||||
});
|
||||
}
|
||||
|
||||
CelestialChunk CelestialMasterDatabase::produceChunk(Vec2I const& chunkIndex) const {
|
||||
CelestialChunk chunkData;
|
||||
chunkData.chunkIndex = chunkIndex;
|
||||
|
||||
RandomSource random(staticRandomU64(chunkIndex[0], chunkIndex[1], "ChunkIndexMix"));
|
||||
|
||||
RectI region = chunkRegion(chunkIndex);
|
||||
List<Vec3I> systemLocations;
|
||||
for (int x = region.xMin(); x < region.xMax(); ++x) {
|
||||
for (int y = region.yMin(); y < region.yMax(); ++y) {
|
||||
if (random.randf() < m_generationInformation.systemProbability) {
|
||||
auto z = random.randi32() % (m_baseInformation.zCoordRange[1] - m_baseInformation.zCoordRange[0])
|
||||
+ m_baseInformation.zCoordRange[0];
|
||||
systemLocations.append(Vec3I(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Vec2I> constellationCandidates;
|
||||
for (auto const& systemLocation : systemLocations) {
|
||||
if (auto systemInformation = produceSystem(random, systemLocation)) {
|
||||
chunkData.systemParameters[systemLocation] = systemInformation.get().first;
|
||||
chunkData.systemObjects[systemLocation] = move(systemInformation.get().second);
|
||||
|
||||
if (systemInformation.get().first.getParameter("magnitude").toFloat()
|
||||
>= m_generationInformation.minimumConstellationMagnitude)
|
||||
constellationCandidates.append(systemLocation.vec2());
|
||||
}
|
||||
}
|
||||
|
||||
chunkData.constellations = produceConstellations(random, constellationCandidates);
|
||||
|
||||
return chunkData;
|
||||
}
|
||||
|
||||
Maybe<pair<CelestialParameters, HashMap<int, CelestialPlanet>>> CelestialMasterDatabase::produceSystem(
|
||||
RandomSource& random, Vec3I const& location) const {
|
||||
float typeSelector = m_generationInformation.systemTypePerlin.get(location[0], location[1]);
|
||||
String systemTypeName = binnedChoiceFromJson(m_generationInformation.systemTypeBins, typeSelector, "").toString();
|
||||
if (systemTypeName.empty())
|
||||
return {};
|
||||
auto systemType = m_generationInformation.systemTypes.get(systemTypeName);
|
||||
|
||||
CelestialCoordinate systemCoordinate(location);
|
||||
uint64_t systemSeed = random.randu64();
|
||||
|
||||
String prefix = m_generationInformation.systemPrefixNames.select(random);
|
||||
String mid = m_generationInformation.systemNames.select(random);
|
||||
String suffix = m_generationInformation.systemSuffixNames.select(random);
|
||||
|
||||
String systemName = String(strf("%s %s %s", prefix, mid, suffix)).trim();
|
||||
|
||||
systemName = systemName.replace("<onedigit>", strf("%01d", random.randu32() % 10));
|
||||
systemName = systemName.replace("<twodigit>", strf("%02d", random.randu32() % 100));
|
||||
systemName = systemName.replace("<threedigit>", strf("%03d", random.randu32() % 1000));
|
||||
systemName = systemName.replace("<fourdigit>", strf("%04d", random.randu32() % 10000));
|
||||
|
||||
CelestialParameters systemParameters = CelestialParameters(systemCoordinate,
|
||||
systemSeed,
|
||||
systemName,
|
||||
jsonMerge(systemType.baseParameters, random.randValueFrom(systemType.variationParameters)));
|
||||
|
||||
List<int> planetaryOrbits;
|
||||
for (int i = 1; i <= m_baseInformation.planetOrbitalLevels; ++i) {
|
||||
if (auto systemOrbitRegion = orbitRegion(systemType.orbitRegions, i)) {
|
||||
if (random.randf() <= systemOrbitRegion->bodyProbability)
|
||||
planetaryOrbits.append(i);
|
||||
}
|
||||
}
|
||||
|
||||
HashMap<int, CelestialPlanet> systemObjects;
|
||||
for (auto planetPair : enumerateIterator(planetaryOrbits)) {
|
||||
auto systemOrbitRegion = orbitRegion(systemType.orbitRegions, planetPair.first);
|
||||
|
||||
auto planetaryTypeName = systemOrbitRegion->planetaryTypes.select(random);
|
||||
if (m_generationInformation.planetaryTypes.contains(planetaryTypeName)) {
|
||||
auto planetaryType = m_generationInformation.planetaryTypes.get(planetaryTypeName);
|
||||
auto planetaryParameters =
|
||||
jsonMerge(planetaryType.baseParameters, random.randValueFrom(planetaryType.variationParameters));
|
||||
|
||||
CelestialCoordinate planetCoordinate(location, planetPair.first);
|
||||
uint64_t planetarySeed = random.randu64();
|
||||
String planetaryName = strf("%s %s", systemName, m_generationInformation.planetarySuffixes.at(planetPair.second));
|
||||
|
||||
CelestialPlanet planet;
|
||||
planet.planetParameters =
|
||||
CelestialParameters(planetCoordinate, planetarySeed, planetaryName, planetaryParameters);
|
||||
|
||||
List<int> satelliteOrbits;
|
||||
for (int i = 1; i <= m_baseInformation.satelliteOrbitalLevels; ++i) {
|
||||
if (satelliteOrbits.size() < planetaryType.maxSatelliteCount
|
||||
&& random.randf() < planetaryType.satelliteProbability)
|
||||
satelliteOrbits.append(i);
|
||||
}
|
||||
|
||||
for (auto satellitePair : enumerateIterator(satelliteOrbits)) {
|
||||
auto satelliteTypeName = systemOrbitRegion->satelliteTypes.select(random);
|
||||
if (m_generationInformation.satelliteTypes.contains(satelliteTypeName)) {
|
||||
auto satelliteType = m_generationInformation.satelliteTypes.get(satelliteTypeName);
|
||||
auto satelliteParameters = jsonMerge(satelliteType.baseParameters,
|
||||
random.randValueFrom(satelliteType.variationParameters),
|
||||
random.randValueFrom(
|
||||
satelliteType.orbitParameters.value(systemOrbitRegion->regionName, JsonArray()).toArray()));
|
||||
|
||||
CelestialCoordinate satelliteCoordinate(location, planetPair.first, satellitePair.first);
|
||||
uint64_t satelliteSeed = random.randu64();
|
||||
String satelliteName =
|
||||
strf("%s %s", planetaryName, m_generationInformation.satelliteSuffixes.at(satellitePair.second));
|
||||
|
||||
planet.satelliteParameters[satellitePair.first] =
|
||||
CelestialParameters(satelliteCoordinate, satelliteSeed, satelliteName, satelliteParameters);
|
||||
}
|
||||
}
|
||||
|
||||
systemObjects[planetPair.first] = move(planet);
|
||||
}
|
||||
}
|
||||
|
||||
return pair<CelestialParameters, HashMap<int, CelestialPlanet>>{move(systemParameters), move(systemObjects)};
|
||||
}
|
||||
|
||||
List<CelestialConstellation> CelestialMasterDatabase::produceConstellations(
|
||||
RandomSource& random, List<Vec2I> const& constellationCandidates) const {
|
||||
List<CelestialConstellation> constellations;
|
||||
|
||||
if (random.randf() < m_generationInformation.constellationProbability && constellationCandidates.size() > 2) {
|
||||
unsigned targetConstellationLineCount = random.randUInt(
|
||||
m_generationInformation.constellationLineCountRange[0], m_generationInformation.constellationLineCountRange[1]);
|
||||
Set<Vec2I> constellationPoints;
|
||||
Set<Line2I> constellationLines;
|
||||
|
||||
unsigned tries = 0;
|
||||
|
||||
while (constellationLines.size() < targetConstellationLineCount) {
|
||||
if (++tries > m_generationInformation.constellationMaxTries)
|
||||
break;
|
||||
|
||||
Vec2I start;
|
||||
if (constellationPoints.empty())
|
||||
start = random.randValueFrom(constellationCandidates);
|
||||
else
|
||||
start = random.randValueFrom(constellationPoints);
|
||||
|
||||
Vec2I end = random.randValueFrom(constellationCandidates);
|
||||
|
||||
Line2I proposedLine(start, end);
|
||||
Line2D proposedLineD(proposedLine);
|
||||
|
||||
if (start == end)
|
||||
continue;
|
||||
|
||||
if (constellationLines.contains(proposedLine) || constellationLines.contains(proposedLine.reversed()))
|
||||
continue;
|
||||
|
||||
if (proposedLineD.diff().magnitude() > m_generationInformation.maximumConstellationLineLength)
|
||||
continue;
|
||||
|
||||
if (proposedLineD.diff().magnitude() < m_generationInformation.minimumConstellationLineLength)
|
||||
continue;
|
||||
|
||||
bool valid = true;
|
||||
for (auto const& constellationLine : constellationLines) {
|
||||
Line2D constellationLineD(constellationLine);
|
||||
auto intersection = proposedLineD.intersection(constellationLineD);
|
||||
if (intersection.intersects && Vec2I::round(intersection.point) != proposedLine.min()
|
||||
&& Vec2I::round(intersection.point) != proposedLine.max()) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (proposedLine.min() != constellationLine.min() && proposedLine.min() != constellationLine.max()
|
||||
&& constellationLineD.distanceTo(proposedLineD.min())
|
||||
< m_generationInformation.minimumConstellationLineCloseness) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (proposedLine.max() != constellationLine.min() && proposedLine.max() != constellationLine.max()
|
||||
&& constellationLineD.distanceTo(proposedLineD.max())
|
||||
< m_generationInformation.minimumConstellationLineCloseness) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
constellationLines.add(proposedLine);
|
||||
constellationPoints.add(proposedLine.min());
|
||||
constellationPoints.add(proposedLine.max());
|
||||
}
|
||||
}
|
||||
|
||||
if (constellationLines.size() > 1) {
|
||||
CelestialConstellation constellation;
|
||||
for (auto const& line : constellationLines)
|
||||
constellation.append({line.min(), line.max()});
|
||||
constellations.append(constellation);
|
||||
}
|
||||
}
|
||||
|
||||
return constellations;
|
||||
}
|
||||
|
||||
CelestialSlaveDatabase::CelestialSlaveDatabase(CelestialBaseInformation baseInformation) {
|
||||
auto config = Root::singleton().assets()->json("/celestial.config");
|
||||
|
||||
m_baseInformation = move(baseInformation);
|
||||
m_requestTimeout = config.getFloat("requestTimeout");
|
||||
}
|
||||
|
||||
void CelestialSlaveDatabase::signalRegion(RectI const& region) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
for (auto location : chunkIndexesFor(region)) {
|
||||
if (!m_chunkCache.ptr(location) && !m_pendingChunkRequests.contains(location))
|
||||
m_pendingChunkRequests[location] = Timer();
|
||||
}
|
||||
}
|
||||
|
||||
void CelestialSlaveDatabase::signalSystem(CelestialCoordinate const& system) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (auto chunk = m_chunkCache.ptr(chunkIndexFor(system))) {
|
||||
if (!chunk->systemObjects.contains(system.location()))
|
||||
m_pendingSystemRequests[system.location()] = Timer();
|
||||
} else {
|
||||
signalRegion(RectI::withSize(system.location().vec2(), {1, 1}));
|
||||
}
|
||||
}
|
||||
|
||||
List<CelestialRequest> CelestialSlaveDatabase::pullRequests() {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
List<CelestialRequest> requests;
|
||||
|
||||
auto chunkIt = makeSMutableMapIterator(m_pendingChunkRequests);
|
||||
while (chunkIt.hasNext()) {
|
||||
auto& pair = chunkIt.next();
|
||||
if (!pair.second.running()) {
|
||||
requests.append(makeLeft(pair.first));
|
||||
pair.second.restart(m_requestTimeout);
|
||||
} else if (pair.second.timeUp()) {
|
||||
chunkIt.remove();
|
||||
}
|
||||
}
|
||||
|
||||
auto systemIt = makeSMutableMapIterator(m_pendingSystemRequests);
|
||||
while (systemIt.hasNext()) {
|
||||
auto& pair = systemIt.next();
|
||||
if (!pair.second.running()) {
|
||||
requests.append(makeRight(pair.first));
|
||||
pair.second.restart(m_requestTimeout);
|
||||
} else if (pair.second.timeUp()) {
|
||||
systemIt.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
void CelestialSlaveDatabase::pushResponses(List<CelestialResponse> responses) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
for (auto& response : responses) {
|
||||
if (auto celestialChunk = response.leftPtr()) {
|
||||
m_pendingChunkRequests.remove(celestialChunk->chunkIndex);
|
||||
m_chunkCache.set(celestialChunk->chunkIndex, move(*celestialChunk));
|
||||
} else if (auto celestialSystemObjects = response.rightPtr()) {
|
||||
m_pendingSystemRequests.remove(celestialSystemObjects->systemLocation);
|
||||
auto chunkLocation = chunkIndexFor(celestialSystemObjects->systemLocation);
|
||||
if (auto chunk = m_chunkCache.ptr(chunkLocation))
|
||||
chunk->systemObjects[celestialSystemObjects->systemLocation] = move(celestialSystemObjects->planets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CelestialSlaveDatabase::cleanup() {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
m_chunkCache.cleanup();
|
||||
}
|
||||
|
||||
Maybe<CelestialParameters> CelestialSlaveDatabase::parameters(CelestialCoordinate const& coordinate) {
|
||||
if (!coordinate)
|
||||
throw CelestialException("CelestialSlaveDatabase::parameters called on null coordinate");
|
||||
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (coordinate.isSystem())
|
||||
signalRegion(RectI::withSize(coordinate.location().vec2(), {1, 1}));
|
||||
else
|
||||
signalSystem(coordinate);
|
||||
|
||||
if (auto chunk = m_chunkCache.ptr(chunkIndexFor(coordinate))) {
|
||||
if (coordinate.isSystem())
|
||||
return chunk->systemParameters.maybe(coordinate.location());
|
||||
|
||||
if (auto systemObjects = chunk->systemObjects.ptr(coordinate.location())) {
|
||||
auto const& planet = systemObjects->get(coordinate.planet().orbitNumber());
|
||||
if (coordinate.isPlanetaryBody())
|
||||
return planet.planetParameters;
|
||||
else if (coordinate.isSatelliteBody())
|
||||
return planet.satelliteParameters.get(coordinate.orbitNumber());
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Maybe<String> CelestialSlaveDatabase::name(CelestialCoordinate const& coordinate) {
|
||||
if (auto p = parameters(coordinate))
|
||||
return p->name();
|
||||
return {};
|
||||
}
|
||||
|
||||
Maybe<bool> CelestialSlaveDatabase::hasChildren(CelestialCoordinate const& coordinate) {
|
||||
if (!coordinate)
|
||||
throw CelestialException("CelestialSlaveDatabase::hasChildren called on null coordinate");
|
||||
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
signalSystem(coordinate);
|
||||
|
||||
if (auto chunk = m_chunkCache.ptr(chunkIndexFor(coordinate))) {
|
||||
if (auto systemObjects = chunk->systemObjects.ptr(coordinate.location())) {
|
||||
if (coordinate.isSystem())
|
||||
return !systemObjects->empty();
|
||||
else if (coordinate.isPlanetaryBody())
|
||||
return !systemObjects->get(coordinate.orbitNumber()).satelliteParameters.empty();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
List<CelestialCoordinate> CelestialSlaveDatabase::children(CelestialCoordinate const& coordinate) {
|
||||
return childOrbits(coordinate).transformed(bind(&CelestialCoordinate::child, coordinate, _1));
|
||||
}
|
||||
|
||||
List<int> CelestialSlaveDatabase::childOrbits(CelestialCoordinate const& coordinate) {
|
||||
if (!coordinate)
|
||||
throw CelestialException("CelestialSlaveDatabase::childOrbits called on null coordinate");
|
||||
|
||||
if (coordinate.isSatelliteBody())
|
||||
throw CelestialException("CelestialSlaveDatabase::childOrbits called on improper type!");
|
||||
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
signalSystem(coordinate);
|
||||
|
||||
if (auto chunk = m_chunkCache.ptr(chunkIndexFor(coordinate))) {
|
||||
if (auto systemObjects = chunk->systemObjects.ptr(coordinate.location())) {
|
||||
if (coordinate.isSystem())
|
||||
return systemObjects->keys();
|
||||
else if (coordinate.isPlanetaryBody())
|
||||
return systemObjects->get(coordinate.orbitNumber()).satelliteParameters.keys();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
List<CelestialCoordinate> CelestialSlaveDatabase::scanSystems(RectI const& region, Maybe<StringSet> const& includedTypes) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
signalRegion(region);
|
||||
|
||||
List<CelestialCoordinate> systems;
|
||||
for (auto const& chunkLocation : chunkIndexesFor(region)) {
|
||||
if (auto chunkData = m_chunkCache.ptr(chunkLocation)) {
|
||||
for (auto const& pair : chunkData->systemParameters) {
|
||||
Vec3I systemLocation = pair.first;
|
||||
if (region.contains(systemLocation.vec2())) {
|
||||
if (includedTypes) {
|
||||
String thisType = pair.second.getParameter("typeName", "").toString();
|
||||
if (!includedTypes->contains(thisType))
|
||||
continue;
|
||||
}
|
||||
systems.append(CelestialCoordinate(systemLocation));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return systems;
|
||||
}
|
||||
|
||||
List<pair<Vec2I, Vec2I>> CelestialSlaveDatabase::scanConstellationLines(RectI const& region) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
signalRegion(region);
|
||||
|
||||
List<pair<Vec2I, Vec2I>> lines;
|
||||
for (auto const& chunkLocation : chunkIndexesFor(region)) {
|
||||
if (auto chunkData = m_chunkCache.ptr(chunkLocation)) {
|
||||
for (auto const& constellation : chunkData->constellations) {
|
||||
for (auto const& line : constellation) {
|
||||
if (region.intersects(Line2I(line.first, line.second)))
|
||||
lines.append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool CelestialSlaveDatabase::scanRegionFullyLoaded(RectI const& region) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
signalRegion(region);
|
||||
|
||||
for (auto const& chunkLocation : chunkIndexesFor(region)) {
|
||||
if (!m_chunkCache.ptr(chunkLocation))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void CelestialSlaveDatabase::invalidateCacheFor(CelestialCoordinate const& coordinate) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
m_chunkCache.remove(chunkIndexFor(coordinate));
|
||||
}
|
||||
|
||||
}
|
228
source/game/StarCelestialDatabase.hpp
Normal file
228
source/game/StarCelestialDatabase.hpp
Normal file
|
@ -0,0 +1,228 @@
|
|||
#ifndef STAR_CELESTIAL_DATABASE_HPP
|
||||
#define STAR_CELESTIAL_DATABASE_HPP
|
||||
|
||||
#include "StarRect.hpp"
|
||||
#include "StarTtlCache.hpp"
|
||||
#include "StarWeightedPool.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarBTreeDatabase.hpp"
|
||||
#include "StarCelestialTypes.hpp"
|
||||
#include "StarPerlin.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CelestialDatabase);
|
||||
STAR_CLASS(CelestialMasterDatabase);
|
||||
STAR_CLASS(CelestialSlaveDatabase);
|
||||
|
||||
class CelestialDatabase {
|
||||
public:
|
||||
virtual ~CelestialDatabase();
|
||||
|
||||
// The x/y region of usable worlds.
|
||||
RectI xyRange() const;
|
||||
|
||||
// The maximum number of bodies that can orbit a single system center /
|
||||
// planetary body. Orbital numbers are up to this number of levels
|
||||
// *inclusive*, so planetary orbit numbers would be 1-N, and planetary orbit
|
||||
// "0", in this system, would refer to the center of the planetary system
|
||||
// itself, e.g. a star. In the same way, satellites around a planetary
|
||||
// object are numbered 1-N, and 0 refers to the planetary object itself.
|
||||
int planetOrbitalLevels() const;
|
||||
int satelliteOrbitalLevels() const;
|
||||
|
||||
// The following methods are allowed to return no information even in the
|
||||
// case of valid coordinates, due to delayed loading.
|
||||
|
||||
virtual Maybe<CelestialParameters> parameters(CelestialCoordinate const& coordinate) = 0;
|
||||
virtual Maybe<String> name(CelestialCoordinate const& coordinate) = 0;
|
||||
|
||||
virtual Maybe<bool> hasChildren(CelestialCoordinate const& coordinate) = 0;
|
||||
virtual List<CelestialCoordinate> children(CelestialCoordinate const& coordinate) = 0;
|
||||
virtual List<int> childOrbits(CelestialCoordinate const& coordinate) = 0;
|
||||
|
||||
// Return all valid system coordinates in the given x/y range. All systems
|
||||
// are guaranteed to have unique x/y coordinates, and are meant to be viewed
|
||||
// from the top in 2d. The z-coordinate is there simpy as a validation
|
||||
// parameter.
|
||||
virtual List<CelestialCoordinate> scanSystems(RectI const& region, Maybe<StringSet> const& includedTypes = {}) = 0;
|
||||
virtual List<pair<Vec2I, Vec2I>> scanConstellationLines(RectI const& region) = 0;
|
||||
|
||||
// Returns false if part or all of the specified region is not loaded. This
|
||||
// is only relevant for calls to scanSystems and scanConstellationLines, and
|
||||
// does not imply that each individual system in the given region is fully
|
||||
// loaded with all planets moons etc, only that scanSystem and
|
||||
// scanConstellationLines are not waiting on missing data.
|
||||
virtual bool scanRegionFullyLoaded(RectI const& region) = 0;
|
||||
|
||||
protected:
|
||||
Vec2I chunkIndexFor(CelestialCoordinate const& coordinate) const;
|
||||
Vec2I chunkIndexFor(Vec2I const& systemXY) const;
|
||||
|
||||
// Returns the chunk indexes for the given region.
|
||||
List<Vec2I> chunkIndexesFor(RectI const& region) const;
|
||||
|
||||
// Returns the region of the given chunk.
|
||||
RectI chunkRegion(Vec2I const& chunkIndex) const;
|
||||
|
||||
// m_baseInformation should only be modified in the constructor, as it is not
|
||||
// thread protected.
|
||||
CelestialBaseInformation m_baseInformation;
|
||||
};
|
||||
|
||||
class CelestialMasterDatabase : public CelestialDatabase {
|
||||
public:
|
||||
CelestialMasterDatabase(Maybe<String> databaseFile = {});
|
||||
|
||||
CelestialBaseInformation baseInformation() const;
|
||||
CelestialResponse respondToRequest(CelestialRequest const& requests);
|
||||
|
||||
// Unload data that has not been used in the configured TTL time, and
|
||||
// periodically commit to the underlying database if it is in use.
|
||||
void cleanupAndCommit();
|
||||
|
||||
// Does this coordinate point to a valid existing object?
|
||||
bool coordinateValid(CelestialCoordinate const& coordinate);
|
||||
|
||||
// Find a planetary or satellite object randomly throughout the entire
|
||||
// celestial space that satisfies the given parameters. May fail to find
|
||||
// anything, though with the defaults this is vanishingly unlikely.
|
||||
Maybe<CelestialCoordinate> findRandomWorld(unsigned tries = 10, unsigned trySpatialRange = 50,
|
||||
function<bool(CelestialCoordinate)> filter = {}, Maybe<uint64_t> seed = {});
|
||||
|
||||
// CelestialMasterDatabase always returns actual data, as it does just in
|
||||
// time generation.
|
||||
|
||||
Maybe<CelestialParameters> parameters(CelestialCoordinate const& coordinate) override;
|
||||
Maybe<String> name(CelestialCoordinate const& coordinate) override;
|
||||
|
||||
Maybe<bool> hasChildren(CelestialCoordinate const& coordinate) override;
|
||||
List<CelestialCoordinate> children(CelestialCoordinate const& coordinate) override;
|
||||
List<int> childOrbits(CelestialCoordinate const& coordinate) override;
|
||||
|
||||
List<CelestialCoordinate> scanSystems(RectI const& region, Maybe<StringSet> const& includedTypes = {}) override;
|
||||
List<pair<Vec2I, Vec2I>> scanConstellationLines(RectI const& region) override;
|
||||
|
||||
bool scanRegionFullyLoaded(RectI const& region) override;
|
||||
|
||||
// overwrite the celestial parameters for the world at the given celestial coordinate
|
||||
void updateParameters(CelestialCoordinate const& coordinate, CelestialParameters const& parameters);
|
||||
|
||||
protected:
|
||||
struct SatelliteType {
|
||||
String typeName;
|
||||
Json baseParameters;
|
||||
JsonArray variationParameters;
|
||||
JsonObject orbitParameters;
|
||||
};
|
||||
|
||||
struct PlanetaryType {
|
||||
String typeName;
|
||||
float satelliteProbability;
|
||||
size_t maxSatelliteCount;
|
||||
Json baseParameters;
|
||||
JsonArray variationParameters;
|
||||
JsonObject orbitParameters;
|
||||
};
|
||||
|
||||
struct SystemType {
|
||||
String typeName;
|
||||
bool constellationCapable;
|
||||
Json baseParameters;
|
||||
JsonArray variationParameters;
|
||||
List<CelestialOrbitRegion> orbitRegions;
|
||||
};
|
||||
|
||||
struct GenerationInformation {
|
||||
float systemProbability;
|
||||
float constellationProbability;
|
||||
Vec2U constellationLineCountRange;
|
||||
unsigned constellationMaxTries;
|
||||
float maximumConstellationLineLength;
|
||||
float minimumConstellationLineLength;
|
||||
float minimumConstellationMagnitude;
|
||||
float minimumConstellationLineCloseness;
|
||||
|
||||
Map<String, SystemType> systemTypes;
|
||||
|
||||
PerlinD systemTypePerlin;
|
||||
Json systemTypeBins;
|
||||
|
||||
StringMap<PlanetaryType> planetaryTypes;
|
||||
StringMap<SatelliteType> satelliteTypes;
|
||||
|
||||
StringList planetarySuffixes;
|
||||
StringList satelliteSuffixes;
|
||||
|
||||
WeightedPool<String> systemPrefixNames;
|
||||
WeightedPool<String> systemNames;
|
||||
WeightedPool<String> systemSuffixNames;
|
||||
};
|
||||
|
||||
static Maybe<CelestialOrbitRegion> orbitRegion(
|
||||
List<CelestialOrbitRegion> const& orbitRegions, int planetaryOrbitNumber);
|
||||
|
||||
CelestialChunk const& getChunk(Vec2I const& chunkLocation);
|
||||
|
||||
CelestialChunk produceChunk(Vec2I const& chunkLocation) const;
|
||||
Maybe<pair<CelestialParameters, HashMap<int, CelestialPlanet>>> produceSystem(
|
||||
RandomSource& random, Vec3I const& location) const;
|
||||
List<CelestialConstellation> produceConstellations(
|
||||
RandomSource& random, List<Vec2I> const& constellationCandidates) const;
|
||||
|
||||
GenerationInformation m_generationInformation;
|
||||
|
||||
mutable RecursiveMutex m_mutex;
|
||||
|
||||
HashTtlCache<Vec2I, CelestialChunk> m_chunkCache;
|
||||
BTreeSha256Database m_database;
|
||||
|
||||
float m_commitInterval;
|
||||
Timer m_commitTimer;
|
||||
};
|
||||
|
||||
class CelestialSlaveDatabase : public CelestialDatabase {
|
||||
public:
|
||||
CelestialSlaveDatabase(CelestialBaseInformation baseInformation);
|
||||
|
||||
// Signal that the given region should be requested from the master database.
|
||||
void signalRegion(RectI const& region);
|
||||
|
||||
// Signal that the given system should be fully requested from the master
|
||||
// database.
|
||||
void signalSystem(CelestialCoordinate const& system);
|
||||
|
||||
// There is an internal activity time for chunk requests to live to prevent
|
||||
// repeatedly requesting the same set of chunks.
|
||||
List<CelestialRequest> pullRequests();
|
||||
void pushResponses(List<CelestialResponse> responses);
|
||||
|
||||
// Unload data that has not been used in the configured TTL time.
|
||||
void cleanup();
|
||||
|
||||
Maybe<CelestialParameters> parameters(CelestialCoordinate const& coordinate) override;
|
||||
Maybe<String> name(CelestialCoordinate const& coordinate) override;
|
||||
|
||||
Maybe<bool> hasChildren(CelestialCoordinate const& coordinate) override;
|
||||
List<CelestialCoordinate> children(CelestialCoordinate const& coordinate) override;
|
||||
List<int> childOrbits(CelestialCoordinate const& coordinate) override;
|
||||
|
||||
List<CelestialCoordinate> scanSystems(RectI const& region, Maybe<StringSet> const& includedTypes = {}) override;
|
||||
List<pair<Vec2I, Vec2I>> scanConstellationLines(RectI const& region) override;
|
||||
|
||||
bool scanRegionFullyLoaded(RectI const& region) override;
|
||||
|
||||
void invalidateCacheFor(CelestialCoordinate const& coordinate);
|
||||
|
||||
private:
|
||||
float m_requestTimeout;
|
||||
|
||||
mutable RecursiveMutex m_mutex;
|
||||
HashTtlCache<Vec2I, CelestialChunk> m_chunkCache;
|
||||
HashMap<Vec2I, Timer> m_pendingChunkRequests;
|
||||
HashMap<Vec3I, Timer> m_pendingSystemRequests;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
288
source/game/StarCelestialGraphics.cpp
Normal file
288
source/game/StarCelestialGraphics.cpp
Normal file
|
@ -0,0 +1,288 @@
|
|||
#include "StarCelestialGraphics.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarFormat.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
#include "StarCelestialDatabase.hpp"
|
||||
#include "StarParallax.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarBiomeDatabase.hpp"
|
||||
#include "StarTerrainDatabase.hpp"
|
||||
#include "StarLiquidsDatabase.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawSystemPlanetaryObject(CelestialParameters const& parameters) {
|
||||
return {{parameters.getParameter("smallImage").toString(), parameters.getParameter("smallImageScale").toFloat()}};
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawSystemCentralBody(CelestialParameters const& parameters) {
|
||||
return {{parameters.getParameter("image").toString(), parameters.getParameter("imageScale").toFloat()}};
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawWorld(
|
||||
CelestialParameters const& celestialParameters, Maybe<CelestialParameters> const& overrideShadowParameters) {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
auto liquidsDatabase = root.liquidsDatabase();
|
||||
|
||||
CelestialParameters shadowParameters = overrideShadowParameters.value(celestialParameters);
|
||||
|
||||
String type = celestialParameters.getParameter("worldType").toString();
|
||||
|
||||
List<pair<String, float>> layers;
|
||||
|
||||
if (type == "Terrestrial") {
|
||||
auto terrestrialParameters = as<TerrestrialWorldParameters>(celestialParameters.visitableParameters());
|
||||
if (!terrestrialParameters)
|
||||
return {};
|
||||
|
||||
auto gfxConfig = jsonMerge(assets->json("/celestial.config:terrestrialGraphics").get("default"),
|
||||
assets->json("/celestial.config:terrestrialGraphics").get(terrestrialParameters->typeName, JsonObject()));
|
||||
|
||||
auto liquidImages = gfxConfig.getString("liquidImages", "");
|
||||
auto baseImages = gfxConfig.getString("baseImages", "");
|
||||
auto shadowImages = gfxConfig.getString("shadowImages", "");
|
||||
auto baseCount = gfxConfig.getInt("baseCount", 0);
|
||||
auto dynamicsImages = gfxConfig.getString("dynamicsImages", "");
|
||||
float imageScale = celestialParameters.getParameter("imageScale", 1.0f).toFloat();
|
||||
|
||||
// If the planet has water, then draw the corresponding water image as the
|
||||
// base layer, otherwise use the bottom most mask image.
|
||||
if (terrestrialParameters->primarySurfaceLiquid != EmptyLiquidId && !liquidImages.empty()) {
|
||||
String liquidBaseImage = liquidImages.replace("<liquid>", liquidsDatabase->liquidName(terrestrialParameters->primarySurfaceLiquid));
|
||||
layers.append({move(liquidBaseImage), imageScale});
|
||||
} else {
|
||||
if (baseCount > 0) {
|
||||
String baseLayer = strf("%s?hueshift=%s", baseImages.replace("<biome>",
|
||||
terrestrialParameters->primaryBiome).replace("<num>", toString(baseCount)), terrestrialParameters->hueShift);
|
||||
layers.append({move(baseLayer), imageScale});
|
||||
}
|
||||
}
|
||||
|
||||
// Then draw all the biome layers
|
||||
for (int i = 0; i < baseCount; ++i) {
|
||||
String baseImage = baseImages.replace("<num>", toString(baseCount - i));
|
||||
String hueShiftString, dynamicMaskString;
|
||||
if (!dynamicsImages.empty())
|
||||
dynamicMaskString = "?addmask=" + dynamicsImages.replace("<num>", toString(celestialParameters.randomizeParameterRange(gfxConfig.getArray("dynamicsRange"), i).toInt()));
|
||||
if (terrestrialParameters->hueShift != 0)
|
||||
hueShiftString = strf("?hueshift=%s", terrestrialParameters->hueShift);
|
||||
String layer = baseImage + hueShiftString + dynamicMaskString;
|
||||
layers.append({move(layer), imageScale});
|
||||
}
|
||||
|
||||
if (!shadowImages.empty()) {
|
||||
String shadow = shadowImages.replace("<num>", toString(shadowParameters.randomizeParameterRange(gfxConfig.getArray("shadowNumber")).toInt()));
|
||||
layers.append({move(shadow), imageScale});
|
||||
}
|
||||
|
||||
} else if (type == "Asteroids") {
|
||||
String maskImages = celestialParameters.getParameter("maskImages").toString();
|
||||
int maskCount = celestialParameters.getParameter("masks").toInt();
|
||||
String dynamicsImages = celestialParameters.getParameter("dynamicsImages").toString();
|
||||
float imageScale = celestialParameters.getParameter("imageScale", 1.0f).toFloat();
|
||||
|
||||
for (int i = 0; i < maskCount; ++i) {
|
||||
String biomeMaskBase = maskImages.replace("<num>", toString(maskCount - i));
|
||||
String dynamicMask = dynamicsImages.replace("<num>", toString(celestialParameters.randomizeParameterRange("dynamicsRange", i).toInt()));
|
||||
String layer = strf("%s?addmask=%s", biomeMaskBase, dynamicMask);
|
||||
layers.append({move(layer), imageScale});
|
||||
}
|
||||
|
||||
} else if (type == "FloatingDungeon") {
|
||||
String image = celestialParameters.getParameter("image").toString();
|
||||
float imageScale = celestialParameters.getParameter("imageScale", 1.0f).toFloat();
|
||||
layers.append({move(image), imageScale});
|
||||
|
||||
if (!celestialParameters.getParameter("dynamicsImages").toString().empty()) {
|
||||
String dynamicsImages = celestialParameters.getParameter("dynamicsImages", "").toString();
|
||||
String dynamicsImage = dynamicsImages.replace("<num>", toString(celestialParameters.randomizeParameterRange("dynamicsRange").toInt()));
|
||||
layers.append({move(dynamicsImage), imageScale});
|
||||
}
|
||||
|
||||
} else if (type == "GasGiant") {
|
||||
auto gfxConfig = assets->json("/celestial.config:gasGiantGraphics");
|
||||
|
||||
auto baseImage = gfxConfig.getString("baseImage", "");
|
||||
auto shadowImages = gfxConfig.getString("shadowImages", "");
|
||||
auto dynamicsImages = gfxConfig.getString("dynamicsImages", "");
|
||||
auto overlayImages = gfxConfig.getString("overlayImages", "");
|
||||
auto overlayCount = gfxConfig.getInt("overlayCount", 0);
|
||||
float imageScale = celestialParameters.getParameter("imageScale", 1.0f).toFloat();
|
||||
|
||||
float hueShift = celestialParameters.randomizeParameterRange(gfxConfig.getArray("primaryHueShiftRange")).toFloat();
|
||||
if (!baseImage.empty())
|
||||
layers.append({strf("%s?hueshift=%s", baseImage, hueShift), imageScale});
|
||||
|
||||
if (!overlayImages.empty()) {
|
||||
for (int i = 0; i < overlayCount; ++i) {
|
||||
hueShift += celestialParameters.randomizeParameterRange(gfxConfig.getArray("hueShiftOffsetRange")).toFloat();
|
||||
String maskImage = dynamicsImages.replace("<num>", toString(celestialParameters.randomizeParameterRange(gfxConfig.getArray("dynamicsRange"), i).toInt()));
|
||||
String overlayImage = overlayImages.replace("<num>", toString(i));
|
||||
layers.append({strf("%s?hueshift=%s?addmask=%s", overlayImage, hueShift, maskImage), imageScale});
|
||||
}
|
||||
}
|
||||
|
||||
if (!shadowImages.empty()) {
|
||||
String shadow = shadowImages.replace("<num>", toString(shadowParameters.randomizeParameterRange(gfxConfig.getArray("shadowNumber")).toInt()));
|
||||
layers.append({move(shadow), imageScale});
|
||||
}
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
List<pair<String, String>> CelestialGraphics::worldHorizonImages(CelestialParameters const& celestialParameters) {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
auto liquidsDatabase = root.liquidsDatabase();
|
||||
|
||||
auto getLR = [](String const& base) -> pair<String, String> {
|
||||
return pair<String, String>(base.replace("<selector>", "l"), base.replace("<selector>", "r"));
|
||||
};
|
||||
|
||||
String type = celestialParameters.getParameter("worldType").toString();
|
||||
|
||||
List<pair<String, String>> res;
|
||||
|
||||
if (type == "Terrestrial") {
|
||||
auto terrestrialParameters = as<TerrestrialWorldParameters>(celestialParameters.visitableParameters());
|
||||
if (!terrestrialParameters)
|
||||
return {};
|
||||
|
||||
auto gfxConfig = jsonMerge(assets->json("/celestial.config:terrestrialHorizonGraphics").get("default"),
|
||||
assets->json("/celestial.config:terrestrialHorizonGraphics").get(terrestrialParameters->typeName, JsonObject()));
|
||||
|
||||
String baseImages = gfxConfig.getString("baseImages");
|
||||
String atmoTextures = gfxConfig.getString("atmosphereTextures");
|
||||
String shadowTextures = gfxConfig.getString("shadowTextures");
|
||||
String maskTextures = gfxConfig.getString("maskTextures");
|
||||
String liquidTextures = gfxConfig.getString("liquidTextures");
|
||||
auto numMasks = jsonToVec2I(gfxConfig.get("maskRange"));
|
||||
auto maskPerPlanetRange = jsonToVec2I(gfxConfig.get("maskPerPlanetRange"));
|
||||
|
||||
auto biomeHueShift = "?" + imageOperationToString(HueShiftImageOperation::hueShiftDegrees(terrestrialParameters->hueShift));
|
||||
|
||||
if (terrestrialParameters->primarySurfaceLiquid != EmptyLiquidId) {
|
||||
auto seed = celestialParameters.seed();
|
||||
RandomSource rand(seed);
|
||||
|
||||
int numPlanetMasks = rand.randInt(maskPerPlanetRange[0], maskPerPlanetRange[1]);
|
||||
List<int> masks;
|
||||
for (int i = 0; i < numPlanetMasks; ++i)
|
||||
masks.append(rand.randInt(numMasks[0], numMasks[1]));
|
||||
|
||||
String liquidBase = liquidTextures.replace("<liquid>", liquidsDatabase->liquidName(terrestrialParameters->primarySurfaceLiquid));
|
||||
res.append(getLR(liquidBase));
|
||||
|
||||
StringList planetMaskListL;
|
||||
StringList planetMaskListR;
|
||||
for (auto m : masks) {
|
||||
String base = maskTextures.replace("<mask>", toString(m));
|
||||
auto lr = getLR(base);
|
||||
planetMaskListL.append(lr.first);
|
||||
planetMaskListR.append(lr.second);
|
||||
}
|
||||
|
||||
String leftMask, rightMask;
|
||||
if (!planetMaskListL.empty())
|
||||
leftMask = "?" + imageOperationToString(AlphaMaskImageOperation{AlphaMaskImageOperation::Additive, planetMaskListL, {0, 0}});
|
||||
if (!planetMaskListR.empty())
|
||||
rightMask = "?" + imageOperationToString(AlphaMaskImageOperation{AlphaMaskImageOperation::Additive, planetMaskListR, {0, 0}});
|
||||
|
||||
auto toAppend = getLR(baseImages + biomeHueShift);
|
||||
res.append({toAppend.first + leftMask, toAppend.second + rightMask});
|
||||
} else {
|
||||
res.append(getLR(baseImages + biomeHueShift));
|
||||
}
|
||||
|
||||
if (celestialParameters.getParameter("atmosphere", true).toBool())
|
||||
res.append(getLR(atmoTextures));
|
||||
|
||||
res.append(getLR(shadowTextures));
|
||||
|
||||
} else if (type == "Asteroids") {
|
||||
res.append(getLR(assets->json("/celestial.config:asteroidsHorizons").toString()));
|
||||
|
||||
} else if (type == "FloatingDungeon") {
|
||||
auto dungeonParameters = as<FloatingDungeonWorldParameters>(celestialParameters.visitableParameters());
|
||||
auto dungeonHorizons = assets->json("/celestial.config:floatingDungeonHorizons");
|
||||
if (dungeonHorizons.contains(dungeonParameters->primaryDungeon))
|
||||
res.append(getLR(dungeonHorizons.get(dungeonParameters->primaryDungeon).toString()));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int CelestialGraphics::worldRadialPosition(CelestialParameters const& parameters) {
|
||||
if (parameters.coordinate().isPlanetaryBody())
|
||||
return staticRandomU32(parameters.seed(), "RadialNumber") % planetRadialPositions();
|
||||
if (parameters.coordinate().isSatelliteBody())
|
||||
return staticRandomU32(parameters.seed(), "RadialNumber") % satelliteRadialPositions();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CelestialGraphics::planetRadialPositions() {
|
||||
return Root::singleton().assets()->json("/celestial.config:planetRadialSlots").toInt();
|
||||
}
|
||||
|
||||
int CelestialGraphics::satelliteRadialPositions() {
|
||||
return Root::singleton().assets()->json("/celestial.config:satelliteRadialSlots").toInt();
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawSystemTwinkle(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& system, double time) {
|
||||
auto parameters = celestialDatabase->parameters(system);
|
||||
if (!parameters)
|
||||
return {};
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
int twinkleFrameCount = assets->json("/celestial.config:twinkleFrames").toInt();
|
||||
float twinkleScale = assets->json("/celestial.config:twinkleScale").toFloat();
|
||||
String twinkleFrameset = parameters->getParameter("twinkleFrames").toString();
|
||||
float twinkleTime = parameters->randomizeParameterRange("twinkleTime").toFloat();
|
||||
String twinkleBackground = parameters->getParameter("twinkleBackground").toString();
|
||||
|
||||
String twinkleFrame = strf("%s:%s", twinkleFrameset, (int)(std::fmod<double>(time / twinkleTime, 1.0f) * twinkleFrameCount));
|
||||
|
||||
return {{move(twinkleBackground), 1.0f}, {move(twinkleFrame), twinkleScale}};
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawSystemPlanetaryObject(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate) {
|
||||
if (auto params = celestialDatabase->parameters(coordinate))
|
||||
return drawSystemPlanetaryObject(params.take());
|
||||
return {};
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawSystemCentralBody(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate) {
|
||||
if (auto params = celestialDatabase->parameters(coordinate))
|
||||
return drawSystemCentralBody(params.take());
|
||||
return {};
|
||||
}
|
||||
|
||||
List<pair<String, float>> CelestialGraphics::drawWorld(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate) {
|
||||
auto params = celestialDatabase->parameters(coordinate);
|
||||
if (!params)
|
||||
return {};
|
||||
|
||||
if (coordinate.isSatelliteBody())
|
||||
return drawWorld(params.take(), celestialDatabase->parameters(coordinate.parent()));
|
||||
else
|
||||
return drawWorld(params.take());
|
||||
}
|
||||
|
||||
List<pair<String, String>> CelestialGraphics::worldHorizonImages(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate) {
|
||||
if (auto params = celestialDatabase->parameters(coordinate))
|
||||
return worldHorizonImages(params.take());
|
||||
return {};
|
||||
}
|
||||
|
||||
int CelestialGraphics::worldRadialPosition(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate) {
|
||||
if (auto params = celestialDatabase->parameters(coordinate))
|
||||
return worldRadialPosition(params.take());
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
60
source/game/StarCelestialGraphics.hpp
Normal file
60
source/game/StarCelestialGraphics.hpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#ifndef STAR_CELESTIAL_GENERATION_HPP
|
||||
#define STAR_CELESTIAL_GENERATION_HPP
|
||||
|
||||
#include "StarCelestialDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CelestialDatabase);
|
||||
|
||||
// Functions for generating and drawing worlds from a celestial database.
|
||||
// Guards against drawing unloaded celestial objects, will return empty if no
|
||||
// information is returned from the celestial database.
|
||||
//
|
||||
// Drawing methods return the stack of images to draw and the scale to draw
|
||||
// them at.
|
||||
class CelestialGraphics {
|
||||
public:
|
||||
// Some static versions of drawing functions are given that do not require an
|
||||
// active CelestialDatabasePtr to draw.
|
||||
|
||||
static List<pair<String, float>> drawSystemPlanetaryObject(CelestialParameters const& celestialParameters);
|
||||
static List<pair<String, float>> drawSystemCentralBody(CelestialParameters const& celestialParameters);
|
||||
|
||||
// Specify the shadowing parameters in order to use the shadowing
|
||||
// information from that body instead of the primary one.
|
||||
static List<pair<String, float>> drawWorld(
|
||||
CelestialParameters const& celestialParameters, Maybe<CelestialParameters> const& shadowingParameters = {});
|
||||
static List<pair<String, String>> worldHorizonImages(CelestialParameters const& celestialParameters);
|
||||
static int worldRadialPosition(CelestialParameters const& celestialParameters);
|
||||
|
||||
// Each orbiting body will occupy a unique orbital slot, but to give
|
||||
// graphical diversity, will also fit into exactly one radial slot for
|
||||
// display purposes. The range of radial numbers is [0, RadialPosiitons)
|
||||
static int planetRadialPositions();
|
||||
static int satelliteRadialPositions();
|
||||
|
||||
static List<pair<String, float>> drawSystemTwinkle(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& system, double twinkleTime);
|
||||
|
||||
// Returns the small graphic for the given planetary object appropriate for a
|
||||
// system-level view.
|
||||
static List<pair<String, float>> drawSystemPlanetaryObject(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate);
|
||||
static List<pair<String, float>> drawSystemCentralBody(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate);
|
||||
|
||||
// Returns the graphics appropriate to draw an entire world (planetary object
|
||||
// or satellite object) in a map view. Shadows the satellite the same as
|
||||
// its parent planetary object.
|
||||
static List<pair<String, float>> drawWorld(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate);
|
||||
|
||||
// Draw all of the left and right image pairs for all the layers for the
|
||||
// world horizon.
|
||||
static List<pair<String, String>> worldHorizonImages(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate);
|
||||
|
||||
static int worldRadialPosition(CelestialDatabasePtr celestialDatabase, CelestialCoordinate const& coordinate);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
131
source/game/StarCelestialParameters.cpp
Normal file
131
source/game/StarCelestialParameters.cpp
Normal file
|
@ -0,0 +1,131 @@
|
|||
#include "StarCelestialParameters.hpp"
|
||||
#include "StarStaticRandom.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamDevices.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarWeatherTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CelestialParameters::CelestialParameters() : m_seed(0) {}
|
||||
|
||||
CelestialParameters::CelestialParameters(CelestialCoordinate coordinate, uint64_t seed, String name, Json parameters)
|
||||
: m_coordinate(move(coordinate)), m_seed(seed), m_name(move(name)), m_parameters(move(parameters)) {
|
||||
if (auto worldType = getParameter("worldType").optString()) {
|
||||
if (worldType->equalsIgnoreCase("Terrestrial")) {
|
||||
auto worldSize = getParameter("worldSize").toString();
|
||||
auto type = randomizeParameterList("terrestrialType").toString();
|
||||
m_visitableParameters = generateTerrestrialWorldParameters(type, worldSize, m_seed);
|
||||
} else if (worldType->equalsIgnoreCase("Asteroids")) {
|
||||
m_visitableParameters = generateAsteroidsWorldParameters(m_seed);
|
||||
} else if (worldType->equalsIgnoreCase("FloatingDungeon")) {
|
||||
m_visitableParameters = generateFloatingDungeonWorldParameters(getParameter("dungeonWorld").toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CelestialParameters::CelestialParameters(ByteArray netStore) {
|
||||
DataStreamBuffer ds(move(netStore));
|
||||
ds >> m_coordinate;
|
||||
ds >> m_seed;
|
||||
ds >> m_name;
|
||||
ds >> m_parameters;
|
||||
m_visitableParameters = netLoadVisitableWorldParameters(ds.read<ByteArray>());
|
||||
}
|
||||
|
||||
CelestialParameters::CelestialParameters(Json const& variant) {
|
||||
m_coordinate = CelestialCoordinate(variant.get("coordinate"));
|
||||
m_seed = variant.getUInt("seed");
|
||||
m_name = variant.getString("name");
|
||||
m_parameters = variant.get("parameters");
|
||||
m_visitableParameters = diskLoadVisitableWorldParameters(variant.get("visitableParameters"));
|
||||
}
|
||||
|
||||
Json CelestialParameters::diskStore() const {
|
||||
return JsonObject{{"coordinate", m_coordinate.toJson()},
|
||||
{"seed", m_seed},
|
||||
{"name", m_name},
|
||||
{"parameters", m_parameters},
|
||||
{"visitableParameters", diskStoreVisitableWorldParameters(m_visitableParameters)}};
|
||||
}
|
||||
|
||||
ByteArray CelestialParameters::netStore() const {
|
||||
DataStreamBuffer ds;
|
||||
ds << m_coordinate;
|
||||
ds << m_seed;
|
||||
ds << m_name;
|
||||
ds << m_parameters;
|
||||
ds.write(netStoreVisitableWorldParameters(m_visitableParameters));
|
||||
|
||||
return ds.takeData();
|
||||
}
|
||||
|
||||
CelestialCoordinate CelestialParameters::coordinate() const {
|
||||
return m_coordinate;
|
||||
}
|
||||
|
||||
String CelestialParameters::name() const {
|
||||
return m_name;
|
||||
}
|
||||
|
||||
uint64_t CelestialParameters::seed() const {
|
||||
return m_seed;
|
||||
}
|
||||
|
||||
Json CelestialParameters::parameters() const {
|
||||
return m_parameters;
|
||||
}
|
||||
|
||||
Json CelestialParameters::getParameter(String const& name, Json def) const {
|
||||
return m_parameters.get(name, move(def));
|
||||
}
|
||||
|
||||
Json CelestialParameters::randomizeParameterList(String const& name, int32_t mix) const {
|
||||
auto parameter = getParameter(name);
|
||||
if (parameter.isNull())
|
||||
return Json();
|
||||
return staticRandomFrom(parameter.toArray(), mix, m_seed, name);
|
||||
}
|
||||
|
||||
Json CelestialParameters::randomizeParameterRange(String const& name, int32_t mix) const {
|
||||
auto parameter = getParameter(name);
|
||||
if (parameter.isNull()) {
|
||||
return Json();
|
||||
} else {
|
||||
JsonArray list = parameter.toArray();
|
||||
if (list.size() != 2)
|
||||
throw CelestialException(
|
||||
strf("Parameter '%s' does not appear to be a range in CelestialParameters::randomizeRange", name));
|
||||
|
||||
return randomizeParameterRange(list, mix, name);
|
||||
}
|
||||
}
|
||||
|
||||
Json CelestialParameters::randomizeParameterRange(
|
||||
JsonArray const& range, int32_t mix, Maybe<String> const& name) const {
|
||||
if (range[0].type() == Json::Type::Int) {
|
||||
int64_t min = range[0].toInt();
|
||||
int64_t max = range[1].toInt();
|
||||
return staticRandomU64(mix, m_seed, name.value("")) % (max - min + 1) + min;
|
||||
} else {
|
||||
double min = range[0].toDouble();
|
||||
double max = range[1].toDouble();
|
||||
return staticRandomDouble(mix, m_seed, name.value("")) * (max - min) + min;
|
||||
}
|
||||
}
|
||||
|
||||
bool CelestialParameters::isVisitable() const {
|
||||
return (bool)m_visitableParameters;
|
||||
}
|
||||
|
||||
VisitableWorldParametersConstPtr CelestialParameters::visitableParameters() const {
|
||||
return m_visitableParameters;
|
||||
}
|
||||
|
||||
void CelestialParameters::setVisitableParameters(VisitableWorldParametersPtr const& newVisitableParameters) {
|
||||
m_visitableParameters = newVisitableParameters;
|
||||
}
|
||||
|
||||
}
|
52
source/game/StarCelestialParameters.hpp
Normal file
52
source/game/StarCelestialParameters.hpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#ifndef STAR_CELESTIAL_PARAMETERS_HPP
|
||||
#define STAR_CELESTIAL_PARAMETERS_HPP
|
||||
|
||||
#include "StarCelestialCoordinate.hpp"
|
||||
#include "StarWorldParameters.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CelestialParameters);
|
||||
|
||||
class CelestialParameters {
|
||||
public:
|
||||
CelestialParameters();
|
||||
CelestialParameters(CelestialCoordinate coordinate, uint64_t seed, String name, Json parameters);
|
||||
explicit CelestialParameters(Json const& diskStore);
|
||||
explicit CelestialParameters(ByteArray netStore);
|
||||
|
||||
Json diskStore() const;
|
||||
ByteArray netStore() const;
|
||||
|
||||
CelestialCoordinate coordinate() const;
|
||||
String name() const;
|
||||
uint64_t seed() const;
|
||||
|
||||
Json parameters() const;
|
||||
Json getParameter(String const& name, Json def = Json()) const;
|
||||
// Predictably select from a json array, given by the named parameter.
|
||||
// Selects based on the name hash and the system seed.
|
||||
Json randomizeParameterList(String const& name, int32_t mix = 0) const;
|
||||
// Predictably select from a range, given by the named parameter. Works for
|
||||
// either floating or integral ranges.
|
||||
Json randomizeParameterRange(String const& name, int32_t mix = 0) const;
|
||||
// Same function, but if you want to specify the range from an external source
|
||||
Json randomizeParameterRange(JsonArray const& range, int32_t mix = 0, Maybe<String> const& name = {}) const;
|
||||
|
||||
// Not all worlds are visitable, if the world is not visitable its
|
||||
// visitableParameters will be empty.
|
||||
bool isVisitable() const;
|
||||
VisitableWorldParametersConstPtr visitableParameters() const;
|
||||
void setVisitableParameters(VisitableWorldParametersPtr const& newVisitableParameters);
|
||||
|
||||
private:
|
||||
CelestialCoordinate m_coordinate;
|
||||
uint64_t m_seed;
|
||||
String m_name;
|
||||
Json m_parameters;
|
||||
VisitableWorldParametersConstPtr m_visitableParameters;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
152
source/game/StarCelestialTypes.cpp
Normal file
152
source/game/StarCelestialTypes.cpp
Normal file
|
@ -0,0 +1,152 @@
|
|||
#include "StarCelestialTypes.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DataStream& operator>>(DataStream& ds, CelestialPlanet& planet) {
|
||||
planet.planetParameters = CelestialParameters(ds.read<ByteArray>());
|
||||
ds.readMapContainer(planet.satelliteParameters,
|
||||
[](DataStream& ds, int& orbit, CelestialParameters& parameters) {
|
||||
ds.read(orbit);
|
||||
parameters = CelestialParameters(ds.read<ByteArray>());
|
||||
});
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, CelestialPlanet const& planet) {
|
||||
ds.write(planet.planetParameters.netStore());
|
||||
ds.writeMapContainer(planet.satelliteParameters,
|
||||
[](DataStream& ds, int orbit, CelestialParameters const& parameters) {
|
||||
ds.write(orbit);
|
||||
ds.write(parameters.netStore());
|
||||
});
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, CelestialSystemObjects& systemObjects) {
|
||||
ds.read(systemObjects.systemLocation);
|
||||
ds.read(systemObjects.planets);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, CelestialSystemObjects const& systemObjects) {
|
||||
ds.write(systemObjects.systemLocation);
|
||||
ds.write(systemObjects.planets);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
CelestialChunk::CelestialChunk() {}
|
||||
|
||||
CelestialChunk::CelestialChunk(Json const& store) {
|
||||
chunkIndex = jsonToVec2I(store.get("chunkIndex"));
|
||||
|
||||
for (auto const& lines : store.getArray("constellations")) {
|
||||
CelestialConstellation constellation;
|
||||
for (auto const& l : lines.toArray())
|
||||
constellation.append({jsonToVec2I(l.get(0)), jsonToVec2I(l.get(1))});
|
||||
constellations.append(move(constellation));
|
||||
}
|
||||
|
||||
for (auto const& p : store.getArray("systemParameters"))
|
||||
systemParameters[jsonToVec3I(p.get(0))] = CelestialParameters(p.get(1));
|
||||
|
||||
for (auto const& p : store.getArray("systemObjects")) {
|
||||
HashMap<int, CelestialPlanet> celestialSystemObjects;
|
||||
for (auto const& planetPair : p.getArray(1)) {
|
||||
CelestialPlanet planet;
|
||||
planet.planetParameters = CelestialParameters(planetPair.get(1).get("parameters"));
|
||||
for (auto const& satellitePair : planetPair.get(1).getArray("satellites"))
|
||||
planet.satelliteParameters.add(satellitePair.getInt(0), CelestialParameters(satellitePair.get(1)));
|
||||
celestialSystemObjects.add(planetPair.getInt(0), move(planet));
|
||||
}
|
||||
|
||||
systemObjects[jsonToVec3I(p.get(0))] = move(celestialSystemObjects);
|
||||
}
|
||||
}
|
||||
|
||||
Json CelestialChunk::toJson() const {
|
||||
JsonArray constellationStore;
|
||||
for (auto const& constellation : constellations) {
|
||||
JsonArray lines;
|
||||
for (auto const& p : constellation)
|
||||
lines.append(JsonArray{jsonFromVec2I(p.first), jsonFromVec2I(p.second)});
|
||||
constellationStore.append(lines);
|
||||
}
|
||||
|
||||
JsonArray systemParametersStore;
|
||||
for (auto const& p : systemParameters)
|
||||
systemParametersStore.append(JsonArray{jsonFromVec3I(p.first), p.second.diskStore()});
|
||||
|
||||
JsonArray systemObjectsStore;
|
||||
for (auto const& systemObjectPair : systemObjects) {
|
||||
JsonArray planetsStore;
|
||||
for (auto const& planetPair : systemObjectPair.second) {
|
||||
JsonArray satellitesStore;
|
||||
for (auto const& satellitePair : planetPair.second.satelliteParameters)
|
||||
satellitesStore.append(JsonArray{satellitePair.first, satellitePair.second.diskStore()});
|
||||
|
||||
planetsStore.append(JsonArray{planetPair.first,
|
||||
JsonObject{
|
||||
{"parameters", planetPair.second.planetParameters.diskStore()}, {"satellites", move(satellitesStore)}}});
|
||||
}
|
||||
systemObjectsStore.append(JsonArray{jsonFromVec3I(systemObjectPair.first), move(planetsStore)});
|
||||
}
|
||||
|
||||
return JsonObject{{"chunkIndex", jsonFromVec2I(chunkIndex)},
|
||||
{"constellations", constellationStore},
|
||||
{"systemParameters", systemParametersStore},
|
||||
{"systemObjects", systemObjectsStore}};
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, CelestialChunk& chunk) {
|
||||
ds.read(chunk.chunkIndex);
|
||||
ds.read(chunk.constellations);
|
||||
ds.readMapContainer(chunk.systemParameters,
|
||||
[](DataStream& ds, Vec3I& location, CelestialParameters& parameters) {
|
||||
ds.read(location);
|
||||
parameters = CelestialParameters(ds.read<ByteArray>());
|
||||
});
|
||||
ds.read(chunk.systemObjects);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, CelestialChunk const& chunk) {
|
||||
ds.write(chunk.chunkIndex);
|
||||
ds.write(chunk.constellations);
|
||||
ds.writeMapContainer(chunk.systemParameters,
|
||||
[](DataStream& ds, Vec3I const& location, CelestialParameters const& parameters) {
|
||||
ds.write(location);
|
||||
ds.write(parameters.netStore());
|
||||
});
|
||||
ds.write(chunk.systemObjects);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, CelestialBaseInformation& celestialInformation) {
|
||||
ds.read(celestialInformation.planetOrbitalLevels);
|
||||
ds.read(celestialInformation.satelliteOrbitalLevels);
|
||||
ds.read(celestialInformation.chunkSize);
|
||||
ds.read(celestialInformation.xyCoordRange);
|
||||
ds.read(celestialInformation.zCoordRange);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, CelestialBaseInformation const& celestialInformation) {
|
||||
ds.write(celestialInformation.planetOrbitalLevels);
|
||||
ds.write(celestialInformation.satelliteOrbitalLevels);
|
||||
ds.write(celestialInformation.chunkSize);
|
||||
ds.write(celestialInformation.xyCoordRange);
|
||||
ds.write(celestialInformation.zCoordRange);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
71
source/game/StarCelestialTypes.hpp
Normal file
71
source/game/StarCelestialTypes.hpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#ifndef STAR_CELESTIAL_TYPES_HPP
|
||||
#define STAR_CELESTIAL_TYPES_HPP
|
||||
|
||||
#include "StarOrderedMap.hpp"
|
||||
#include "StarRect.hpp"
|
||||
#include "StarEither.hpp"
|
||||
#include "StarCelestialParameters.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_STRUCT(CelestialSystemObjects);
|
||||
STAR_STRUCT(CelestialChunk);
|
||||
STAR_STRUCT(CelestialBaseInformation);
|
||||
|
||||
typedef List<pair<Vec2I, Vec2I>> CelestialConstellation;
|
||||
|
||||
struct CelestialOrbitRegion {
|
||||
String regionName;
|
||||
Vec2I orbitRange;
|
||||
float bodyProbability;
|
||||
WeightedPool<String> planetaryTypes;
|
||||
WeightedPool<String> satelliteTypes;
|
||||
};
|
||||
|
||||
struct CelestialPlanet {
|
||||
CelestialParameters planetParameters;
|
||||
HashMap<int, CelestialParameters> satelliteParameters;
|
||||
};
|
||||
DataStream& operator>>(DataStream& ds, CelestialPlanet& planet);
|
||||
DataStream& operator<<(DataStream& ds, CelestialPlanet const& planet);
|
||||
|
||||
struct CelestialSystemObjects {
|
||||
Vec3I systemLocation;
|
||||
HashMap<int, CelestialPlanet> planets;
|
||||
};
|
||||
DataStream& operator>>(DataStream& ds, CelestialSystemObjects& systemObjects);
|
||||
DataStream& operator<<(DataStream& ds, CelestialSystemObjects const& systemObjects);
|
||||
|
||||
struct CelestialChunk {
|
||||
CelestialChunk();
|
||||
CelestialChunk(Json const& store);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
Vec2I chunkIndex;
|
||||
List<CelestialConstellation> constellations;
|
||||
HashMap<Vec3I, CelestialParameters> systemParameters;
|
||||
|
||||
// System objects are kept separate from systemParameters here so that there
|
||||
// can be two phases of loading, one for basic system-level parameters for an
|
||||
// entire chunk the other for each set of sub objects for each system.
|
||||
HashMap<Vec3I, HashMap<int, CelestialPlanet>> systemObjects;
|
||||
};
|
||||
DataStream& operator>>(DataStream& ds, CelestialChunk& chunk);
|
||||
DataStream& operator<<(DataStream& ds, CelestialChunk const& chunk);
|
||||
|
||||
typedef Either<Vec2I, Vec3I> CelestialRequest;
|
||||
typedef Either<CelestialChunk, CelestialSystemObjects> CelestialResponse;
|
||||
|
||||
struct CelestialBaseInformation {
|
||||
int planetOrbitalLevels;
|
||||
int satelliteOrbitalLevels;
|
||||
int chunkSize;
|
||||
Vec2I xyCoordRange;
|
||||
Vec2I zCoordRange;
|
||||
};
|
||||
DataStream& operator>>(DataStream& ds, CelestialBaseInformation& celestialInformation);
|
||||
DataStream& operator<<(DataStream& ds, CelestialBaseInformation const& celestialInformation);
|
||||
}
|
||||
|
||||
#endif
|
35
source/game/StarChatAction.cpp
Normal file
35
source/game/StarChatAction.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include "StarChatAction.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
SayChatAction::SayChatAction() {
|
||||
entity = NullEntityId;
|
||||
}
|
||||
|
||||
SayChatAction::SayChatAction(EntityId entity, String const& text, Vec2F const& position)
|
||||
: entity(entity), text(text), position(position) {}
|
||||
|
||||
SayChatAction::SayChatAction(EntityId entity, String const& text, Vec2F const& position, Json const& config)
|
||||
: entity(entity), text(text), position(position), config(config) {}
|
||||
|
||||
SayChatAction::operator bool() const {
|
||||
return !text.empty();
|
||||
}
|
||||
|
||||
PortraitChatAction::PortraitChatAction() {
|
||||
entity = NullEntityId;
|
||||
}
|
||||
|
||||
PortraitChatAction::PortraitChatAction(
|
||||
EntityId entity, String const& portrait, String const& text, Vec2F const& position)
|
||||
: entity(entity), portrait(portrait), text(text), position(position) {}
|
||||
|
||||
PortraitChatAction::PortraitChatAction(
|
||||
EntityId entity, String const& portrait, String const& text, Vec2F const& position, Json const& config)
|
||||
: entity(entity), portrait(portrait), text(text), position(position), config(config) {}
|
||||
|
||||
PortraitChatAction::operator bool() const {
|
||||
return !text.empty();
|
||||
}
|
||||
|
||||
}
|
40
source/game/StarChatAction.hpp
Normal file
40
source/game/StarChatAction.hpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#ifndef STAR_CHAT_ACTION_HPP
|
||||
#define STAR_CHAT_ACTION_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct SayChatAction {
|
||||
SayChatAction();
|
||||
SayChatAction(EntityId entity, String const& text, Vec2F const& position);
|
||||
SayChatAction(EntityId entity, String const& text, Vec2F const& position, Json const& config);
|
||||
|
||||
explicit operator bool() const;
|
||||
|
||||
EntityId entity;
|
||||
String text;
|
||||
Vec2F position;
|
||||
Json config;
|
||||
};
|
||||
|
||||
struct PortraitChatAction {
|
||||
PortraitChatAction();
|
||||
PortraitChatAction(EntityId entity, String const& portrait, String const& text, Vec2F const& position);
|
||||
PortraitChatAction(EntityId entity, String const& portrait, String const& text, Vec2F const& position, Json const& config);
|
||||
|
||||
explicit operator bool() const;
|
||||
|
||||
EntityId entity;
|
||||
String portrait;
|
||||
String text;
|
||||
Vec2F position;
|
||||
Json config;
|
||||
};
|
||||
|
||||
typedef MVariant<SayChatAction, PortraitChatAction> ChatAction;
|
||||
|
||||
}
|
||||
|
||||
#endif
|
257
source/game/StarChatProcessor.cpp
Normal file
257
source/game/StarChatProcessor.cpp
Normal file
|
@ -0,0 +1,257 @@
|
|||
#include "StarChatProcessor.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
char const* ChatProcessor::ServerNick = "server";
|
||||
|
||||
String ChatProcessor::connectClient(ConnectionId clientId, String nick) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (nick.empty())
|
||||
nick = strf("Player_%s", clientId);
|
||||
|
||||
nick = makeNickUnique(nick);
|
||||
|
||||
for (auto& pair : m_clients) {
|
||||
pair.second.pendingMessages.append({
|
||||
{MessageContext::Broadcast},
|
||||
ServerConnectionId,
|
||||
ServerNick,
|
||||
strf("Player '%s' connected", nick)
|
||||
});
|
||||
}
|
||||
|
||||
m_clients.add(clientId, ClientInfo(clientId, nick));
|
||||
m_nicks[nick] = clientId;
|
||||
return nick;
|
||||
}
|
||||
|
||||
List<ChatReceivedMessage> ChatProcessor::disconnectClient(ConnectionId clientId) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
for (auto channel : clientChannels(clientId))
|
||||
leaveChannel(clientId, channel);
|
||||
|
||||
auto clientInfo = m_clients.take(clientId);
|
||||
|
||||
m_nicks.remove(clientInfo.nick);
|
||||
|
||||
for (auto& pair : m_clients) {
|
||||
pair.second.pendingMessages.append({
|
||||
{MessageContext::Broadcast},
|
||||
ServerConnectionId,
|
||||
ServerNick,
|
||||
strf("Player '%s' disconnected", clientInfo.nick)
|
||||
});
|
||||
}
|
||||
|
||||
return clientInfo.pendingMessages;
|
||||
}
|
||||
|
||||
List<ConnectionId> ChatProcessor::clients() const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
return m_clients.keys();
|
||||
}
|
||||
|
||||
bool ChatProcessor::hasClient(ConnectionId clientId) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
return m_clients.contains(clientId);
|
||||
}
|
||||
|
||||
Maybe<ConnectionId> ChatProcessor::findNick(String const& nick) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
if (auto m = m_nicks.maybe(nick))
|
||||
return m;
|
||||
if (nick == ServerNick)
|
||||
return ServerConnectionId;
|
||||
return {};
|
||||
}
|
||||
|
||||
String ChatProcessor::connectionNick(ConnectionId clientId) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (clientId == ServerConnectionId)
|
||||
return ServerNick;
|
||||
else
|
||||
return m_clients.get(clientId).nick;
|
||||
}
|
||||
|
||||
String ChatProcessor::renick(ConnectionId clientId, String const& nick) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
auto& clientInfo = m_clients.get(clientId);
|
||||
m_nicks.remove(clientInfo.nick);
|
||||
|
||||
clientInfo.nick = makeNickUnique(nick);
|
||||
m_clients.get(clientId).nick = nick;
|
||||
m_nicks[nick] = clientId;
|
||||
return nick;
|
||||
}
|
||||
|
||||
bool ChatProcessor::joinChannel(ConnectionId clientId, String const& channelName) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
// Right now channels are simply created on join if they don't exist.
|
||||
return m_channels[channelName].add(clientId);
|
||||
}
|
||||
|
||||
bool ChatProcessor::leaveChannel(ConnectionId clientId, String const& channelName) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
return m_channels[channelName].remove(clientId);
|
||||
}
|
||||
|
||||
StringList ChatProcessor::clientChannels(ConnectionId clientId) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
StringList channels;
|
||||
for (auto const& pair : m_channels) {
|
||||
if (pair.second.contains(clientId))
|
||||
channels.append(pair.first);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
StringList ChatProcessor::activeChannels() const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
StringList channels;
|
||||
for (auto const& pair : m_channels) {
|
||||
if (!pair.second.empty())
|
||||
channels.append(pair.first);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
void ChatProcessor::broadcast(ConnectionId sourceConnectionId, String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
ChatReceivedMessage message = {
|
||||
{MessageContext::Broadcast},
|
||||
sourceConnectionId,
|
||||
connectionNick(sourceConnectionId),
|
||||
text
|
||||
};
|
||||
|
||||
if (handleCommand(message))
|
||||
return;
|
||||
|
||||
for (auto& pair : m_clients)
|
||||
pair.second.pendingMessages.append(message);
|
||||
}
|
||||
|
||||
void ChatProcessor::message(ConnectionId sourceConnectionId, MessageContext::Mode mode, String const& channelName, String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
ChatReceivedMessage message = {
|
||||
{mode, channelName},
|
||||
sourceConnectionId,
|
||||
connectionNick(sourceConnectionId),
|
||||
text
|
||||
};
|
||||
|
||||
if (handleCommand(message))
|
||||
return;
|
||||
|
||||
for (auto clientId : m_channels[channelName]) {
|
||||
auto& clientInfo = m_clients.get(clientId);
|
||||
clientInfo.pendingMessages.append(message);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatProcessor::whisper(ConnectionId sourceConnectionId, ConnectionId targetClientId, String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
ChatReceivedMessage message = {
|
||||
{MessageContext::Whisper}, sourceConnectionId, connectionNick(sourceConnectionId), text};
|
||||
|
||||
if (handleCommand(message))
|
||||
return;
|
||||
|
||||
if (sourceConnectionId != ServerConnectionId)
|
||||
m_clients.get(sourceConnectionId).pendingMessages.append(message);
|
||||
|
||||
m_clients.get(targetClientId).pendingMessages.append(message);
|
||||
}
|
||||
|
||||
void ChatProcessor::adminBroadcast(String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
broadcast(ServerConnectionId, text);
|
||||
}
|
||||
|
||||
void ChatProcessor::adminMessage(MessageContext::Mode context, String const& channelName, String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
ChatProcessor::message(ServerConnectionId, context, channelName, text);
|
||||
}
|
||||
|
||||
void ChatProcessor::adminWhisper(ConnectionId targetClientId, String const& text) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
whisper(ServerConnectionId, targetClientId, text);
|
||||
}
|
||||
|
||||
List<ChatReceivedMessage> ChatProcessor::pullPendingMessages(ConnectionId clientId) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
if (m_clients.contains(clientId))
|
||||
return take(m_clients.get(clientId).pendingMessages);
|
||||
return {};
|
||||
}
|
||||
|
||||
void ChatProcessor::setCommandHandler(CommandHandler commandHandler) {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
m_commandHandler = commandHandler;
|
||||
}
|
||||
|
||||
void ChatProcessor::clearCommandHandler() {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
m_commandHandler = {};
|
||||
}
|
||||
|
||||
ChatProcessor::ClientInfo::ClientInfo(ConnectionId clientId, String const& nick) : clientId(clientId), nick(nick) {}
|
||||
|
||||
String ChatProcessor::makeNickUnique(String nick) {
|
||||
while (m_nicks.contains(nick) || nick == ServerNick)
|
||||
nick.append("_");
|
||||
|
||||
return nick;
|
||||
}
|
||||
|
||||
bool ChatProcessor::handleCommand(ChatReceivedMessage& message) {
|
||||
if (!message.text.beginsWith("/")) {
|
||||
return false;
|
||||
} else if (message.text.beginsWith("//")) {
|
||||
message.text = message.text.substr(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
String commandLine = message.text.substr(1);
|
||||
String command = commandLine.extract();
|
||||
|
||||
String response;
|
||||
|
||||
if (command == "nick") {
|
||||
auto newNick = renick(message.fromConnection, commandLine.trim());
|
||||
response = strf("Nick changed to %s", newNick);
|
||||
} else if (command == "w") {
|
||||
String target = commandLine.extract();
|
||||
if (m_nicks.contains(target))
|
||||
whisper(message.fromConnection, m_nicks.get(target), commandLine.trim());
|
||||
else
|
||||
response = strf("No such nick %s", target);
|
||||
} else if (m_commandHandler) {
|
||||
response = m_commandHandler(message.fromConnection, command, commandLine);
|
||||
} else {
|
||||
response = strf("No such command %s", command);
|
||||
}
|
||||
|
||||
if (!response.empty()) {
|
||||
m_clients.get(message.fromConnection).pendingMessages.append({
|
||||
MessageContext(MessageContext::CommandResult),
|
||||
ServerConnectionId,
|
||||
connectionNick(ServerConnectionId),
|
||||
response
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
86
source/game/StarChatProcessor.hpp
Normal file
86
source/game/StarChatProcessor.hpp
Normal file
|
@ -0,0 +1,86 @@
|
|||
#ifndef STAR_CHAT_PROCESSOR_HPP
|
||||
#define STAR_CHAT_PROCESSOR_HPP
|
||||
|
||||
#include "StarChatTypes.hpp"
|
||||
#include "StarSet.hpp"
|
||||
#include "StarThread.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ChatProcessor);
|
||||
|
||||
// Handles all chat routing and command parsing for client / server chat.
|
||||
// Thread safe.
|
||||
class ChatProcessor {
|
||||
public:
|
||||
static char const* ServerNick;
|
||||
|
||||
// CommandHandler is passed the origin connection, the command portion
|
||||
// excluding the '/' character, and the remaining command line in full.
|
||||
typedef function<String(ConnectionId, String, String)> CommandHandler;
|
||||
|
||||
String connectClient(ConnectionId clientId, String nick = "");
|
||||
// Returns any pending messages.
|
||||
List<ChatReceivedMessage> disconnectClient(ConnectionId clientId);
|
||||
|
||||
List<ConnectionId> clients() const;
|
||||
bool hasClient(ConnectionId clientId) const;
|
||||
|
||||
// Clears all clients and channels
|
||||
void reset();
|
||||
|
||||
// Will return nothing if nick is not found.
|
||||
Maybe<ConnectionId> findNick(String const& nick) const;
|
||||
String connectionNick(ConnectionId connectionId) const;
|
||||
String renick(ConnectionId clientId, String const& nick);
|
||||
|
||||
// join / leave return true in the even that the client channel state was
|
||||
// actually changed.
|
||||
bool joinChannel(ConnectionId clientId, String const& channelName);
|
||||
bool leaveChannel(ConnectionId clientId, String const& channelName);
|
||||
|
||||
StringList clientChannels(ConnectionId clientId) const;
|
||||
StringList activeChannels() const;
|
||||
|
||||
void broadcast(ConnectionId sourceConnectionId, String const& text);
|
||||
void message(ConnectionId sourceConnectionId, MessageContext::Mode context, String const& channelName, String const& text);
|
||||
void whisper(ConnectionId sourceConnectionId, ConnectionId targetClientId, String const& text);
|
||||
|
||||
// Shorthand for passing ServerConnectionId as sourceConnectionId to
|
||||
// broadcast / message / whisper
|
||||
void adminBroadcast(String const& text);
|
||||
void adminMessage(MessageContext::Mode context, String const& channelName, String const& text);
|
||||
void adminWhisper(ConnectionId targetClientId, String const& text);
|
||||
|
||||
List<ChatReceivedMessage> pullPendingMessages(ConnectionId clientId);
|
||||
|
||||
void setCommandHandler(CommandHandler commandHandler);
|
||||
void clearCommandHandler();
|
||||
|
||||
private:
|
||||
struct ClientInfo {
|
||||
ClientInfo(ConnectionId clientId, String const& nick);
|
||||
|
||||
ConnectionId clientId;
|
||||
String nick;
|
||||
List<ChatReceivedMessage> pendingMessages;
|
||||
};
|
||||
|
||||
String makeNickUnique(String nick);
|
||||
|
||||
// Returns true if message was handled completely and needs no further
|
||||
// processing.
|
||||
bool handleCommand(ChatReceivedMessage& message);
|
||||
|
||||
mutable RecursiveMutex m_mutex;
|
||||
|
||||
HashMap<ConnectionId, ClientInfo> m_clients;
|
||||
StringMap<ConnectionId> m_nicks;
|
||||
StringMap<Set<ConnectionId>> m_channels;
|
||||
|
||||
CommandHandler m_commandHandler;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
69
source/game/StarChatTypes.cpp
Normal file
69
source/game/StarChatTypes.cpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "StarChatTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<ChatSendMode> const ChatSendModeNames{
|
||||
{ChatSendMode::Broadcast, "Broadcast"},
|
||||
{ChatSendMode::Local, "Local"},
|
||||
{ChatSendMode::Party, "Party"}
|
||||
};
|
||||
|
||||
MessageContext::MessageContext() : mode() {}
|
||||
|
||||
MessageContext::MessageContext(Mode mode) : mode(mode) {}
|
||||
|
||||
MessageContext::MessageContext(Mode mode, String const& channelName) : mode(mode), channelName(channelName) {}
|
||||
|
||||
EnumMap<MessageContext::Mode> const MessageContextModeNames{
|
||||
{MessageContext::Mode::Local, "Local"},
|
||||
{MessageContext::Mode::Party, "Party"},
|
||||
{MessageContext::Mode::Broadcast, "Broadcast"},
|
||||
{MessageContext::Mode::Whisper, "Whisper"},
|
||||
{MessageContext::Mode::CommandResult, "CommandResult"},
|
||||
{MessageContext::Mode::RadioMessage, "RadioMessage"},
|
||||
{MessageContext::Mode::World, "World"}
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, MessageContext& messageContext) {
|
||||
ds.read(messageContext.mode);
|
||||
ds.read(messageContext.channelName);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, MessageContext const& messageContext) {
|
||||
ds.write(messageContext.mode);
|
||||
ds.write(messageContext.channelName);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
ChatReceivedMessage::ChatReceivedMessage() : fromConnection() {}
|
||||
|
||||
ChatReceivedMessage::ChatReceivedMessage(MessageContext context, ConnectionId fromConnection, String const& fromNick, String const& text)
|
||||
: context(context), fromConnection(fromConnection), fromNick(fromNick), text(text) {}
|
||||
|
||||
ChatReceivedMessage::ChatReceivedMessage(MessageContext context, ConnectionId fromConnection, String const& fromNick, String const& text, String const& portrait)
|
||||
: context(context), fromConnection(fromConnection), fromNick(fromNick), portrait(portrait), text(text) {}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ChatReceivedMessage& receivedMessage) {
|
||||
ds.read(receivedMessage.context);
|
||||
ds.read(receivedMessage.fromConnection);
|
||||
ds.read(receivedMessage.fromNick);
|
||||
ds.read(receivedMessage.portrait);
|
||||
ds.read(receivedMessage.text);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, ChatReceivedMessage const& receivedMessage) {
|
||||
ds.write(receivedMessage.context);
|
||||
ds.write(receivedMessage.fromConnection);
|
||||
ds.write(receivedMessage.fromNick);
|
||||
ds.write(receivedMessage.portrait);
|
||||
ds.write(receivedMessage.text);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
62
source/game/StarChatTypes.hpp
Normal file
62
source/game/StarChatTypes.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#ifndef STAR_CHAT_TYPES_HPP
|
||||
#define STAR_CHAT_TYPES_HPP
|
||||
|
||||
#include "StarDataStream.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
enum class ChatSendMode : uint8_t {
|
||||
Broadcast,
|
||||
Local,
|
||||
Party
|
||||
};
|
||||
|
||||
extern EnumMap<ChatSendMode> const ChatSendModeNames;
|
||||
|
||||
struct MessageContext {
|
||||
enum Mode : uint8_t {
|
||||
Local,
|
||||
Party,
|
||||
Broadcast,
|
||||
Whisper,
|
||||
CommandResult,
|
||||
RadioMessage,
|
||||
World
|
||||
};
|
||||
|
||||
MessageContext();
|
||||
MessageContext(Mode mode);
|
||||
MessageContext(Mode mode, String const& channelName);
|
||||
|
||||
Mode mode;
|
||||
|
||||
// only for Local and Party modes
|
||||
String channelName;
|
||||
};
|
||||
|
||||
extern EnumMap<MessageContext::Mode> const MessageContextModeNames;
|
||||
|
||||
DataStream& operator>>(DataStream& ds, MessageContext& messageContext);
|
||||
DataStream& operator<<(DataStream& ds, MessageContext const& messageContext);
|
||||
|
||||
struct ChatReceivedMessage {
|
||||
ChatReceivedMessage();
|
||||
ChatReceivedMessage(MessageContext context, ConnectionId fromConnection, String const& fromNick, String const& text);
|
||||
ChatReceivedMessage(MessageContext context, ConnectionId fromConnection, String const& fromNick, String const& text, String const& portrait);
|
||||
|
||||
MessageContext context;
|
||||
|
||||
ConnectionId fromConnection;
|
||||
String fromNick;
|
||||
String portrait;
|
||||
|
||||
String text;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ChatReceivedMessage& receivedMessage);
|
||||
DataStream& operator<<(DataStream& ds, ChatReceivedMessage const& receivedMessage);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
94
source/game/StarClientContext.cpp
Normal file
94
source/game/StarClientContext.cpp
Normal file
|
@ -0,0 +1,94 @@
|
|||
#include "StarClientContext.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DataStream& operator>>(DataStream& ds, ShipUpgrades& upgrades) {
|
||||
ds.read(upgrades.shipLevel);
|
||||
ds.read(upgrades.maxFuel);
|
||||
ds.read(upgrades.crewSize);
|
||||
ds.read(upgrades.fuelEfficiency);
|
||||
ds.read(upgrades.shipSpeed);
|
||||
ds.read(upgrades.capabilities);
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, ShipUpgrades const& upgrades) {
|
||||
ds.write(upgrades.shipLevel);
|
||||
ds.write(upgrades.maxFuel);
|
||||
ds.write(upgrades.crewSize);
|
||||
ds.write(upgrades.fuelEfficiency);
|
||||
ds.write(upgrades.shipSpeed);
|
||||
ds.write(upgrades.capabilities);
|
||||
return ds;
|
||||
}
|
||||
|
||||
ClientContext::ClientContext(Uuid serverUuid) {
|
||||
m_serverUuid = move(serverUuid);
|
||||
m_rpc = make_shared<JsonRpc>();
|
||||
|
||||
m_netGroup.addNetElement(&m_orbitWarpActionNetState);
|
||||
m_netGroup.addNetElement(&m_playerWorldIdNetState);
|
||||
m_netGroup.addNetElement(&m_isAdminNetState);
|
||||
m_netGroup.addNetElement(&m_teamNetState);
|
||||
m_netGroup.addNetElement(&m_shipUpgrades);
|
||||
m_netGroup.addNetElement(&m_shipCoordinate);
|
||||
}
|
||||
|
||||
Uuid ClientContext::serverUuid() const {
|
||||
return m_serverUuid;
|
||||
}
|
||||
|
||||
CelestialCoordinate ClientContext::shipCoordinate() const {
|
||||
return m_shipCoordinate.get();
|
||||
}
|
||||
|
||||
Maybe<pair<WarpAction, WarpMode>> ClientContext::orbitWarpAction() const {
|
||||
return m_orbitWarpActionNetState.get();
|
||||
}
|
||||
|
||||
WorldId ClientContext::playerWorldId() const {
|
||||
return m_playerWorldIdNetState.get();
|
||||
}
|
||||
|
||||
bool ClientContext::isAdmin() const {
|
||||
return m_isAdminNetState.get();
|
||||
}
|
||||
|
||||
EntityDamageTeam ClientContext::team() const {
|
||||
return m_teamNetState.get();
|
||||
}
|
||||
|
||||
JsonRpcInterfacePtr ClientContext::rpcInterface() const {
|
||||
return m_rpc;
|
||||
}
|
||||
|
||||
WorldChunks ClientContext::newShipUpdates() {
|
||||
return take(m_newShipUpdates);
|
||||
}
|
||||
|
||||
ShipUpgrades ClientContext::shipUpgrades() const {
|
||||
return m_shipUpgrades.get();
|
||||
}
|
||||
|
||||
void ClientContext::readUpdate(ByteArray data) {
|
||||
if (data.empty())
|
||||
return;
|
||||
|
||||
DataStreamBuffer ds(move(data));
|
||||
|
||||
m_rpc->receive(ds.read<ByteArray>());
|
||||
|
||||
auto shipUpdates = ds.read<ByteArray>();
|
||||
if (!shipUpdates.empty())
|
||||
m_newShipUpdates.merge(DataStreamBuffer::deserialize<WorldChunks>(move(shipUpdates)), true);
|
||||
|
||||
m_netGroup.readNetState(ds.read<ByteArray>());
|
||||
}
|
||||
|
||||
ByteArray ClientContext::writeUpdate() {
|
||||
return m_rpc->send();
|
||||
}
|
||||
|
||||
}
|
61
source/game/StarClientContext.hpp
Normal file
61
source/game/StarClientContext.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#ifndef STAR_CLIENT_CONTEXT_HPP
|
||||
#define STAR_CLIENT_CONTEXT_HPP
|
||||
|
||||
#include "StarNetElementSystem.hpp"
|
||||
#include "StarJsonRpc.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarDamageTypes.hpp"
|
||||
#include "StarCelestialCoordinate.hpp"
|
||||
#include "StarWarping.hpp"
|
||||
#include "StarWorldStorage.hpp"
|
||||
#include "StarPlayerTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CelestialLog);
|
||||
STAR_CLASS(ClientContext);
|
||||
|
||||
class ClientContext {
|
||||
public:
|
||||
ClientContext(Uuid serverUuid);
|
||||
|
||||
Uuid serverUuid() const;
|
||||
|
||||
// The coordinate for the world which the player's ship is currently
|
||||
// orbiting.
|
||||
CelestialCoordinate shipCoordinate() const;
|
||||
|
||||
Maybe<pair<WarpAction, WarpMode>> orbitWarpAction() const;
|
||||
|
||||
// The current world id of the player
|
||||
WorldId playerWorldId() const;
|
||||
|
||||
bool isAdmin() const;
|
||||
EntityDamageTeam team() const;
|
||||
|
||||
JsonRpcInterfacePtr rpcInterface() const;
|
||||
|
||||
WorldChunks newShipUpdates();
|
||||
ShipUpgrades shipUpgrades() const;
|
||||
|
||||
void readUpdate(ByteArray data);
|
||||
ByteArray writeUpdate();
|
||||
|
||||
private:
|
||||
Uuid m_serverUuid;
|
||||
JsonRpcPtr m_rpc;
|
||||
|
||||
NetElementTopGroup m_netGroup;
|
||||
NetElementData<Maybe<pair<WarpAction, WarpMode>>> m_orbitWarpActionNetState;
|
||||
NetElementData<WorldId> m_playerWorldIdNetState;
|
||||
NetElementBool m_isAdminNetState;
|
||||
NetElementData<EntityDamageTeam> m_teamNetState;
|
||||
NetElementData<ShipUpgrades> m_shipUpgrades;
|
||||
NetElementData<CelestialCoordinate> m_shipCoordinate;
|
||||
|
||||
WorldChunks m_newShipUpdates;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
71
source/game/StarCodex.cpp
Normal file
71
source/game/StarCodex.cpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#include "StarCodex.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
Codex::Codex(Json const& config, String const& directory) {
|
||||
m_directory = directory;
|
||||
m_id = config.getString("id");
|
||||
m_species = config.getString("species", "other");
|
||||
m_title = config.getString("title");
|
||||
m_description = config.getString("description", "");
|
||||
m_icon = config.getString("icon");
|
||||
m_pages = jsonToStringList(config.get("contentPages"));
|
||||
m_itemConfig = config.get("itemConfig", Json());
|
||||
}
|
||||
|
||||
Json Codex::toJson() const {
|
||||
auto result = JsonObject{
|
||||
{"id", m_id},
|
||||
{"species", m_species},
|
||||
{"title", m_title},
|
||||
{"description", m_description},
|
||||
{"icon", m_icon},
|
||||
{"contentPages", jsonFromStringList(m_pages)},
|
||||
{"itemConfig", m_itemConfig}};
|
||||
return result;
|
||||
}
|
||||
|
||||
String Codex::id() const {
|
||||
return m_id;
|
||||
}
|
||||
|
||||
String Codex::species() const {
|
||||
return m_species;
|
||||
}
|
||||
|
||||
String Codex::title() const {
|
||||
return m_title;
|
||||
}
|
||||
|
||||
String Codex::description() const {
|
||||
return m_description;
|
||||
}
|
||||
|
||||
String Codex::icon() const {
|
||||
return m_icon;
|
||||
}
|
||||
|
||||
String Codex::page(size_t pageNum) const {
|
||||
if (pageNum < m_pages.size())
|
||||
return m_pages[pageNum];
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> Codex::pages() const {
|
||||
return m_pages;
|
||||
}
|
||||
|
||||
size_t Codex::pageCount() const {
|
||||
return m_pages.size();
|
||||
}
|
||||
|
||||
Json Codex::itemConfig() const {
|
||||
return m_itemConfig;
|
||||
}
|
||||
|
||||
String Codex::directory() const {
|
||||
return m_directory;
|
||||
}
|
||||
|
||||
}
|
39
source/game/StarCodex.hpp
Normal file
39
source/game/StarCodex.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
#ifndef STAR_CODEX_HPP
|
||||
#define STAR_CODEX_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Codex);
|
||||
|
||||
class Codex {
|
||||
public:
|
||||
Codex(Json const& config, String const& directory);
|
||||
Json toJson() const;
|
||||
|
||||
String id() const;
|
||||
String species() const;
|
||||
String title() const;
|
||||
String description() const;
|
||||
String icon() const;
|
||||
String page(size_t pageNum) const;
|
||||
List<String> pages() const;
|
||||
size_t pageCount() const;
|
||||
Json itemConfig() const;
|
||||
String directory() const;
|
||||
|
||||
private:
|
||||
String m_id;
|
||||
String m_species;
|
||||
String m_title;
|
||||
String m_description;
|
||||
String m_icon;
|
||||
List<String> m_pages;
|
||||
Json m_itemConfig;
|
||||
String m_directory;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
41
source/game/StarCodexDatabase.cpp
Normal file
41
source/game/StarCodexDatabase.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#include "StarCodexDatabase.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CodexDatabase::CodexDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto files = assets->scanExtension("codex");
|
||||
auto codexConfig = assets->json("/codex.config");
|
||||
assets->queueJsons(files);
|
||||
for (auto const& file : files) {
|
||||
try {
|
||||
auto codexJson = assets->json(file);
|
||||
codexJson = codexJson.set("icon",
|
||||
AssetPath::relativeTo(AssetPath::directory(file), codexJson.getString("icon", codexConfig.getString("defaultIcon"))));
|
||||
|
||||
auto codex = make_shared<Codex>(codexJson, AssetPath::directory(file));
|
||||
|
||||
if (m_codexes.contains(codex->id()))
|
||||
throw CodexDatabaseException::format("Duplicate codex named '%s', config file '%s'", codex->id(), file);
|
||||
|
||||
m_codexes[codex->id()] = codex;
|
||||
} catch (std::exception const& e) {
|
||||
throw CodexDatabaseException(strf("Error reading codex config %s", file), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StringMap<CodexConstPtr> CodexDatabase::codexes() const {
|
||||
return m_codexes;
|
||||
}
|
||||
|
||||
CodexConstPtr CodexDatabase::codex(String const& codexId) const {
|
||||
if (auto codex = m_codexes.maybe(codexId))
|
||||
return codex.take();
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
26
source/game/StarCodexDatabase.hpp
Normal file
26
source/game/StarCodexDatabase.hpp
Normal file
|
@ -0,0 +1,26 @@
|
|||
#ifndef STAR_CODEX_DATABASE_HPP
|
||||
#define STAR_CODEX_DATABASE_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarCodex.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(CodexDatabaseException, StarException);
|
||||
|
||||
STAR_CLASS(CodexDatabase);
|
||||
|
||||
class CodexDatabase {
|
||||
public:
|
||||
CodexDatabase();
|
||||
|
||||
StringMap<CodexConstPtr> codexes() const;
|
||||
CodexConstPtr codex(String const& codexId) const;
|
||||
|
||||
private:
|
||||
StringMap<CodexConstPtr> m_codexes;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
128
source/game/StarCollectionDatabase.cpp
Normal file
128
source/game/StarCollectionDatabase.cpp
Normal file
|
@ -0,0 +1,128 @@
|
|||
#include "StarCollectionDatabase.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarMonsterDatabase.hpp"
|
||||
#include "StarItemDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<CollectionType> const CollectionTypeNames {
|
||||
{CollectionType::Generic, "generic"},
|
||||
{CollectionType::Item, "item"},
|
||||
{CollectionType::Monster, "monster"}
|
||||
};
|
||||
|
||||
Collection::Collection() : name(), title(), type() {}
|
||||
|
||||
Collection::Collection(String const& name, CollectionType type, String const& title) : name(name), title(title), type(type) {}
|
||||
|
||||
Collectable::Collectable() : name(), order(), title(), description(), icon() {}
|
||||
|
||||
Collectable::Collectable(String const& name, int order, String const& title, String const& description, String const& icon)
|
||||
: name(name), order(order), title(title), description(description), icon(icon) {};
|
||||
|
||||
CollectionDatabase::CollectionDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto files = assets->scanExtension("collection");
|
||||
assets->queueJsons(files);
|
||||
for (auto file : files) {
|
||||
auto config = assets->json(file);
|
||||
|
||||
Collection collection;
|
||||
|
||||
collection.name = config.getString("name");
|
||||
collection.title = config.getString("title", collection.name);
|
||||
collection.type = CollectionTypeNames.getLeft(config.getString("type", "generic"));
|
||||
|
||||
m_collectables[collection.name] = {};
|
||||
for (auto pair : config.get("collectables").iterateObject()) {
|
||||
Collectable collectable;
|
||||
if (collection.type == CollectionType::Monster)
|
||||
collectable = parseMonsterCollectable(pair.first, pair.second);
|
||||
else if (collection.type == CollectionType::Item)
|
||||
collectable = parseItemCollectable(pair.first, pair.second);
|
||||
else
|
||||
collectable = parseGenericCollectable(pair.first, pair.second);
|
||||
|
||||
m_collectables[collection.name][collectable.name] = collectable;
|
||||
}
|
||||
|
||||
m_collections[collection.name] = collection;
|
||||
}
|
||||
}
|
||||
|
||||
List<Collection> CollectionDatabase::collections() const {
|
||||
return m_collections.values();
|
||||
}
|
||||
|
||||
Collection CollectionDatabase::collection(String const& collectionName) const {
|
||||
try {
|
||||
return m_collections.get(collectionName);
|
||||
} catch (MapException e) {
|
||||
throw CollectionDatabaseException(strf("Collection '%s' not found", collectionName), e);
|
||||
}
|
||||
}
|
||||
|
||||
List<Collectable> CollectionDatabase::collectables(String const& collectionName) const {
|
||||
try {
|
||||
return m_collectables.get(collectionName).values();
|
||||
} catch (MapException e) {
|
||||
throw CollectionDatabaseException(strf("Collection '%s' not found", collectionName), e);
|
||||
}
|
||||
}
|
||||
|
||||
Collectable CollectionDatabase::collectable(String const& collectionName, String const& collectableName) const {
|
||||
try {
|
||||
return m_collectables.get(collectionName).get(collectableName);
|
||||
} catch (MapException e) {
|
||||
throw CollectionDatabaseException(strf("Collectable '%s' not found in collection '%s'", collectableName, collectionName), e);
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectionDatabase::hasCollectable(String const& collectionName, String const& collectableName) const {
|
||||
return (m_collections.contains(collectionName) && m_collectables.get(collectionName).contains(collectableName));
|
||||
}
|
||||
|
||||
Collectable CollectionDatabase::parseGenericCollectable(String const& name, Json const& config) const {
|
||||
Collectable collectable;
|
||||
collectable.name = name;
|
||||
collectable.order = config.getInt("order", 0);
|
||||
|
||||
collectable.title = config.getString("title", "");
|
||||
collectable.description = config.getString("description", "");
|
||||
collectable.icon = config.getString("icon", "");
|
||||
|
||||
return collectable;
|
||||
}
|
||||
|
||||
Collectable CollectionDatabase::parseMonsterCollectable(String const& name, Json const& config) const {
|
||||
Collectable collectable = parseGenericCollectable(name, config);
|
||||
auto seed = 0; // use a static seed to utilize caching
|
||||
auto variant = Root::singleton().monsterDatabase()->monsterVariant(config.getString("monsterType"), seed);
|
||||
|
||||
collectable.title = variant.shortDescription.value("");
|
||||
collectable.description = variant.description.value("");
|
||||
|
||||
return collectable;
|
||||
}
|
||||
|
||||
Collectable CollectionDatabase::parseItemCollectable(String const& name, Json const& config) const {
|
||||
Collectable collectable = parseGenericCollectable(name, config);
|
||||
auto itemDatabase = Root::singleton().itemDatabase();
|
||||
auto item = itemDatabase->item(ItemDescriptor(config.getString("item")));
|
||||
|
||||
collectable.title = item->friendlyName();
|
||||
collectable.description = item->description();
|
||||
|
||||
if (config.contains("icon")) {
|
||||
collectable.icon = config.getString("icon");
|
||||
} else {
|
||||
auto inventoryIcon = item->instanceValue("inventoryIcon", "");
|
||||
if (inventoryIcon.isType(Json::Type::String))
|
||||
collectable.icon = AssetPath::relativeTo(itemDatabase->itemConfig(item->name(), JsonObject()).directory, inventoryIcon.toString());
|
||||
}
|
||||
|
||||
return collectable;
|
||||
}
|
||||
|
||||
}
|
62
source/game/StarCollectionDatabase.hpp
Normal file
62
source/game/StarCollectionDatabase.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#ifndef STAR_COLLECTION_DATABASE_HPP
|
||||
#define STAR_COLLECTION_DATABASE_HPP
|
||||
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(CollectionDatabaseException, StarException);
|
||||
|
||||
STAR_CLASS(CollectionDatabase);
|
||||
|
||||
enum class CollectionType : uint16_t {
|
||||
Generic,
|
||||
Item,
|
||||
Monster
|
||||
};
|
||||
extern EnumMap<CollectionType> const CollectionTypeNames;
|
||||
|
||||
struct Collectable {
|
||||
Collectable();
|
||||
Collectable(String const& name, int order, String const& title, String const& description, String const& icon);
|
||||
|
||||
String name;
|
||||
int order;
|
||||
String title;
|
||||
String description;
|
||||
String icon;
|
||||
};
|
||||
|
||||
struct Collection {
|
||||
Collection();
|
||||
Collection(String const& name, CollectionType type, String const& icon);
|
||||
|
||||
String name;
|
||||
String title;
|
||||
CollectionType type;
|
||||
};
|
||||
|
||||
class CollectionDatabase {
|
||||
public:
|
||||
CollectionDatabase();
|
||||
|
||||
List<Collection> collections() const;
|
||||
Collection collection(String const& collectionName) const;
|
||||
List<Collectable> collectables(String const& collectionName) const;
|
||||
Collectable collectable(String const& collectionName, String const& collectableName) const;
|
||||
|
||||
bool hasCollectable(String const& collectionName, String const& collectableName) const;
|
||||
|
||||
private:
|
||||
Collectable parseGenericCollectable(String const& name, Json const& config) const;
|
||||
Collectable parseMonsterCollectable(String const& name, Json const& config) const;
|
||||
Collectable parseItemCollectable(String const& name, Json const& config) const;
|
||||
|
||||
StringMap<Collection> m_collections;
|
||||
StringMap<StringMap<Collectable>> m_collectables;
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
#endif
|
14
source/game/StarCollisionBlock.cpp
Normal file
14
source/game/StarCollisionBlock.cpp
Normal file
|
@ -0,0 +1,14 @@
|
|||
#include "StarCollisionBlock.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<CollisionKind> const CollisionKindNames{
|
||||
{CollisionKind::Null, "Null"},
|
||||
{CollisionKind::None, "None"},
|
||||
{CollisionKind::Platform, "Platform"},
|
||||
{CollisionKind::Dynamic, "Dynamic"},
|
||||
{CollisionKind::Slippery, "Slippery"},
|
||||
{CollisionKind::Block, "Block"}
|
||||
};
|
||||
|
||||
}
|
115
source/game/StarCollisionBlock.hpp
Normal file
115
source/game/StarCollisionBlock.hpp
Normal file
|
@ -0,0 +1,115 @@
|
|||
#ifndef STAR_COLLISION_BLOCK_HPP
|
||||
#define STAR_COLLISION_BLOCK_HPP
|
||||
|
||||
#include "StarPoly.hpp"
|
||||
#include "StarList.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
enum class CollisionKind : uint8_t {
|
||||
// Special collision block that is used for unloaded / un-generated tiles.
|
||||
// Collides the same as "Block", but does not tile with it.
|
||||
Null,
|
||||
None,
|
||||
Platform,
|
||||
Dynamic,
|
||||
Slippery,
|
||||
Block
|
||||
};
|
||||
|
||||
class CollisionSet {
|
||||
public:
|
||||
CollisionSet();
|
||||
CollisionSet(initializer_list<CollisionKind> kinds);
|
||||
|
||||
void insert(CollisionKind kind);
|
||||
void remove(CollisionKind kind);
|
||||
bool contains(CollisionKind kind) const;
|
||||
|
||||
private:
|
||||
static uint8_t kindBit(CollisionKind kind);
|
||||
|
||||
uint8_t m_kinds;
|
||||
};
|
||||
|
||||
// The default CollisionSet consists of Null, Slippery, Dynamic and Block
|
||||
CollisionSet const DefaultCollisionSet({CollisionKind::Null, CollisionKind::Slippery, CollisionKind::Dynamic, CollisionKind::Block});
|
||||
|
||||
// Defines what can be "blocks" e.g. for tile rendering: Block and Slippery
|
||||
CollisionSet const BlockCollisionSet({CollisionKind::Block, CollisionKind::Slippery});
|
||||
|
||||
extern EnumMap<CollisionKind> const CollisionKindNames;
|
||||
|
||||
bool isColliding(CollisionKind kind, CollisionSet const& collisionSet);
|
||||
bool isSolidColliding(CollisionKind kind);
|
||||
|
||||
// Returns the highest priority collision kind, where Block > Slippery >
|
||||
// Dynamic > Platform > None > Null
|
||||
CollisionKind maxCollision(CollisionKind first, CollisionKind second);
|
||||
|
||||
struct CollisionBlock {
|
||||
// Make a null collision block for the given space.
|
||||
static CollisionBlock nullBlock(Vec2I const& space);
|
||||
|
||||
CollisionKind kind;
|
||||
Vec2I space;
|
||||
PolyF poly;
|
||||
RectF polyBounds;
|
||||
};
|
||||
|
||||
inline CollisionSet::CollisionSet()
|
||||
: m_kinds(0) {}
|
||||
|
||||
inline CollisionSet::CollisionSet(initializer_list<CollisionKind> kinds)
|
||||
: CollisionSet() {
|
||||
for (auto kind : kinds) {
|
||||
insert(kind);
|
||||
}
|
||||
}
|
||||
|
||||
inline void CollisionSet::insert(CollisionKind kind) {
|
||||
m_kinds = m_kinds | kindBit(kind);
|
||||
}
|
||||
|
||||
inline void CollisionSet::remove(CollisionKind kind) {
|
||||
m_kinds = m_kinds & ~kindBit(kind);
|
||||
}
|
||||
|
||||
inline bool CollisionSet::contains(CollisionKind kind) const {
|
||||
return m_kinds & kindBit(kind);
|
||||
}
|
||||
|
||||
inline uint8_t CollisionSet::kindBit(CollisionKind kind) {
|
||||
return 1 << ((uint8_t)kind + 1);
|
||||
}
|
||||
|
||||
inline bool isColliding(CollisionKind kind, CollisionSet const& collisionSet) {
|
||||
return collisionSet.contains(kind);
|
||||
}
|
||||
|
||||
inline bool isSolidColliding(CollisionKind kind) {
|
||||
return isColliding(kind, DefaultCollisionSet);
|
||||
}
|
||||
|
||||
inline CollisionKind maxCollision(CollisionKind first, CollisionKind second) {
|
||||
return max(first, second);
|
||||
}
|
||||
|
||||
inline CollisionBlock CollisionBlock::nullBlock(Vec2I const& space) {
|
||||
CollisionBlock block;
|
||||
block.kind = CollisionKind::Null;
|
||||
block.space = space;
|
||||
block.poly = {
|
||||
Vec2F(space) + Vec2F(0, 0),
|
||||
Vec2F(space) + Vec2F(1, 0),
|
||||
Vec2F(space) + Vec2F(1, 1),
|
||||
Vec2F(space) + Vec2F(0, 1)
|
||||
};
|
||||
block.polyBounds = RectF::withSize(Vec2F(space), Vec2F(1, 1));
|
||||
return block;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
233
source/game/StarCollisionGenerator.cpp
Normal file
233
source/game/StarCollisionGenerator.cpp
Normal file
|
@ -0,0 +1,233 @@
|
|||
#include "StarCollisionGenerator.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void CollisionGenerator::init(CollisionKindAccessor accessor) {
|
||||
m_accessor = accessor;
|
||||
}
|
||||
|
||||
List<CollisionBlock> CollisionGenerator::getBlocks(RectI const& region) const {
|
||||
if (region.isNull())
|
||||
return {};
|
||||
|
||||
List<CollisionBlock> list;
|
||||
|
||||
populateCollisionBuffer(region);
|
||||
|
||||
getBlocksMarchingSquares(list, region, CollisionKind::Dynamic);
|
||||
getBlocksPlatforms(list, region, CollisionKind::Platform);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void CollisionGenerator::getBlocksPlatforms(List<CollisionBlock>& list, RectI const& region, CollisionKind kind) const {
|
||||
int xMin = region.xMin();
|
||||
int xMax = region.xMax();
|
||||
int yMin = region.yMin();
|
||||
int yMax = region.yMax();
|
||||
|
||||
for (int x = xMin; x < xMax; ++x) {
|
||||
for (int y = yMin; y < yMax; ++y) {
|
||||
if (collisionKind(x, y) == kind) {
|
||||
auto addBlock = [&](PolyF::VertexList vertices) {
|
||||
CollisionBlock block;
|
||||
block.space = Vec2I(x, y);
|
||||
block.kind = kind;
|
||||
block.poly = PolyF(move(vertices));
|
||||
block.polyBounds = block.poly.boundBox();
|
||||
list.append(move(block));
|
||||
};
|
||||
|
||||
// This was once simple and elegant and made sense but then I made it
|
||||
// match the actual platform rendering more closely and now it's a big
|
||||
// shitty pile of special cases again. RIP.
|
||||
|
||||
bool right = collisionKind(x + 1, y) == kind;
|
||||
bool left = collisionKind(x - 1, y) == kind;
|
||||
|
||||
bool downRight = collisionKind(x + 1, y - 1) == kind && collisionKind(x + 1, y) != kind;
|
||||
bool downLeft = collisionKind(x - 1, y - 1) == kind && collisionKind(x - 1, y) != kind;
|
||||
|
||||
bool upRight = collisionKind(x + 1, y + 1) == kind && !left && !right;
|
||||
bool upLeft = collisionKind(x - 1, y + 1) == kind && !left && !right;
|
||||
|
||||
bool above = collisionKind(x, y + 1) == kind;
|
||||
bool below = collisionKind(x, y - 1) == kind;
|
||||
|
||||
if (downRight && downLeft && upRight && upLeft) {
|
||||
addBlock({Vec2F(x, y), Vec2F(x + 1, y + 1)});
|
||||
addBlock({Vec2F(x + 1, y), Vec2F(x, y + 1)});
|
||||
} else if (above && below) {
|
||||
addBlock({Vec2F(x, y + 1), Vec2F(x + 1, y + 1)});
|
||||
} else if (upLeft && downLeft && !upRight && !downRight) {
|
||||
addBlock({Vec2F(x + 1, y), Vec2F(x, y + 1)});
|
||||
} else if (upRight && downRight && !upLeft && !upRight) {
|
||||
addBlock({Vec2F(x, y), Vec2F(x + 1, y + 1)});
|
||||
} else if (upRight && downLeft) {
|
||||
addBlock({Vec2F(x, y), Vec2F(x + 1, y + 1)});
|
||||
|
||||
// special case block for connecting flat platform above
|
||||
if (above && collisionKind(x + 1, y + 1) == kind)
|
||||
addBlock({Vec2F(x + 1, y + 1), Vec2F(x + 2, y + 2)});
|
||||
} else if (upLeft && downRight) {
|
||||
addBlock({Vec2F(x + 1, y), Vec2F(x, y + 1)});
|
||||
|
||||
// special case block for connecting flat platform above
|
||||
if (above && collisionKind(x - 1, y + 1) == kind)
|
||||
addBlock({Vec2F(x, y + 1), Vec2F(x - 1, y + 2)});
|
||||
} else if (above && !downRight && !downLeft) {
|
||||
addBlock({Vec2F(x, y + 1), Vec2F(x + 1, y + 1)});
|
||||
} else if (upLeft && !upRight) {
|
||||
addBlock({Vec2F(x + 1, y), Vec2F(x, y + 1)});
|
||||
} else if (upRight && !upLeft) {
|
||||
addBlock({Vec2F(x, y), Vec2F(x + 1, y + 1)});
|
||||
} else if (downRight && (left || !below)) {
|
||||
addBlock({Vec2F(x + 1, y), Vec2F(x, y + 1)});
|
||||
} else if (downLeft && (right || !below)) {
|
||||
addBlock({Vec2F(x, y), Vec2F(x + 1, y + 1)});
|
||||
} else {
|
||||
addBlock({Vec2F(x, y + 1), Vec2F(x + 1, y + 1)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CollisionGenerator::getBlocksMarchingSquares(List<CollisionBlock>& list, RectI const& region, CollisionKind kind) const {
|
||||
|
||||
// uses binary masking to assign each group of 4 tiles a value between 0 and 15
|
||||
// with corners ul = 1, ur = 2, lr = 4, ll = 8
|
||||
|
||||
// points spaced at 0.5 around the edge of a 1x1 square, clockwise from bottom left,
|
||||
// plus the center point for special cases
|
||||
static Vec2F const msv[9] = {
|
||||
Vec2F(0.5, 0.5),
|
||||
Vec2F(0.5, 1.0),
|
||||
Vec2F(0.5, 1.5),
|
||||
Vec2F(1.0, 1.5),
|
||||
Vec2F(1.5, 1.5),
|
||||
Vec2F(1.5, 1.0),
|
||||
Vec2F(1.5, 0.5),
|
||||
Vec2F(1.0, 0.5),
|
||||
Vec2F(1.0, 1.0)
|
||||
};
|
||||
|
||||
// refers to vertex offset indices in msv, with 8 being no vertex
|
||||
static unsigned const msp[22][6] = {
|
||||
{9, 9, 9, 9, 9, 9},
|
||||
{1, 2, 3, 9, 9, 9},
|
||||
{3, 4, 5, 9, 9, 9},
|
||||
{1, 2, 4, 5, 9, 9},
|
||||
{7, 5, 6, 9, 9, 9},
|
||||
{1, 2, 3, 5, 6, 7},
|
||||
{7, 3, 4, 6, 9, 9},
|
||||
{1, 2, 4, 6, 7, 9},
|
||||
{0, 1, 7, 9, 9, 9},
|
||||
{0, 2, 3, 7, 9, 9},
|
||||
{0, 1, 3, 4, 5, 7},
|
||||
{0, 2, 4, 5, 7, 9},
|
||||
{0, 1, 5, 6, 9, 9},
|
||||
{0, 2, 3, 5, 6, 9},
|
||||
{0, 1, 3, 4, 6, 9},
|
||||
{0, 2, 4, 6, 9, 9},
|
||||
// special cases for squared off top corners
|
||||
{5, 6, 7, 8, 9, 9}, // top left corner
|
||||
{0, 1, 8, 7, 9, 9}, // top right corner
|
||||
// special cases for hollowed out bottom corners
|
||||
{0, 2, 3, 8, 9, 9}, // lower left corner part 1
|
||||
{0, 8, 5, 6, 9, 9}, // lower left corner part 2
|
||||
{0, 1, 8, 6, 9, 9}, // lower right corner part 1
|
||||
{6, 8, 3, 4, 9, 9} // lower right corner part 2
|
||||
};
|
||||
|
||||
auto addBlock = [&](int x, int y, uint8_t svi) {
|
||||
CollisionBlock block;
|
||||
block.space = Vec2I(x, y);
|
||||
block.poly.clear();
|
||||
for (auto i : msp[svi]) {
|
||||
if (i == 9)
|
||||
break;
|
||||
block.poly.add(Vec2F(x, y) + msv[i]);
|
||||
}
|
||||
block.polyBounds = block.poly.boundBox();
|
||||
block.kind = std::max({collisionKind(x, y), collisionKind(x + 1, y), collisionKind(x, y + 1), collisionKind(x + 1, y + 1)});
|
||||
list.append(move(block));
|
||||
};
|
||||
|
||||
int xMin = region.xMin();
|
||||
int xMax = region.xMax();
|
||||
int yMin = region.yMin();
|
||||
int yMax = region.yMax();
|
||||
|
||||
for (int x = xMin; x < xMax; ++x) {
|
||||
for (int y = yMin; y < yMax; ++y) {
|
||||
uint8_t neighborMask = 0;
|
||||
if (collisionKind(x, y + 1) >= kind)
|
||||
neighborMask |= 1;
|
||||
if (collisionKind(x + 1, y + 1) >= kind)
|
||||
neighborMask |= 2;
|
||||
if (collisionKind(x + 1, y) >= kind)
|
||||
neighborMask |= 4;
|
||||
if (collisionKind(x, y) >= kind)
|
||||
neighborMask |= 8;
|
||||
|
||||
if (neighborMask == 4) {
|
||||
if (collisionKind(x + 2, y) >= kind &&
|
||||
collisionKind(x + 2, y + 1) < kind &&
|
||||
collisionKind(x, y - 1) < kind) {
|
||||
|
||||
addBlock(x, y, 16);
|
||||
continue;
|
||||
}
|
||||
} else if (neighborMask == 8) {
|
||||
if (collisionKind(x - 1, y) >= kind &&
|
||||
collisionKind(x - 1, y + 1) < kind &&
|
||||
collisionKind(x + 1, y - 1) < kind) {
|
||||
|
||||
addBlock(x, y, 17);
|
||||
continue;
|
||||
}
|
||||
} else if (neighborMask == 13) {
|
||||
if (collisionKind(x, y + 2) >= kind &&
|
||||
collisionKind(x + 1, y + 2) < kind &&
|
||||
collisionKind(x + 2, y) >= kind) {
|
||||
|
||||
addBlock(x, y, 18);
|
||||
addBlock(x, y, 19);
|
||||
continue;
|
||||
}
|
||||
} else if (neighborMask == 14) {
|
||||
if (collisionKind(x, y + 2) < kind &&
|
||||
collisionKind(x + 1, y + 2) >= kind &&
|
||||
collisionKind(x - 1, y) >= kind) {
|
||||
|
||||
addBlock(x, y, 20);
|
||||
addBlock(x, y, 21);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (neighborMask != 0)
|
||||
addBlock(x, y, neighborMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CollisionGenerator::populateCollisionBuffer(RectI const& region) const {
|
||||
int xmin = region.xMin() - BlockInfluenceRadius;
|
||||
int ymin = region.yMin() - BlockInfluenceRadius;
|
||||
int xmax = region.xMax() + BlockInfluenceRadius;
|
||||
int ymax = region.yMax() + BlockInfluenceRadius;
|
||||
|
||||
m_collisionBufferCorner = {xmin, ymin};
|
||||
m_collisionBuffer.resize(xmax - xmin, ymax - ymin);
|
||||
for (int x = xmin; x < xmax; ++x)
|
||||
for (int y = ymin; y < ymax; ++y)
|
||||
m_collisionBuffer(x - xmin, y - ymin) = m_accessor(x, y);
|
||||
}
|
||||
|
||||
CollisionKind CollisionGenerator::collisionKind(int x, int y) const {
|
||||
return m_collisionBuffer(x - m_collisionBufferCorner[0], y - m_collisionBufferCorner[1]);
|
||||
}
|
||||
|
||||
}
|
50
source/game/StarCollisionGenerator.hpp
Normal file
50
source/game/StarCollisionGenerator.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef STAR_COLLISION_GENERATOR_HPP
|
||||
#define STAR_COLLISION_GENERATOR_HPP
|
||||
|
||||
#include "StarCollisionBlock.hpp"
|
||||
#include "StarMultiArray.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(CollisionGenerator);
|
||||
|
||||
// Turns cell geometry into "smoothed" polygonal geometry. Used by World to
|
||||
// generate ramps and slopes based on tiles.
|
||||
class CollisionGenerator {
|
||||
public:
|
||||
// The maximum number of spaces away from a block that can influence the
|
||||
// collision geometry of a given block.
|
||||
static int const BlockInfluenceRadius = 2;
|
||||
|
||||
// The Maximum number of blocks that will be generated for a single tile
|
||||
// space.
|
||||
static size_t const MaximumCollisionsPerSpace = 4;
|
||||
|
||||
// Callback function to tell what kind of collision geometry is in a cell.
|
||||
// Will be called up to BlockInfluenceRadius outside of the given query
|
||||
// region.
|
||||
typedef std::function<CollisionKind(int, int)> CollisionKindAccessor;
|
||||
|
||||
void init(CollisionKindAccessor accessor);
|
||||
|
||||
// Get collision geometry for the given block region.
|
||||
List<CollisionBlock> getBlocks(RectI const& region) const;
|
||||
|
||||
private:
|
||||
void getBlocksPlatforms(List<CollisionBlock>& output, RectI const& region, CollisionKind kind) const;
|
||||
void getBlocksMarchingSquares(List<CollisionBlock>& output, RectI const& region, CollisionKind kind) const;
|
||||
|
||||
void populateCollisionBuffer(RectI const& region) const;
|
||||
CollisionKind collisionKind(int x, int y) const;
|
||||
|
||||
CollisionKindAccessor m_accessor;
|
||||
|
||||
mutable Vec2I m_collisionBufferCorner;
|
||||
mutable MultiArray<CollisionKind, 2> m_collisionBuffer;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1021
source/game/StarCommandProcessor.cpp
Normal file
1021
source/game/StarCommandProcessor.cpp
Normal file
File diff suppressed because it is too large
Load diff
77
source/game/StarCommandProcessor.hpp
Normal file
77
source/game/StarCommandProcessor.hpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#ifndef STAR_COMMAND_PROCESSOR_HPP
|
||||
#define STAR_COMMAND_PROCESSOR_HPP
|
||||
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarShellParser.hpp"
|
||||
#include "StarLuaComponents.hpp"
|
||||
#include "StarLuaRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(UniverseServer);
|
||||
STAR_CLASS(CommandProcessor);
|
||||
|
||||
class CommandProcessor {
|
||||
public:
|
||||
CommandProcessor(UniverseServer* universe);
|
||||
|
||||
String adminCommand(String const& command, String const& argumentString);
|
||||
String userCommand(ConnectionId clientId, String const& command, String const& argumentString);
|
||||
|
||||
private:
|
||||
static Maybe<ConnectionId> playerCidFromCommand(String const& player, UniverseServer* universe);
|
||||
|
||||
String help(ConnectionId connectionId, String const& argumentString);
|
||||
String admin(ConnectionId connectionId, String const& argumentString);
|
||||
String pvp(ConnectionId connectionId, String const& argumentString);
|
||||
String whoami(ConnectionId connectionId, String const& argumentString);
|
||||
|
||||
String warp(ConnectionId connectionId, String const& argumentString);
|
||||
String warpRandom(ConnectionId connectionId, String const& argumentString);
|
||||
String timewarp(ConnectionId connectionId, String const& argumentString);
|
||||
String setTileProtection(ConnectionId connectionId, String const& argumentString);
|
||||
String setDungeonId(ConnectionId connectionId, String const& argumentString);
|
||||
String setPlayerStart(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnItem(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnTreasure(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnMonster(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnNpc(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnVehicle(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnStagehand(ConnectionId connectionId, String const& argumentString);
|
||||
String clearStagehand(ConnectionId connectionId, String const& argumentString);
|
||||
String spawnLiquid(ConnectionId connectionId, String const& argumentString);
|
||||
String kick(ConnectionId connectionId, String const& argumentString);
|
||||
String ban(ConnectionId connectionId, String const& argumentString);
|
||||
String unbanIp(ConnectionId connectionId, String const& argumentString);
|
||||
String unbanUuid(ConnectionId connectionId, String const& argumentString);
|
||||
String list(ConnectionId connectionId, String const& argumentString);
|
||||
String clientCoordinate(ConnectionId connectionId, String const& argumentString);
|
||||
String serverReload(ConnectionId connectionId, String const& argumentString);
|
||||
String eval(ConnectionId connectionId, String const& lua);
|
||||
String entityEval(ConnectionId connectionId, String const& lua);
|
||||
String enableSpawning(ConnectionId connectionId, String const& argumentString);
|
||||
String disableSpawning(ConnectionId connectionId, String const& argumentString);
|
||||
String placeDungeon(ConnectionId connectionId, String const& argumentString);
|
||||
String setUniverseFlag(ConnectionId connectionId, String const& argumentString);
|
||||
String resetUniverseFlags(ConnectionId connectionId, String const& argumentString);
|
||||
String addBiomeRegion(ConnectionId connectionId, String const& argumentString);
|
||||
String expandBiomeRegion(ConnectionId connectionId, String const& argumentString);
|
||||
String updatePlanetType(ConnectionId connectionId, String const& argumentString);
|
||||
String setEnvironmentBiome(ConnectionId connectionId, String const& argumentString);
|
||||
|
||||
mutable Mutex m_mutex;
|
||||
|
||||
String handleCommand(ConnectionId connectionId, String const& command, String const& argumentString);
|
||||
Maybe<String> adminCheck(ConnectionId connectionId, String const& commandDescription) const;
|
||||
Maybe<String> localCheck(ConnectionId connectionId, String const& commandDescription) const;
|
||||
LuaCallbacks makeCommandCallbacks();
|
||||
|
||||
UniverseServer* m_universe;
|
||||
ShellParser m_parser;
|
||||
|
||||
LuaBaseComponent m_scriptComponent;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
311
source/game/StarDamage.cpp
Normal file
311
source/game/StarDamage.cpp
Normal file
|
@ -0,0 +1,311 @@
|
|||
#include "StarDamage.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DamageSource::DamageSource()
|
||||
: damageType(DamageType::NoDamage), damage(0.0), sourceEntityId(NullEntityId), rayCheck(false) {}
|
||||
|
||||
DamageSource::DamageSource(Json const& config) {
|
||||
if (auto dtype = config.optString("damageType"))
|
||||
damageType = DamageTypeNames.getLeft(*dtype);
|
||||
else
|
||||
damageType = DamageType::Damage;
|
||||
|
||||
if (config.contains("poly"))
|
||||
damageArea = jsonToPolyF(config.get("poly"));
|
||||
else if (config.contains("line"))
|
||||
damageArea = jsonToLine2F(config.get("line"));
|
||||
else
|
||||
throw StarException("No 'poly' or 'line' key in DamageSource config");
|
||||
|
||||
damage = config.getFloat("damage");
|
||||
|
||||
trackSourceEntity = config.getBool("trackSourceEntity", true);
|
||||
sourceEntityId = config.getInt("sourceEntityId", NullEntityId);
|
||||
|
||||
if (auto tconfig = config.opt("team")) {
|
||||
team = EntityDamageTeam(*tconfig);
|
||||
} else {
|
||||
team.type = TeamTypeNames.getLeft(config.getString("teamType", "passive"));
|
||||
team.team = config.getUInt("teamNumber", 0);
|
||||
}
|
||||
|
||||
damageRepeatGroup = config.optString("damageRepeatGroup");
|
||||
damageRepeatTimeout = config.optFloat("damageRepeatTimeout");
|
||||
|
||||
damageSourceKind = config.getString("damageSourceKind", "");
|
||||
|
||||
statusEffects = config.getArray("statusEffects", {}).transformed(jsonToEphemeralStatusEffect);
|
||||
|
||||
auto knockbackJson = config.get("knockback", Json(0.0f));
|
||||
if (knockbackJson.isType(Json::Type::Array))
|
||||
knockback = jsonToVec2F(knockbackJson);
|
||||
else
|
||||
knockback = knockbackJson.toFloat();
|
||||
|
||||
rayCheck = config.getBool("rayCheck", false);
|
||||
}
|
||||
|
||||
DamageSource::DamageSource(DamageType damageType,
|
||||
DamageArea damageArea,
|
||||
float damage,
|
||||
bool trackSourceEntity,
|
||||
EntityId sourceEntityId,
|
||||
EntityDamageTeam team,
|
||||
Maybe<String> damageRepeatGroup,
|
||||
Maybe<float> damageRepeatTimeout,
|
||||
String damageSourceKind,
|
||||
List<EphemeralStatusEffect> statusEffects,
|
||||
Knockback knockback,
|
||||
bool rayCheck)
|
||||
: damageType(move(damageType)),
|
||||
damageArea(move(damageArea)),
|
||||
damage(move(damage)),
|
||||
trackSourceEntity(move(trackSourceEntity)),
|
||||
sourceEntityId(move(sourceEntityId)),
|
||||
team(move(team)),
|
||||
damageRepeatGroup(move(damageRepeatGroup)),
|
||||
damageRepeatTimeout(move(damageRepeatTimeout)),
|
||||
damageSourceKind(move(damageSourceKind)),
|
||||
statusEffects(move(statusEffects)),
|
||||
knockback(move(knockback)),
|
||||
rayCheck(move(rayCheck)) {}
|
||||
|
||||
Json DamageSource::toJson() const {
|
||||
Json damageAreaJson;
|
||||
if (auto p = damageArea.ptr<PolyF>())
|
||||
damageAreaJson = jsonFromPolyF(*p);
|
||||
else if (auto l = damageArea.ptr<Line2F>())
|
||||
damageAreaJson = jsonFromLine2F(*l);
|
||||
|
||||
Json knockbackJson;
|
||||
if (auto p = knockback.ptr<float>())
|
||||
knockbackJson = *p;
|
||||
else if (auto p = knockback.ptr<Vec2F>())
|
||||
knockbackJson = jsonFromVec2F(*p);
|
||||
|
||||
return JsonObject{{"damageType", DamageTypeNames.getRight(damageType)},
|
||||
{"damageArea", damageAreaJson},
|
||||
{"damage", damage},
|
||||
{"trackSourceEntity", trackSourceEntity},
|
||||
{"sourceEntityId", sourceEntityId},
|
||||
{"team", team.toJson()},
|
||||
{"damageRepeatGroup", jsonFromMaybe(damageRepeatGroup)},
|
||||
{"damageRepeatTimeout", jsonFromMaybe(damageRepeatTimeout)},
|
||||
{"damageSourceKind", damageSourceKind},
|
||||
{"statusEffects", statusEffects.transformed(jsonFromEphemeralStatusEffect)},
|
||||
{"knockback", knockbackJson},
|
||||
{"rayCheck", rayCheck}};
|
||||
}
|
||||
|
||||
bool DamageSource::intersectsWithPoly(WorldGeometry const& geometry, PolyF const& targetPoly) const {
|
||||
if (auto poly = damageArea.ptr<PolyF>())
|
||||
return geometry.polyIntersectsPoly(*poly, targetPoly);
|
||||
else if (auto line = damageArea.ptr<Line2F>())
|
||||
return geometry.lineIntersectsPoly(*line, targetPoly);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec2F DamageSource::knockbackMomentum(WorldGeometry const& worldGeometry, Vec2F const& targetCenter) const {
|
||||
if (auto v = knockback.ptr<Vec2F>()) {
|
||||
return *v;
|
||||
} else if (auto s = knockback.ptr<float>()) {
|
||||
if (*s != 0) {
|
||||
if (auto poly = damageArea.ptr<PolyF>())
|
||||
return worldGeometry.diff(targetCenter, poly->center()).normalized() * *s;
|
||||
else if (auto line = damageArea.ptr<Line2F>())
|
||||
return vnorm(line->diff()) * *s;
|
||||
}
|
||||
}
|
||||
|
||||
return Vec2F();
|
||||
}
|
||||
|
||||
bool DamageSource::operator==(DamageSource const& rhs) const {
|
||||
return tie(damageType, damageArea, damage, trackSourceEntity, sourceEntityId, team, damageSourceKind, statusEffects, knockback, rayCheck)
|
||||
== tie(rhs.damageType,
|
||||
rhs.damageArea,
|
||||
rhs.damage,
|
||||
rhs.trackSourceEntity,
|
||||
rhs.sourceEntityId,
|
||||
rhs.team,
|
||||
rhs.damageSourceKind,
|
||||
rhs.statusEffects,
|
||||
rhs.knockback,
|
||||
rhs.rayCheck);
|
||||
}
|
||||
|
||||
DamageSource& DamageSource::translate(Vec2F const& position) {
|
||||
if (auto poly = damageArea.ptr<PolyF>())
|
||||
poly->translate(position);
|
||||
else if (auto line = damageArea.ptr<Line2F>())
|
||||
line->translate(position);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageSource const& damageSource) {
|
||||
ds.write(damageSource.damageType);
|
||||
ds.write(damageSource.damageArea);
|
||||
ds.write(damageSource.damage);
|
||||
ds.write(damageSource.trackSourceEntity);
|
||||
ds.write(damageSource.sourceEntityId);
|
||||
ds.write(damageSource.team);
|
||||
ds.write(damageSource.damageRepeatGroup);
|
||||
ds.write(damageSource.damageRepeatTimeout);
|
||||
ds.write(damageSource.damageSourceKind);
|
||||
ds.write(damageSource.statusEffects);
|
||||
ds.write(damageSource.knockback);
|
||||
ds.write(damageSource.rayCheck);
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, DamageSource& damageSource) {
|
||||
ds.read(damageSource.damageType);
|
||||
ds.read(damageSource.damageArea);
|
||||
ds.read(damageSource.damage);
|
||||
ds.read(damageSource.trackSourceEntity);
|
||||
ds.read(damageSource.sourceEntityId);
|
||||
ds.read(damageSource.team);
|
||||
ds.read(damageSource.damageRepeatGroup);
|
||||
ds.read(damageSource.damageRepeatTimeout);
|
||||
ds.read(damageSource.damageSourceKind);
|
||||
ds.read(damageSource.statusEffects);
|
||||
ds.read(damageSource.knockback);
|
||||
ds.read(damageSource.rayCheck);
|
||||
return ds;
|
||||
}
|
||||
|
||||
DamageRequest::DamageRequest()
|
||||
: hitType(HitType::Hit), damageType(DamageType::Damage), damage(0.0f), sourceEntityId(NullEntityId) {}
|
||||
|
||||
DamageRequest::DamageRequest(Json const& v) {
|
||||
hitType = HitTypeNames.getLeft(v.getString("hitType", "hit"));
|
||||
damageType = DamageTypeNames.getLeft(v.getString("damageType", "damage"));
|
||||
damage = v.getFloat("damage");
|
||||
knockbackMomentum = jsonToVec2F(v.get("knockbackMomentum", JsonArray{0, 0}));
|
||||
sourceEntityId = v.getInt("sourceEntityId", NullEntityId);
|
||||
damageSourceKind = v.getString("damageSourceKind", "");
|
||||
statusEffects = v.getArray("statusEffects", {}).transformed(jsonToEphemeralStatusEffect);
|
||||
}
|
||||
|
||||
DamageRequest::DamageRequest(HitType hitType,
|
||||
DamageType damageType,
|
||||
float damage,
|
||||
Vec2F const& knockbackMomentum,
|
||||
EntityId sourceEntityId,
|
||||
String const& damageSourceKind,
|
||||
List<EphemeralStatusEffect> const& statusEffects)
|
||||
: hitType(hitType),
|
||||
damageType(damageType),
|
||||
damage(damage),
|
||||
knockbackMomentum(knockbackMomentum),
|
||||
sourceEntityId(sourceEntityId),
|
||||
damageSourceKind(damageSourceKind),
|
||||
statusEffects(statusEffects) {}
|
||||
|
||||
Json DamageRequest::toJson() const {
|
||||
return JsonObject{{"hitType", HitTypeNames.getRight(hitType)},
|
||||
{"damageType", DamageTypeNames.getRight(damageType)},
|
||||
{"damage", damage},
|
||||
{"knockbackMomentum", jsonFromVec2F(knockbackMomentum)},
|
||||
{"sourceEntityId", sourceEntityId},
|
||||
{"damageSourceKind", damageSourceKind},
|
||||
{"statusEffects", statusEffects.transformed(jsonFromEphemeralStatusEffect)}};
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageRequest const& damageRequest) {
|
||||
ds << damageRequest.hitType;
|
||||
ds << damageRequest.damageType;
|
||||
ds << damageRequest.damage;
|
||||
ds << damageRequest.knockbackMomentum;
|
||||
ds << damageRequest.sourceEntityId;
|
||||
ds << damageRequest.damageSourceKind;
|
||||
ds << damageRequest.statusEffects;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, DamageRequest& damageRequest) {
|
||||
ds >> damageRequest.hitType;
|
||||
ds >> damageRequest.damageType;
|
||||
ds >> damageRequest.damage;
|
||||
ds >> damageRequest.knockbackMomentum;
|
||||
ds >> damageRequest.sourceEntityId;
|
||||
ds >> damageRequest.damageSourceKind;
|
||||
ds >> damageRequest.statusEffects;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DamageNotification::DamageNotification() : sourceEntityId(), targetEntityId(), damageDealt(), healthLost() {}
|
||||
|
||||
DamageNotification::DamageNotification(Json const& v) {
|
||||
sourceEntityId = v.getInt("sourceEntityId");
|
||||
targetEntityId = v.getInt("targetEntityId");
|
||||
position = jsonToVec2F(v.get("position"));
|
||||
damageDealt = v.getFloat("damageDealt");
|
||||
healthLost = v.getFloat("healthLost");
|
||||
hitType = HitTypeNames.getLeft(v.getString("hitType"));
|
||||
damageSourceKind = v.getString("damageSourceKind");
|
||||
targetMaterialKind = v.getString("targetMaterialKind");
|
||||
}
|
||||
|
||||
DamageNotification::DamageNotification(EntityId sourceEntityId,
|
||||
EntityId targetEntityId,
|
||||
Vec2F position,
|
||||
float damageDealt,
|
||||
float healthLost,
|
||||
HitType hitType,
|
||||
String damageSourceKind,
|
||||
String targetMaterialKind)
|
||||
: sourceEntityId(sourceEntityId),
|
||||
targetEntityId(targetEntityId),
|
||||
position(position),
|
||||
damageDealt(damageDealt),
|
||||
healthLost(healthLost),
|
||||
hitType(hitType),
|
||||
damageSourceKind(move(damageSourceKind)),
|
||||
targetMaterialKind(move(targetMaterialKind)) {}
|
||||
|
||||
Json DamageNotification::toJson() const {
|
||||
return JsonObject{{"sourceEntityId", sourceEntityId},
|
||||
{"targetEntityId", targetEntityId},
|
||||
{"position", jsonFromVec2F(position)},
|
||||
{"damageDealt", damageDealt},
|
||||
{"healthLost", healthLost},
|
||||
{"hitType", HitTypeNames.getRight(hitType)},
|
||||
{"damageSourceKind", damageSourceKind},
|
||||
{"targetMaterialKind", targetMaterialKind}};
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageNotification const& damageNotification) {
|
||||
ds.viwrite(damageNotification.sourceEntityId);
|
||||
ds.viwrite(damageNotification.targetEntityId);
|
||||
ds.vfwrite(damageNotification.position[0], 0.01f);
|
||||
ds.vfwrite(damageNotification.position[1], 0.01f);
|
||||
ds.write(damageNotification.damageDealt);
|
||||
ds.write(damageNotification.healthLost);
|
||||
ds.write(damageNotification.hitType);
|
||||
ds.write(damageNotification.damageSourceKind);
|
||||
ds.write(damageNotification.targetMaterialKind);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, DamageNotification& damageNotification) {
|
||||
ds.viread(damageNotification.sourceEntityId);
|
||||
ds.viread(damageNotification.targetEntityId);
|
||||
ds.vfread(damageNotification.position[0], 0.01f);
|
||||
ds.vfread(damageNotification.position[1], 0.01f);
|
||||
ds.read(damageNotification.damageDealt);
|
||||
ds.read(damageNotification.healthLost);
|
||||
ds.read(damageNotification.hitType);
|
||||
ds.read(damageNotification.damageSourceKind);
|
||||
ds.read(damageNotification.targetMaterialKind);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
124
source/game/StarDamage.hpp
Normal file
124
source/game/StarDamage.hpp
Normal file
|
@ -0,0 +1,124 @@
|
|||
#ifndef STAR_DAMAGE_HPP
|
||||
#define STAR_DAMAGE_HPP
|
||||
|
||||
#include "StarDamageTypes.hpp"
|
||||
#include "StarWorldGeometry.hpp"
|
||||
#include "StarStatusTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct DamageSource {
|
||||
typedef MVariant<PolyF, Line2F> DamageArea;
|
||||
typedef MVariant<float, Vec2F> Knockback;
|
||||
|
||||
DamageSource();
|
||||
DamageSource(Json const& v);
|
||||
DamageSource(DamageType damageType,
|
||||
DamageArea damageArea,
|
||||
float damage,
|
||||
bool trackSourceEntity,
|
||||
EntityId sourceEntityId,
|
||||
EntityDamageTeam team,
|
||||
Maybe<String> damageRepeatGroup,
|
||||
Maybe<float> damageRepeatTimeout,
|
||||
String damageSourceKind,
|
||||
List<EphemeralStatusEffect> statusEffects,
|
||||
Knockback knockback,
|
||||
bool rayCheck);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
DamageSource& translate(Vec2F const& position);
|
||||
|
||||
bool intersectsWithPoly(WorldGeometry const& worldGeometry, PolyF const& poly) const;
|
||||
Vec2F knockbackMomentum(WorldGeometry const& worldGeometry, Vec2F const& targetCenter) const;
|
||||
|
||||
bool operator==(DamageSource const& rhs) const;
|
||||
|
||||
DamageType damageType;
|
||||
DamageArea damageArea;
|
||||
float damage;
|
||||
|
||||
bool trackSourceEntity;
|
||||
// The originating entity for the damage, which can be different than the
|
||||
// actual causing entity. Optional, defaults to NullEntityId.
|
||||
EntityId sourceEntityId;
|
||||
EntityDamageTeam team;
|
||||
|
||||
// Applying damage will block other DamageSources with the same
|
||||
// damageRepeatGroup from applying damage until the timeout expires, to
|
||||
// prevent damages being applied every tick. This is optional, and if it is
|
||||
// omitted then the repeat group will instead be based on the causing entity
|
||||
// id.
|
||||
Maybe<String> damageRepeatGroup;
|
||||
// Can override the default repeat damage timeout with a custom timeout.
|
||||
Maybe<float> damageRepeatTimeout;
|
||||
|
||||
String damageSourceKind;
|
||||
List<EphemeralStatusEffect> statusEffects;
|
||||
// Either directionless knockback momentum or directional knockback momentum
|
||||
Knockback knockback;
|
||||
// Should a collision check from the source entity to the impact center be
|
||||
// peformed before applying the damage?
|
||||
bool rayCheck;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageSource const& damageSource);
|
||||
DataStream& operator>>(DataStream& ds, DamageSource& damageSource);
|
||||
|
||||
struct DamageRequest {
|
||||
DamageRequest();
|
||||
DamageRequest(Json const& v);
|
||||
DamageRequest(HitType hitType,
|
||||
DamageType type,
|
||||
float damage,
|
||||
Vec2F const& knockbackMomentum,
|
||||
EntityId sourceEntityId,
|
||||
String const& damageSourceKind,
|
||||
List<EphemeralStatusEffect> const& statusEffects);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
HitType hitType;
|
||||
DamageType damageType;
|
||||
float damage;
|
||||
Vec2F knockbackMomentum;
|
||||
// May be different than the entity that actually caused damage, for example,
|
||||
// a player firing a projectile.
|
||||
EntityId sourceEntityId;
|
||||
String damageSourceKind;
|
||||
List<EphemeralStatusEffect> statusEffects;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageRequest const& damageRequest);
|
||||
DataStream& operator>>(DataStream& ds, DamageRequest& damageRequest);
|
||||
|
||||
struct DamageNotification {
|
||||
DamageNotification();
|
||||
DamageNotification(Json const& v);
|
||||
DamageNotification(EntityId sourceEntityId,
|
||||
EntityId targetEntityId,
|
||||
Vec2F position,
|
||||
float damageDealt,
|
||||
float healthLost,
|
||||
HitType hitType,
|
||||
String damageSourceKind,
|
||||
String targetMaterialKind);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
EntityId sourceEntityId;
|
||||
EntityId targetEntityId;
|
||||
Vec2F position;
|
||||
float damageDealt;
|
||||
float healthLost;
|
||||
HitType hitType;
|
||||
String damageSourceKind;
|
||||
String targetMaterialKind;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DamageNotification const& damageNotification);
|
||||
DataStream& operator>>(DataStream& ds, DamageNotification& damageNotification);
|
||||
}
|
||||
|
||||
#endif
|
69
source/game/StarDamageDatabase.cpp
Normal file
69
source/game/StarDamageDatabase.cpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "StarDamageDatabase.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DamageDatabase::DamageDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
auto elementalConfig = assets->json("/damage/elementaltypes.config");
|
||||
for (auto p : elementalConfig.iterateObject()) {
|
||||
ElementalType type;
|
||||
type.resistanceStat = p.second.getString("resistanceStat");
|
||||
for (auto particle : p.second.getObject("damageNumberParticles")) {
|
||||
type.damageNumberParticles.set(HitTypeNames.getLeft(particle.first), particle.second.toString());
|
||||
}
|
||||
m_elementalTypes.set(p.first, move(type));
|
||||
}
|
||||
|
||||
auto files = assets->scanExtension("damage");
|
||||
assets->queueJsons(files);
|
||||
for (auto file : files) {
|
||||
auto config = assets->json(file);
|
||||
String name = config.getString("kind");
|
||||
if (m_damageKinds.contains(name))
|
||||
throw StarException(strf("Duplicate damage kind Name %s. configfile %s", name, file));
|
||||
|
||||
DamageKind kind;
|
||||
kind.name = name;
|
||||
for (auto effect : config.getObject("effects", JsonObject())) {
|
||||
TargetMaterial material = effect.first;
|
||||
kind.effects.set(material, {});
|
||||
for (auto hit : effect.second.toObject()) {
|
||||
DamageEffect effect = DamageEffect {
|
||||
hit.second.get("sounds", JsonArray()),
|
||||
hit.second.get("particles", JsonArray())
|
||||
};
|
||||
kind.effects[material].set(HitTypeNames.getLeft(hit.first), effect);
|
||||
}
|
||||
}
|
||||
kind.elementalType = config.getString("elementalType", "default");
|
||||
if (!m_elementalTypes.contains(kind.elementalType))
|
||||
throw StarException(strf("Undefined elemental type %s in damage kind %s", kind.elementalType, name));
|
||||
|
||||
m_damageKinds.set(name, move(kind));
|
||||
}
|
||||
}
|
||||
|
||||
DamageKind const& DamageDatabase::damageKind(String kind) const {
|
||||
if (kind.empty())
|
||||
kind = "default";
|
||||
else
|
||||
kind = kind.toLower();
|
||||
|
||||
if (!m_damageKinds.contains(kind))
|
||||
throw StarException(strf("Unknown damage definition with kind '%s'.", kind));
|
||||
|
||||
return m_damageKinds.get(kind);
|
||||
}
|
||||
|
||||
ElementalType const& DamageDatabase::elementalType(String const& name) const {
|
||||
if (!m_damageKinds.contains(name))
|
||||
throw StarException(strf("Unknown elemental type with name '%s'.", name));
|
||||
|
||||
return m_elementalTypes.get(name);
|
||||
}
|
||||
|
||||
}
|
46
source/game/StarDamageDatabase.hpp
Normal file
46
source/game/StarDamageDatabase.hpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#ifndef STAR_DAMAGE_DATABASE_HPP
|
||||
#define STAR_DAMAGE_DATABASE_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarDamageTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_STRUCT(DamageKind);
|
||||
STAR_CLASS(DamageDatabase);
|
||||
STAR_STRUCT(ElementalType);
|
||||
|
||||
typedef String TargetMaterial;
|
||||
|
||||
struct ElementalType {
|
||||
String resistanceStat;
|
||||
HashMap<HitType, String> damageNumberParticles;
|
||||
};
|
||||
|
||||
struct DamageEffect {
|
||||
Json sounds;
|
||||
Json particles;
|
||||
};
|
||||
|
||||
struct DamageKind {
|
||||
String name;
|
||||
HashMap<TargetMaterial, HashMap<HitType, DamageEffect>> effects;
|
||||
String elementalType;
|
||||
};
|
||||
|
||||
class DamageDatabase {
|
||||
public:
|
||||
DamageDatabase();
|
||||
|
||||
DamageKind const& damageKind(String name) const;
|
||||
ElementalType const& elementalType(String const& name) const;
|
||||
|
||||
private:
|
||||
StringMap<DamageKind> m_damageKinds;
|
||||
StringMap<ElementalType> m_elementalTypes;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
263
source/game/StarDamageManager.cpp
Normal file
263
source/game/StarDamageManager.cpp
Normal file
|
@ -0,0 +1,263 @@
|
|||
#include "StarDamageManager.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarIterator.hpp"
|
||||
#include "StarEntityMap.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarColor.hpp"
|
||||
#include "StarWorld.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
ConnectionId RemoteHitRequest::destinationConnection() const {
|
||||
return connectionForEntity(causingEntityId);
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteHitRequest const& hitRequest) {
|
||||
ds << hitRequest.causingEntityId;
|
||||
ds << hitRequest.targetEntityId;
|
||||
ds << hitRequest.damageRequest;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, RemoteHitRequest& hitRequest) {
|
||||
ds >> hitRequest.causingEntityId;
|
||||
ds >> hitRequest.targetEntityId;
|
||||
ds >> hitRequest.damageRequest;
|
||||
return ds;
|
||||
}
|
||||
|
||||
ConnectionId RemoteDamageRequest::destinationConnection() const {
|
||||
return connectionForEntity(targetEntityId);
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteDamageRequest const& damageRequest) {
|
||||
ds << damageRequest.causingEntityId;
|
||||
ds << damageRequest.targetEntityId;
|
||||
ds << damageRequest.damageRequest;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, RemoteDamageRequest& damageRequest) {
|
||||
ds >> damageRequest.causingEntityId;
|
||||
ds >> damageRequest.targetEntityId;
|
||||
ds >> damageRequest.damageRequest;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteDamageNotification const& damageNotification) {
|
||||
ds << damageNotification.sourceEntityId;
|
||||
ds << damageNotification.damageNotification;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, RemoteDamageNotification& damageNotification) {
|
||||
ds >> damageNotification.sourceEntityId;
|
||||
ds >> damageNotification.damageNotification;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DamageManager::DamageManager(World* world, ConnectionId connectionId) : m_world(world), m_connectionId(connectionId) {}
|
||||
|
||||
void DamageManager::update() {
|
||||
float const DefaultDamageTimeout = 1.0f;
|
||||
|
||||
auto damageIt = makeSMutableMapIterator(m_recentEntityDamages);
|
||||
while (damageIt.hasNext()) {
|
||||
auto& events = damageIt.next().second;
|
||||
auto eventIt = makeSMutableIterator(events);
|
||||
while (eventIt.hasNext()) {
|
||||
auto& event = eventIt.next();
|
||||
event.timeout -= WorldTimestep;
|
||||
auto entityIdTimeoutGroup = event.timeoutGroup.maybe<EntityId>();
|
||||
if (event.timeout <= 0.0f || (entityIdTimeoutGroup && !m_world->entity(*entityIdTimeoutGroup)))
|
||||
eventIt.remove();
|
||||
}
|
||||
if (events.empty())
|
||||
damageIt.remove();
|
||||
}
|
||||
|
||||
m_world->forAllEntities([&](EntityPtr const& causingEntity) {
|
||||
for (auto& damageSource : causingEntity->damageSources()) {
|
||||
if (damageSource.trackSourceEntity)
|
||||
damageSource.translate(causingEntity->position());
|
||||
if (auto poly = damageSource.damageArea.ptr<PolyF>())
|
||||
SpatialLogger::logPoly("world", *poly, Color::Orange.toRgba());
|
||||
else if (auto line = damageSource.damageArea.ptr<Line2F>())
|
||||
SpatialLogger::logLine("world", *line, Color::Orange.toRgba());
|
||||
|
||||
for (auto const& hitResultPair : queryHit(damageSource, causingEntity->entityId())) {
|
||||
auto targetEntity = m_world->entity(hitResultPair.first);
|
||||
if (!isAuthoritative(causingEntity, targetEntity))
|
||||
continue;
|
||||
|
||||
auto& eventList = m_recentEntityDamages[hitResultPair.first];
|
||||
// Guard against rapidly repeating damages by either the causing
|
||||
// entity id, or optionally the repeat group if specified.
|
||||
bool allowDamage = true;
|
||||
for (auto const& event : eventList) {
|
||||
if (damageSource.damageRepeatGroup) {
|
||||
if (event.timeoutGroup == *damageSource.damageRepeatGroup)
|
||||
allowDamage = false;
|
||||
} else {
|
||||
if (event.timeoutGroup == causingEntity->entityId())
|
||||
allowDamage = false;
|
||||
}
|
||||
}
|
||||
if (allowDamage) {
|
||||
float timeout = damageSource.damageRepeatTimeout.value(DefaultDamageTimeout);
|
||||
if (damageSource.damageRepeatGroup)
|
||||
eventList.append({*damageSource.damageRepeatGroup, timeout});
|
||||
else
|
||||
eventList.append({causingEntity->entityId(), timeout});
|
||||
|
||||
auto damageRequest = DamageRequest(hitResultPair.second, damageSource.damageType, damageSource.damage,
|
||||
damageSource.knockbackMomentum(m_world->geometry(), targetEntity->position()),
|
||||
damageSource.sourceEntityId, damageSource.damageSourceKind, damageSource.statusEffects);
|
||||
addHitRequest({causingEntity->entityId(), targetEntity->entityId(), damageRequest});
|
||||
|
||||
if (damageSource.damageType != NoDamage)
|
||||
addDamageRequest({causingEntity->entityId(), targetEntity->entityId(), move(damageRequest)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& damageNotification : causingEntity->selfDamageNotifications())
|
||||
addDamageNotification({causingEntity->entityId(), damageNotification});
|
||||
});
|
||||
}
|
||||
|
||||
void DamageManager::pushRemoteHitRequest(RemoteHitRequest const& remoteHitRequest) {
|
||||
if (remoteHitRequest.destinationConnection() != m_connectionId)
|
||||
throw StarException("RemoteDamageRequest routed to wrong DamageManager");
|
||||
|
||||
if (auto causingEntity = m_world->entity(remoteHitRequest.causingEntityId)) {
|
||||
starAssert(causingEntity->isMaster());
|
||||
causingEntity->hitOther(remoteHitRequest.targetEntityId, remoteHitRequest.damageRequest);
|
||||
}
|
||||
}
|
||||
|
||||
void DamageManager::pushRemoteDamageRequest(RemoteDamageRequest const& remoteDamageRequest) {
|
||||
if (remoteDamageRequest.destinationConnection() != m_connectionId)
|
||||
throw StarException("RemoteDamageRequest routed to wrong DamageManager");
|
||||
|
||||
if (auto targetEntity = m_world->entity(remoteDamageRequest.targetEntityId)) {
|
||||
starAssert(targetEntity->isMaster());
|
||||
for (auto& damageNotification : targetEntity->applyDamage(remoteDamageRequest.damageRequest))
|
||||
addDamageNotification({remoteDamageRequest.damageRequest.sourceEntityId, move(damageNotification)});
|
||||
}
|
||||
}
|
||||
|
||||
void DamageManager::pushRemoteDamageNotification(RemoteDamageNotification remoteDamageNotification) {
|
||||
if (auto sourceEntity = m_world->entity(remoteDamageNotification.sourceEntityId)) {
|
||||
if (sourceEntity->isMaster()
|
||||
&& sourceEntity->entityId() != remoteDamageNotification.damageNotification.targetEntityId)
|
||||
sourceEntity->damagedOther(remoteDamageNotification.damageNotification);
|
||||
}
|
||||
|
||||
m_pendingNotifications.append(move(remoteDamageNotification.damageNotification));
|
||||
}
|
||||
|
||||
List<RemoteHitRequest> DamageManager::pullRemoteHitRequests() {
|
||||
return take(m_pendingRemoteHitRequests);
|
||||
}
|
||||
|
||||
List<RemoteDamageRequest> DamageManager::pullRemoteDamageRequests() {
|
||||
return take(m_pendingRemoteDamageRequests);
|
||||
}
|
||||
|
||||
List<RemoteDamageNotification> DamageManager::pullRemoteDamageNotifications() {
|
||||
return take(m_pendingRemoteNotifications);
|
||||
}
|
||||
|
||||
List<DamageNotification> DamageManager::pullPendingNotifications() {
|
||||
return take(m_pendingNotifications);
|
||||
}
|
||||
|
||||
SmallList<pair<EntityId, HitType>, 4> DamageManager::queryHit(DamageSource const& source, EntityId causingId) const {
|
||||
SmallList<pair<EntityId, HitType>, 4> resultList;
|
||||
auto doQueryHit = [&source, &resultList, causingId, this](EntityPtr const& targetEntity) {
|
||||
if (targetEntity->entityId() == causingId)
|
||||
return;
|
||||
|
||||
if (!source.team.canDamage(targetEntity->getTeam(), targetEntity->entityId() == source.sourceEntityId))
|
||||
return;
|
||||
|
||||
if (source.rayCheck) {
|
||||
if (auto poly = source.damageArea.ptr<PolyF>()) {
|
||||
if (auto sourceEntity = m_world->entity(source.sourceEntityId)) {
|
||||
auto overlap = m_world->geometry().rectOverlap(targetEntity->metaBoundBox().translated(targetEntity->position()), poly->boundBox());
|
||||
if (!overlap.isEmpty() && m_world->lineTileCollision(overlap.center(), sourceEntity->position()))
|
||||
return;
|
||||
}
|
||||
} else if (auto line = source.damageArea.ptr<Line2F>()) {
|
||||
if (auto hitPoly = targetEntity->hitPoly()) {
|
||||
if (auto intersection = m_world->geometry().lineIntersectsPolyAt(*line, *hitPoly)) {
|
||||
if (m_world->lineTileCollision(line->min(), *intersection))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto hitResult = targetEntity->queryHit(source))
|
||||
resultList.append({targetEntity->entityId(), *hitResult});
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if (auto poly = source.damageArea.ptr<PolyF>())
|
||||
m_world->forEachEntity(poly->boundBox(), doQueryHit);
|
||||
else if (auto line = source.damageArea.ptr<Line2F>())
|
||||
m_world->forEachEntityLine(line->min(), line->max(), doQueryHit);
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
bool DamageManager::isAuthoritative(EntityPtr const& causingEntity, EntityPtr const& targetEntity) {
|
||||
// Damage manager is authoritative if either one of the entities is
|
||||
// masterOnly, OR the manager is server-side and both entities are
|
||||
// server-side master entities, OR the damage manager is server-side and both
|
||||
// entities are different clients, OR if the manager is client-side and the
|
||||
// source is client-side master and the target is server-side master, OR if
|
||||
// the manager is client-side and the target is client-side master.
|
||||
//
|
||||
// This means that PvE and EvP are both decided on the player doing the
|
||||
// hitting or getting hit, and PvP is decided on the server, except for
|
||||
// master-only entities whose interactions are always decided on the machine
|
||||
// they are residing on.
|
||||
|
||||
auto causingClient = connectionForEntity(causingEntity->entityId());
|
||||
auto targetClient = connectionForEntity(targetEntity->entityId());
|
||||
|
||||
if (causingEntity->masterOnly() || targetEntity->masterOnly())
|
||||
return true;
|
||||
else if (causingClient == ServerConnectionId && targetClient == ServerConnectionId)
|
||||
return m_connectionId == ServerConnectionId;
|
||||
else if (causingClient != ServerConnectionId && targetClient != ServerConnectionId && causingClient != targetClient)
|
||||
return m_connectionId == ServerConnectionId;
|
||||
else if (targetClient == ServerConnectionId)
|
||||
return causingClient == m_connectionId;
|
||||
else
|
||||
return targetClient == m_connectionId;
|
||||
}
|
||||
|
||||
void DamageManager::addHitRequest(RemoteHitRequest const& remoteHitRequest) {
|
||||
if (remoteHitRequest.destinationConnection() == m_connectionId)
|
||||
pushRemoteHitRequest(remoteHitRequest);
|
||||
else
|
||||
m_pendingRemoteHitRequests.append(remoteHitRequest);
|
||||
}
|
||||
|
||||
void DamageManager::addDamageRequest(RemoteDamageRequest remoteDamageRequest) {
|
||||
if (remoteDamageRequest.destinationConnection() == m_connectionId)
|
||||
pushRemoteDamageRequest(move(remoteDamageRequest));
|
||||
else
|
||||
m_pendingRemoteDamageRequests.append(move(remoteDamageRequest));
|
||||
}
|
||||
|
||||
void DamageManager::addDamageNotification(RemoteDamageNotification remoteDamageNotification) {
|
||||
pushRemoteDamageNotification(remoteDamageNotification);
|
||||
m_pendingRemoteNotifications.append(move(remoteDamageNotification));
|
||||
}
|
||||
|
||||
}
|
99
source/game/StarDamageManager.hpp
Normal file
99
source/game/StarDamageManager.hpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
#ifndef STAR_DAMAGE_MANAGER_HPP
|
||||
#define STAR_DAMAGE_MANAGER_HPP
|
||||
|
||||
#include "StarDamage.hpp"
|
||||
#include "StarDamageTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(World);
|
||||
STAR_CLASS(Entity);
|
||||
STAR_CLASS(DamageManager);
|
||||
|
||||
struct RemoteHitRequest {
|
||||
ConnectionId destinationConnection() const;
|
||||
|
||||
EntityId causingEntityId;
|
||||
EntityId targetEntityId;
|
||||
DamageRequest damageRequest;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteHitRequest const& hitRequest);
|
||||
DataStream& operator>>(DataStream& ds, RemoteHitRequest& hitRequest);
|
||||
|
||||
struct RemoteDamageRequest {
|
||||
ConnectionId destinationConnection() const;
|
||||
|
||||
EntityId causingEntityId;
|
||||
EntityId targetEntityId;
|
||||
DamageRequest damageRequest;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteDamageRequest const& damageRequest);
|
||||
DataStream& operator>>(DataStream& ds, RemoteDamageRequest& damageRequest);
|
||||
|
||||
struct RemoteDamageNotification {
|
||||
EntityId sourceEntityId;
|
||||
DamageNotification damageNotification;
|
||||
};
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RemoteDamageNotification const& damageNotification);
|
||||
DataStream& operator>>(DataStream& ds, RemoteDamageNotification& damageNotification);
|
||||
|
||||
// Right now, handles entity -> entity damage and ensures that no repeat damage
|
||||
// is applied within the damage cutoff time from the same causing entity.
|
||||
class DamageManager {
|
||||
public:
|
||||
DamageManager(World* world, ConnectionId connectionId);
|
||||
|
||||
// Notify entities that they have caused damage, apply damage to master
|
||||
// entities, produce damage notifications, and run down damage timeouts.
|
||||
void update();
|
||||
|
||||
// Incoming RemoteHitRequest and RemoteDamageRequest must have the
|
||||
// destinationConnection equal to the DamageManager's connectionId
|
||||
|
||||
void pushRemoteHitRequest(RemoteHitRequest const& remoteHitRequest);
|
||||
void pushRemoteDamageRequest(RemoteDamageRequest const& remoteDamageRequest);
|
||||
void pushRemoteDamageNotification(RemoteDamageNotification remoteDamageNotification);
|
||||
|
||||
List<RemoteHitRequest> pullRemoteHitRequests();
|
||||
List<RemoteDamageRequest> pullRemoteDamageRequests();
|
||||
List<RemoteDamageNotification> pullRemoteDamageNotifications();
|
||||
|
||||
// Pending *local* notifications. Sum of all notifications either generated
|
||||
// locally or recieved.
|
||||
List<DamageNotification> pullPendingNotifications();
|
||||
|
||||
private:
|
||||
struct EntityDamageEvent {
|
||||
Variant<EntityId, String> timeoutGroup;
|
||||
float timeout;
|
||||
};
|
||||
|
||||
// Searches for and queries for hit to any entity within range of the
|
||||
// damage source. Skips over source.sourceEntityId, if set.
|
||||
SmallList<pair<EntityId, HitType>, 4> queryHit(DamageSource const& source, EntityId causingId) const;
|
||||
|
||||
bool isAuthoritative(EntityPtr const& causingEntity, EntityPtr const& targetEntity);
|
||||
|
||||
void addHitRequest(RemoteHitRequest const& remoteHitRequest);
|
||||
void addDamageRequest(RemoteDamageRequest remoteDamageRequest);
|
||||
void addDamageNotification(RemoteDamageNotification remoteDamageNotification);
|
||||
|
||||
World* m_world;
|
||||
ConnectionId m_connectionId;
|
||||
|
||||
// Maps target entity to all of the recent damage events that entity has
|
||||
// received, to prevent rapidly repeating damage.
|
||||
HashMap<EntityId, List<EntityDamageEvent>> m_recentEntityDamages;
|
||||
|
||||
List<RemoteHitRequest> m_pendingRemoteHitRequests;
|
||||
List<RemoteDamageRequest> m_pendingRemoteDamageRequests;
|
||||
List<RemoteDamageNotification> m_pendingRemoteNotifications;
|
||||
List<DamageNotification> m_pendingNotifications;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
100
source/game/StarDamageTypes.cpp
Normal file
100
source/game/StarDamageTypes.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "StarDamageTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<DamageType> const DamageTypeNames{{DamageType::NoDamage, "NoDamage"},
|
||||
{DamageType::Damage, "Damage"},
|
||||
{DamageType::IgnoresDef, "IgnoresDef"},
|
||||
{DamageType::Knockback, "Knockback"},
|
||||
{DamageType::Environment, "Environment"},
|
||||
{DamageType::Status, "Status"}};
|
||||
|
||||
EnumMap<HitType> const HitTypeNames{
|
||||
{HitType::Hit, "Hit"},
|
||||
{HitType::StrongHit, "StrongHit"},
|
||||
{HitType::WeakHit, "WeakHit"},
|
||||
{HitType::ShieldHit, "ShieldHit"},
|
||||
{HitType::Kill, "Kill"}};
|
||||
|
||||
EnumMap<TeamType> const TeamTypeNames{{TeamType::Null, "null"},
|
||||
{TeamType::Friendly, "friendly"},
|
||||
{TeamType::Enemy, "enemy"},
|
||||
{TeamType::PVP, "pvp"},
|
||||
{TeamType::Passive, "passive"},
|
||||
{TeamType::Ghostly, "ghostly"},
|
||||
{TeamType::Environment, "environment"},
|
||||
{TeamType::Indiscriminate, "indiscriminate"},
|
||||
{TeamType::Assistant, "assistant"}};
|
||||
|
||||
EntityDamageTeam::EntityDamageTeam() : type(TeamType::Null), team(0) {}
|
||||
|
||||
EntityDamageTeam::EntityDamageTeam(TeamType type, TeamNumber team) : type(type), team(team) {}
|
||||
|
||||
EntityDamageTeam::EntityDamageTeam(Json const& json) {
|
||||
type = TeamTypeNames.getLeft(json.getString("type"));
|
||||
team = json.getUInt("team", 0);
|
||||
}
|
||||
|
||||
Json EntityDamageTeam::toJson() const {
|
||||
return JsonObject{{"type", TeamTypeNames.getRight(type)}, {"team", team}};
|
||||
}
|
||||
|
||||
bool EntityDamageTeam::canDamage(EntityDamageTeam victim, bool victimIsSelf) const {
|
||||
if (victimIsSelf) {
|
||||
if (type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
} else if (type == TeamType::Friendly) {
|
||||
if (victim.type == TeamType::Enemy || victim.type == TeamType::Passive || victim.type == TeamType::Environment || victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
} else if (type == TeamType::Enemy) {
|
||||
if (victim.type == TeamType::Friendly || victim.type == TeamType::PVP || victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
else if (victim.type == TeamType::Enemy && team != victim.team)
|
||||
return true;
|
||||
} else if (type == TeamType::PVP) {
|
||||
if (victim.type == TeamType::Enemy || victim.type == TeamType::Passive || victim.type == TeamType::Environment || victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
else if (victim.type == TeamType::PVP && (team == 0 || team != victim.team))
|
||||
return true;
|
||||
} else if (type == TeamType::Passive) {
|
||||
// never deal damage
|
||||
} else if (type == TeamType::Ghostly) {
|
||||
// never deal damage
|
||||
} else if (type == TeamType::Environment) {
|
||||
if (victim.type == TeamType::Friendly || victim.type == TeamType::PVP || victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
} else if (type == TeamType::Indiscriminate) {
|
||||
if (victim.type == TeamType::Friendly || victim.type == TeamType::Enemy || victim.type == TeamType::PVP
|
||||
|| victim.type == TeamType::Passive
|
||||
|| victim.type == TeamType::Environment
|
||||
|| victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
} else if (type == TeamType::Assistant) {
|
||||
if (victim.type == TeamType::Enemy || victim.type == TeamType::Passive || victim.type == TeamType::Environment || victim.type == TeamType::Indiscriminate)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EntityDamageTeam::operator==(EntityDamageTeam const& rhs) const {
|
||||
return tie(type, team) == tie(rhs.type, rhs.team);
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, EntityDamageTeam const& team) {
|
||||
ds.write(team.type);
|
||||
ds.write(team.team);
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, EntityDamageTeam& team) {
|
||||
ds.read(team.type);
|
||||
ds.read(team.team);
|
||||
return ds;
|
||||
}
|
||||
|
||||
TeamNumber soloPvpTeam(ConnectionId clientId) {
|
||||
return (TeamNumber)clientId;
|
||||
}
|
||||
|
||||
}
|
61
source/game/StarDamageTypes.hpp
Normal file
61
source/game/StarDamageTypes.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#ifndef STAR_DAMAGE_TYPES_HPP
|
||||
#define STAR_DAMAGE_TYPES_HPP
|
||||
|
||||
#include "StarVector.hpp"
|
||||
#include "StarDataStream.hpp"
|
||||
#include "StarJson.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
enum DamageType : uint8_t { NoDamage, Damage, IgnoresDef, Knockback, Environment, Status };
|
||||
extern EnumMap<DamageType> const DamageTypeNames;
|
||||
|
||||
enum class HitType { Hit, StrongHit, WeakHit, ShieldHit, Kill };
|
||||
extern EnumMap<HitType> const HitTypeNames;
|
||||
|
||||
enum class TeamType : uint8_t {
|
||||
Null,
|
||||
// non-PvP-enabled players and player allied NPCs
|
||||
Friendly,
|
||||
// hostile and neutral NPCs and monsters
|
||||
Enemy,
|
||||
// PvP-enabled players
|
||||
PVP,
|
||||
// cannot damage anything, can be damaged by Friendly/PVP/Assistant
|
||||
Passive,
|
||||
// cannot damage or be damaged
|
||||
Ghostly,
|
||||
// cannot damage enemies, can be damaged by anything except enemy
|
||||
Environment,
|
||||
// damages anything except ghostly, damaged by anything except ghostly/passive
|
||||
// used for self damage
|
||||
Indiscriminate,
|
||||
// cannot damage friendlies and cannot be damaged by anything
|
||||
Assistant
|
||||
};
|
||||
extern EnumMap<TeamType> const TeamTypeNames;
|
||||
|
||||
typedef uint16_t TeamNumber;
|
||||
|
||||
struct EntityDamageTeam {
|
||||
EntityDamageTeam();
|
||||
explicit EntityDamageTeam(TeamType type, TeamNumber team = 0);
|
||||
explicit EntityDamageTeam(Json const& json);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
bool canDamage(EntityDamageTeam victim, bool victimIsSelf) const;
|
||||
|
||||
bool operator==(EntityDamageTeam const& rhs) const;
|
||||
|
||||
TeamType type;
|
||||
TeamNumber team;
|
||||
};
|
||||
DataStream& operator<<(DataStream& ds, EntityDamageTeam const& team);
|
||||
DataStream& operator>>(DataStream& ds, EntityDamageTeam& team);
|
||||
|
||||
TeamNumber soloPvpTeam(ConnectionId clientId);
|
||||
}
|
||||
|
||||
#endif
|
64
source/game/StarDanceDatabase.cpp
Normal file
64
source/game/StarDanceDatabase.cpp
Normal file
|
@ -0,0 +1,64 @@
|
|||
#include "StarDanceDatabase.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DanceDatabase::DanceDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
auto files = assets->scanExtension("dance");
|
||||
for (auto file : files) {
|
||||
try {
|
||||
DancePtr dance = readDance(file);
|
||||
m_dances[dance->name] = dance;
|
||||
} catch (std::exception const& e) {
|
||||
Logger::error("Error loading dance file %s: %s", file, outputException(e, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DancePtr DanceDatabase::getDance(String const& name) const {
|
||||
return m_dances.get(name);
|
||||
}
|
||||
|
||||
DancePtr DanceDatabase::readDance(String const& path) {
|
||||
auto assets = Root::singleton().assets();
|
||||
Json config = assets->json(path);
|
||||
|
||||
String name = config.getString("name");
|
||||
List<String> states = config.getArray("states").transformed([](Json const& state) { return state.toString(); });
|
||||
float cycle = config.getFloat("cycle");
|
||||
bool cyclic = config.getBool("cyclic");
|
||||
float duration = config.getFloat("duration");
|
||||
List<DanceStep> steps = config.getArray("steps").transformed([](Json const& step) {
|
||||
if (step.isType(Json::Type::Object)) {
|
||||
Maybe<String> bodyFrame = step.optString("bodyFrame");
|
||||
Maybe<String> frontArmFrame = step.optString("frontArmFrame");
|
||||
Maybe<String> backArmFrame = step.optString("backArmFrame");
|
||||
Vec2F headOffset = step.opt("headOffset").apply(jsonToVec2F).value(Vec2F());
|
||||
Vec2F frontArmOffset = step.opt("frontArmOffset").apply(jsonToVec2F).value(Vec2F());
|
||||
Vec2F backArmOffset = step.opt("backArmOffset").apply(jsonToVec2F).value(Vec2F());
|
||||
float frontArmRotation = step.optFloat("frontArmRotation").value(0.0f);
|
||||
float backArmRotation = step.optFloat("frontArmRotation").value(0.0f);
|
||||
return DanceStep{bodyFrame,
|
||||
frontArmFrame,
|
||||
backArmFrame,
|
||||
headOffset,
|
||||
frontArmOffset,
|
||||
backArmOffset,
|
||||
frontArmRotation,
|
||||
backArmRotation};
|
||||
} else {
|
||||
Maybe<String> bodyFrame = step.get(0).optString();
|
||||
Maybe<String> frontArmFrame = step.get(1).optString();
|
||||
Maybe<String> backArmFrame = step.get(2).optString();
|
||||
Vec2F headOffset = step.get(3).opt().apply(jsonToVec2F).value(Vec2F());
|
||||
Vec2F frontArmOffset = step.get(4).opt().apply(jsonToVec2F).value(Vec2F());
|
||||
Vec2F backArmOffset = step.get(5).opt().apply(jsonToVec2F).value(Vec2F());
|
||||
return DanceStep{bodyFrame, frontArmFrame, backArmFrame, headOffset, frontArmOffset, backArmOffset, 0.0f, 0.0f};
|
||||
}
|
||||
});
|
||||
|
||||
return make_shared<Dance>(Dance{name, states, cycle, cyclic, duration, steps});
|
||||
}
|
||||
|
||||
}
|
47
source/game/StarDanceDatabase.hpp
Normal file
47
source/game/StarDanceDatabase.hpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
#ifndef STAR_DANCE_DATABASE_HPP
|
||||
#define STAR_DANCE_DATABASE_HPP
|
||||
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_STRUCT(DanceStep);
|
||||
STAR_STRUCT(Dance);
|
||||
STAR_CLASS(DanceDatabase);
|
||||
|
||||
struct DanceStep {
|
||||
Maybe<String> bodyFrame;
|
||||
Maybe<String> frontArmFrame;
|
||||
Maybe<String> backArmFrame;
|
||||
Vec2F headOffset;
|
||||
Vec2F frontArmOffset;
|
||||
Vec2F backArmOffset;
|
||||
float frontArmRotation;
|
||||
float backArmRotation;
|
||||
};
|
||||
|
||||
struct Dance {
|
||||
String name;
|
||||
List<String> states;
|
||||
float cycle;
|
||||
bool cyclic;
|
||||
float duration;
|
||||
List<DanceStep> steps;
|
||||
};
|
||||
|
||||
class DanceDatabase {
|
||||
public:
|
||||
DanceDatabase();
|
||||
|
||||
DancePtr getDance(String const& name) const;
|
||||
|
||||
private:
|
||||
static DancePtr readDance(String const& path);
|
||||
|
||||
StringMap<DancePtr> m_dances;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
273
source/game/StarDrawable.cpp
Normal file
273
source/game/StarDrawable.cpp
Normal file
|
@ -0,0 +1,273 @@
|
|||
#include "StarDrawable.hpp"
|
||||
#include "StarColor.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void Drawable::ImagePart::addDirectives(String const& directives, bool keepImageCenterPosition) {
|
||||
if (directives.empty())
|
||||
return;
|
||||
|
||||
if (keepImageCenterPosition) {
|
||||
auto imageMetadata = Root::singleton().imageMetadataDatabase();
|
||||
Vec2F imageSize = Vec2F(imageMetadata->imageSize(image));
|
||||
image = AssetPath::addDirectives(image, {directives});
|
||||
Vec2F newImageSize = Vec2F(imageMetadata->imageSize(image));
|
||||
|
||||
// If we are trying to maintain the image center, PRE translate the image by
|
||||
// the change in size / 2
|
||||
transformation *= Mat3F::translation((imageSize - newImageSize) / 2);
|
||||
} else {
|
||||
image = AssetPath::addDirectives(image, {directives});
|
||||
}
|
||||
}
|
||||
|
||||
void Drawable::ImagePart::removeDirectives(bool keepImageCenterPosition) {
|
||||
if (keepImageCenterPosition) {
|
||||
auto imageMetadata = Root::singleton().imageMetadataDatabase();
|
||||
Vec2F imageSize = Vec2F(imageMetadata->imageSize(image));
|
||||
image = AssetPath::removeDirectives(image);
|
||||
Vec2F newImageSize = Vec2F(imageMetadata->imageSize(image));
|
||||
|
||||
// If we are trying to maintain the image center, PRE translate the image by
|
||||
// the change in size / 2
|
||||
transformation *= Mat3F::translation((imageSize - newImageSize) / 2);
|
||||
} else {
|
||||
image = AssetPath::removeDirectives(image);
|
||||
}
|
||||
}
|
||||
|
||||
Drawable Drawable::makeLine(Line2F const& line, float lineWidth, Color const& color, Vec2F const& position) {
|
||||
Drawable drawable;
|
||||
drawable.part = LinePart{move(line), lineWidth};
|
||||
drawable.color = color;
|
||||
drawable.position = position;
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
Drawable Drawable::makePoly(PolyF poly, Color const& color, Vec2F const& position) {
|
||||
Drawable drawable;
|
||||
drawable.part = PolyPart{move(poly)};
|
||||
drawable.color = color;
|
||||
drawable.position = position;
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
Drawable Drawable::makeImage(String image, float pixelSize, bool centered, Vec2F const& position, Color const& color) {
|
||||
Drawable drawable;
|
||||
Mat3F transformation = Mat3F::identity();
|
||||
if (centered) {
|
||||
auto imageMetadata = Root::singleton().imageMetadataDatabase();
|
||||
Vec2F imageSize = Vec2F(imageMetadata->imageSize(image));
|
||||
transformation.translate(-imageSize / 2);
|
||||
}
|
||||
|
||||
transformation.scale(pixelSize);
|
||||
|
||||
drawable.part = ImagePart{move(image), move(transformation)};
|
||||
drawable.position = position;
|
||||
drawable.color = color;
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
Drawable::Drawable()
|
||||
: color(Color::White), fullbright(false) {}
|
||||
|
||||
Drawable::Drawable(Json const& json) {
|
||||
if (auto line = json.opt("line")) {
|
||||
part = LinePart{jsonToLine2F(*line), json.getFloat("width")};
|
||||
} else if (auto poly = json.opt("poly")) {
|
||||
part = PolyPart{jsonToPolyF(*poly)};
|
||||
} else if (auto image = json.opt("image")) {
|
||||
auto imageString = image->toString();
|
||||
Mat3F transformation = Mat3F::identity();
|
||||
if (auto transformationConfig = json.opt("transformation")) {
|
||||
transformation = jsonToMat3F(*transformationConfig);
|
||||
} else {
|
||||
if (json.getBool("centered", true)) {
|
||||
auto imageMetadata = Root::singleton().imageMetadataDatabase();
|
||||
Vec2F imageSize = Vec2F(imageMetadata->imageSize(imageString));
|
||||
transformation.translate(-imageSize / 2);
|
||||
}
|
||||
if (auto rotation = json.optFloat("rotation"))
|
||||
transformation.rotate(*rotation);
|
||||
if (json.getBool("mirrored", false))
|
||||
transformation.scale(Vec2F(-1, 1));
|
||||
if (auto scale = json.optFloat("scale"))
|
||||
transformation.scale(*scale);
|
||||
}
|
||||
|
||||
part = ImagePart{move(imageString), move(transformation)};
|
||||
}
|
||||
position = json.opt("position").apply(jsonToVec2F).value();
|
||||
color = json.opt("color").apply(jsonToColor).value(Color::White);
|
||||
fullbright = json.getBool("fullbright", false);
|
||||
}
|
||||
|
||||
Json Drawable::toJson() const {
|
||||
JsonObject json;
|
||||
if (auto line = part.ptr<LinePart>()) {
|
||||
json.set("line", jsonFromLine2F(line->line));
|
||||
json.set("width", line->width);
|
||||
} else if (auto poly = part.ptr<PolyPart>()) {
|
||||
json.set("poly", jsonFromPolyF(poly->poly));
|
||||
} else if (auto image = part.ptr<ImagePart>()) {
|
||||
json.set("image", image->image);
|
||||
json.set("transformation", jsonFromMat3F(image->transformation));
|
||||
}
|
||||
|
||||
json.set("position", jsonFromVec2F(position));
|
||||
json.set("color", jsonFromColor(color));
|
||||
json.set("fullbright", fullbright);
|
||||
|
||||
return move(json);
|
||||
}
|
||||
|
||||
void Drawable::translate(Vec2F const& translation) {
|
||||
position += translation;
|
||||
}
|
||||
|
||||
void Drawable::rotate(float rotation, Vec2F const& rotationCenter) {
|
||||
if (auto line = part.ptr<LinePart>())
|
||||
line->line.rotate(rotation);
|
||||
else if (auto poly = part.ptr<PolyPart>())
|
||||
poly->poly.rotate(rotation);
|
||||
else if (auto image = part.ptr<ImagePart>())
|
||||
image->transformation.rotate(rotation);
|
||||
|
||||
position = (position - rotationCenter).rotate(rotation) + rotationCenter;
|
||||
}
|
||||
|
||||
void Drawable::scale(float scaling, Vec2F const& scaleCenter) {
|
||||
scale(Vec2F::filled(scaling), scaleCenter);
|
||||
}
|
||||
|
||||
void Drawable::scale(Vec2F const& scaling, Vec2F const& scaleCenter) {
|
||||
if (auto line = part.ptr<LinePart>())
|
||||
line->line.scale(scaling);
|
||||
else if (auto poly = part.ptr<PolyPart>())
|
||||
poly->poly.scale(scaling);
|
||||
else if (auto image = part.ptr<ImagePart>())
|
||||
image->transformation.scale(scaling);
|
||||
|
||||
position = (position - scaleCenter).piecewiseMultiply(scaling) + scaleCenter;
|
||||
}
|
||||
|
||||
void Drawable::transform(Mat3F const& transformation) {
|
||||
Vec2F localTranslation = transformation.transformVec2(Vec2F());
|
||||
Mat3F localTransform = Mat3F::translation(-localTranslation) * transformation;
|
||||
|
||||
if (auto line = part.ptr<LinePart>())
|
||||
line->line.transform(localTransform);
|
||||
else if (auto poly = part.ptr<PolyPart>())
|
||||
poly->poly.transform(localTransform);
|
||||
else if (auto image = part.ptr<ImagePart>())
|
||||
image->transformation = localTransform * image->transformation;
|
||||
|
||||
position = transformation.transformVec2(position);
|
||||
}
|
||||
|
||||
void Drawable::rebase(Vec2F const& newBase) {
|
||||
if (auto line = part.ptr<LinePart>())
|
||||
line->line.translate(position - newBase);
|
||||
else if (auto poly = part.ptr<PolyPart>())
|
||||
poly->poly.translate(position - newBase);
|
||||
else if (auto image = part.ptr<ImagePart>())
|
||||
image->transformation.translate(position - newBase);
|
||||
|
||||
position = newBase;
|
||||
}
|
||||
|
||||
RectF Drawable::boundBox(bool cropImages) const {
|
||||
RectF boundBox = RectF::null();
|
||||
if (auto line = part.ptr<LinePart>()) {
|
||||
boundBox.combine(line->line.min());
|
||||
boundBox.combine(line->line.max());
|
||||
|
||||
} else if (auto poly = part.ptr<PolyPart>()) {
|
||||
boundBox.combine(poly->poly.boundBox());
|
||||
|
||||
} else if (auto image = part.ptr<ImagePart>()) {
|
||||
auto imageMetadata = Root::singleton().imageMetadataDatabase();
|
||||
RectF imageRegion = RectF::null();
|
||||
if (cropImages) {
|
||||
RectU nonEmptyRegion = imageMetadata->nonEmptyRegion(image->image);
|
||||
if (!nonEmptyRegion.isNull())
|
||||
imageRegion = RectF(nonEmptyRegion);
|
||||
} else {
|
||||
imageRegion = RectF::withSize(Vec2F(), Vec2F(imageMetadata->imageSize(image->image)));
|
||||
}
|
||||
|
||||
if (!imageRegion.isNull()) {
|
||||
boundBox.combine(image->transformation.transformVec2(Vec2F(imageRegion.xMin(), imageRegion.yMin())));
|
||||
boundBox.combine(image->transformation.transformVec2(Vec2F(imageRegion.xMax(), imageRegion.yMin())));
|
||||
boundBox.combine(image->transformation.transformVec2(Vec2F(imageRegion.xMin(), imageRegion.yMax())));
|
||||
boundBox.combine(image->transformation.transformVec2(Vec2F(imageRegion.xMax(), imageRegion.yMax())));
|
||||
}
|
||||
}
|
||||
|
||||
if (!boundBox.isNull())
|
||||
boundBox.translate(position);
|
||||
|
||||
return boundBox;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, Drawable::LinePart& line) {
|
||||
ds >> line.line;
|
||||
ds >> line.width;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, Drawable::LinePart const& line) {
|
||||
ds << line.line;
|
||||
ds << line.width;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, Drawable::PolyPart& poly) {
|
||||
ds >> poly.poly;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, Drawable::PolyPart const& poly) {
|
||||
ds << poly.poly;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, Drawable::ImagePart& image) {
|
||||
ds >> image.image;
|
||||
ds >> image.transformation;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, Drawable::ImagePart const& image) {
|
||||
ds << image.image;
|
||||
ds << image.transformation;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, Drawable& drawable) {
|
||||
ds >> drawable.part;
|
||||
ds >> drawable.position;
|
||||
ds >> drawable.color;
|
||||
ds >> drawable.fullbright;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, Drawable const& drawable) {
|
||||
ds << drawable.part;
|
||||
ds << drawable.position;
|
||||
ds << drawable.color;
|
||||
ds << drawable.fullbright;
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
187
source/game/StarDrawable.hpp
Normal file
187
source/game/StarDrawable.hpp
Normal file
|
@ -0,0 +1,187 @@
|
|||
#ifndef STAR_DRAWABLE_HPP
|
||||
#define STAR_DRAWABLE_HPP
|
||||
|
||||
#include "StarString.hpp"
|
||||
#include "StarDataStream.hpp"
|
||||
#include "StarPoly.hpp"
|
||||
#include "StarColor.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct Drawable {
|
||||
struct LinePart {
|
||||
Line2F line;
|
||||
float width;
|
||||
};
|
||||
|
||||
struct PolyPart {
|
||||
PolyF poly;
|
||||
};
|
||||
|
||||
struct ImagePart {
|
||||
String image;
|
||||
// Transformation of the image in pixel space (0, 0) - (width, height) to
|
||||
// the final drawn space
|
||||
Mat3F transformation;
|
||||
|
||||
// Add directives to this ImagePart, while optionally keeping the
|
||||
// transformed center of the image the same if the directives change the
|
||||
// image size.
|
||||
void addDirectives(String const& directives, bool keepImageCenterPosition);
|
||||
|
||||
// Remove directives from this ImagePart, while optionally keeping the
|
||||
// transformed center of the image the same if the directives change the
|
||||
// image size.
|
||||
void removeDirectives(bool keepImageCenterPosition);
|
||||
};
|
||||
|
||||
static Drawable makeLine(Line2F const& line, float lineWidth, Color const& color, Vec2F const& position = Vec2F());
|
||||
static Drawable makePoly(PolyF poly, Color const& color, Vec2F const& position = Vec2F());
|
||||
static Drawable makeImage(String image, float pixelSize, bool centered, Vec2F const& position, Color const& color = Color::White);
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void translateAll(DrawablesContainer& drawables, Vec2F const& translation);
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void rotateAll(DrawablesContainer& drawables, float rotation, Vec2F const& rotationCenter = Vec2F());
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void scaleAll(DrawablesContainer& drawables, float scaling, Vec2F const& scaleCenter = Vec2F());
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void scaleAll(DrawablesContainer& drawables, Vec2F const& scaling, Vec2F const& scaleCenter = Vec2F());
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void transformAll(DrawablesContainer& drawables, Mat3F const& transformation);
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static void rebaseAll(DrawablesContainer& drawables, Vec2F const& newBase = Vec2F());
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
static RectF boundBoxAll(DrawablesContainer const& drawables, bool cropImages);
|
||||
|
||||
Drawable();
|
||||
explicit Drawable(Json const& json);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
void translate(Vec2F const& translation);
|
||||
void rotate(float rotation, Vec2F const& rotationCenter = Vec2F());
|
||||
void scale(float scaling, Vec2F const& scaleCenter = Vec2F());
|
||||
void scale(Vec2F const& scaling, Vec2F const& scaleCenter = Vec2F());
|
||||
void transform(Mat3F const& transformation);
|
||||
|
||||
// Change the base position of a drawable without changing the position that
|
||||
// the drawable appears, useful to re-base a set of drawables at the same
|
||||
// position so that they will be transformed together with minimal drift
|
||||
// between them.
|
||||
void rebase(Vec2F const& newBase = Vec2F());
|
||||
|
||||
RectF boundBox(bool cropImages) const;
|
||||
|
||||
bool isLine() const;
|
||||
LinePart& linePart();
|
||||
LinePart const& linePart() const;
|
||||
|
||||
bool isPoly() const;
|
||||
PolyPart& polyPart();
|
||||
PolyPart const& polyPart() const;
|
||||
|
||||
bool isImage() const;
|
||||
ImagePart& imagePart();
|
||||
ImagePart const& imagePart() const;
|
||||
|
||||
MVariant<LinePart, PolyPart, ImagePart> part;
|
||||
|
||||
Vec2F position;
|
||||
Color color;
|
||||
bool fullbright;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, Drawable& drawable);
|
||||
DataStream& operator<<(DataStream& ds, Drawable const& drawable);
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::translateAll(DrawablesContainer& drawables, Vec2F const& translation) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.translate(translation);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::rotateAll(DrawablesContainer& drawables, float rotation, Vec2F const& rotationCenter) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.rotate(rotation, rotationCenter);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::scaleAll(DrawablesContainer& drawables, float scaling, Vec2F const& scaleCenter) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.scale(scaling, scaleCenter);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::scaleAll(DrawablesContainer& drawables, Vec2F const& scaling, Vec2F const& scaleCenter) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.scale(scaling, scaleCenter);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::transformAll(DrawablesContainer& drawables, Mat3F const& transformation) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.transform(transformation);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
void Drawable::rebaseAll(DrawablesContainer& drawables, Vec2F const& newBase) {
|
||||
for (auto& drawable : drawables)
|
||||
drawable.rebase(newBase);
|
||||
}
|
||||
|
||||
template <typename DrawablesContainer>
|
||||
RectF Drawable::boundBoxAll(DrawablesContainer const& drawables, bool cropImages) {
|
||||
RectF boundBox = RectF::null();
|
||||
for (auto const& drawable : drawables)
|
||||
boundBox.combine(drawable.boundBox(cropImages));
|
||||
return boundBox;
|
||||
}
|
||||
|
||||
inline bool Drawable::isLine() const {
|
||||
return part.is<LinePart>();
|
||||
}
|
||||
|
||||
inline Drawable::LinePart& Drawable::linePart() {
|
||||
return part.get<LinePart>();
|
||||
}
|
||||
|
||||
inline Drawable::LinePart const& Drawable::linePart() const {
|
||||
return part.get<LinePart>();
|
||||
}
|
||||
|
||||
inline bool Drawable::isPoly() const {
|
||||
return part.is<PolyPart>();
|
||||
}
|
||||
|
||||
inline Drawable::PolyPart& Drawable::polyPart() {
|
||||
return part.get<PolyPart>();
|
||||
}
|
||||
|
||||
inline Drawable::PolyPart const& Drawable::polyPart() const {
|
||||
return part.get<PolyPart>();
|
||||
}
|
||||
|
||||
inline bool Drawable::isImage() const {
|
||||
return part.is<ImagePart>();
|
||||
}
|
||||
|
||||
inline Drawable::ImagePart& Drawable::imagePart() {
|
||||
return part.get<ImagePart>();
|
||||
}
|
||||
|
||||
inline Drawable::ImagePart const& Drawable::imagePart() const {
|
||||
return part.get<ImagePart>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1602
source/game/StarDungeonGenerator.cpp
Normal file
1602
source/game/StarDungeonGenerator.cpp
Normal file
File diff suppressed because it is too large
Load diff
714
source/game/StarDungeonGenerator.hpp
Normal file
714
source/game/StarDungeonGenerator.hpp
Normal file
|
@ -0,0 +1,714 @@
|
|||
#ifndef STAR_DUNGEON_GENERATOR_HPP
|
||||
#define STAR_DUNGEON_GENERATOR_HPP
|
||||
|
||||
#include "StarImage.hpp"
|
||||
#include "StarItemDescriptor.hpp"
|
||||
#include "StarWorldGeometry.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarSet.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarLruCache.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(DungeonException, StarException);
|
||||
|
||||
STAR_CLASS(DungeonGeneratorWorldFacade);
|
||||
STAR_CLASS(DungeonDefinition);
|
||||
STAR_CLASS(DungeonDefinitions);
|
||||
|
||||
class DungeonGeneratorWorldFacade {
|
||||
public:
|
||||
virtual ~DungeonGeneratorWorldFacade() {}
|
||||
|
||||
// Hint that the given rectangular region is dungeon generated, and thus
|
||||
// would not receive the normal entity generation steps.
|
||||
virtual void markRegion(RectI const& region) = 0;
|
||||
// Mark the region as needing terrain to properly integrate with the dungeon
|
||||
virtual void markTerrain(PolyF const& region) = 0;
|
||||
// Mark the region as needing space to properly integrate with the dungeon
|
||||
virtual void markSpace(PolyF const& region) = 0;
|
||||
|
||||
virtual void setForegroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) = 0;
|
||||
virtual void setBackgroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) = 0;
|
||||
virtual void setForegroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) = 0;
|
||||
virtual void setBackgroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) = 0;
|
||||
virtual void placeObject(Vec2I const& pos, String const& objectName, Star::Direction direction, Json const& parameters) = 0;
|
||||
virtual void placeVehicle(Vec2F const& pos, String const& vehicleName, Json const& parameters) = 0;
|
||||
virtual void placeSurfaceBiomeItems(Vec2I const& pos) = 0;
|
||||
virtual void placeBiomeTree(Vec2I const& pos) = 0;
|
||||
virtual void addDrop(Vec2F const& position, ItemDescriptor const& item) = 0;
|
||||
virtual void spawnNpc(Vec2F const& position, Json const& parameters) = 0;
|
||||
virtual void spawnStagehand(Vec2F const& position, Json const& definition) = 0;
|
||||
virtual void setLiquid(Vec2I const& pos, LiquidStore const& liquid) = 0;
|
||||
virtual void connectWireGroup(List<Vec2I> const& wireGroup) = 0;
|
||||
virtual void setTileProtection(DungeonId dungeonId, bool isProtected) = 0;
|
||||
virtual bool checkSolid(Vec2I const& position, TileLayer layer) = 0;
|
||||
virtual bool checkOpen(Vec2I const& position, TileLayer layer) = 0;
|
||||
virtual bool checkOceanLiquid(Vec2I const& position) = 0;
|
||||
virtual DungeonId getDungeonIdAt(Vec2I const& position) = 0;
|
||||
virtual void setDungeonIdAt(Vec2I const& position, DungeonId dungeonId) = 0;
|
||||
virtual void clearTileEntities(RectI const& bounds, Set<Vec2I> const& positions, bool clearAnchoredObjects) = 0;
|
||||
|
||||
virtual WorldGeometry getWorldGeometry() const = 0;
|
||||
|
||||
virtual void setPlayerStart(Vec2F const& startPosition) = 0;
|
||||
};
|
||||
|
||||
namespace Dungeon {
|
||||
STAR_CLASS(DungeonGeneratorWriter);
|
||||
STAR_CLASS(PartReader);
|
||||
STAR_CLASS(Part);
|
||||
STAR_CLASS(Rule);
|
||||
STAR_CLASS(Brush);
|
||||
STAR_STRUCT(Tile);
|
||||
STAR_CLASS(Connector);
|
||||
|
||||
class DungeonGeneratorWriter {
|
||||
public:
|
||||
DungeonGeneratorWriter(DungeonGeneratorWorldFacadePtr facade, Maybe<int> terrainMarkingSurfaceLevel, Maybe<int> terrainSurfaceSpaceExtends);
|
||||
|
||||
Vec2I wrapPosition(Vec2I const& pos) const;
|
||||
|
||||
void setMarkDungeonId(Maybe<DungeonId> markDungeonId = {});
|
||||
|
||||
void requestLiquid(Vec2I const& pos, LiquidStore const& liquid);
|
||||
void setLiquid(Vec2I const& pos, LiquidStore const& liquid);
|
||||
void setForegroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant);
|
||||
void setBackgroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant);
|
||||
void setForegroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift);
|
||||
void setBackgroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift);
|
||||
bool needsForegroundBiomeMod(Vec2I const& position);
|
||||
bool needsBackgroundBiomeMod(Vec2I const& position);
|
||||
void placeObject(Vec2I const& position, String const& objectType, Direction direction, Json const& parameters);
|
||||
void placeVehicle(Vec2F const& pos, String const& vehicleName, Json const& parameters);
|
||||
void placeSurfaceBiomeItems(Vec2I const& pos);
|
||||
void placeBiomeTree(Vec2I const& pos);
|
||||
void addDrop(Vec2F const& position, ItemDescriptor const& item);
|
||||
void requestWire(Vec2I const& position, String const& wireGroup, bool partLocal);
|
||||
void spawnNpc(Vec2F const& pos, Json const& definition);
|
||||
void spawnStagehand(Vec2F const& pos, Json const& definition);
|
||||
void setPlayerStart(Vec2F const& startPosition);
|
||||
bool checkSolid(Vec2I position, TileLayer layer);
|
||||
bool checkOpen(Vec2I position, TileLayer layer);
|
||||
bool checkLiquid(Vec2I const& position);
|
||||
bool otherDungeonPresent(Vec2I position);
|
||||
void setDungeonId(Vec2I const& pos, DungeonId dungeonId);
|
||||
void markPosition(Vec2F const& pos);
|
||||
void markPosition(Vec2I const& pos);
|
||||
void finishPart();
|
||||
|
||||
void clearTileEntities(RectI const& bounds, Set<Vec2I> const& positions, bool clearAnchoredObjects);
|
||||
|
||||
void flushLiquid();
|
||||
void flush();
|
||||
|
||||
List<RectI> boundingBoxes() const;
|
||||
|
||||
void reset();
|
||||
|
||||
private:
|
||||
struct Material {
|
||||
MaterialId material;
|
||||
MaterialHue hueshift;
|
||||
MaterialColorVariant colorVariant;
|
||||
};
|
||||
|
||||
struct Mod {
|
||||
ModId mod;
|
||||
MaterialHue hueshift;
|
||||
};
|
||||
|
||||
struct ObjectSettings {
|
||||
ObjectSettings() : direction() {}
|
||||
ObjectSettings(String const& objectName, Direction direction, Json const& parameters)
|
||||
: objectName(objectName), direction(direction), parameters(parameters) {}
|
||||
|
||||
String objectName;
|
||||
Direction direction;
|
||||
Json parameters;
|
||||
};
|
||||
|
||||
DungeonGeneratorWorldFacadePtr m_facade;
|
||||
Maybe<int> m_terrainMarkingSurfaceLevel;
|
||||
Maybe<int> m_terrainSurfaceSpaceExtends;
|
||||
|
||||
Map<Vec2I, LiquidStore> m_pendingLiquids;
|
||||
|
||||
Map<Vec2I, Material> m_foregroundMaterial;
|
||||
Map<Vec2I, Material> m_backgroundMaterial;
|
||||
Map<Vec2I, Mod> m_foregroundMod;
|
||||
Map<Vec2I, Mod> m_backgroundMod;
|
||||
|
||||
Map<Vec2I, ObjectSettings> m_objects;
|
||||
Map<Vec2F, pair<String, Json>> m_vehicles;
|
||||
Set<Vec2I> m_biomeTrees;
|
||||
Set<Vec2I> m_biomeItems;
|
||||
Map<Vec2F, ItemDescriptor> m_drops;
|
||||
Map<Vec2F, Json> m_npcs;
|
||||
Map<Vec2F, Json> m_stagehands;
|
||||
Map<Vec2I, DungeonId> m_dungeonIds;
|
||||
|
||||
Map<Vec2I, LiquidStore> m_liquids;
|
||||
|
||||
StringMap<Set<Vec2I>> m_globalWires;
|
||||
List<Set<Vec2I>> m_localWires;
|
||||
StringMap<Set<Vec2I>> m_openLocalWires;
|
||||
|
||||
Maybe<DungeonId> m_markDungeonId;
|
||||
RectI m_currentBounds;
|
||||
List<RectI> m_boundingBoxes;
|
||||
};
|
||||
|
||||
class Rule {
|
||||
public:
|
||||
static Maybe<RuleConstPtr> parse(Json const& rule);
|
||||
static List<RuleConstPtr> readRules(Json const& rules);
|
||||
|
||||
virtual ~Rule() {}
|
||||
|
||||
virtual bool checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const;
|
||||
|
||||
virtual bool overdrawable() const;
|
||||
virtual bool ignorePartMaximum() const;
|
||||
virtual bool allowSpawnCount(int currentCount) const;
|
||||
|
||||
virtual bool doesNotConnectToPart(String const& name) const;
|
||||
virtual bool checkPartCombinationsAllowed(StringMap<int> const& placementCounter) const;
|
||||
|
||||
virtual bool requiresOpen() const;
|
||||
virtual bool requiresSolid() const;
|
||||
virtual bool requiresLiquid() const;
|
||||
|
||||
protected:
|
||||
Rule() {}
|
||||
};
|
||||
|
||||
class WorldGenMustContainAirRule : public Rule {
|
||||
public:
|
||||
WorldGenMustContainAirRule(TileLayer layer) : layer(layer) {}
|
||||
|
||||
virtual bool checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
virtual bool requiresOpen() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
TileLayer layer;
|
||||
};
|
||||
|
||||
class WorldGenMustContainSolidRule : public Rule {
|
||||
public:
|
||||
WorldGenMustContainSolidRule(TileLayer layer) : layer(layer) {}
|
||||
|
||||
virtual bool checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
virtual bool requiresSolid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
TileLayer layer;
|
||||
};
|
||||
|
||||
class WorldGenMustContainLiquidRule : public Rule {
|
||||
public:
|
||||
WorldGenMustContainLiquidRule() {}
|
||||
|
||||
virtual bool checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
virtual bool requiresLiquid() const override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class WorldGenMustNotContainLiquidRule : public Rule {
|
||||
public:
|
||||
WorldGenMustNotContainLiquidRule() {}
|
||||
|
||||
virtual bool checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const override;
|
||||
};
|
||||
|
||||
class AllowOverdrawingRule : public Rule {
|
||||
public:
|
||||
AllowOverdrawingRule() {}
|
||||
|
||||
virtual bool overdrawable() const override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class IgnorePartMaximumRule : public Rule {
|
||||
public:
|
||||
IgnorePartMaximumRule() {}
|
||||
|
||||
virtual bool ignorePartMaximum() const override {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class MaxSpawnCountRule : public Rule {
|
||||
public:
|
||||
MaxSpawnCountRule(Json const& rule) {
|
||||
m_maxCount = rule.toArray()[1].toArray()[0].toInt();
|
||||
}
|
||||
|
||||
virtual bool allowSpawnCount(int currentCount) const override {
|
||||
return currentCount < m_maxCount;
|
||||
}
|
||||
|
||||
private:
|
||||
int m_maxCount;
|
||||
};
|
||||
|
||||
class DoNotConnectToPartRule : public Rule {
|
||||
public:
|
||||
DoNotConnectToPartRule(Json const& rule) {
|
||||
for (auto entry : rule.toArray()[1].toArray())
|
||||
m_partNames.add(entry.toString());
|
||||
}
|
||||
|
||||
virtual bool doesNotConnectToPart(String const& name) const override {
|
||||
return m_partNames.contains(name);
|
||||
}
|
||||
|
||||
private:
|
||||
StringSet m_partNames;
|
||||
};
|
||||
|
||||
class DoNotCombineWithRule : public Rule {
|
||||
public:
|
||||
DoNotCombineWithRule(Json const& rule) {
|
||||
for (auto part : rule.toArray()[1].toArray())
|
||||
m_parts.add(part.toString());
|
||||
}
|
||||
|
||||
virtual bool checkPartCombinationsAllowed(StringMap<int> const& placementCounter) const override {
|
||||
for (auto part : m_parts) {
|
||||
if (placementCounter.contains(part) && (placementCounter.get(part) > 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
StringSet m_parts;
|
||||
};
|
||||
|
||||
enum class Phase {
|
||||
ClearPhase,
|
||||
WallPhase,
|
||||
ModsPhase,
|
||||
ObjectPhase,
|
||||
BiomeTreesPhase,
|
||||
BiomeItemsPhase,
|
||||
WirePhase,
|
||||
ItemPhase,
|
||||
NpcPhase,
|
||||
DungeonIdPhase
|
||||
};
|
||||
|
||||
class Brush {
|
||||
public:
|
||||
static BrushConstPtr parse(Json const& brush);
|
||||
static List<BrushConstPtr> readBrushes(Json const& brushes);
|
||||
|
||||
virtual ~Brush() {}
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const = 0;
|
||||
|
||||
protected:
|
||||
Brush() {}
|
||||
};
|
||||
|
||||
class RandomBrush : public Brush {
|
||||
public:
|
||||
RandomBrush(Json const& brush);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
List<BrushConstPtr> m_brushes;
|
||||
int64_t m_seed;
|
||||
};
|
||||
|
||||
class ClearBrush : public Brush {
|
||||
public:
|
||||
ClearBrush() {}
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
};
|
||||
|
||||
class FrontBrush : public Brush {
|
||||
public:
|
||||
FrontBrush(String const& material, Maybe<String> mod, Maybe<float> hueshift, Maybe<float> modhueshift, Maybe<MaterialColorVariant> colorVariant);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_material;
|
||||
MaterialHue m_materialHue;
|
||||
MaterialColorVariant m_materialColorVariant;
|
||||
Maybe<String> m_mod;
|
||||
MaterialHue m_modHue;
|
||||
};
|
||||
|
||||
class BackBrush : public Brush {
|
||||
public:
|
||||
BackBrush(String const& material, Maybe<String> mod, Maybe<float> hueshift, Maybe<float> modhueshift, Maybe<MaterialColorVariant> colorVariant);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_material;
|
||||
MaterialHue m_materialHue;
|
||||
MaterialColorVariant m_materialColorVariant;
|
||||
Maybe<String> m_mod;
|
||||
MaterialHue m_modHue;
|
||||
};
|
||||
|
||||
class ObjectBrush : public Brush {
|
||||
public:
|
||||
ObjectBrush(String const& object, Star::Direction direction, Json const& parameters);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_object;
|
||||
Star::Direction m_direction;
|
||||
Json m_parameters;
|
||||
};
|
||||
|
||||
class VehicleBrush : public Brush {
|
||||
public:
|
||||
VehicleBrush(String const& vehicle, Json const& parameters);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_vehicle;
|
||||
Json m_parameters;
|
||||
};
|
||||
|
||||
class BiomeItemsBrush : public Brush {
|
||||
public:
|
||||
BiomeItemsBrush() {}
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
};
|
||||
|
||||
class BiomeTreeBrush : public Brush {
|
||||
public:
|
||||
BiomeTreeBrush() {}
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
};
|
||||
|
||||
class ItemBrush : public Brush {
|
||||
public:
|
||||
ItemBrush(ItemDescriptor const& item);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
ItemDescriptor m_item;
|
||||
};
|
||||
|
||||
class NpcBrush : public Brush {
|
||||
public:
|
||||
NpcBrush(Json const& brush);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
Json m_npc;
|
||||
};
|
||||
|
||||
class StagehandBrush : public Brush {
|
||||
public:
|
||||
StagehandBrush(Json const& definition);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
Json m_definition;
|
||||
};
|
||||
|
||||
class DungeonIdBrush : public Brush {
|
||||
public:
|
||||
DungeonIdBrush(DungeonId dungeonId);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
DungeonId m_dungeonId;
|
||||
};
|
||||
|
||||
class SurfaceBrush : public Brush {
|
||||
public:
|
||||
SurfaceBrush(Maybe<int> variant, Maybe<String> mod);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
int m_variant;
|
||||
Maybe<String> m_mod;
|
||||
};
|
||||
|
||||
class SurfaceBackgroundBrush : public Brush {
|
||||
public:
|
||||
SurfaceBackgroundBrush(Maybe<int> variant, Maybe<String> mod);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
int m_variant;
|
||||
Maybe<String> m_mod;
|
||||
};
|
||||
|
||||
class LiquidBrush : public Brush {
|
||||
public:
|
||||
LiquidBrush(String const& liquidName, float quantity, bool source);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_liquid;
|
||||
float m_quantity;
|
||||
bool m_source;
|
||||
};
|
||||
|
||||
class WireBrush : public Brush {
|
||||
public:
|
||||
WireBrush(String wireGroup, bool partLocal) : m_wireGroup(wireGroup), m_partLocal(partLocal) {}
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
String m_wireGroup;
|
||||
bool m_partLocal;
|
||||
};
|
||||
|
||||
class PlayerStartBrush : public Brush {
|
||||
public:
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
};
|
||||
|
||||
// InvalidBrush reports an error when it is painted. This brush is used on
|
||||
// tiles
|
||||
// that represent objects that have been removed from the game.
|
||||
class InvalidBrush : public Brush {
|
||||
public:
|
||||
InvalidBrush(Maybe<String> nameHint);
|
||||
|
||||
virtual void paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const override;
|
||||
|
||||
private:
|
||||
Maybe<String> m_nameHint;
|
||||
};
|
||||
|
||||
enum class Direction : uint8_t {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Up = 2,
|
||||
Down = 3,
|
||||
Unknown = 4,
|
||||
Any = 5
|
||||
};
|
||||
extern EnumMap<Dungeon::Direction> const DungeonDirectionNames;
|
||||
|
||||
class Connector {
|
||||
public:
|
||||
Connector(Part* part, String value, bool forwardOnly, Direction direction, Vec2I offset);
|
||||
|
||||
bool connectsTo(ConnectorConstPtr connector) const;
|
||||
|
||||
String value() const;
|
||||
Vec2I positionAdjustment() const;
|
||||
Part* part() const;
|
||||
Vec2I offset() const;
|
||||
|
||||
private:
|
||||
String m_value;
|
||||
bool m_forwardOnly;
|
||||
Direction m_direction;
|
||||
Vec2I m_offset;
|
||||
Part* m_part;
|
||||
};
|
||||
|
||||
typedef function<bool(Vec2I, Tile const&)> TileCallback;
|
||||
|
||||
class PartReader {
|
||||
public:
|
||||
virtual ~PartReader() {}
|
||||
|
||||
virtual void readAsset(String const& asset) = 0;
|
||||
|
||||
// Returns the dimensions of the part
|
||||
virtual Vec2U size() const = 0;
|
||||
|
||||
// Iterate over every tile in every layer of the part.
|
||||
// The callback receives the position of the tile (within the part), and
|
||||
// the tile at that position.
|
||||
// The callback can return true to exit from the loop early.
|
||||
virtual void forEachTile(TileCallback const& callback) const = 0;
|
||||
|
||||
// Calls the callback with only the tiles at the given position.
|
||||
virtual void forEachTileAt(Vec2I pos, TileCallback const& callback) const = 0;
|
||||
|
||||
protected:
|
||||
PartReader() {}
|
||||
};
|
||||
|
||||
class Part {
|
||||
public:
|
||||
Part(DungeonDefinition* dungeon, Json const& part, PartReaderPtr reader);
|
||||
|
||||
String const& name() const;
|
||||
Vec2U size() const;
|
||||
Vec2I anchorPoint() const;
|
||||
float chance() const;
|
||||
bool markDungeonId() const;
|
||||
Maybe<float> minimumThreatLevel() const;
|
||||
Maybe<float> maximumThreatLevel() const;
|
||||
bool clearAnchoredObjects() const;
|
||||
int placementLevelConstraint() const;
|
||||
bool ignoresPartMaximum() const;
|
||||
bool allowsPlacement(int currentPlacementCount) const;
|
||||
List<ConnectorConstPtr> const& connections() const;
|
||||
bool doesNotConnectTo(Part* part) const;
|
||||
bool checkPartCombinationsAllowed(StringMap<int> const& placementCounts) const;
|
||||
bool collidesWithPlaces(Vec2I pos, Set<Vec2I>& places) const;
|
||||
|
||||
bool canPlace(Vec2I pos, DungeonGeneratorWriter* writer) const;
|
||||
|
||||
void place(Vec2I pos, Set<Vec2I> const& places, DungeonGeneratorWriter* writer) const;
|
||||
|
||||
void forEachTile(TileCallback const& callback) const;
|
||||
|
||||
private:
|
||||
void placePhase(Vec2I pos, Phase phase, Set<Vec2I> const& places, DungeonGeneratorWriter* writer) const;
|
||||
|
||||
bool tileUsesPlaces(Vec2I pos) const;
|
||||
Direction pickByEdge(Vec2I position, Vec2U size) const;
|
||||
Direction pickByNeighbours(Vec2I pos) const;
|
||||
void scanConnectors();
|
||||
void scanAnchor();
|
||||
|
||||
PartReaderConstPtr m_reader;
|
||||
|
||||
String m_name;
|
||||
List<RuleConstPtr> m_rules;
|
||||
DungeonDefinition* m_dungeon;
|
||||
List<ConnectorConstPtr> m_connections;
|
||||
Vec2I m_anchorPoint;
|
||||
bool m_overrideAllowAlways;
|
||||
Maybe<float> m_minimumThreatLevel;
|
||||
Maybe<float> m_maximumThreatLevel;
|
||||
bool m_clearAnchoredObjects;
|
||||
Vec2U m_size;
|
||||
float m_chance;
|
||||
bool m_markDungeonId;
|
||||
};
|
||||
|
||||
struct TileConnector {
|
||||
String value;
|
||||
bool forwardOnly;
|
||||
Direction direction = Direction::Unknown;
|
||||
};
|
||||
|
||||
struct Tile {
|
||||
bool canPlace(Vec2I position, DungeonGeneratorWriter* writer) const;
|
||||
void place(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const;
|
||||
bool usesPlaces() const;
|
||||
bool modifiesPlaces() const;
|
||||
bool collidesWithPlaces() const;
|
||||
bool requiresOpen() const;
|
||||
bool requiresSolid() const;
|
||||
bool requiresLiquid() const;
|
||||
|
||||
List<BrushConstPtr> brushes;
|
||||
List<RuleConstPtr> rules;
|
||||
Maybe<TileConnector> connector;
|
||||
};
|
||||
}
|
||||
|
||||
class DungeonDefinition {
|
||||
public:
|
||||
DungeonDefinition(JsonObject const& definition, String const& directory);
|
||||
|
||||
JsonObject metadata() const;
|
||||
String directory() const;
|
||||
String name() const;
|
||||
String displayName() const;
|
||||
bool isProtected() const;
|
||||
Maybe<float> gravity() const;
|
||||
Maybe<bool> breathable() const;
|
||||
StringMap<Dungeon::PartConstPtr> const& parts() const;
|
||||
|
||||
List<String> const& anchors() const;
|
||||
Maybe<Json> const& optTileset() const;
|
||||
int maxParts() const;
|
||||
int maxRadius() const;
|
||||
int extendSurfaceFreeSpace() const;
|
||||
|
||||
JsonObject metaData() const;
|
||||
|
||||
private:
|
||||
JsonObject m_metadata;
|
||||
String m_directory;
|
||||
String m_name;
|
||||
String m_displayName;
|
||||
String m_species;
|
||||
bool m_isProtected;
|
||||
List<Dungeon::RuleConstPtr> m_rules;
|
||||
StringMap<Dungeon::PartConstPtr> m_parts;
|
||||
List<String> m_anchors;
|
||||
Maybe<Json> m_tileset;
|
||||
|
||||
int m_maxRadius;
|
||||
int m_maxParts;
|
||||
int m_extendSurfaceFreeSpace;
|
||||
|
||||
Maybe<float> m_gravity;
|
||||
Maybe<bool> m_breathable;
|
||||
};
|
||||
|
||||
class DungeonDefinitions {
|
||||
public:
|
||||
DungeonDefinitions();
|
||||
|
||||
DungeonDefinitionConstPtr get(String const& name) const;
|
||||
JsonObject getMetadata(String const& name) const;
|
||||
|
||||
private:
|
||||
static DungeonDefinitionPtr readDefinition(String const& path);
|
||||
|
||||
StringMap<String> m_paths;
|
||||
mutable Mutex m_cacheMutex;
|
||||
mutable HashLruCache<String, DungeonDefinitionPtr> m_definitionCache;
|
||||
};
|
||||
|
||||
class DungeonGenerator {
|
||||
public:
|
||||
DungeonGenerator(String const& dungeonName, uint64_t seed, float threatLevel, Maybe<DungeonId> dungeonId);
|
||||
|
||||
Maybe<pair<List<RectI>, Set<Vec2I>>> generate(DungeonGeneratorWorldFacadePtr facade, Vec2I position, bool markSurfaceAndTerrain, bool forcePlacement);
|
||||
|
||||
pair<List<RectI>, Set<Vec2I>> buildDungeon(Dungeon::PartConstPtr anchor, Vec2I pos, Dungeon::DungeonGeneratorWriter* writer, bool forcePlacement);
|
||||
Dungeon::PartConstPtr pickAnchor();
|
||||
List<Dungeon::ConnectorConstPtr> findConnectablePart(Dungeon::ConnectorConstPtr connector) const;
|
||||
|
||||
DungeonDefinitionConstPtr definition() const;
|
||||
|
||||
private:
|
||||
DungeonDefinitionConstPtr m_def;
|
||||
|
||||
RandomSource m_rand;
|
||||
float m_threatLevel;
|
||||
Maybe<DungeonId> m_dungeonId;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
131
source/game/StarDungeonImagePart.cpp
Normal file
131
source/game/StarDungeonImagePart.cpp
Normal file
|
@ -0,0 +1,131 @@
|
|||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarCasting.hpp"
|
||||
#include "StarImage.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDungeonImagePart.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace Dungeon {
|
||||
|
||||
void ImagePartReader::readAsset(String const& asset) {
|
||||
auto assets = Root::singleton().assets();
|
||||
m_images.push_back(make_pair(asset, assets->image(asset)));
|
||||
}
|
||||
|
||||
Vec2U ImagePartReader::size() const {
|
||||
if (m_images.empty())
|
||||
return Vec2U{0, 0};
|
||||
return m_images[0].second->size();
|
||||
}
|
||||
|
||||
void ImagePartReader::forEachTile(TileCallback const& callback) const {
|
||||
for (auto const& entry : m_images) {
|
||||
String const& file = entry.first;
|
||||
ImageConstPtr const& image = entry.second;
|
||||
for (size_t y = 0; y < image->height(); y++) {
|
||||
for (size_t x = 0; x < image->width(); x++) {
|
||||
|
||||
Vec2I position(x, y);
|
||||
Vec4B tileColor = image->get(x, y);
|
||||
|
||||
if (auto const& tile = m_tileset->getTile(tileColor)) {
|
||||
bool exitEarly = callback(position, *tile);
|
||||
if (exitEarly)
|
||||
return;
|
||||
} else {
|
||||
throw StarException::format("Dungeon image %s uses unknown tile color: #%02x%02x%02x%02x",
|
||||
file,
|
||||
tileColor[0],
|
||||
tileColor[1],
|
||||
tileColor[2],
|
||||
tileColor[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImagePartReader::forEachTileAt(Vec2I pos, TileCallback const& callback) const {
|
||||
for (auto const& entry : m_images) {
|
||||
String const& file = entry.first;
|
||||
ImageConstPtr const& image = entry.second;
|
||||
Vec4B tileColor = image->get(pos.x(), pos.y());
|
||||
|
||||
if (auto const& tile = m_tileset->getTile(tileColor)) {
|
||||
bool exitEarly = callback(pos, *tile);
|
||||
if (exitEarly)
|
||||
return;
|
||||
} else {
|
||||
throw StarException::format("Dungeon image %s uses unknown tile color: #%02x%02x%02x%02x",
|
||||
file,
|
||||
tileColor[0],
|
||||
tileColor[1],
|
||||
tileColor[2],
|
||||
tileColor[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String connectorColorValue(Vec4B const& color) {
|
||||
return strf("%d,%d,%d,%d", color[0], color[1], color[2], color[3]);
|
||||
}
|
||||
|
||||
Tile variantMapToTile(JsonObject const& tile) {
|
||||
Tile result;
|
||||
if (tile.contains("brush"))
|
||||
result.brushes = Brush::readBrushes(tile.get("brush"));
|
||||
if (tile.contains("rules"))
|
||||
result.rules = Rule::readRules(tile.get("rules"));
|
||||
|
||||
if (tile.contains("connector") && tile.get("connector").toBool()) {
|
||||
auto connector = TileConnector();
|
||||
|
||||
connector.forwardOnly = tile.contains("connectForwardOnly") && tile.get("connectForwardOnly").toBool();
|
||||
|
||||
Json connectorValue = tile.get("value");
|
||||
if (Maybe<Json> value = tile.maybe("connector-value"))
|
||||
connectorValue = value.take();
|
||||
|
||||
if (connectorValue.isType(Json::Type::String))
|
||||
connector.value = connectorValue.toString();
|
||||
else
|
||||
connector.value = connectorColorValue(jsonToVec4B(connectorValue));
|
||||
|
||||
if (tile.contains("direction"))
|
||||
connector.direction = DungeonDirectionNames.getLeft(tile.get("direction").toString());
|
||||
|
||||
result.connector = connector;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ImageTileset::ImageTileset(Json const& tileset) {
|
||||
for (Json const& tileDef : tileset.iterateArray()) {
|
||||
Vec4B color = jsonToVec4B(tileDef.get("value"));
|
||||
m_tiles.insert(colorAsInt(color), variantMapToTile(tileDef.toObject()));
|
||||
}
|
||||
}
|
||||
|
||||
Tile const* ImageTileset::getTile(Vec4B color) const {
|
||||
if (color[3] == 0)
|
||||
color = Vec4B(255, 255, 255, 0);
|
||||
if (color[3] != 255)
|
||||
return nullptr;
|
||||
return &m_tiles.get(colorAsInt(color));
|
||||
}
|
||||
|
||||
unsigned ImageTileset::colorAsInt(Vec4B color) const {
|
||||
if ((color[3] != 0) && (color[3] != 255)) {
|
||||
starAssert(false);
|
||||
return 0;
|
||||
}
|
||||
if (color[3] == 0)
|
||||
color = Vec4B(255, 255, 255, 0);
|
||||
return (((unsigned)color[2]) << 16) | (((unsigned)color[1]) << 8) | ((unsigned)color[0]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
42
source/game/StarDungeonImagePart.hpp
Normal file
42
source/game/StarDungeonImagePart.hpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#ifndef STAR_DUNGEON_IMAGE_PART_HPP
|
||||
#define STAR_DUNGEON_IMAGE_PART_HPP
|
||||
|
||||
#include "StarDungeonGenerator.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace Dungeon {
|
||||
|
||||
STAR_CLASS(ImagePartReader);
|
||||
STAR_CLASS(ImageTileset);
|
||||
|
||||
class ImagePartReader : public PartReader {
|
||||
public:
|
||||
ImagePartReader(ImageTilesetConstPtr tileset) : m_tileset(tileset) {}
|
||||
|
||||
virtual void readAsset(String const& asset) override;
|
||||
virtual Vec2U size() const override;
|
||||
|
||||
virtual void forEachTile(TileCallback const& callback) const override;
|
||||
virtual void forEachTileAt(Vec2I pos, TileCallback const& callback) const override;
|
||||
|
||||
private:
|
||||
List<pair<String, ImageConstPtr>> m_images;
|
||||
ImageTilesetConstPtr m_tileset;
|
||||
};
|
||||
|
||||
class ImageTileset {
|
||||
public:
|
||||
ImageTileset(Json const& tileset);
|
||||
|
||||
Tile const* getTile(Vec4B color) const;
|
||||
|
||||
private:
|
||||
unsigned colorAsInt(Vec4B color) const;
|
||||
|
||||
Map<unsigned, Tile> m_tiles;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
466
source/game/StarDungeonTMXPart.cpp
Normal file
466
source/game/StarDungeonTMXPart.cpp
Normal file
|
@ -0,0 +1,466 @@
|
|||
#include "StarCompression.hpp"
|
||||
#include "StarEncode.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarCasting.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarRect.hpp"
|
||||
#include "StarObjectDatabase.hpp"
|
||||
#include "StarDungeonTMXPart.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace Dungeon {
|
||||
|
||||
bool TMXMap::forEachTile(TileCallback const& callback) const {
|
||||
for (auto const& layer : tileLayers()) {
|
||||
if (layer->forEachTile(this, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
for (auto const& group : objectGroups()) {
|
||||
if (group->forEachTile(this, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXTileLayer::forEachTile(TMXMap const* map, TileCallback const& callback) const {
|
||||
auto const& tilesets = map->tilesets();
|
||||
unsigned height = map->height();
|
||||
|
||||
for (int y = m_rect.yMin(); y <= m_rect.yMax(); ++y) {
|
||||
for (int x = m_rect.xMin(); x <= m_rect.xMax(); ++x) {
|
||||
if (callback(Vec2I(x, height - 1 - y), getTile(tilesets, Vec2I(x, y))))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String TMXObjectGroup::name() const {
|
||||
return m_name;
|
||||
}
|
||||
|
||||
bool TMXObjectGroup::forEachTile(TMXMap const* map, TileCallback const& callback) const {
|
||||
for (auto const& object : objects()) {
|
||||
if (object->forEachTile(map, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXObject::forEachTile(TMXMap const* map, TileCallback const& callback) const {
|
||||
if (m_kind == ObjectKind::Stagehand) {
|
||||
auto cPos = Vec2I(rect().center()[0], ceilf(RectF(rect()).center()[1]));
|
||||
return callback(Vec2I(cPos[0], map->height() - cPos[1]), tile());
|
||||
}
|
||||
|
||||
if (m_kind == ObjectKind::Tile) {
|
||||
// Used for placing Starbound-Tiles and Starbound-Objects
|
||||
Vec2I position(pos().x(), map->height() - pos().y());
|
||||
return callback(position, tile());
|
||||
}
|
||||
|
||||
if (m_kind == ObjectKind::Rectangle) {
|
||||
// Used for creating custom brushes and rules
|
||||
for (int x = m_rect.min().x(); x < m_rect.max().x(); ++x) {
|
||||
for (int y = m_rect.min().y(); y < m_rect.max().y(); ++y) {
|
||||
Vec2I position(x, map->height() - 1 - y);
|
||||
if (callback(position, tile()))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
starAssert(m_kind == ObjectKind::Polyline);
|
||||
// Used for wiring. Treat each vertex in the polyline as a tile with the
|
||||
// wire brush.
|
||||
for (Vec2I point : m_polyline) {
|
||||
Vec2I position(m_rect.min().x() + point.x(), map->height() - 1 - m_rect.min().y() - point.y());
|
||||
if (callback(position, tile()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXMap::forEachTileAt(Vec2I pos, TileCallback const& callback) const {
|
||||
for (auto const& layer : tileLayers()) {
|
||||
if (layer->forEachTileAt(pos, this, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
for (auto const& group : objectGroups()) {
|
||||
if (group->forEachTileAt(pos, this, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXTileLayer::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
|
||||
Vec2I tilePos(pos.x(), map->height() - 1 - pos.y());
|
||||
if (!m_rect.contains(tilePos))
|
||||
return false;
|
||||
|
||||
auto const& tile = getTile(map->tilesets(), tilePos);
|
||||
if (callback(pos, tile))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXObjectGroup::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
|
||||
for (auto const& object : objects()) {
|
||||
if (object->forEachTileAt(pos, map, callback))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TMXObject::forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const {
|
||||
if (m_kind == ObjectKind::Stagehand) {
|
||||
Vec2I cPos = Vec2I(rect().center()[0], ceilf(RectF(rect()).center()[1]));
|
||||
if (pos == cPos)
|
||||
return callback(Vec2I(pos[0], map->height() - 1 - pos[1]), tile());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_kind == ObjectKind::Tile) {
|
||||
Vec2I vertexPos(pos.x(), map->height() - pos.y());
|
||||
if (vertexPos != m_rect.min())
|
||||
return false;
|
||||
return callback(pos, tile());
|
||||
}
|
||||
|
||||
if (m_kind == ObjectKind::Rectangle) {
|
||||
if (!m_rect.contains(Vec2I(pos.x(), map->height() - 1 - pos.y())))
|
||||
return false;
|
||||
return callback(pos, tile());
|
||||
}
|
||||
|
||||
starAssert(m_kind == ObjectKind::Polyline);
|
||||
for (Vec2I point : m_polyline) {
|
||||
Vec2I pointPos(m_rect.min().x() + point.x(), map->height() - 1 - m_rect.min().y() - point.y());
|
||||
if (pos == pointPos && callback(pos, tile()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TMXTileLayer::TMXTileLayer(Json const& layer) {
|
||||
unsigned width = layer.getInt("width"), height = layer.getInt("height");
|
||||
int x = layer.getInt("x", 0), y = layer.getInt("y", 0);
|
||||
m_rect = RectI({x, y}, {x + (int)width - 1, y + (int)height - 1});
|
||||
|
||||
m_name = layer.getString("name");
|
||||
m_layer = Tiled::LayerNames.getLeft(m_name);
|
||||
|
||||
if (layer.optString("compression") == String("zlib")) {
|
||||
ByteArray compressedData = base64Decode(layer.getString("data"));
|
||||
ByteArray bytes = uncompressData(compressedData);
|
||||
for (size_t i = 0; i + 3 < bytes.size(); i += 4) {
|
||||
uint32_t gid = (uint8_t)bytes[i] | ((uint8_t)bytes[i + 1] << 8) | ((uint8_t)bytes[i + 2] << 16) | ((uint8_t)bytes[i + 3] << 24);
|
||||
m_tileData.append(gid & ~TileFlip::AllBits);
|
||||
}
|
||||
} else if (!layer.contains("compression")) {
|
||||
for (Json const& index : layer.getArray("data")) {
|
||||
// Ignore flipped tiles. Tiled can flip selected regions with X, but
|
||||
// this
|
||||
// also flips individual tiles (setting the high bits on the GID).
|
||||
// Starbound has no support for flipped tiles, but being able to flip
|
||||
// regions is still useful.
|
||||
m_tileData.append(index.toUInt() & ~TileFlip::AllBits);
|
||||
}
|
||||
} else {
|
||||
throw StarException::format("TMXTileLayer does not support compression mode %s", layer.getString("compression"));
|
||||
}
|
||||
|
||||
if (m_tileData.count() != width * height)
|
||||
throw StarException("TMXTileLayer data length was inconsistent with width/height");
|
||||
}
|
||||
|
||||
TMXMap::TMXMap(Json const& tmx) {
|
||||
if (tmx.getUInt("tileheight") != 8 || tmx.getUInt("tilewidth") != 8)
|
||||
throw StarException("Invalid tile size");
|
||||
|
||||
m_width = tmx.getUInt("width");
|
||||
m_height = tmx.getUInt("height");
|
||||
|
||||
m_tilesets = make_shared<TMXTilesets>(tmx.getArray("tilesets"));
|
||||
|
||||
for (Json const& tmxLayer : tmx.get("layers").iterateArray()) {
|
||||
String layerType = tmxLayer.getString("type");
|
||||
|
||||
if (layerType == "tilelayer") {
|
||||
TMXTileLayerPtr layer = make_shared<TMXTileLayer>(tmxLayer);
|
||||
m_tileLayers.append(layer);
|
||||
|
||||
} else if (layerType == "objectgroup") {
|
||||
TMXObjectGroupPtr group = make_shared<TMXObjectGroup>(tmxLayer, m_tilesets);
|
||||
m_objectGroups.append(group);
|
||||
|
||||
} else {
|
||||
throw StarException(strf("Unknown layer type '%s'", layerType.utf8Ptr()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tiled::Tile const& TMXTileLayer::getTile(TMXTilesetsPtr const& tilesets, Vec2I pos) const {
|
||||
if (!m_rect.contains(pos))
|
||||
return tilesets->nullTile();
|
||||
|
||||
int dx = pos.x() - m_rect.xMin();
|
||||
int dy = pos.y() - m_rect.yMin();
|
||||
unsigned tileIndex = dx + dy * width();
|
||||
|
||||
return tilesets->getTile(m_tileData[tileIndex], m_layer);
|
||||
}
|
||||
|
||||
String tilesetAssetPath(String const& relativePath) {
|
||||
// Tiled stores tileset paths relative to the map file, which can go below
|
||||
// the assets root if it's referencing a tileset in another asset package.
|
||||
// The solution chosen here is to ignore everything in the path up until a
|
||||
// known path segment, e.g.:
|
||||
// "source" : "..\/..\/..\/..\/packed\/tilesets\/packed\/materials.json"
|
||||
// We ignore everything up until the 'tilesets' path segment, and the asset
|
||||
// we actually load is located at:
|
||||
// /tilesets/packed/materials.json
|
||||
|
||||
size_t i = relativePath.findLast("/tilesets/", String::CaseInsensitive);
|
||||
if (i == NPos)
|
||||
// Couldn't extract the right path, try the one we were given in the Json.
|
||||
return relativePath;
|
||||
return relativePath.slice(i);
|
||||
}
|
||||
|
||||
TMXTilesets::TMXTilesets(Json const& tmx) {
|
||||
for (Json const& tilesetJson : tmx.iterateArray()) {
|
||||
if (!tilesetJson.contains("source"))
|
||||
throw StarException::format("Tiled map has embedded tileset %s", tilesetJson.optString("name"));
|
||||
|
||||
String sourcePath = tilesetAssetPath(tilesetJson.getString("source"));
|
||||
Tiled::TilesetConstPtr tileset = Root::singleton().tilesetDatabase()->get(sourcePath);
|
||||
m_tilesets.append(tileset);
|
||||
|
||||
size_t firstGid = tilesetJson.getUInt("firstgid");
|
||||
for (size_t i = 0; i < tileset->size(); ++i) {
|
||||
m_foregroundTilesByGid.set(firstGid + i, tileset->getTile(i, TileLayer::Foreground).get());
|
||||
m_backgroundTilesByGid.set(firstGid + i, tileset->getTile(i, TileLayer::Background).get());
|
||||
}
|
||||
}
|
||||
|
||||
m_nullTile = make_shared<Tiled::Tile>(Tiled::Properties(), TileLayer::Foreground);
|
||||
|
||||
JsonObject emptyBackProperties;
|
||||
emptyBackProperties["clear"] = "true";
|
||||
m_emptyBackTile = make_shared<Tiled::Tile>(Tiled::Properties(emptyBackProperties), TileLayer::Background);
|
||||
}
|
||||
|
||||
Tiled::Tile const& TMXTilesets::getTile(unsigned gid, TileLayer layer) const {
|
||||
Tiled::Tile const* tilePtr;
|
||||
if (layer == TileLayer::Foreground)
|
||||
tilePtr = m_foregroundTilesByGid[gid];
|
||||
else
|
||||
tilePtr = m_backgroundTilesByGid[gid];
|
||||
|
||||
if (tilePtr)
|
||||
return *tilePtr;
|
||||
if (layer == TileLayer::Foreground)
|
||||
return *m_nullTile;
|
||||
return *m_emptyBackTile;
|
||||
}
|
||||
|
||||
bool TMXTilesets::tilesetComparator(TilesetInfo const& a, TilesetInfo const& b) {
|
||||
return a.firstGid > b.firstGid;
|
||||
}
|
||||
|
||||
TMXObject::TMXObject(Maybe<Json> const& groupProperties, Json const& tmx, TMXTilesetsPtr tilesets) {
|
||||
m_objectId = tmx.getUInt("id");
|
||||
|
||||
// convert object properties in array format to object format
|
||||
Maybe<Json> objectProperties = tmx.opt("properties").apply([](Json const& properties) -> Json {
|
||||
if (properties.type() == Json::Type::Array) {
|
||||
JsonObject objectProperties;
|
||||
for (auto& p : properties.toArray())
|
||||
objectProperties.set(p.getString("name"), p.get("value"));
|
||||
return objectProperties;
|
||||
} else {
|
||||
return properties.toObject();
|
||||
}
|
||||
});
|
||||
|
||||
m_layer = getLayer(groupProperties, objectProperties);
|
||||
|
||||
Maybe<TileObjectInfo> tileObjectInfo = getTileObjectInfo(tmx, tilesets, m_layer);
|
||||
|
||||
// Merge properties in this order:
|
||||
// Object
|
||||
// Tile (and tileset by proxy)
|
||||
// ObjectGroup
|
||||
Tiled::Properties properties;
|
||||
if (objectProperties)
|
||||
properties = properties.inherit(*objectProperties);
|
||||
if (tileObjectInfo.isValid())
|
||||
properties = properties.inherit(tileObjectInfo->tileProperties);
|
||||
if (groupProperties.isValid())
|
||||
properties = properties.inherit(*groupProperties);
|
||||
|
||||
// Check whether the object was flipped horizontally before creating this
|
||||
// object's Tile
|
||||
bool flipX = tileObjectInfo.isValid() && (tileObjectInfo->flipBits & TileFlip::Horizontal) != 0;
|
||||
|
||||
m_kind = getObjectKind(tmx, objectProperties);
|
||||
|
||||
Vec2I pos = getPos(tmx) - getImagePosition(properties);
|
||||
Vec2I size = getSize(tmx);
|
||||
m_rect = RectI(pos, Vec2I(pos.x() + size.x(), pos.y() + size.y()));
|
||||
|
||||
JsonObject computedProperties;
|
||||
if (m_kind == ObjectKind::Stagehand) {
|
||||
Vec2I cPos = rect().center();
|
||||
RectI broadcastArea(rect().min() - cPos, rect().max() - cPos);
|
||||
computedProperties["broadcastArea"] = jsonFromRectI(broadcastArea).repr();
|
||||
}
|
||||
|
||||
Maybe<float> rotation = tmx.optFloat("rotation");
|
||||
if (rotation.isValid() && *rotation != 0.0f)
|
||||
throw tmxObjectError(tmx, "object is rotated, which is not supported");
|
||||
|
||||
Maybe<JsonArray> polyline = tmx.optArray("polyline");
|
||||
if (polyline.isValid()) {
|
||||
for (Json const& point : *polyline)
|
||||
m_polyline.append(getPos(point));
|
||||
computedProperties["wire"] = "_polylineWire" + toString(m_objectId);
|
||||
computedProperties["local"] = "true";
|
||||
}
|
||||
|
||||
properties = properties.inherit(computedProperties);
|
||||
m_tile = make_shared<Tiled::Tile>(properties, m_layer, flipX);
|
||||
}
|
||||
|
||||
Vec2I TMXObject::getSize(Json const& tmx) {
|
||||
Vec2I size;
|
||||
if (tmx.contains("width") && tmx.contains("height"))
|
||||
size = Vec2I(tmx.getUInt("width"), tmx.getUInt("height")) / TilePixels;
|
||||
return size;
|
||||
}
|
||||
|
||||
Vec2I TMXObject::getImagePosition(Tiled::Properties const& properties) {
|
||||
int x = (int)(properties.opt<float>("imagePositionX").value(0) / TilePixels);
|
||||
int y = (int)(properties.opt<float>("imagePositionY").value(0) / TilePixels);
|
||||
return Vec2I(x, -y);
|
||||
}
|
||||
|
||||
ObjectKind TMXObject::getObjectKind(Json const& tmx, Maybe<Json> const& objectProperties) {
|
||||
if (objectProperties && objectProperties->contains("stagehand"))
|
||||
return ObjectKind::Stagehand;
|
||||
else if (tmx.contains("gid"))
|
||||
// Tile / object
|
||||
return ObjectKind::Tile;
|
||||
else if (tmx.contains("ellipse"))
|
||||
throw tmxObjectError(tmx, "object has unsupported ellipse shape");
|
||||
else if (tmx.contains("polygon"))
|
||||
throw tmxObjectError(tmx, "object has unsupported polygon shape");
|
||||
else if (tmx.contains("polyline"))
|
||||
// Wiring
|
||||
return ObjectKind::Polyline;
|
||||
else
|
||||
// Custom brush
|
||||
return ObjectKind::Rectangle;
|
||||
}
|
||||
|
||||
Maybe<TMXObject::TileObjectInfo> TMXObject::getTileObjectInfo(
|
||||
Json const& tmx, TMXTilesetsPtr tilesets, TileLayer layer) {
|
||||
Maybe<unsigned> optGid = tmx.optUInt("gid");
|
||||
if (optGid.isNothing())
|
||||
return {};
|
||||
|
||||
unsigned flipBits = *optGid & TileFlip::AllBits;
|
||||
unsigned gid = *optGid & ~TileFlip::AllBits;
|
||||
|
||||
if (flipBits & (TileFlip::Vertical | TileFlip::Diagonal))
|
||||
throw tmxObjectError(tmx, "object contains vertical or diagonal flips, which are not supported");
|
||||
|
||||
Tiled::Tile const& gidTile = tilesets->getTile(gid, layer);
|
||||
return TileObjectInfo{gidTile.properties, flipBits};
|
||||
}
|
||||
|
||||
TileLayer TMXObject::getLayer(Maybe<Json> const& groupProperties, Maybe<Json> const& objectProperties) {
|
||||
if (objectProperties.isValid() && objectProperties->contains("layer"))
|
||||
return Tiled::LayerNames.getLeft(objectProperties->getString("layer"));
|
||||
if (groupProperties.isValid() && groupProperties->contains("layer"))
|
||||
return Tiled::LayerNames.getLeft(groupProperties->getString("layer"));
|
||||
return TileLayer::Foreground;
|
||||
}
|
||||
|
||||
Vec2I TMXObject::getPos(Json const& tmx) {
|
||||
return Vec2I(tmx.getInt("x"), tmx.getInt("y")) / TilePixels;
|
||||
}
|
||||
|
||||
StarException TMXObject::tmxObjectError(Json const& tmx, String const& msg) {
|
||||
Vec2I pos = getPos(tmx);
|
||||
return StarException::format("At %d,%d: %s", pos[0], pos[1], msg);
|
||||
}
|
||||
|
||||
TMXObjectGroup::TMXObjectGroup(Json const& tmx, TMXTilesetsPtr tilesets) {
|
||||
m_name = tmx.getString("name");
|
||||
|
||||
// convert group properties in array format to object format
|
||||
Maybe<JsonObject> groupProperties = tmx.opt("properties").apply([](Json const& properties) {
|
||||
if (properties.type() == Json::Type::Array) {
|
||||
JsonObject objectProperties;
|
||||
for (auto& p : properties.toArray())
|
||||
objectProperties.set(p.getString("name"), p.get("value"));
|
||||
return objectProperties;
|
||||
} else {
|
||||
return properties.toObject();
|
||||
}
|
||||
});
|
||||
for (Json const& tmxObject : tmx.getArray("objects")) {
|
||||
TMXObjectPtr object = make_shared<TMXObject>(groupProperties, tmxObject, tilesets);
|
||||
m_objects.append(object);
|
||||
}
|
||||
}
|
||||
|
||||
void TMXPartReader::readAsset(String const& asset) {
|
||||
auto assets = Root::singleton().assets();
|
||||
m_maps.append(make_pair(asset, make_shared<const TMXMap>(assets->json(asset))));
|
||||
}
|
||||
|
||||
Vec2U TMXPartReader::size() const {
|
||||
Vec2U size;
|
||||
forEachMap([&size](TMXMapConstPtr const& map) {
|
||||
size = Vec2U{map->width(), map->height()};
|
||||
return true;
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
void TMXPartReader::forEachTile(TileCallback const& callback) const {
|
||||
forEachMap([callback](TMXMapConstPtr const& map) {
|
||||
return map->forEachTile(callback);
|
||||
});
|
||||
}
|
||||
|
||||
void TMXPartReader::forEachTileAt(Vec2I pos, TileCallback const& callback) const {
|
||||
forEachMap([pos, callback](TMXMapConstPtr const& map) {
|
||||
return map->forEachTileAt(pos, callback);
|
||||
});
|
||||
}
|
||||
|
||||
void TMXPartReader::forEachMap(function<bool(TMXMapConstPtr const&)> func) const {
|
||||
for (auto const& map : m_maps) {
|
||||
func(map.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
215
source/game/StarDungeonTMXPart.hpp
Normal file
215
source/game/StarDungeonTMXPart.hpp
Normal file
|
@ -0,0 +1,215 @@
|
|||
#ifndef STAR_DUNGEON_TMX_PART_HPP
|
||||
#define STAR_DUNGEON_TMX_PART_HPP
|
||||
|
||||
#include "StarRect.hpp"
|
||||
#include "StarDungeonGenerator.hpp"
|
||||
#include "StarTilesetDatabase.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace Dungeon {
|
||||
STAR_CLASS(TMXTilesets);
|
||||
STAR_CLASS(TMXTileLayer);
|
||||
STAR_CLASS(TMXObject);
|
||||
STAR_CLASS(TMXObjectGroup);
|
||||
STAR_CLASS(TMXMap);
|
||||
|
||||
class TMXTilesets {
|
||||
public:
|
||||
TMXTilesets(Json const& tmx);
|
||||
|
||||
Tiled::Tile const& getTile(unsigned gid, TileLayer layer) const;
|
||||
|
||||
Tiled::Tile const& nullTile() const {
|
||||
return *m_nullTile;
|
||||
}
|
||||
|
||||
private:
|
||||
struct TilesetInfo {
|
||||
Tiled::Tileset const* tileset;
|
||||
size_t firstGid;
|
||||
size_t lastGid;
|
||||
};
|
||||
|
||||
static bool tilesetComparator(TilesetInfo const& a, TilesetInfo const& b);
|
||||
|
||||
// The default empty background tile has clear=true. (If you use the pink
|
||||
// tile in the background, clear will be false instead.) Analogous to
|
||||
// EmptyMaterialId.
|
||||
Tiled::TileConstPtr m_emptyBackTile;
|
||||
// The default foreground tile doesn't have a 'clear' property. Also
|
||||
// returned by tile layers when given coordinates outside the bounds of the
|
||||
// layer. Analogous to the NullMaterialId that mission maps are initially
|
||||
// filled with.
|
||||
Tiled::TileConstPtr m_nullTile;
|
||||
|
||||
List<Tiled::TilesetConstPtr> m_tilesets;
|
||||
List<Tiled::Tile const*> m_foregroundTilesByGid;
|
||||
List<Tiled::Tile const*> m_backgroundTilesByGid;
|
||||
};
|
||||
|
||||
class TMXTileLayer {
|
||||
public:
|
||||
TMXTileLayer(Json const& tmx);
|
||||
|
||||
Tiled::Tile const& getTile(TMXTilesetsPtr const& tilesets, Vec2I pos) const;
|
||||
|
||||
unsigned width() const {
|
||||
return m_rect.xMax() - m_rect.xMin() + 1;
|
||||
}
|
||||
unsigned height() const {
|
||||
return m_rect.yMax() - m_rect.yMin() + 1;
|
||||
}
|
||||
|
||||
RectI const& rect() const {
|
||||
return m_rect;
|
||||
}
|
||||
String const& name() const {
|
||||
return m_name;
|
||||
}
|
||||
|
||||
TileLayer layer() const {
|
||||
return m_layer;
|
||||
}
|
||||
|
||||
bool forEachTile(TMXMap const* map, TileCallback const& callback) const;
|
||||
bool forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const;
|
||||
|
||||
private:
|
||||
RectI m_rect;
|
||||
String m_name;
|
||||
TileLayer m_layer;
|
||||
List<unsigned> m_tileData;
|
||||
};
|
||||
|
||||
enum class ObjectKind {
|
||||
Tile,
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
Polygon,
|
||||
Polyline,
|
||||
Stagehand
|
||||
};
|
||||
|
||||
enum TileFlip {
|
||||
Horizontal = 0x80000000u,
|
||||
Vertical = 0x40000000u,
|
||||
Diagonal = 0x20000000u,
|
||||
AllBits = 0xe0000000u
|
||||
};
|
||||
|
||||
class TMXObject {
|
||||
public:
|
||||
TMXObject(Maybe<Json> const& groupProperties, Json const& tmx, TMXTilesetsPtr tilesets);
|
||||
|
||||
Vec2I const& pos() const {
|
||||
return m_rect.min();
|
||||
}
|
||||
RectI const& rect() const {
|
||||
return m_rect;
|
||||
}
|
||||
Tiled::Tile const& tile() const {
|
||||
return *m_tile;
|
||||
}
|
||||
ObjectKind kind() const {
|
||||
return m_kind;
|
||||
}
|
||||
|
||||
bool forEachTile(TMXMap const* map, TileCallback const& callback) const;
|
||||
bool forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const;
|
||||
|
||||
private:
|
||||
// "Tile Objects" in Tiled are objects that contain an image from a tileset,
|
||||
// and have a bunch of their own Tile Object-specific properties.
|
||||
struct TileObjectInfo {
|
||||
Tiled::Properties tileProperties;
|
||||
unsigned flipBits;
|
||||
};
|
||||
|
||||
static Vec2I getSize(Json const& tmx);
|
||||
static Vec2I getImagePosition(Tiled::Properties const& properties);
|
||||
static ObjectKind getObjectKind(Json const& tmx, Maybe<Json >const& objectProperties);
|
||||
static Maybe<TileObjectInfo> getTileObjectInfo(Json const& tmx, TMXTilesetsPtr tilesets, TileLayer layer);
|
||||
static TileLayer getLayer(Maybe<Json> const& groupProperties, Maybe<Json> const& objectProperties);
|
||||
|
||||
static Vec2I getPos(Json const& tmx);
|
||||
static StarException tmxObjectError(Json const& tmx, String const& msg);
|
||||
|
||||
RectI m_rect;
|
||||
Tiled::TileConstPtr m_tile;
|
||||
TileLayer m_layer;
|
||||
ObjectKind m_kind;
|
||||
unsigned m_objectId;
|
||||
List<Vec2I> m_polyline;
|
||||
};
|
||||
|
||||
class TMXObjectGroup {
|
||||
public:
|
||||
TMXObjectGroup(Json const& tmx, TMXTilesetsPtr tilesets);
|
||||
|
||||
List<TMXObjectPtr> const& objects() const {
|
||||
return m_objects;
|
||||
}
|
||||
|
||||
String name() const;
|
||||
|
||||
bool forEachTile(TMXMap const* map, TileCallback const& callback) const;
|
||||
bool forEachTileAt(Vec2I pos, TMXMap const* map, TileCallback const& callback) const;
|
||||
|
||||
private:
|
||||
String m_name;
|
||||
List<TMXObjectPtr> m_objects;
|
||||
};
|
||||
|
||||
class TMXMap {
|
||||
public:
|
||||
TMXMap(Json const& tmx);
|
||||
|
||||
List<TMXTileLayerPtr> const& tileLayers() const {
|
||||
return m_tileLayers;
|
||||
}
|
||||
List<TMXObjectGroupPtr> const& objectGroups() const {
|
||||
return m_objectGroups;
|
||||
}
|
||||
TMXTilesetsPtr const& tilesets() const {
|
||||
return m_tilesets;
|
||||
}
|
||||
unsigned width() const {
|
||||
return m_width;
|
||||
}
|
||||
unsigned height() const {
|
||||
return m_height;
|
||||
}
|
||||
|
||||
bool forEachTile(TileCallback const& callback) const;
|
||||
bool forEachTileAt(Vec2I pos, TileCallback const& callback) const;
|
||||
|
||||
private:
|
||||
List<TMXTileLayerPtr> m_tileLayers;
|
||||
List<TMXObjectGroupPtr> m_objectGroups;
|
||||
|
||||
TMXTilesetsPtr m_tilesets;
|
||||
unsigned m_width, m_height;
|
||||
};
|
||||
|
||||
class TMXPartReader : public PartReader {
|
||||
public:
|
||||
virtual void readAsset(String const& asset) override;
|
||||
|
||||
virtual Vec2U size() const override;
|
||||
|
||||
virtual void forEachTile(TileCallback const& callback) const override;
|
||||
virtual void forEachTileAt(Vec2I pos, TileCallback const& callback) const override;
|
||||
|
||||
private:
|
||||
// Return true in the callback to exit early without processing later maps
|
||||
void forEachMap(function<bool(TMXMapConstPtr const&)> func) const;
|
||||
|
||||
List<pair<String, TMXMapConstPtr>> m_maps;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
110
source/game/StarEffectEmitter.cpp
Normal file
110
source/game/StarEffectEmitter.cpp
Normal file
|
@ -0,0 +1,110 @@
|
|||
#include "StarEffectEmitter.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarParticleDatabase.hpp"
|
||||
#include "StarEntityRendering.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EffectEmitter::EffectEmitter() {
|
||||
m_renders = false;
|
||||
m_direction = Direction::Right;
|
||||
addNetElement(&m_activeSources);
|
||||
}
|
||||
|
||||
void EffectEmitter::addEffectSources(String const& position, StringSet effectSources) {
|
||||
for (auto& e : effectSources)
|
||||
m_newSources.add({position, move(e)});
|
||||
}
|
||||
|
||||
void EffectEmitter::setSourcePosition(String name, Vec2F const& position) {
|
||||
m_positions[move(name)] = position;
|
||||
}
|
||||
|
||||
void EffectEmitter::setDirection(Direction direction) {
|
||||
m_direction = direction;
|
||||
}
|
||||
|
||||
void EffectEmitter::setBaseVelocity(Vec2F const& velocity) {
|
||||
m_baseVelocity = velocity;
|
||||
}
|
||||
|
||||
void EffectEmitter::tick(EntityMode mode) {
|
||||
if (mode == EntityMode::Master) {
|
||||
m_activeSources.set(move(m_newSources));
|
||||
m_newSources.clear();
|
||||
} else {
|
||||
if (!m_newSources.empty())
|
||||
throw StarException("EffectEmitters can only be added to the master entity.");
|
||||
}
|
||||
if (m_renders) {
|
||||
eraseWhere(m_sources, [](EffectSourcePtr const& source) { return source->expired(); });
|
||||
|
||||
for (auto& ps : m_sources)
|
||||
ps->tick();
|
||||
|
||||
Set<pair<String, String>> current;
|
||||
for (auto& ps : m_sources) {
|
||||
pair<String, String> entry = {ps->suggestedSpawnLocation(), ps->kind()};
|
||||
current.add(entry);
|
||||
if (!m_activeSources.get().contains(entry)) {
|
||||
ps->stop();
|
||||
}
|
||||
}
|
||||
for (auto& c : m_activeSources.get()) {
|
||||
if (!current.contains(c)) {
|
||||
m_sources.append(Root::singleton().effectSourceDatabase()->effectSourceConfig(c.second)->instance(c.first));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EffectEmitter::reset() {
|
||||
m_sources.clear();
|
||||
m_newSources.clear();
|
||||
m_activeSources.set({});
|
||||
}
|
||||
|
||||
void EffectEmitter::render(RenderCallback* renderCallback) {
|
||||
m_renders = true;
|
||||
if (m_sources.empty())
|
||||
return;
|
||||
for (auto& ps : m_sources) {
|
||||
Vec2F position = m_positions.get(ps->effectSpawnLocation());
|
||||
for (auto& p : ps->particles()) {
|
||||
Particle particle = Root::singleton().particleDatabase()->particle(p);
|
||||
if (m_direction == Direction::Left) {
|
||||
particle.flip = true;
|
||||
particle.position[0] = -particle.position[0];
|
||||
particle.velocity[0] = -particle.velocity[0];
|
||||
particle.finalVelocity[0] = -particle.finalVelocity[0];
|
||||
}
|
||||
particle.velocity += m_baseVelocity;
|
||||
particle.finalVelocity += m_baseVelocity;
|
||||
particle.position += position;
|
||||
renderCallback->addParticle(particle);
|
||||
}
|
||||
for (auto& s : ps->sounds(position))
|
||||
renderCallback->addAudio(s);
|
||||
}
|
||||
for (auto& ps : m_sources)
|
||||
ps->postRender();
|
||||
}
|
||||
|
||||
Json EffectEmitter::toJson() const {
|
||||
return JsonObject{{"activeSources",
|
||||
jsonFromSet<pair<String, String>>(m_activeSources.get(),
|
||||
[](pair<String, String> const& entry) {
|
||||
return JsonObject{{"position", entry.first}, {"source", entry.second}};
|
||||
})}};
|
||||
}
|
||||
|
||||
void EffectEmitter::fromJson(Json const& diskStore) {
|
||||
m_activeSources.set(jsonToSet<pair<String, String>>(diskStore.get("activeSources"),
|
||||
[](Json const& v) {
|
||||
return pair<String, String>{v.getString("position"), v.getString("source")};
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
44
source/game/StarEffectEmitter.hpp
Normal file
44
source/game/StarEffectEmitter.hpp
Normal file
|
@ -0,0 +1,44 @@
|
|||
#ifndef STAR_EFFECT_EMITTER_HPP
|
||||
#define STAR_EFFECT_EMITTER_HPP
|
||||
|
||||
#include "StarNetElementSystem.hpp"
|
||||
#include "StarEffectSourceDatabase.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(RenderCallback);
|
||||
STAR_CLASS(EffectEmitter);
|
||||
|
||||
class EffectEmitter : public NetElementGroup {
|
||||
public:
|
||||
EffectEmitter();
|
||||
|
||||
void addEffectSources(String const& position, StringSet effectSources);
|
||||
void setSourcePosition(String name, Vec2F const& position);
|
||||
void setDirection(Direction direction);
|
||||
void setBaseVelocity(Vec2F const& velocity);
|
||||
|
||||
void tick(EntityMode mode);
|
||||
void reset();
|
||||
|
||||
void render(RenderCallback* renderCallback);
|
||||
|
||||
Json toJson() const;
|
||||
void fromJson(Json const& diskStore);
|
||||
|
||||
private:
|
||||
Set<pair<String, String>> m_newSources;
|
||||
List<EffectSourcePtr> m_sources;
|
||||
NetElementData<Set<pair<String, String>>> m_activeSources;
|
||||
|
||||
StringMap<Vec2F> m_positions;
|
||||
Direction m_direction;
|
||||
Vec2F m_baseVelocity;
|
||||
|
||||
bool m_renders;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
193
source/game/StarEffectSourceDatabase.cpp
Normal file
193
source/game/StarEffectSourceDatabase.cpp
Normal file
|
@ -0,0 +1,193 @@
|
|||
#include "StarEffectSourceDatabase.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarParticleDatabase.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarMixer.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EffectSourceDatabase::EffectSourceDatabase() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
auto files = assets->scanExtension("effectsource");
|
||||
assets->queueJsons(files);
|
||||
for (auto const& file : files) {
|
||||
auto sourceConfig = make_shared<EffectSourceConfig>(assets->json(file));
|
||||
if (m_sourceConfigs.contains(sourceConfig->kind()))
|
||||
throw StarException(
|
||||
strf("Duplicate effect source asset kind Name %s. configfile %s", sourceConfig->kind(), file));
|
||||
auto k = sourceConfig->kind().toLower();
|
||||
m_sourceConfigs[k] = sourceConfig;
|
||||
}
|
||||
}
|
||||
|
||||
EffectSourceConfigPtr EffectSourceDatabase::effectSourceConfig(String const& kind) const {
|
||||
auto k = kind.toLower();
|
||||
if (!m_sourceConfigs.contains(k))
|
||||
throw StarException(strf("Unknown effect source definition with kind '%s'.", kind));
|
||||
return m_sourceConfigs.get(k);
|
||||
}
|
||||
|
||||
EffectSourceConfig::EffectSourceConfig(Json const& config) {
|
||||
m_kind = config.getString("kind");
|
||||
m_config = config;
|
||||
}
|
||||
|
||||
String const& EffectSourceConfig::kind() {
|
||||
return m_kind;
|
||||
}
|
||||
|
||||
EffectSourcePtr EffectSourceConfig::instance(String const& suggestedSpawnLocation) {
|
||||
return make_shared<EffectSource>(kind(), suggestedSpawnLocation, m_config.getObject("definition"));
|
||||
}
|
||||
|
||||
EffectSource::EffectSource(String const& kind, String suggestedSpawnLocation, Json const& definition) {
|
||||
m_kind = kind;
|
||||
m_config = definition;
|
||||
m_expired = false;
|
||||
m_loopDuration = m_config.getFloat("duration", 0);
|
||||
m_durationVariance = m_config.getFloat("durationVariance", 0);
|
||||
m_loops = m_config.getBool("loops", m_loopDuration != 0);
|
||||
m_timer = Random::randf() * (m_loopDuration + 0.5 * m_durationVariance);
|
||||
m_stop = false;
|
||||
m_initialTick = true;
|
||||
m_loopTick = false;
|
||||
m_finalTick = false;
|
||||
m_effectSpawnLocation = m_config.getString("location", "normal");
|
||||
m_suggestedSpawnLocation = suggestedSpawnLocation;
|
||||
}
|
||||
|
||||
String const& EffectSource::kind() const {
|
||||
return m_kind;
|
||||
}
|
||||
|
||||
bool EffectSource::expired() const {
|
||||
return m_expired;
|
||||
}
|
||||
|
||||
void EffectSource::stop() {
|
||||
m_stop = true;
|
||||
}
|
||||
|
||||
void EffectSource::tick() {
|
||||
m_timer -= WorldTimestep;
|
||||
if ((m_timer <= 0) && m_loops) {
|
||||
m_timer = m_loopDuration + m_durationVariance * Random::randf(-0.5f, 0.5f);
|
||||
m_loopTick = true;
|
||||
}
|
||||
if (m_stop || (m_timer <= 0))
|
||||
if (!m_expired)
|
||||
m_finalTick = true;
|
||||
}
|
||||
|
||||
List<String> EffectSource::particles() {
|
||||
auto pickParticleSources = [](Json const& config, List<String>& particles) {
|
||||
particles.appendAll(jsonToStringList(Random::randValueFrom(config.toArray(), JsonArray())));
|
||||
};
|
||||
List<String> result;
|
||||
if (m_initialTick)
|
||||
pickParticleSources(m_config.get("start", JsonObject()).get("particles", JsonArray()), result);
|
||||
if (m_loopTick)
|
||||
pickParticleSources(m_config.get("particles", JsonArray()), result);
|
||||
if (m_finalTick)
|
||||
pickParticleSources(m_config.get("stop", JsonObject()).get("particles", JsonArray()), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
List<AudioInstancePtr> EffectSource::sounds(Vec2F offset) {
|
||||
List<AudioInstancePtr> result;
|
||||
if (m_initialTick) {
|
||||
result.appendAll(soundsFromDefinition(m_config.get("start", JsonObject()).get("sounds", Json()), offset));
|
||||
|
||||
m_mainSounds = soundsFromDefinition(m_config.get("sounds", Json()), offset);
|
||||
result.appendAll(m_mainSounds);
|
||||
}
|
||||
if (m_finalTick) {
|
||||
for (auto& s : m_mainSounds)
|
||||
s->stop();
|
||||
result.appendAll(soundsFromDefinition(m_config.get("stop", JsonObject()).get("sounds", Json()), offset));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EffectSource::postRender() {
|
||||
m_initialTick = false;
|
||||
m_loopTick = false;
|
||||
if (m_finalTick) {
|
||||
m_finalTick = false;
|
||||
m_expired = true;
|
||||
}
|
||||
}
|
||||
|
||||
String EffectSource::effectSpawnLocation() const {
|
||||
if ((m_effectSpawnLocation == "normal") && (!m_suggestedSpawnLocation.empty()))
|
||||
return m_suggestedSpawnLocation;
|
||||
return m_effectSpawnLocation;
|
||||
}
|
||||
|
||||
String EffectSource::suggestedSpawnLocation() const {
|
||||
return m_suggestedSpawnLocation;
|
||||
}
|
||||
|
||||
List<Particle> particlesFromDefinition(Json const& config, Vec2F const& position) {
|
||||
Json particles;
|
||||
if (config.type() == Json::Type::Array)
|
||||
particles = Random::randValueFrom(config.toArray(), Json());
|
||||
else
|
||||
particles = config;
|
||||
if (!particles.isNull()) {
|
||||
if (particles.type() != Json::Type::Array)
|
||||
particles = JsonArray{particles};
|
||||
List<Particle> result;
|
||||
for (auto entry : particles.iterateArray()) {
|
||||
if (entry.type() != Json::Type::Object) {
|
||||
result.append(Root::singleton().particleDatabase()->particle(entry.toString()));
|
||||
} else {
|
||||
Particle particle(entry.toObject());
|
||||
Particle variance(entry.getObject("variance", {}));
|
||||
particle.applyVariance(variance);
|
||||
particle.position += position;
|
||||
result.append(particle);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
List<AudioInstancePtr> soundsFromDefinition(Json const& config, Vec2F const& position) {
|
||||
Json sound;
|
||||
if (config.type() == Json::Type::Array)
|
||||
sound = Random::randValueFrom(config.toArray(), Json());
|
||||
else
|
||||
sound = config;
|
||||
if (!sound.isNull()) {
|
||||
if (sound.type() != Json::Type::Array)
|
||||
sound = JsonArray{sound};
|
||||
List<AudioInstancePtr> result;
|
||||
auto assets = Root::singleton().assets();
|
||||
for (auto entry : sound.iterateArray()) {
|
||||
if (entry.type() != Json::Type::Object) {
|
||||
JsonObject t;
|
||||
t["resource"] = entry.toString();
|
||||
entry = t;
|
||||
}
|
||||
|
||||
auto sample = make_shared<AudioInstance>(*assets->audio(entry.getString("resource")));
|
||||
sample->setLoops(entry.getInt("loops", 0));
|
||||
sample->setVolume(entry.getFloat("volume", 1.0f));
|
||||
sample->setPitchMultiplier(entry.getFloat("pitch", 1.0f) + Random::randf(-1, 1) * entry.getFloat("pitchVariability", 0.0f));
|
||||
sample->setRangeMultiplier(entry.getFloat("audioRangeMultiplier", 1.0f));
|
||||
sample->setPosition(position);
|
||||
|
||||
result.append(move(sample));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
74
source/game/StarEffectSourceDatabase.hpp
Normal file
74
source/game/StarEffectSourceDatabase.hpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#ifndef STAR_EFFECT_SOURCE_DATABASE_HPP
|
||||
#define STAR_EFFECT_SOURCE_DATABASE_HPP
|
||||
|
||||
#include "StarVector.hpp"
|
||||
#include "StarJson.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarParticle.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(AudioInstance);
|
||||
STAR_CLASS(EffectSource);
|
||||
STAR_CLASS(EffectSourceConfig);
|
||||
STAR_CLASS(EffectSourceDatabase);
|
||||
|
||||
class EffectSource {
|
||||
public:
|
||||
EffectSource(String const& kind, String suggestedSpawnLocation, Json const& definition);
|
||||
String const& kind() const;
|
||||
void tick();
|
||||
bool expired() const;
|
||||
void stop();
|
||||
List<String> particles();
|
||||
List<AudioInstancePtr> sounds(Vec2F offset);
|
||||
void postRender();
|
||||
String effectSpawnLocation() const;
|
||||
String suggestedSpawnLocation() const;
|
||||
|
||||
private:
|
||||
String m_kind;
|
||||
Json m_config;
|
||||
bool m_loops;
|
||||
float m_loopDuration;
|
||||
float m_durationVariance;
|
||||
String m_effectSpawnLocation;
|
||||
String m_suggestedSpawnLocation;
|
||||
|
||||
bool m_initialTick;
|
||||
bool m_loopTick;
|
||||
bool m_finalTick;
|
||||
float m_timer;
|
||||
bool m_expired;
|
||||
bool m_stop;
|
||||
|
||||
List<AudioInstancePtr> m_mainSounds;
|
||||
};
|
||||
|
||||
class EffectSourceConfig {
|
||||
public:
|
||||
EffectSourceConfig(Json const& config);
|
||||
String const& kind();
|
||||
EffectSourcePtr instance(String const& suggestedSpawnLocation);
|
||||
|
||||
private:
|
||||
String m_kind;
|
||||
Json m_config;
|
||||
};
|
||||
|
||||
class EffectSourceDatabase {
|
||||
public:
|
||||
EffectSourceDatabase();
|
||||
|
||||
EffectSourceConfigPtr effectSourceConfig(String const& kind) const;
|
||||
|
||||
private:
|
||||
StringMap<EffectSourceConfigPtr> m_sourceConfigs;
|
||||
};
|
||||
|
||||
List<Particle> particlesFromDefinition(Json const& config, Vec2F const& position = Vec2F());
|
||||
List<AudioInstancePtr> soundsFromDefinition(Json const& config, Vec2F const& position = Vec2F());
|
||||
|
||||
}
|
||||
|
||||
#endif
|
61
source/game/StarEmoteProcessor.cpp
Normal file
61
source/game/StarEmoteProcessor.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
#include "StarEmoteProcessor.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EmoteProcessor::EmoteProcessor() {
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
m_emoteBindings.clear();
|
||||
auto cfg = assets->json("/emotes.config");
|
||||
for (auto binding : cfg.get("emoteBindings").iterateObject()) {
|
||||
for (auto text : binding.second.toArray()) {
|
||||
EmoteBinding emoteBinding;
|
||||
emoteBinding.emote = HumanoidEmoteNames.getLeft(binding.first);
|
||||
emoteBinding.text = text.toString();
|
||||
m_emoteBindings.append(emoteBinding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HumanoidEmote EmoteProcessor::detectEmotes(String const& chatter) const {
|
||||
auto isShouty = [](String const& text) -> bool {
|
||||
int caps = 0;
|
||||
int noCaps = 0;
|
||||
for (auto c : text) {
|
||||
if (String::toUpper(c) != String::toLower(c)) {
|
||||
if (String::toUpper(c) == c)
|
||||
caps++;
|
||||
else
|
||||
noCaps++;
|
||||
}
|
||||
}
|
||||
return caps > noCaps;
|
||||
};
|
||||
|
||||
HumanoidEmote result = HumanoidEmote::Idle;
|
||||
if (!chatter.empty()) {
|
||||
if (isShouty(chatter))
|
||||
result = HumanoidEmote::Shouting;
|
||||
else
|
||||
result = HumanoidEmote::Blabbering;
|
||||
}
|
||||
|
||||
float bestMatch = -1;
|
||||
|
||||
for (auto option : m_emoteBindings) {
|
||||
auto p = chatter.find(option.text);
|
||||
if (p == NPos)
|
||||
continue;
|
||||
float r = p + (float)option.text.length() * 0.01f;
|
||||
if (r > bestMatch) {
|
||||
bestMatch = r;
|
||||
result = option.emote;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
27
source/game/StarEmoteProcessor.hpp
Normal file
27
source/game/StarEmoteProcessor.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#ifndef STAR_EMOTE_PROCESSOR_HPP
|
||||
#define STAR_EMOTE_PROCESSOR_HPP
|
||||
|
||||
#include "StarHumanoid.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(EmoteProcessor);
|
||||
|
||||
class EmoteProcessor {
|
||||
public:
|
||||
EmoteProcessor();
|
||||
|
||||
HumanoidEmote detectEmotes(String const& chatter) const;
|
||||
|
||||
private:
|
||||
struct EmoteBinding {
|
||||
EmoteBinding() : emote() {}
|
||||
String text;
|
||||
HumanoidEmote emote;
|
||||
};
|
||||
List<EmoteBinding> m_emoteBindings;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
173
source/game/StarEntityFactory.cpp
Normal file
173
source/game/StarEntityFactory.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
#include "StarEntityFactory.hpp"
|
||||
#include "StarPlayer.hpp"
|
||||
#include "StarPlayerFactory.hpp"
|
||||
#include "StarMonster.hpp"
|
||||
#include "StarObject.hpp"
|
||||
#include "StarObjectDatabase.hpp"
|
||||
#include "StarPlant.hpp"
|
||||
#include "StarPlantDrop.hpp"
|
||||
#include "StarProjectile.hpp"
|
||||
#include "StarProjectileDatabase.hpp"
|
||||
#include "StarItemDrop.hpp"
|
||||
#include "StarNpc.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarStagehand.hpp"
|
||||
#include "StarVehicleDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<EntityType> const EntityFactory::EntityStorageIdentifiers{
|
||||
{EntityType::Player, "PlayerEntity"},
|
||||
{EntityType::Monster, "MonsterEntity"},
|
||||
{EntityType::Object, "ObjectEntity"},
|
||||
{EntityType::ItemDrop, "ItemDropEntity"},
|
||||
{EntityType::Projectile, "ProjectileEntity"},
|
||||
{EntityType::Plant, "PlantEntity"},
|
||||
{EntityType::PlantDrop, "PlantDropEntity"},
|
||||
{EntityType::Npc, "NpcEntity"},
|
||||
{EntityType::Stagehand, "StagehandEntity"},
|
||||
{EntityType::Vehicle, "VehicleEntity"}
|
||||
};
|
||||
|
||||
EntityFactory::EntityFactory() {
|
||||
auto& root = Root::singleton();
|
||||
m_playerFactory = root.playerFactory();
|
||||
m_monsterDatabase = root.monsterDatabase();
|
||||
m_objectDatabase = root.objectDatabase();
|
||||
m_projectileDatabase = root.projectileDatabase();
|
||||
m_npcDatabase = root.npcDatabase();
|
||||
m_vehicleDatabase = root.vehicleDatabase();
|
||||
m_versioningDatabase = root.versioningDatabase();
|
||||
}
|
||||
|
||||
ByteArray EntityFactory::netStoreEntity(EntityPtr const& entity) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (auto player = as<Player>(entity)) {
|
||||
return player->netStore();
|
||||
} else if (auto monster = as<Monster>(entity)) {
|
||||
return monster->netStore();
|
||||
} else if (auto object = as<Object>(entity)) {
|
||||
return object->netStore();
|
||||
} else if (auto plant = as<Plant>(entity)) {
|
||||
return plant->netStore();
|
||||
} else if (auto plantDrop = as<PlantDrop>(entity)) {
|
||||
return plantDrop->netStore();
|
||||
} else if (auto projectile = as<Projectile>(entity)) {
|
||||
return projectile->netStore();
|
||||
} else if (auto itemDrop = as<ItemDrop>(entity)) {
|
||||
return itemDrop->netStore();
|
||||
} else if (auto npc = as<Npc>(entity)) {
|
||||
return npc->netStore();
|
||||
} else if (auto stagehand = as<Stagehand>(entity)) {
|
||||
return stagehand->netStore();
|
||||
} else if (auto vehicle = as<Vehicle>(entity)) {
|
||||
return m_vehicleDatabase->netStore(vehicle);
|
||||
} else {
|
||||
throw EntityFactoryException::format("Don't know how to make net store for entity type '%s'", EntityTypeNames.getRight(entity->entityType()));
|
||||
}
|
||||
}
|
||||
|
||||
EntityPtr EntityFactory::netLoadEntity(EntityType type, ByteArray const& netStore) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (type == EntityType::Player) {
|
||||
return m_playerFactory->netLoadPlayer(netStore);
|
||||
} else if (type == EntityType::Monster) {
|
||||
return m_monsterDatabase->netLoadMonster(netStore);
|
||||
} else if (type == EntityType::Object) {
|
||||
return m_objectDatabase->netLoadObject(netStore);
|
||||
} else if (type == EntityType::Plant) {
|
||||
return make_shared<Plant>(netStore);
|
||||
} else if (type == EntityType::PlantDrop) {
|
||||
return make_shared<PlantDrop>(netStore);
|
||||
} else if (type == EntityType::Projectile) {
|
||||
return m_projectileDatabase->netLoadProjectile(netStore);
|
||||
} else if (type == EntityType::ItemDrop) {
|
||||
return make_shared<ItemDrop>(netStore);
|
||||
} else if (type == EntityType::Npc) {
|
||||
return m_npcDatabase->netLoadNpc(netStore);
|
||||
} else if (type == EntityType::Stagehand) {
|
||||
return make_shared<Stagehand>(netStore);
|
||||
} else if (type == EntityType::Vehicle) {
|
||||
return m_vehicleDatabase->netLoad(netStore);
|
||||
} else {
|
||||
throw EntityFactoryException::format("Don't know how to create entity type '%s' from net store", EntityTypeNames.getRight(type));
|
||||
}
|
||||
}
|
||||
|
||||
Json EntityFactory::diskStoreEntity(EntityPtr const& entity) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (auto player = as<Player>(entity)) {
|
||||
return player->diskStore();
|
||||
} else if (auto monster = as<Monster>(entity)) {
|
||||
return monster->diskStore();
|
||||
} else if (auto object = as<Object>(entity)) {
|
||||
return object->diskStore();
|
||||
} else if (auto plant = as<Plant>(entity)) {
|
||||
return plant->diskStore();
|
||||
} else if (auto itemDrop = as<ItemDrop>(entity)) {
|
||||
return itemDrop->diskStore();
|
||||
} else if (auto npc = as<Npc>(entity)) {
|
||||
return npc->diskStore();
|
||||
} else if (auto stagehand = as<Stagehand>(entity)) {
|
||||
return stagehand->diskStore();
|
||||
} else if (auto vehicle = as<Vehicle>(entity)) {
|
||||
return m_vehicleDatabase->diskStore(vehicle);
|
||||
} else {
|
||||
throw EntityFactoryException::format("Don't know how to make disk store for entity type '%s'", EntityTypeNames.getRight(entity->entityType()));
|
||||
}
|
||||
}
|
||||
|
||||
EntityPtr EntityFactory::diskLoadEntity(EntityType type, Json const& diskStore) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
if (type == EntityType::Player) {
|
||||
return m_playerFactory->diskLoadPlayer(diskStore);
|
||||
} else if (type == EntityType::Monster) {
|
||||
return m_monsterDatabase->diskLoadMonster(diskStore);
|
||||
} else if (type == EntityType::Object) {
|
||||
return m_objectDatabase->diskLoadObject(diskStore);
|
||||
} else if (type == EntityType::Plant) {
|
||||
return make_shared<Plant>(diskStore);
|
||||
} else if (type == EntityType::ItemDrop) {
|
||||
return make_shared<ItemDrop>(diskStore);
|
||||
} else if (type == EntityType::Npc) {
|
||||
return m_npcDatabase->diskLoadNpc(diskStore);
|
||||
} else if (type == EntityType::Stagehand) {
|
||||
return make_shared<Stagehand>(diskStore);
|
||||
} else if (type == EntityType::Vehicle) {
|
||||
return m_vehicleDatabase->diskLoad(diskStore);
|
||||
} else {
|
||||
throw EntityFactoryException::format("Don't know how to create entity type '%s' from disk store", EntityTypeNames.getRight(type));
|
||||
}
|
||||
}
|
||||
|
||||
Json EntityFactory::loadVersionedJson(VersionedJson const& versionedJson, EntityType expectedType) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
String identifier = EntityStorageIdentifiers.getRight(expectedType);
|
||||
return m_versioningDatabase->loadVersionedJson(versionedJson, identifier);
|
||||
}
|
||||
|
||||
VersionedJson EntityFactory::storeVersionedJson(EntityType type, Json const& store) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
String identifier = EntityStorageIdentifiers.getRight(type);
|
||||
return m_versioningDatabase->makeCurrentVersionedJson(identifier, store);
|
||||
}
|
||||
|
||||
EntityPtr EntityFactory::loadVersionedEntity(VersionedJson const& versionedJson) const {
|
||||
RecursiveMutexLocker locker(m_mutex);
|
||||
|
||||
EntityType type = EntityStorageIdentifiers.getLeft(versionedJson.identifier);
|
||||
auto store = loadVersionedJson(versionedJson, type);
|
||||
return diskLoadEntity(type, store);
|
||||
}
|
||||
|
||||
VersionedJson EntityFactory::storeVersionedEntity(EntityPtr const& entityPtr) const {
|
||||
return storeVersionedJson(entityPtr->entityType(), diskStoreEntity(entityPtr));
|
||||
}
|
||||
|
||||
}
|
55
source/game/StarEntityFactory.hpp
Normal file
55
source/game/StarEntityFactory.hpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#ifndef STAR_ENTITY_FACTORY_HPP
|
||||
#define STAR_ENTITY_FACTORY_HPP
|
||||
|
||||
#include "StarVersioningDatabase.hpp"
|
||||
#include "StarEntity.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(VersioningDatabase);
|
||||
STAR_CLASS(PlayerFactory);
|
||||
STAR_CLASS(MonsterDatabase);
|
||||
STAR_CLASS(ObjectDatabase);
|
||||
STAR_CLASS(ProjectileDatabase);
|
||||
STAR_CLASS(NpcDatabase);
|
||||
|
||||
STAR_CLASS(EntityFactory);
|
||||
|
||||
STAR_EXCEPTION(EntityFactoryException, StarException);
|
||||
|
||||
class EntityFactory {
|
||||
public:
|
||||
EntityFactory();
|
||||
|
||||
ByteArray netStoreEntity(EntityPtr const& entity) const;
|
||||
EntityPtr netLoadEntity(EntityType type, ByteArray const& netStore) const;
|
||||
|
||||
Json diskStoreEntity(EntityPtr const& entity) const;
|
||||
EntityPtr diskLoadEntity(EntityType type, Json const& diskStore) const;
|
||||
|
||||
Json loadVersionedJson(VersionedJson const& versionedJson, EntityType expectedType) const;
|
||||
VersionedJson storeVersionedJson(EntityType type, Json const& store) const;
|
||||
|
||||
// Wraps the normal Json based Entity store / load in a VersionedJson, and
|
||||
// uses sripts in the VersionedingDatabase to bring the version of the store
|
||||
// forward to match the current version.
|
||||
EntityPtr loadVersionedEntity(VersionedJson const& versionedJson) const;
|
||||
VersionedJson storeVersionedEntity(EntityPtr const& entityPtr) const;
|
||||
|
||||
private:
|
||||
static EnumMap<EntityType> const EntityStorageIdentifiers;
|
||||
|
||||
mutable RecursiveMutex m_mutex;
|
||||
|
||||
PlayerFactoryConstPtr m_playerFactory;
|
||||
MonsterDatabaseConstPtr m_monsterDatabase;
|
||||
ObjectDatabaseConstPtr m_objectDatabase;
|
||||
ProjectileDatabaseConstPtr m_projectileDatabase;
|
||||
NpcDatabaseConstPtr m_npcDatabase;
|
||||
VehicleDatabaseConstPtr m_vehicleDatabase;
|
||||
VersioningDatabaseConstPtr m_versioningDatabase;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
334
source/game/StarEntityMap.cpp
Normal file
334
source/game/StarEntityMap.cpp
Normal file
|
@ -0,0 +1,334 @@
|
|||
#include "StarEntityMap.hpp"
|
||||
#include "StarTileEntity.hpp"
|
||||
#include "StarInteractiveEntity.hpp"
|
||||
#include "StarProjectile.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
float const EntityMapSpatialHashSectorSize = 16.0f;
|
||||
int const EntityMap::MaximumEntityBoundBox = 10000;
|
||||
|
||||
EntityMap::EntityMap(Vec2U const& worldSize, EntityId beginIdSpace, EntityId endIdSpace)
|
||||
: m_geometry(worldSize),
|
||||
m_spatialMap(EntityMapSpatialHashSectorSize),
|
||||
m_nextId(beginIdSpace),
|
||||
m_beginIdSpace(beginIdSpace),
|
||||
m_endIdSpace(endIdSpace) {}
|
||||
|
||||
EntityId EntityMap::reserveEntityId() {
|
||||
if (m_spatialMap.size() >= (size_t)(m_endIdSpace - m_beginIdSpace))
|
||||
throw EntityMapException("No more entity id space in EntityMap::reserveEntityId");
|
||||
|
||||
EntityId id = m_nextId;
|
||||
while (m_spatialMap.contains(id))
|
||||
id = cycleIncrement(id, m_beginIdSpace, m_endIdSpace);
|
||||
m_nextId = cycleIncrement(id, m_beginIdSpace, m_endIdSpace);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void EntityMap::addEntity(EntityPtr entity) {
|
||||
auto position = entity->position();
|
||||
auto boundBox = entity->metaBoundBox();
|
||||
auto entityId = entity->entityId();
|
||||
auto uniqueId = entity->uniqueId();
|
||||
|
||||
if (m_spatialMap.contains(entityId))
|
||||
throw EntityMapException::format("Duplicate entity id '%s' in EntityMap::addEntity", entityId);
|
||||
|
||||
if (boundBox.isNegative() || boundBox.width() > MaximumEntityBoundBox || boundBox.height() > MaximumEntityBoundBox) {
|
||||
throw EntityMapException::format("Entity id: %s type: %s bound box is negative or beyond the maximum entity bound box size in EntityMap::addEntity",
|
||||
entity->entityId(), (int)entity->entityType());
|
||||
}
|
||||
|
||||
if (entityId == NullEntityId)
|
||||
throw EntityMapException::format("Null entity id in EntityMap::addEntity");
|
||||
|
||||
if (uniqueId && m_uniqueMap.hasLeftValue(*uniqueId))
|
||||
throw EntityMapException::format("Duplicate entity unique id (%s) on entity id (%s) in EntityMap::addEntity", *uniqueId, entityId);
|
||||
|
||||
m_spatialMap.set(entityId, m_geometry.splitRect(boundBox, position), move(entity));
|
||||
if (uniqueId)
|
||||
m_uniqueMap.add(*uniqueId, entityId);
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::removeEntity(EntityId entityId) {
|
||||
if (auto entity = m_spatialMap.remove(entityId)) {
|
||||
m_uniqueMap.removeRight(entityId);
|
||||
return entity.take();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t EntityMap::size() const {
|
||||
return m_spatialMap.size();
|
||||
}
|
||||
|
||||
List<EntityId> EntityMap::entityIds() const {
|
||||
return m_spatialMap.keys();
|
||||
}
|
||||
|
||||
void EntityMap::updateAllEntities(EntityCallback const& callback, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder) {
|
||||
auto updateEntityInfo = [&](SpatialMap::Entry const& entry) {
|
||||
auto const& entity = entry.value;
|
||||
|
||||
auto position = entity->position();
|
||||
auto boundBox = entity->metaBoundBox();
|
||||
|
||||
if (boundBox.isNegative() || boundBox.width() > MaximumEntityBoundBox || boundBox.height() > MaximumEntityBoundBox) {
|
||||
throw EntityMapException::format("Entity id: %s type: %s bound box is negative or beyond the maximum entity bound box size in EntityMap::addEntity",
|
||||
entity->entityId(), (int)entity->entityType());
|
||||
}
|
||||
|
||||
auto entityId = entity->entityId();
|
||||
if (entityId == NullEntityId)
|
||||
throw EntityMapException::format("Null entity id in EntityMap::setEntityInfo");
|
||||
|
||||
auto rects = m_geometry.splitRect(boundBox, position);
|
||||
if (!containersEqual(rects, entry.rects))
|
||||
m_spatialMap.set(entityId, rects);
|
||||
|
||||
auto uniqueId = entity->uniqueId();
|
||||
if (uniqueId) {
|
||||
if (auto existingEntityId = m_uniqueMap.maybeRight(*uniqueId)) {
|
||||
if (entityId != *existingEntityId)
|
||||
throw EntityMapException::format("Duplicate entity unique id on entity ids (%s) and (%s)", *existingEntityId, entityId);
|
||||
} else {
|
||||
m_uniqueMap.removeRight(entityId);
|
||||
m_uniqueMap.add(*uniqueId, entityId);
|
||||
}
|
||||
} else {
|
||||
m_uniqueMap.removeRight(entityId);
|
||||
}
|
||||
};
|
||||
|
||||
// Even if there is no sort order, we still copy pointers to a temporary
|
||||
// list, so that it is safe to call addEntity from the callback.
|
||||
m_entrySortBuffer.clear();
|
||||
for (auto const& entry : m_spatialMap.entries())
|
||||
m_entrySortBuffer.append(&entry.second);
|
||||
|
||||
if (sortOrder) {
|
||||
m_entrySortBuffer.sort([&sortOrder](auto a, auto b) {
|
||||
return sortOrder(a->value, b->value);
|
||||
});
|
||||
}
|
||||
|
||||
for (auto entry : m_entrySortBuffer) {
|
||||
if (callback)
|
||||
callback(entry->value);
|
||||
updateEntityInfo(*entry);
|
||||
}
|
||||
}
|
||||
|
||||
EntityId EntityMap::uniqueEntityId(String const& uniqueId) const {
|
||||
return m_uniqueMap.maybeRight(uniqueId).value(NullEntityId);
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::entity(EntityId entityId) const {
|
||||
auto entity = m_spatialMap.value(entityId);
|
||||
starAssert(!entity || entity->entityId() == entityId);
|
||||
return entity;
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::uniqueEntity(String const& uniqueId) const {
|
||||
return entity(uniqueEntityId(uniqueId));
|
||||
}
|
||||
|
||||
List<EntityPtr> EntityMap::entityQuery(RectF const& boundBox, EntityFilter const& filter) const {
|
||||
List<EntityPtr> values;
|
||||
forEachEntity(boundBox, [&](EntityPtr const& entity) {
|
||||
if (!filter || filter(entity))
|
||||
values.append(entity);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
List<EntityPtr> EntityMap::entitiesAt(Vec2F const& pos, EntityFilter const& filter) const {
|
||||
auto entityList = entityQuery(RectF::withCenter(pos, {0, 0}), filter);
|
||||
|
||||
sortByComputedValue(entityList, [&](EntityPtr const& entity) -> float {
|
||||
return vmagSquared(entity->position() - pos);
|
||||
});
|
||||
return entityList;
|
||||
}
|
||||
|
||||
List<TileEntityPtr> EntityMap::entitiesAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter) const {
|
||||
List<TileEntityPtr> values;
|
||||
forEachEntityAtTile(pos, [&](TileEntityPtr const& entity) {
|
||||
if (!filter || filter(entity))
|
||||
values.append(entity);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
void EntityMap::forEachEntity(RectF const& boundBox, EntityCallback const& callback) const {
|
||||
m_spatialMap.forEach(m_geometry.splitRect(boundBox), callback);
|
||||
}
|
||||
|
||||
void EntityMap::forEachEntityLine(Vec2F const& begin, Vec2F const& end, EntityCallback const& callback) const {
|
||||
return m_spatialMap.forEach(m_geometry.splitRect(RectF::boundBoxOf(begin, end)), [&](EntityPtr const& entity) {
|
||||
if (m_geometry.lineIntersectsRect({begin, end}, entity->metaBoundBox().translated(entity->position())))
|
||||
callback(entity);
|
||||
});
|
||||
}
|
||||
|
||||
void EntityMap::forEachEntityAtTile(Vec2I const& pos, EntityCallbackOf<TileEntity> const& callback) const {
|
||||
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
||||
forEachEntity(rect, [&](EntityPtr const& entity) {
|
||||
if (auto tileEntity = as<TileEntity>(entity)) {
|
||||
for (Vec2I space : tileEntity->spaces()) {
|
||||
if (m_geometry.equal(pos, space + tileEntity->tilePosition()))
|
||||
callback(tileEntity);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EntityMap::forAllEntities(EntityCallback const& callback, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder) const {
|
||||
// Even if there is no sort order, we still copy pointers to a temporary
|
||||
// list, so that it is safe to call addEntity from the callback.
|
||||
List<EntityPtr const*> allEntities;
|
||||
for (auto const& entry : m_spatialMap.entries())
|
||||
allEntities.append(&entry.second.value);
|
||||
|
||||
if (sortOrder) {
|
||||
allEntities.sort([&sortOrder](EntityPtr const* a, EntityPtr const* b) {
|
||||
return sortOrder(*a, *b);
|
||||
});
|
||||
}
|
||||
|
||||
for (auto ptr : allEntities)
|
||||
callback(*ptr);
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::findEntity(RectF const& boundBox, EntityFilter const& filter) const {
|
||||
EntityPtr res;
|
||||
forEachEntity(boundBox, [&filter, &res](EntityPtr const& entity) {
|
||||
if (res)
|
||||
return;
|
||||
if (filter(entity))
|
||||
res = entity;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::findEntityLine(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter) const {
|
||||
return findEntity(RectF::boundBoxOf(begin, end), [&](EntityPtr const& entity) {
|
||||
if (m_geometry.lineIntersectsRect({begin, end}, entity->metaBoundBox().translated(entity->position()))) {
|
||||
if (filter(entity))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::findEntityAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter) const {
|
||||
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
||||
return findEntity(rect, [&](EntityPtr const& entity) {
|
||||
if (auto tileEntity = as<TileEntity>(entity)) {
|
||||
for (Vec2I space : tileEntity->spaces()) {
|
||||
if (m_geometry.equal(pos, space + tileEntity->tilePosition())) {
|
||||
if (filter(tileEntity))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
List<EntityPtr> EntityMap::entityLineQuery(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter) const {
|
||||
List<EntityPtr> values;
|
||||
forEachEntityLine(begin, end, [&](EntityPtr const& entity) {
|
||||
if (!filter || filter(entity))
|
||||
values.append(entity);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
EntityPtr EntityMap::closestEntity(Vec2F const& center, float radius, EntityFilter const& filter) const {
|
||||
EntityPtr closest;
|
||||
float distSquared = square(radius);
|
||||
RectF boundBox(center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius);
|
||||
|
||||
m_spatialMap.forEach(m_geometry.splitRect(boundBox), [&](EntityPtr const& entity) {
|
||||
Vec2F pos = entity->position();
|
||||
float thisDistSquared = m_geometry.diff(center, pos).magnitudeSquared();
|
||||
if (distSquared > thisDistSquared) {
|
||||
if (!filter || filter(entity)) {
|
||||
distSquared = thisDistSquared;
|
||||
closest = entity;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
InteractiveEntityPtr EntityMap::interactiveEntityNear(Vec2F const& pos, float maxRadius) const {
|
||||
auto rect = RectF::withCenter(pos, Vec2F::filled(maxRadius));
|
||||
InteractiveEntityPtr interactiveEntity;
|
||||
double bestDistance = maxRadius + 100;
|
||||
double bestCenterDistance = maxRadius + 100;
|
||||
m_spatialMap.forEach(m_geometry.splitRect(rect), [&](EntityPtr const& entity) {
|
||||
if (auto ie = as<InteractiveEntity>(entity)) {
|
||||
if (ie->isInteractive()) {
|
||||
if (auto tileEntity = as<TileEntity>(entity)) {
|
||||
for (Vec2I space : tileEntity->interactiveSpaces()) {
|
||||
auto dist = m_geometry.diff(pos, centerOfTile(space + tileEntity->tilePosition())).magnitude();
|
||||
auto centerDist = m_geometry.diff(tileEntity->metaBoundBox().center() + tileEntity->position(), pos).magnitude();
|
||||
if ((dist < bestDistance) || ((dist == bestDistance) && (centerDist < bestCenterDistance))) {
|
||||
interactiveEntity = ie;
|
||||
bestDistance = dist;
|
||||
bestCenterDistance = centerDist;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto box = ie->interactiveBoundBox().translated(entity->position());
|
||||
auto dist = m_geometry.diffToNearestCoordInBox(box, pos).magnitude();
|
||||
auto centerDist = m_geometry.diff(box.center(), pos).magnitude();
|
||||
if ((dist < bestDistance) || ((dist == bestDistance) && (centerDist < bestCenterDistance))) {
|
||||
interactiveEntity = ie;
|
||||
bestDistance = dist;
|
||||
bestCenterDistance = centerDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (bestDistance <= maxRadius)
|
||||
return interactiveEntity;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool EntityMap::tileIsOccupied(Vec2I const& pos, bool includeEphemeral) const {
|
||||
RectF rect(Vec2F(pos[0], pos[1]), Vec2F(pos[0] + 1, pos[1] + 1));
|
||||
return (bool)findEntity(rect, [&](EntityPtr const& entity) {
|
||||
if (auto tileEntity = as<TileEntity>(entity)) {
|
||||
if (includeEphemeral || !tileEntity->ephemeral()) {
|
||||
for (Vec2I space : tileEntity->spaces()) {
|
||||
if (m_geometry.equal(pos, space + tileEntity->tilePosition())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool EntityMap::spaceIsOccupied(RectF const& rect, bool includesEphemeral) const {
|
||||
for (auto const& entity : entityQuery(rect)) {
|
||||
if (!includesEphemeral && entity->ephemeral())
|
||||
continue;
|
||||
|
||||
for (RectF const& c : m_geometry.splitRect(entity->collisionArea(), entity->position())) {
|
||||
if (!c.isNull() && rect.intersects(c))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
189
source/game/StarEntityMap.hpp
Normal file
189
source/game/StarEntityMap.hpp
Normal file
|
@ -0,0 +1,189 @@
|
|||
#ifndef STAR_ENTITY_MAP_HPP
|
||||
#define STAR_ENTITY_MAP_HPP
|
||||
|
||||
#include "StarSpatialHash2D.hpp"
|
||||
#include "StarEntity.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(EntityMap);
|
||||
STAR_CLASS(TileEntity);
|
||||
STAR_CLASS(InteractiveEntity);
|
||||
|
||||
STAR_EXCEPTION(EntityMapException, StarException);
|
||||
|
||||
// Class used by WorldServer and WorldClient to store entites organized in a
|
||||
// spatial hash. Provides convenient ways of querying entities based on
|
||||
// different selection criteria.
|
||||
//
|
||||
// Several of the methods in EntityMap take callbacks or filters that will be
|
||||
// called while iterating over internal structures. They are all designed so
|
||||
// that adding new entities is safe to do from the callback, but removing
|
||||
// entities is never safe to do from any callback function.
|
||||
class EntityMap {
|
||||
public:
|
||||
static float const SpatialHashSectorSize;
|
||||
static int const MaximumEntityBoundBox;
|
||||
|
||||
// beginIdSpace and endIdSpace is the *inclusive* range for new enittyIds.
|
||||
EntityMap(Vec2U const& worldSize, EntityId beginIdSpace, EntityId endIdSpace);
|
||||
|
||||
// Get the next free id in the entity id space.
|
||||
EntityId reserveEntityId();
|
||||
|
||||
// Add an entity to this EntityMap. The entity must already be initialized
|
||||
// and have a unique EntityId returned by reserveEntityId.
|
||||
void addEntity(EntityPtr entity);
|
||||
EntityPtr removeEntity(EntityId entityId);
|
||||
|
||||
size_t size() const;
|
||||
List<EntityId> entityIds() const;
|
||||
|
||||
// Iterates through the entity map optionally in the given order, updating
|
||||
// the spatial information for each entity along the way.
|
||||
void updateAllEntities(EntityCallback const& callback = {}, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder = {});
|
||||
|
||||
// If the given unique entity is in this map, then return its entity id
|
||||
EntityId uniqueEntityId(String const& uniqueId) const;
|
||||
|
||||
EntityPtr entity(EntityId entityId) const;
|
||||
EntityPtr uniqueEntity(String const& uniqueId) const;
|
||||
|
||||
// Queries entities based on metaBoundBox
|
||||
List<EntityPtr> entityQuery(RectF const& boundBox, EntityFilter const& filter = {}) const;
|
||||
|
||||
// A fuzzy query of the entities at this position, sorted by closeness.
|
||||
List<EntityPtr> entitiesAt(Vec2F const& pos, EntityFilter const& filter = {}) const;
|
||||
|
||||
List<TileEntityPtr> entitiesAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter = {}) const;
|
||||
|
||||
// Sort of a fuzzy line intersection test. Tests if a given line intersects
|
||||
// the bounding box of any entities, and returns them.
|
||||
List<EntityPtr> entityLineQuery(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter = {}) const;
|
||||
|
||||
// Callback versions of query functions.
|
||||
void forEachEntity(RectF const& boundBox, EntityCallback const& callback) const;
|
||||
void forEachEntityLine(Vec2F const& begin, Vec2F const& end, EntityCallback const& callback) const;
|
||||
// Returns tile-based entities that occupy the given tile position.
|
||||
void forEachEntityAtTile(Vec2I const& pos, EntityCallbackOf<TileEntity> const& callback) const;
|
||||
|
||||
// Iterate through all the entities, optionally in the given sort order.
|
||||
void forAllEntities(EntityCallback const& callback, function<bool(EntityPtr const&, EntityPtr const&)> sortOrder = {}) const;
|
||||
|
||||
// Stops searching when filter returns true, and returns the entity which
|
||||
// caused it.
|
||||
EntityPtr findEntity(RectF const& boundBox, EntityFilter const& filter) const;
|
||||
EntityPtr findEntityLine(Vec2F const& begin, Vec2F const& end, EntityFilter const& filter) const;
|
||||
EntityPtr findEntityAtTile(Vec2I const& pos, EntityFilterOf<TileEntity> const& filter) const;
|
||||
|
||||
// Closest entity that satisfies the given selector, if given.
|
||||
EntityPtr closestEntity(Vec2F const& center, float radius, EntityFilter const& filter = {}) const;
|
||||
|
||||
// Returns interactive entity that is near the given world position
|
||||
InteractiveEntityPtr interactiveEntityNear(Vec2F const& pos, float maxRadius = 1.5f) const;
|
||||
|
||||
// Whether or not any tile entity occupies this tile
|
||||
bool tileIsOccupied(Vec2I const& pos, bool includeEphemeral = false) const;
|
||||
|
||||
// Intersects any entity's collision area
|
||||
bool spaceIsOccupied(RectF const& rect, bool includeEphemeral = false) const;
|
||||
|
||||
// Convenience template methods that filter based on dynamic cast and deal
|
||||
// with pointers to a derived entity type.
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> get(EntityId entityId) const;
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> getUnique(String const& uniqueId) const;
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> query(RectF const& boundBox, EntityFilterOf<EntityT> const& filter = {}) const;
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> all(EntityFilterOf<EntityT> const& filter = {}) const;
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> lineQuery(Vec2F const& begin, Vec2F const& end, EntityFilterOf<EntityT> const& filter = {}) const;
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> closest(Vec2F const& center, float radius, EntityFilterOf<EntityT> const& filter = {}) const;
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> atTile(Vec2I const& pos) const;
|
||||
|
||||
private:
|
||||
typedef SpatialHash2D<EntityId, float, EntityPtr> SpatialMap;
|
||||
|
||||
WorldGeometry m_geometry;
|
||||
|
||||
SpatialMap m_spatialMap;
|
||||
BiHashMap<String, EntityId> m_uniqueMap;
|
||||
|
||||
EntityId m_nextId;
|
||||
EntityId m_beginIdSpace;
|
||||
EntityId m_endIdSpace;
|
||||
|
||||
List<SpatialMap::Entry const*> m_entrySortBuffer;
|
||||
};
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> EntityMap::get(EntityId entityId) const {
|
||||
return as<EntityT>(entity(entityId));
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> EntityMap::getUnique(String const& uniqueId) const {
|
||||
return as<EntityT>(uniqueEntity(uniqueId));
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> EntityMap::query(RectF const& boundBox, EntityFilterOf<EntityT> const& filter) const {
|
||||
List<shared_ptr<EntityT>> entities;
|
||||
for (auto const& entity : entityQuery(boundBox, entityTypeFilter(filter)))
|
||||
entities.append(as<EntityT>(entity));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> EntityMap::all(EntityFilterOf<EntityT> const& filter) const {
|
||||
List<shared_ptr<EntityT>> entities;
|
||||
forAllEntities([&](EntityPtr const& entity) {
|
||||
if (auto e = as<EntityT>(entity)) {
|
||||
if (!filter || filter(e))
|
||||
entities.append(e);
|
||||
}
|
||||
});
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> EntityMap::lineQuery(Vec2F const& begin, Vec2F const& end, EntityFilterOf<EntityT> const& filter) const {
|
||||
List<shared_ptr<EntityT>> entities;
|
||||
for (auto const& entity : entityLineQuery(begin, end, entityTypeFilter(filter)))
|
||||
entities.append(as<EntityT>(entity));
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
shared_ptr<EntityT> EntityMap::closest(Vec2F const& center, float radius, EntityFilterOf<EntityT> const& filter) const {
|
||||
return as<EntityT>(closestEntity(center, radius, entityTypeFilter(filter)));
|
||||
}
|
||||
|
||||
template <typename EntityT>
|
||||
List<shared_ptr<EntityT>> EntityMap::atTile(Vec2I const& pos) const {
|
||||
List<shared_ptr<EntityT>> list;
|
||||
forEachEntityAtTile(pos, [&](TileEntityPtr const& entity) {
|
||||
if (auto e = as<EntityT>(entity))
|
||||
list.append(move(e));
|
||||
return false;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
49
source/game/StarEntityRendering.cpp
Normal file
49
source/game/StarEntityRendering.cpp
Normal file
|
@ -0,0 +1,49 @@
|
|||
#include "StarEntityRendering.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
RenderCallback::~RenderCallback() {}
|
||||
|
||||
void RenderCallback::addDrawables(List<Drawable> drawables, EntityRenderLayer renderLayer, Vec2F translate) {
|
||||
for (auto& drawable : drawables) {
|
||||
drawable.translate(translate);
|
||||
addDrawable(move(drawable), renderLayer);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderCallback::addLightSources(List<LightSource> lightSources, Vec2F translate) {
|
||||
for (auto& lightSource : lightSources) {
|
||||
lightSource.translate(translate);
|
||||
addLightSource(move(lightSource));
|
||||
}
|
||||
}
|
||||
|
||||
void RenderCallback::addParticles(List<Particle> particles, Vec2F translate) {
|
||||
for (auto& particle : particles) {
|
||||
particle.translate(translate);
|
||||
addParticle(move(particle));
|
||||
}
|
||||
}
|
||||
|
||||
void RenderCallback::addAudios(List<AudioInstancePtr> audios, Vec2F translate) {
|
||||
for (auto& audio : audios) {
|
||||
audio->translate(translate);
|
||||
addAudio(move(audio));
|
||||
}
|
||||
}
|
||||
|
||||
void RenderCallback::addTilePreviews(List<PreviewTile> previews) {
|
||||
for (auto& preview : previews)
|
||||
addTilePreview(move(preview));
|
||||
}
|
||||
|
||||
void RenderCallback::addOverheadBars(List<OverheadBar> bars, Vec2F translate) {
|
||||
for (auto& bar : bars) {
|
||||
bar.entityPosition += translate;
|
||||
addOverheadBar(move(bar));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
41
source/game/StarEntityRendering.hpp
Normal file
41
source/game/StarEntityRendering.hpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#ifndef STAR_RENDERING_HPP
|
||||
#define STAR_RENDERING_HPP
|
||||
|
||||
#include "StarMixer.hpp"
|
||||
#include "StarEntityRenderingTypes.hpp"
|
||||
#include "StarParticle.hpp"
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarLightSource.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(RenderCallback);
|
||||
|
||||
// Callback interface for entities to produce light sources, particles,
|
||||
// drawables, and sounds on render. Everything added is expected to already be
|
||||
// translated into world space.
|
||||
class RenderCallback {
|
||||
public:
|
||||
virtual ~RenderCallback();
|
||||
|
||||
virtual void addDrawable(Drawable drawable, EntityRenderLayer renderLayer) = 0;
|
||||
virtual void addLightSource(LightSource lightSource) = 0;
|
||||
virtual void addParticle(Particle particle) = 0;
|
||||
virtual void addAudio(AudioInstancePtr audio) = 0;
|
||||
virtual void addTilePreview(PreviewTile preview) = 0;
|
||||
virtual void addOverheadBar(OverheadBar bar) = 0;
|
||||
|
||||
// Convenience non-virtuals
|
||||
|
||||
void addDrawables(List<Drawable> drawables, EntityRenderLayer renderLayer, Vec2F translate = Vec2F());
|
||||
void addLightSources(List<LightSource> lightSources, Vec2F translate = Vec2F());
|
||||
void addParticles(List<Particle> particles, Vec2F translate = Vec2F());
|
||||
void addAudios(List<AudioInstancePtr> audios, Vec2F translate = Vec2F());
|
||||
void addTilePreviews(List<PreviewTile> previews);
|
||||
void addOverheadBars(List<OverheadBar> bars, Vec2F translate = Vec2F());
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
127
source/game/StarEntityRenderingTypes.cpp
Normal file
127
source/game/StarEntityRenderingTypes.cpp
Normal file
|
@ -0,0 +1,127 @@
|
|||
#include "StarEntityRenderingTypes.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EntityRenderLayer parseRenderLayer(String renderLayer) {
|
||||
static CaseInsensitiveStringMap<EntityRenderLayer> RenderLayerMap{
|
||||
{"BackgroundOverlay", RenderLayerBackgroundOverlay},
|
||||
{"BackgroundTile", RenderLayerBackgroundTile},
|
||||
{"Platform", RenderLayerPlatform},
|
||||
{"Plant", RenderLayerPlant},
|
||||
{"PlantDrop", RenderLayerPlantDrop},
|
||||
{"Object", RenderLayerObject},
|
||||
{"PreviewObject", RenderLayerPreviewObject},
|
||||
{"BackParticle", RenderLayerBackParticle},
|
||||
{"Vehicle", RenderLayerVehicle},
|
||||
{"Effect", RenderLayerEffect},
|
||||
{"Projectile", RenderLayerProjectile},
|
||||
{"Monster", RenderLayerMonster},
|
||||
{"Npc", RenderLayerNpc},
|
||||
{"Player", RenderLayerPlayer},
|
||||
{"ItemDrop", RenderLayerItemDrop},
|
||||
{"Liquid", RenderLayerLiquid},
|
||||
{"MiddleParticle", RenderLayerMiddleParticle},
|
||||
{"ForegroundTile", RenderLayerForegroundTile},
|
||||
{"ForegroundEntity", RenderLayerForegroundEntity},
|
||||
{"ForegroundOverlay", RenderLayerForegroundOverlay},
|
||||
{"FrontParticle", RenderLayerFrontParticle},
|
||||
{"Overlay", RenderLayerOverlay},
|
||||
};
|
||||
|
||||
int offset = 0;
|
||||
if (renderLayer.contains("+")) {
|
||||
StringList parts = renderLayer.split("+", 1);
|
||||
renderLayer = parts.at(0);
|
||||
offset = lexicalCast<int>(parts.at(1));
|
||||
} else if (renderLayer.contains("-")) {
|
||||
StringList parts = renderLayer.split("-", 1);
|
||||
renderLayer = parts.at(0);
|
||||
offset = -lexicalCast<int>(parts.at(1));
|
||||
}
|
||||
|
||||
return RenderLayerMap.get(renderLayer) + offset;
|
||||
}
|
||||
|
||||
PreviewTile::PreviewTile()
|
||||
: foreground(false),
|
||||
liqId(EmptyLiquidId),
|
||||
matId(NullMaterialId),
|
||||
hueShift(0),
|
||||
updateMatId(false),
|
||||
colorVariant(DefaultMaterialColorVariant),
|
||||
updateLight(false) {}
|
||||
|
||||
PreviewTile::PreviewTile(
|
||||
Vec2I const& position, bool foreground, MaterialId matId, MaterialHue hueShift, bool updateMatId)
|
||||
: position(position),
|
||||
foreground(foreground),
|
||||
liqId(EmptyLiquidId),
|
||||
matId(matId),
|
||||
hueShift(hueShift),
|
||||
updateMatId(updateMatId),
|
||||
colorVariant(DefaultMaterialColorVariant),
|
||||
updateLight(false) {}
|
||||
|
||||
PreviewTile::PreviewTile(Vec2I const& position, bool foreground, Vec3B const& light, bool updateLight)
|
||||
: position(position),
|
||||
foreground(foreground),
|
||||
liqId(EmptyLiquidId),
|
||||
matId(NullMaterialId),
|
||||
hueShift(0),
|
||||
updateMatId(false),
|
||||
colorVariant(DefaultMaterialColorVariant),
|
||||
light(light),
|
||||
updateLight(updateLight) {}
|
||||
|
||||
PreviewTile::PreviewTile(Vec2I const& position,
|
||||
bool foreground,
|
||||
MaterialId matId,
|
||||
MaterialHue hueShift,
|
||||
bool updateMatId,
|
||||
Vec3B const& light,
|
||||
bool updateLight,
|
||||
MaterialColorVariant colorVariant)
|
||||
: position(position),
|
||||
foreground(foreground),
|
||||
liqId(EmptyLiquidId),
|
||||
matId(matId),
|
||||
hueShift(hueShift),
|
||||
updateMatId(updateMatId),
|
||||
colorVariant(colorVariant),
|
||||
light(light),
|
||||
updateLight(updateLight) {}
|
||||
|
||||
PreviewTile::PreviewTile(Vec2I const& position, LiquidId liqId)
|
||||
: position(position),
|
||||
foreground(true),
|
||||
liqId(liqId),
|
||||
matId(NullMaterialId),
|
||||
hueShift(0),
|
||||
updateMatId(false),
|
||||
colorVariant(DefaultMaterialColorVariant),
|
||||
updateLight(false) {}
|
||||
|
||||
OverheadBar::OverheadBar() : percentage(0.0f), detailOnly(false) {}
|
||||
|
||||
OverheadBar::OverheadBar(Json const& json) {
|
||||
entityPosition = json.opt("position").apply(jsonToVec2F).value();
|
||||
icon = json.optString("icon");
|
||||
percentage = json.getFloat("percentage");
|
||||
color = jsonToColor(json.get("color"));
|
||||
detailOnly = json.getBool("detailOnly", false);
|
||||
}
|
||||
|
||||
OverheadBar::OverheadBar(Maybe<String> icon, float percentage, Color color, bool detailOnly)
|
||||
: icon(move(icon)), percentage(percentage), color(move(color)), detailOnly(detailOnly) {}
|
||||
|
||||
EnumMap<EntityHighlightEffectType> const EntityHighlightEffectTypeNames{
|
||||
{EntityHighlightEffectType::None, "none"},
|
||||
{EntityHighlightEffectType::Interactive, "interactive"},
|
||||
{EntityHighlightEffectType::Inspectable, "inspectable"},
|
||||
{EntityHighlightEffectType::Interesting, "interesting"},
|
||||
{EntityHighlightEffectType::Inspected, "inspected"}
|
||||
};
|
||||
|
||||
}
|
89
source/game/StarEntityRenderingTypes.hpp
Normal file
89
source/game/StarEntityRenderingTypes.hpp
Normal file
|
@ -0,0 +1,89 @@
|
|||
#ifndef STAR_ENTITY_RENDERING_TYPES_HPP
|
||||
#define STAR_ENTITY_RENDERING_TYPES_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarColor.hpp"
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
typedef uint32_t EntityRenderLayer;
|
||||
|
||||
unsigned const RenderLayerUpperBits = 5;
|
||||
unsigned const RenderLayerLowerBits = 32 - RenderLayerUpperBits;
|
||||
EntityRenderLayer const RenderLayerLowerMask = (EntityRenderLayer)-1 >> RenderLayerUpperBits;
|
||||
|
||||
EntityRenderLayer const RenderLayerBackgroundOverlay = 1 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerBackgroundTile = 2 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerPlatform = 3 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerPlant = 4 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerPlantDrop = 5 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerObject = 6 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerPreviewObject = 7 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerBackParticle = 8 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerVehicle = 9 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerEffect = 10 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerProjectile = 11 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerMonster = 12 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerNpc = 13 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerPlayer = 14 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerItemDrop = 15 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerLiquid = 16 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerMiddleParticle = 17 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerForegroundTile = 18 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerForegroundEntity = 19 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerForegroundOverlay = 20 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerFrontParticle = 21 << RenderLayerLowerBits;
|
||||
EntityRenderLayer const RenderLayerOverlay = 22 << RenderLayerLowerBits;
|
||||
|
||||
EntityRenderLayer parseRenderLayer(String renderLayer);
|
||||
|
||||
struct PreviewTile {
|
||||
PreviewTile();
|
||||
PreviewTile(Vec2I const& position, bool foreground, MaterialId matId, MaterialHue hueShift, bool updateMatId);
|
||||
PreviewTile(Vec2I const& position, bool foreground, Vec3B const& light, bool updateLight);
|
||||
PreviewTile(Vec2I const& position, bool foreground, MaterialId matId, MaterialHue hueShift, bool updateMatId, Vec3B const& light, bool updateLight, MaterialColorVariant colorVariant);
|
||||
PreviewTile(Vec2I const& position, LiquidId liqId);
|
||||
|
||||
Vec2I position;
|
||||
bool foreground;
|
||||
|
||||
LiquidId liqId;
|
||||
MaterialId matId;
|
||||
MaterialHue hueShift;
|
||||
bool updateMatId;
|
||||
MaterialColorVariant colorVariant;
|
||||
Vec3B light;
|
||||
bool updateLight;
|
||||
};
|
||||
|
||||
struct OverheadBar {
|
||||
OverheadBar();
|
||||
OverheadBar(Json const& json);
|
||||
OverheadBar(Maybe<String> icon, float percentage, Color color, bool detailOnly);
|
||||
|
||||
Vec2F entityPosition;
|
||||
Maybe<String> icon;
|
||||
float percentage;
|
||||
Color color;
|
||||
bool detailOnly;
|
||||
};
|
||||
|
||||
enum class EntityHighlightEffectType {
|
||||
None,
|
||||
Interactive,
|
||||
Inspectable,
|
||||
Interesting,
|
||||
Inspected
|
||||
};
|
||||
extern EnumMap<EntityHighlightEffectType> const EntityHighlightEffectTypeNames;
|
||||
|
||||
struct EntityHighlightEffect {
|
||||
EntityHighlightEffectType type = EntityHighlightEffectType::None;
|
||||
float level = 0.0f;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
50
source/game/StarEntitySplash.cpp
Normal file
50
source/game/StarEntitySplash.cpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#include "StarEntitySplash.hpp"
|
||||
#include "StarWorld.hpp"
|
||||
#include "StarLiquidsDatabase.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EntitySplashConfig::EntitySplashConfig() {}
|
||||
|
||||
EntitySplashConfig::EntitySplashConfig(Json const& config) {
|
||||
splashSpeedMin = config.get("splashSpeedMin").toFloat();
|
||||
splashMinWaterLevel = config.get("splashMinWaterLevel").toFloat();
|
||||
splashBottomSensor = jsonToVec2F(config.get("splashBottomSensor"));
|
||||
splashTopSensor = jsonToVec2F(config.get("splashTopSensor"));
|
||||
numSplashParticles = config.get("numSplashParticles").toInt();
|
||||
splashYVelocityFactor = config.get("splashYVelocityFactor").toFloat();
|
||||
splashParticle = Particle(config.get("splashParticle").toObject());
|
||||
splashParticleVariance = Particle(config.get("splashParticleVariance").toObject());
|
||||
}
|
||||
|
||||
List<Particle> EntitySplashConfig::doSplash(Vec2F position, Vec2F velocity, World* world) const {
|
||||
List<Particle> particles;
|
||||
if (std::fabs(velocity[1]) >= splashSpeedMin) {
|
||||
auto liquidDb = Root::singleton().liquidsDatabase();
|
||||
Vec2I bottom = Vec2I::floor(position + splashBottomSensor);
|
||||
Vec2I top = Vec2I::floor(position + splashTopSensor);
|
||||
if (world->liquidLevel(bottom).level - world->liquidLevel(top).level >= splashMinWaterLevel) {
|
||||
LiquidId liquidType;
|
||||
auto bottomLiquid = world->liquidLevel(bottom);
|
||||
auto topLiquid = world->liquidLevel(top);
|
||||
if (bottomLiquid.level > 0 && (int)bottomLiquid.liquid)
|
||||
liquidType = bottomLiquid.liquid;
|
||||
else
|
||||
liquidType = topLiquid.liquid;
|
||||
Color particleColor = Color::rgba(liquidDb->liquidSettings(liquidType)->liquidColor);
|
||||
for (int i = 0; i < numSplashParticles; ++i) {
|
||||
Particle newSplashParticle = splashParticle;
|
||||
newSplashParticle.position = position;
|
||||
newSplashParticle.velocity[1] = std::fabs(velocity[1]) * splashYVelocityFactor;
|
||||
newSplashParticle.color = particleColor;
|
||||
newSplashParticle.applyVariance(splashParticleVariance);
|
||||
particles.append(newSplashParticle);
|
||||
}
|
||||
}
|
||||
}
|
||||
return particles;
|
||||
}
|
||||
|
||||
}
|
30
source/game/StarEntitySplash.hpp
Normal file
30
source/game/StarEntitySplash.hpp
Normal file
|
@ -0,0 +1,30 @@
|
|||
#ifndef STAR_ENTITY_UTILITIES_HPP
|
||||
#define STAR_ENTITY_UTILITIES_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarParticle.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(World);
|
||||
STAR_STRUCT(EntitySplashConfig);
|
||||
STAR_CLASS(EntitySplashHelper);
|
||||
|
||||
struct EntitySplashConfig {
|
||||
EntitySplashConfig();
|
||||
EntitySplashConfig(Json const& config);
|
||||
float splashSpeedMin;
|
||||
Vec2F splashBottomSensor;
|
||||
Vec2F splashTopSensor;
|
||||
float splashMinWaterLevel;
|
||||
int numSplashParticles;
|
||||
Particle splashParticle;
|
||||
Particle splashParticleVariance;
|
||||
float splashYVelocityFactor;
|
||||
|
||||
List<Particle> doSplash(Vec2F position, Vec2F velocity, World* world) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
81
source/game/StarFallingBlocksAgent.cpp
Normal file
81
source/game/StarFallingBlocksAgent.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include "StarFallingBlocksAgent.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
FallingBlocksAgent::FallingBlocksAgent(FallingBlocksFacadePtr worldFacade)
|
||||
: m_facade(move(worldFacade)) {
|
||||
m_immediateUpwardPropagateProbability = Root::singleton().assets()->json("/worldserver.config:fallingBlocksImmediateUpwardPropogateProbability").toFloat();
|
||||
}
|
||||
|
||||
void FallingBlocksAgent::update() {
|
||||
HashSet<Vec2I> processing = take(m_pending);
|
||||
|
||||
while (!processing.empty()) {
|
||||
List<Vec2I> positions;
|
||||
for (auto const& pos : take(processing))
|
||||
positions.append(pos);
|
||||
|
||||
m_random.shuffle(positions);
|
||||
|
||||
positions.sort([](auto const& a, auto const& b) {
|
||||
return a[1] < b[1];
|
||||
});
|
||||
|
||||
for (auto const& pos : positions) {
|
||||
Vec2I belowPos = pos + Vec2I(0, -1);
|
||||
Vec2I belowLeftPos = pos + Vec2I(-1, -1);
|
||||
Vec2I belowRightPos = pos + Vec2I(1, -1);
|
||||
|
||||
FallingBlockType thisBlock = m_facade->blockType(pos);
|
||||
FallingBlockType belowBlock = m_facade->blockType(belowPos);
|
||||
|
||||
Maybe<Vec2I> moveTo;
|
||||
|
||||
if (thisBlock == FallingBlockType::Falling) {
|
||||
if (belowBlock == FallingBlockType::Open)
|
||||
moveTo = belowPos;
|
||||
} else if (thisBlock == FallingBlockType::Cascading) {
|
||||
if (belowBlock == FallingBlockType::Open) {
|
||||
moveTo = belowPos;
|
||||
} else {
|
||||
FallingBlockType belowLeftBlock = m_facade->blockType(belowLeftPos);
|
||||
FallingBlockType belowRightBlock = m_facade->blockType(belowRightPos);
|
||||
|
||||
if (belowLeftBlock == FallingBlockType::Open && belowRightBlock == FallingBlockType::Open)
|
||||
moveTo = m_random.randb() ? belowLeftPos : belowRightPos;
|
||||
else if (belowLeftBlock == FallingBlockType::Open)
|
||||
moveTo = belowLeftPos;
|
||||
else if (belowRightBlock == FallingBlockType::Open)
|
||||
moveTo = belowRightPos;
|
||||
}
|
||||
}
|
||||
|
||||
if (moveTo) {
|
||||
m_facade->moveBlock(pos, *moveTo);
|
||||
if (m_random.randf() < m_immediateUpwardPropagateProbability) {
|
||||
processing.add(pos + Vec2I(0, 1));
|
||||
processing.add(pos + Vec2I(-1, 1));
|
||||
processing.add(pos + Vec2I(1, 1));
|
||||
}
|
||||
|
||||
visitLocation(pos);
|
||||
visitLocation(*moveTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FallingBlocksAgent::visitLocation(Vec2I const& location) {
|
||||
visitRegion(RectI::withSize(location, Vec2I(1, 1)));
|
||||
}
|
||||
|
||||
void FallingBlocksAgent::visitRegion(RectI const& region) {
|
||||
for (int x = region.xMin() - 1; x <= region.xMax(); ++x) {
|
||||
for (int y = region.yMin(); y <= region.yMax(); ++y)
|
||||
m_pending.add({x, y});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
50
source/game/StarFallingBlocksAgent.hpp
Normal file
50
source/game/StarFallingBlocksAgent.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef STAR_FALLING_BLOCKS_AGENT_HPP
|
||||
#define STAR_FALLING_BLOCKS_AGENT_HPP
|
||||
|
||||
#include "StarVector.hpp"
|
||||
#include "StarSet.hpp"
|
||||
#include "StarMap.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarWorldTiles.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(MaterialDatabase);
|
||||
STAR_CLASS(FallingBlocksFacade);
|
||||
STAR_CLASS(FallingBlocksAgent);
|
||||
|
||||
enum class FallingBlockType {
|
||||
Immovable,
|
||||
Falling,
|
||||
Cascading,
|
||||
Open
|
||||
};
|
||||
|
||||
class FallingBlocksFacade {
|
||||
public:
|
||||
virtual ~FallingBlocksFacade() = default;
|
||||
|
||||
virtual FallingBlockType blockType(Vec2I const& pos) = 0;
|
||||
virtual void moveBlock(Vec2I const& from, Vec2I const& to) = 0;
|
||||
};
|
||||
|
||||
class FallingBlocksAgent {
|
||||
public:
|
||||
FallingBlocksAgent(FallingBlocksFacadePtr worldFacade);
|
||||
|
||||
void update();
|
||||
|
||||
void visitLocation(Vec2I const& location);
|
||||
void visitRegion(RectI const& region);
|
||||
|
||||
private:
|
||||
FallingBlocksFacadePtr m_facade;
|
||||
float m_immediateUpwardPropagateProbability;
|
||||
HashSet<Vec2I> m_pending;
|
||||
RandomSource m_random;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
199
source/game/StarForceRegions.cpp
Normal file
199
source/game/StarForceRegions.cpp
Normal file
|
@ -0,0 +1,199 @@
|
|||
#include "StarForceRegions.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
PhysicsCategoryFilter PhysicsCategoryFilter::whitelist(StringSet categories) {
|
||||
return PhysicsCategoryFilter(Whitelist, move(categories));
|
||||
}
|
||||
|
||||
PhysicsCategoryFilter PhysicsCategoryFilter::blacklist(StringSet categories) {
|
||||
return PhysicsCategoryFilter(Blacklist, move(categories));
|
||||
}
|
||||
|
||||
PhysicsCategoryFilter::PhysicsCategoryFilter(Type type, StringSet categories)
|
||||
: type(type), categories(move(categories)) {}
|
||||
|
||||
bool PhysicsCategoryFilter::check(StringSet const& otherCategories) const {
|
||||
bool intersection = categories.hasIntersection(otherCategories);
|
||||
if (type == Whitelist)
|
||||
return intersection;
|
||||
else
|
||||
return !intersection;
|
||||
}
|
||||
|
||||
bool PhysicsCategoryFilter::operator==(PhysicsCategoryFilter const& rhs) const {
|
||||
return tie(type, categories) == tie(rhs.type, rhs.categories);
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, PhysicsCategoryFilter& pcf) {
|
||||
ds >> pcf.type;
|
||||
ds >> pcf.categories;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, PhysicsCategoryFilter const& pcf) {
|
||||
ds << pcf.type;
|
||||
ds << pcf.categories;
|
||||
return ds;
|
||||
}
|
||||
|
||||
PhysicsCategoryFilter jsonToPhysicsCategoryFilter(Json const& json) {
|
||||
auto whitelist = json.opt("categoryWhitelist");
|
||||
auto blacklist = json.opt("categoryBlacklist");
|
||||
if (whitelist && blacklist)
|
||||
throw JsonException::format("Cannot specify both a physics category whitelist and blacklist");
|
||||
if (whitelist)
|
||||
return PhysicsCategoryFilter::whitelist(jsonToStringSet(*whitelist));
|
||||
if (blacklist)
|
||||
return PhysicsCategoryFilter::blacklist(jsonToStringSet(*blacklist));
|
||||
return PhysicsCategoryFilter();
|
||||
}
|
||||
|
||||
DirectionalForceRegion DirectionalForceRegion::fromJson(Json const& json) {
|
||||
DirectionalForceRegion dfr;
|
||||
if (json.contains("polyRegion"))
|
||||
dfr.region = jsonToPolyF(json.get("polyRegion"));
|
||||
else
|
||||
dfr.region = PolyF(jsonToRectF(json.get("rectRegion")));
|
||||
dfr.xTargetVelocity = json.optFloat("xTargetVelocity");
|
||||
dfr.yTargetVelocity = json.optFloat("yTargetVelocity");
|
||||
dfr.controlForce = json.getFloat("controlForce");
|
||||
dfr.categoryFilter = jsonToPhysicsCategoryFilter(json);
|
||||
return dfr;
|
||||
}
|
||||
|
||||
RectF DirectionalForceRegion::boundBox() const {
|
||||
return region.boundBox();
|
||||
}
|
||||
|
||||
void DirectionalForceRegion::translate(Vec2F const& pos) {
|
||||
region.translate(pos);
|
||||
}
|
||||
|
||||
bool DirectionalForceRegion::operator==(DirectionalForceRegion const& rhs) const {
|
||||
return tie(region, xTargetVelocity, yTargetVelocity, controlForce, categoryFilter)
|
||||
== tie(rhs.region, rhs.xTargetVelocity, rhs.yTargetVelocity, rhs.controlForce, rhs.categoryFilter);
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, DirectionalForceRegion& dfr) {
|
||||
ds >> dfr.region;
|
||||
ds >> dfr.xTargetVelocity;
|
||||
ds >> dfr.yTargetVelocity;
|
||||
ds >> dfr.controlForce;
|
||||
ds >> dfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, DirectionalForceRegion const& dfr) {
|
||||
ds << dfr.region;
|
||||
ds << dfr.xTargetVelocity;
|
||||
ds << dfr.yTargetVelocity;
|
||||
ds << dfr.controlForce;
|
||||
ds << dfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
RadialForceRegion RadialForceRegion::fromJson(Json const& json) {
|
||||
RadialForceRegion rfr;
|
||||
rfr.center = json.opt("center").apply(jsonToVec2F).value();
|
||||
rfr.outerRadius = json.getFloat("outerRadius");
|
||||
rfr.innerRadius = json.getFloat("innerRadius");
|
||||
rfr.targetRadialVelocity = json.getFloat("targetRadialVelocity");
|
||||
rfr.controlForce = json.getFloat("controlForce");
|
||||
rfr.categoryFilter = jsonToPhysicsCategoryFilter(json);
|
||||
return rfr;
|
||||
}
|
||||
|
||||
RectF RadialForceRegion::boundBox() const {
|
||||
return RectF::withCenter(center, Vec2F::filled(outerRadius));
|
||||
}
|
||||
|
||||
void RadialForceRegion::translate(Vec2F const& pos) {
|
||||
center += pos;
|
||||
}
|
||||
|
||||
bool RadialForceRegion::operator==(RadialForceRegion const& rhs) const {
|
||||
return tie(center, outerRadius, innerRadius, targetRadialVelocity, controlForce, categoryFilter)
|
||||
== tie(rhs.center, rhs.outerRadius, rhs.innerRadius, rhs.targetRadialVelocity, rhs.controlForce, rhs.categoryFilter);
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, RadialForceRegion& rfr) {
|
||||
ds >> rfr.center;
|
||||
ds >> rfr.outerRadius;
|
||||
ds >> rfr.innerRadius;
|
||||
ds >> rfr.targetRadialVelocity;
|
||||
ds >> rfr.controlForce;
|
||||
ds >> rfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, RadialForceRegion const& gfr) {
|
||||
ds << gfr.center;
|
||||
ds << gfr.outerRadius;
|
||||
ds << gfr.innerRadius;
|
||||
ds << gfr.targetRadialVelocity;
|
||||
ds << gfr.controlForce;
|
||||
ds << gfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
GradientForceRegion GradientForceRegion::fromJson(Json const& json) {
|
||||
GradientForceRegion gfr;
|
||||
if (json.contains("polyRegion"))
|
||||
gfr.region = jsonToPolyF(json.get("polyRegion"));
|
||||
else
|
||||
gfr.region = PolyF(jsonToRectF(json.get("rectRegion")));
|
||||
gfr.gradient = jsonToLine2F(json.get("gradient"));
|
||||
gfr.baseTargetVelocity = json.getFloat("baseTargetVelocity");
|
||||
gfr.baseControlForce = json.getFloat("baseControlForce");
|
||||
gfr.categoryFilter = jsonToPhysicsCategoryFilter(json);
|
||||
return gfr;
|
||||
}
|
||||
|
||||
RectF GradientForceRegion::boundBox() const {
|
||||
return region.boundBox();
|
||||
}
|
||||
|
||||
void GradientForceRegion::translate(Vec2F const& pos) {
|
||||
region.translate(pos);
|
||||
gradient.translate(pos);
|
||||
}
|
||||
|
||||
bool GradientForceRegion::operator==(GradientForceRegion const& rhs) const {
|
||||
return tie(region, gradient, baseTargetVelocity, baseControlForce, categoryFilter)
|
||||
== tie(rhs.region, rhs.gradient, rhs.baseTargetVelocity, rhs.baseControlForce, rhs.categoryFilter);
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, GradientForceRegion& gfr) {
|
||||
ds >> gfr.region;
|
||||
ds >> gfr.gradient;
|
||||
ds >> gfr.baseTargetVelocity;
|
||||
ds >> gfr.baseControlForce;
|
||||
ds >> gfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, GradientForceRegion const& gfr) {
|
||||
ds << gfr.region;
|
||||
ds << gfr.gradient;
|
||||
ds << gfr.baseTargetVelocity;
|
||||
ds << gfr.baseControlForce;
|
||||
ds << gfr.categoryFilter;
|
||||
return ds;
|
||||
}
|
||||
|
||||
PhysicsForceRegion jsonToPhysicsForceRegion(Json const& json) {
|
||||
String type = json.getString("type");
|
||||
if (type.equalsIgnoreCase("DirectionalForceRegion"))
|
||||
return DirectionalForceRegion::fromJson(json);
|
||||
else if (type.equalsIgnoreCase("RadialForceRegion"))
|
||||
return RadialForceRegion::fromJson(json);
|
||||
else if (type.equalsIgnoreCase("GradientForceRegion"))
|
||||
return GradientForceRegion::fromJson(json);
|
||||
else
|
||||
throw JsonException::format("No such physics force region type '%s'", type);
|
||||
}
|
||||
|
||||
}
|
95
source/game/StarForceRegions.hpp
Normal file
95
source/game/StarForceRegions.hpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#ifndef STAR_FORCE_REGIONS_HPP
|
||||
#define STAR_FORCE_REGIONS_HPP
|
||||
|
||||
#include "StarPoly.hpp"
|
||||
#include "StarVariant.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct PhysicsCategoryFilter {
|
||||
enum Type { Whitelist, Blacklist };
|
||||
|
||||
static PhysicsCategoryFilter whitelist(StringSet categories);
|
||||
static PhysicsCategoryFilter blacklist(StringSet categories);
|
||||
|
||||
PhysicsCategoryFilter(Type type = Blacklist, StringSet categories = {});
|
||||
|
||||
bool check(StringSet const& categories) const;
|
||||
|
||||
bool operator==(PhysicsCategoryFilter const& rhs) const;
|
||||
|
||||
Type type;
|
||||
StringSet categories;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, PhysicsCategoryFilter& rfr);
|
||||
DataStream& operator<<(DataStream& ds, PhysicsCategoryFilter const& rfr);
|
||||
|
||||
PhysicsCategoryFilter jsonToPhysicsCategoryFilter(Json const& json);
|
||||
|
||||
struct DirectionalForceRegion {
|
||||
static DirectionalForceRegion fromJson(Json const& json);
|
||||
|
||||
RectF boundBox() const;
|
||||
|
||||
void translate(Vec2F const& pos);
|
||||
|
||||
bool operator==(DirectionalForceRegion const& rhs) const;
|
||||
|
||||
PolyF region;
|
||||
Maybe<float> xTargetVelocity;
|
||||
Maybe<float> yTargetVelocity;
|
||||
float controlForce;
|
||||
PhysicsCategoryFilter categoryFilter;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, DirectionalForceRegion& rfr);
|
||||
DataStream& operator<<(DataStream& ds, DirectionalForceRegion const& rfr);
|
||||
|
||||
struct RadialForceRegion {
|
||||
static RadialForceRegion fromJson(Json const& json);
|
||||
|
||||
RectF boundBox() const;
|
||||
|
||||
void translate(Vec2F const& pos);
|
||||
|
||||
bool operator==(RadialForceRegion const& rhs) const;
|
||||
|
||||
Vec2F center;
|
||||
float outerRadius;
|
||||
float innerRadius;
|
||||
float targetRadialVelocity;
|
||||
float controlForce;
|
||||
PhysicsCategoryFilter categoryFilter;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, RadialForceRegion& rfr);
|
||||
DataStream& operator<<(DataStream& ds, RadialForceRegion const& rfr);
|
||||
|
||||
struct GradientForceRegion {
|
||||
static GradientForceRegion fromJson(Json const& json);
|
||||
|
||||
RectF boundBox() const;
|
||||
|
||||
void translate(Vec2F const& pos);
|
||||
|
||||
bool operator==(GradientForceRegion const& rhs) const;
|
||||
|
||||
PolyF region;
|
||||
Line2F gradient;
|
||||
float baseTargetVelocity;
|
||||
float baseControlForce;
|
||||
PhysicsCategoryFilter categoryFilter;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, GradientForceRegion& rfr);
|
||||
DataStream& operator<<(DataStream& ds, GradientForceRegion const& rfr);
|
||||
|
||||
typedef Variant<DirectionalForceRegion, RadialForceRegion, GradientForceRegion> PhysicsForceRegion;
|
||||
|
||||
PhysicsForceRegion jsonToPhysicsForceRegion(Json const& json);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
162
source/game/StarGameTimers.cpp
Normal file
162
source/game/StarGameTimers.cpp
Normal file
|
@ -0,0 +1,162 @@
|
|||
#include "StarGameTimers.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
GameTimer::GameTimer() : time(), timer() {}
|
||||
|
||||
GameTimer::GameTimer(float time) : time(time) {
|
||||
reset();
|
||||
}
|
||||
|
||||
bool GameTimer::tick(float dt) {
|
||||
timer = approach(0.0f, timer, dt);
|
||||
return timer == 0.0f;
|
||||
}
|
||||
|
||||
bool GameTimer::ready() const {
|
||||
return timer == 0.0f;
|
||||
}
|
||||
|
||||
bool GameTimer::wrapTick(float dt) {
|
||||
auto res = tick(dt);
|
||||
if (res)
|
||||
reset();
|
||||
return res;
|
||||
}
|
||||
|
||||
void GameTimer::reset() {
|
||||
timer = time;
|
||||
}
|
||||
|
||||
void GameTimer::setDone() {
|
||||
timer = 0.0f;
|
||||
}
|
||||
|
||||
void GameTimer::invert() {
|
||||
timer = time - timer;
|
||||
}
|
||||
|
||||
float GameTimer::percent() const {
|
||||
if (time)
|
||||
return timer / time;
|
||||
else
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, GameTimer& gt) {
|
||||
ds >> gt.time;
|
||||
ds >> gt.timer;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, GameTimer const& gt) {
|
||||
ds << gt.time;
|
||||
ds << gt.timer;
|
||||
return ds;
|
||||
}
|
||||
|
||||
SlidingWindow::SlidingWindow() : windowSize(1.0f), resolution(1) {}
|
||||
|
||||
SlidingWindow::SlidingWindow(float windowSize, size_t resolution, float initialValue)
|
||||
: windowSize(windowSize), resolution(resolution) {
|
||||
sampleTimer = GameTimer(windowSize / resolution);
|
||||
window = std::vector<float>(resolution);
|
||||
reset(initialValue);
|
||||
}
|
||||
|
||||
void SlidingWindow::reset(float initialValue) {
|
||||
sampleTimer.reset();
|
||||
currentIndex = 0;
|
||||
currentMin = initialValue;
|
||||
currentMax = initialValue;
|
||||
currentAverage = initialValue;
|
||||
std::fill(window.begin(), window.end(), initialValue);
|
||||
}
|
||||
|
||||
void SlidingWindow::update(function<float()> sampleFunction) {
|
||||
if (sampleTimer.wrapTick()) {
|
||||
processUpdate(sampleFunction());
|
||||
}
|
||||
}
|
||||
|
||||
void SlidingWindow::update(float newValue) {
|
||||
if (sampleTimer.wrapTick()) {
|
||||
processUpdate(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
void SlidingWindow::processUpdate(float newValue) {
|
||||
++currentIndex;
|
||||
currentIndex = currentIndex % resolution;
|
||||
window[currentIndex] = newValue;
|
||||
|
||||
currentMin = std::numeric_limits<float>::max();
|
||||
currentMax = 0;
|
||||
float total = 0;
|
||||
for (float v : window) {
|
||||
total += v;
|
||||
currentMin = std::min(currentMin, v);
|
||||
currentMax = std::max(currentMax, v);
|
||||
}
|
||||
currentAverage = total / resolution;
|
||||
}
|
||||
|
||||
float SlidingWindow::min() {
|
||||
return currentMin;
|
||||
}
|
||||
|
||||
float SlidingWindow::max() {
|
||||
return currentMax;
|
||||
}
|
||||
|
||||
float SlidingWindow::average() {
|
||||
return currentAverage;
|
||||
}
|
||||
|
||||
EpochTimer::EpochTimer() : m_elapsedTime(0.0) {}
|
||||
|
||||
EpochTimer::EpochTimer(Json json) {
|
||||
m_lastSeenEpochTime = json.get("lastEpochTime").optDouble();
|
||||
m_elapsedTime = json.getDouble("elapsedTime");
|
||||
}
|
||||
|
||||
Json EpochTimer::toJson() const {
|
||||
return JsonObject{{"lastEpochTime", jsonFromMaybe(m_lastSeenEpochTime)}, {"elapsedTime", m_elapsedTime}};
|
||||
}
|
||||
|
||||
void EpochTimer::update(double newEpochTime) {
|
||||
if (!m_lastSeenEpochTime) {
|
||||
m_lastSeenEpochTime = newEpochTime;
|
||||
} else {
|
||||
// Don't allow elapsed time to go backwards in the case of the epoch time
|
||||
// being lost or wrong.
|
||||
double difference = newEpochTime - *m_lastSeenEpochTime;
|
||||
if (difference > 0)
|
||||
m_elapsedTime += difference;
|
||||
m_lastSeenEpochTime = newEpochTime;
|
||||
}
|
||||
}
|
||||
|
||||
double EpochTimer::elapsedTime() const {
|
||||
return m_elapsedTime;
|
||||
}
|
||||
|
||||
void EpochTimer::setElapsedTime(double elapsedTime) {
|
||||
m_elapsedTime = elapsedTime;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, EpochTimer& et) {
|
||||
ds >> et.m_lastSeenEpochTime;
|
||||
ds >> et.m_elapsedTime;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, EpochTimer const& et) {
|
||||
ds << et.m_lastSeenEpochTime;
|
||||
ds << et.m_elapsedTime;
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
77
source/game/StarGameTimers.hpp
Normal file
77
source/game/StarGameTimers.hpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#ifndef STAR_GAME_TIMERS_HPP
|
||||
#define STAR_GAME_TIMERS_HPP
|
||||
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct GameTimer {
|
||||
GameTimer();
|
||||
explicit GameTimer(float time);
|
||||
|
||||
float time;
|
||||
float timer;
|
||||
|
||||
bool tick(float dt = WorldTimestep); // returns true if time is up
|
||||
bool wrapTick(float dt = WorldTimestep); // auto resets
|
||||
void reset();
|
||||
void setDone();
|
||||
void invert();
|
||||
|
||||
bool ready() const;
|
||||
float percent() const;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, GameTimer& gt);
|
||||
DataStream& operator<<(DataStream& ds, GameTimer const& gt);
|
||||
|
||||
struct SlidingWindow {
|
||||
SlidingWindow();
|
||||
SlidingWindow(float windowSize, size_t resolution, float initialValue);
|
||||
|
||||
GameTimer sampleTimer;
|
||||
float windowSize;
|
||||
size_t resolution;
|
||||
|
||||
float currentMin;
|
||||
float currentMax;
|
||||
float currentAverage;
|
||||
|
||||
size_t currentIndex;
|
||||
std::vector<float> window;
|
||||
|
||||
void reset(float initialValue);
|
||||
void update(function<float()> sampleFunction);
|
||||
void update(float newValue);
|
||||
void processUpdate(float newValue);
|
||||
|
||||
float min();
|
||||
float max();
|
||||
float average();
|
||||
};
|
||||
|
||||
// Keeps long term track of elapsed time based on epochTime.
|
||||
class EpochTimer {
|
||||
public:
|
||||
EpochTimer();
|
||||
explicit EpochTimer(Json json);
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
void update(double newEpochTime);
|
||||
|
||||
double elapsedTime() const;
|
||||
void setElapsedTime(double elapsedTime);
|
||||
|
||||
friend DataStream& operator>>(DataStream& ds, EpochTimer& et);
|
||||
friend DataStream& operator<<(DataStream& ds, EpochTimer const& et);
|
||||
|
||||
private:
|
||||
Maybe<double> m_lastSeenEpochTime;
|
||||
double m_elapsedTime;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
97
source/game/StarGameTypes.cpp
Normal file
97
source/game/StarGameTypes.cpp
Normal file
|
@ -0,0 +1,97 @@
|
|||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<Direction> const DirectionNames{
|
||||
{Direction::Left, "left"},
|
||||
{Direction::Right, "right"},
|
||||
};
|
||||
|
||||
EnumMap<Gender> const GenderNames{
|
||||
{Gender::Male, "male"},
|
||||
{Gender::Female, "female"},
|
||||
};
|
||||
|
||||
EnumMap<FireMode> const FireModeNames{
|
||||
{FireMode::None, "none"},
|
||||
{FireMode::Primary, "primary"},
|
||||
{FireMode::Alt, "alt"}
|
||||
};
|
||||
|
||||
EnumMap<ToolHand> const ToolHandNames{
|
||||
{ToolHand::Primary, "primary"},
|
||||
{ToolHand::Alt, "alt"}
|
||||
};
|
||||
|
||||
EnumMap<TileLayer> const TileLayerNames{
|
||||
{TileLayer::Foreground, "foreground"},
|
||||
{TileLayer::Background, "background"}
|
||||
};
|
||||
|
||||
EnumMap<MoveControlType> const MoveControlTypeNames{
|
||||
{MoveControlType::Left, "left"},
|
||||
{MoveControlType::Right, "right"},
|
||||
{MoveControlType::Down, "down"},
|
||||
{MoveControlType::Up, "up"},
|
||||
{MoveControlType::Jump, "jump"}
|
||||
};
|
||||
|
||||
EnumMap<PortraitMode> const PortraitModeNames{
|
||||
{PortraitMode::Head, "head"},
|
||||
{PortraitMode::Bust, "bust"},
|
||||
{PortraitMode::Full, "full"},
|
||||
{PortraitMode::FullNeutral, "fullneutral"},
|
||||
{PortraitMode::FullNude, "fullnude"},
|
||||
{PortraitMode::FullNeutralNude, "fullneutralnude"}
|
||||
};
|
||||
|
||||
EnumMap<Rarity> const RarityNames{
|
||||
{Rarity::Common, "common"},
|
||||
{Rarity::Uncommon, "uncommon"},
|
||||
{Rarity::Rare, "rare"},
|
||||
{Rarity::Legendary, "legendary"},
|
||||
{Rarity::Essential, "essential"}
|
||||
};
|
||||
|
||||
std::pair<EntityId, EntityId> connectionEntitySpace(ConnectionId connectionId) {
|
||||
if (connectionId == ServerConnectionId) {
|
||||
return {MinServerEntityId, MaxServerEntityId};
|
||||
} else if (connectionId >= MinClientConnectionId && connectionId <= MaxClientConnectionId) {
|
||||
EntityId beginIdSpace = (EntityId)connectionId * -65536;
|
||||
EntityId endIdSpace = beginIdSpace + 65535;
|
||||
return {beginIdSpace, endIdSpace};
|
||||
} else {
|
||||
throw StarException::format("Invalid connection id in clientEntitySpace(%s)", connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
bool entityIdInSpace(EntityId entityId, ConnectionId connectionId) {
|
||||
auto pair = connectionEntitySpace(connectionId);
|
||||
return entityId >= pair.first && entityId <= pair.second;
|
||||
}
|
||||
|
||||
ConnectionId connectionForEntity(EntityId entityId) {
|
||||
if (entityId > 0)
|
||||
return ServerConnectionId;
|
||||
else
|
||||
return (-entityId - 1) / 65536 + 1;
|
||||
}
|
||||
|
||||
pair<float, Direction> getAngleSide(float angle, bool ccRotation) {
|
||||
angle = constrainAngle(angle);
|
||||
Direction direction = Direction::Right;
|
||||
if (angle > Constants::pi / 2) {
|
||||
direction = Direction::Left;
|
||||
angle = Constants::pi - angle;
|
||||
} else if (angle < -Constants::pi / 2) {
|
||||
direction = Direction::Left;
|
||||
angle = -Constants::pi - angle;
|
||||
}
|
||||
|
||||
if (direction == Direction::Left && ccRotation)
|
||||
angle *= -1;
|
||||
|
||||
return make_pair(angle, direction);
|
||||
}
|
||||
|
||||
}
|
169
source/game/StarGameTypes.hpp
Normal file
169
source/game/StarGameTypes.hpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#ifndef STAR_GAME_TYPES_HPP
|
||||
#define STAR_GAME_TYPES_HPP
|
||||
|
||||
#include "StarString.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
#include "StarVector.hpp"
|
||||
#include "StarLiquidTypes.hpp"
|
||||
#include "StarMaterialTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
enum class Direction : uint8_t {
|
||||
Left,
|
||||
Right
|
||||
};
|
||||
extern EnumMap<Direction> const DirectionNames;
|
||||
|
||||
inline Direction operator-(Direction dir) {
|
||||
if (dir == Direction::Left)
|
||||
return Direction::Right;
|
||||
return Direction::Left;
|
||||
}
|
||||
|
||||
inline int numericalDirection(Maybe<Direction> direction) {
|
||||
if (!direction)
|
||||
return 0;
|
||||
else
|
||||
return *direction == Direction::Left ? -1 : 1;
|
||||
}
|
||||
|
||||
template <typename NumType>
|
||||
inline Maybe<Direction> directionOf(NumType const& n) {
|
||||
if (n == 0)
|
||||
return {};
|
||||
else
|
||||
return n < 0 ? Direction::Left : Direction::Right;
|
||||
}
|
||||
|
||||
enum class Gender : uint8_t {
|
||||
Male,
|
||||
Female
|
||||
};
|
||||
extern EnumMap<Gender> const GenderNames;
|
||||
|
||||
enum class FireMode : uint8_t {
|
||||
None,
|
||||
Primary,
|
||||
Alt
|
||||
};
|
||||
extern EnumMap<FireMode> const FireModeNames;
|
||||
|
||||
enum class ToolHand : uint8_t {
|
||||
Primary,
|
||||
Alt
|
||||
};
|
||||
extern EnumMap<ToolHand> const ToolHandNames;
|
||||
|
||||
enum class TileLayer : uint8_t {
|
||||
Foreground,
|
||||
Background
|
||||
};
|
||||
extern EnumMap<TileLayer> const TileLayerNames;
|
||||
|
||||
enum class MoveControlType : uint8_t {
|
||||
Left,
|
||||
Right,
|
||||
Down,
|
||||
Up,
|
||||
Jump
|
||||
};
|
||||
extern EnumMap<MoveControlType> const MoveControlTypeNames;
|
||||
|
||||
enum class PortraitMode {
|
||||
Head,
|
||||
Bust,
|
||||
Full,
|
||||
FullNeutral,
|
||||
FullNude,
|
||||
FullNeutralNude
|
||||
};
|
||||
extern EnumMap<PortraitMode> const PortraitModeNames;
|
||||
|
||||
enum class Rarity {
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Legendary,
|
||||
Essential
|
||||
};
|
||||
extern EnumMap<Rarity> const RarityNames;
|
||||
|
||||
// Transformation from tile space to pixel space. Number of pixels in 1.0
|
||||
// distance (one tile).
|
||||
unsigned const TilePixels = 8;
|
||||
|
||||
float const WorldTimestep = 1.0f / 60.0f;
|
||||
float const SystemWorldTimestep = 1.0f / 20.0f;
|
||||
|
||||
size_t const WorldSectorSize = 32;
|
||||
|
||||
typedef int32_t EntityId;
|
||||
EntityId const NullEntityId = 0;
|
||||
EntityId const MinServerEntityId = 1;
|
||||
EntityId const MaxServerEntityId = highest<EntityId>();
|
||||
|
||||
// Whether this entity is controlled by it's world, or synced from a different
|
||||
// world. Does not necessarily correspond to client / server world (player is
|
||||
// master on client).
|
||||
enum class EntityMode {
|
||||
Master,
|
||||
Slave
|
||||
};
|
||||
|
||||
typedef uint16_t ConnectionId;
|
||||
ConnectionId const ServerConnectionId = 0;
|
||||
// Minimum and maximum valid client ids
|
||||
ConnectionId const MinClientConnectionId = 1;
|
||||
ConnectionId const MaxClientConnectionId = 32767;
|
||||
|
||||
template <typename Vec2T>
|
||||
inline Vec2F centerOfTile(Vec2T const& tile) {
|
||||
return Vec2F(tile.floor()) + Vec2F::filled(0.5);
|
||||
}
|
||||
|
||||
typedef uint16_t DungeonId;
|
||||
|
||||
static const DungeonId NoDungeonId = 65535;
|
||||
static const DungeonId SpawnDungeonId = 65534;
|
||||
static const DungeonId BiomeMicroDungeonId = 65533;
|
||||
// meta dungeon signalling player built structures
|
||||
static const DungeonId ConstructionDungeonId = 65532;
|
||||
// indicates a block that has been destroyed
|
||||
static const DungeonId DestroyedBlockDungeonId = 65531;
|
||||
|
||||
// dungeonId for zero-g areas with and without tile protection
|
||||
static const DungeonId ZeroGDungeonId = 65525;
|
||||
static const DungeonId ProtectedZeroGDungeonId = 65524;
|
||||
|
||||
// The first dungeon id that is reserved for special hard-coded dungeon values.
|
||||
DungeonId const FirstMetaDungeonId = 65520;
|
||||
|
||||
inline bool isRealDungeon(DungeonId dungeon) {
|
||||
return dungeon < FirstMetaDungeonId;
|
||||
}
|
||||
|
||||
// Returns the inclusive beginning and end of the entity id space for the
|
||||
// given connection. All client connection id spaces will be within the range
|
||||
// [-2^31, -1].
|
||||
pair<EntityId, EntityId> connectionEntitySpace(ConnectionId connectionId);
|
||||
|
||||
bool entityIdInSpace(EntityId entityId, ConnectionId connectionId);
|
||||
|
||||
ConnectionId connectionForEntity(EntityId entityId);
|
||||
|
||||
// Returns an angle in the range [-pi / 2, pi / 2], and the horizontal
|
||||
// hemisphere of the angle. The angle is specified as positive being upward
|
||||
// rotation and negative being downward rotation, unless ccRotation is true, in
|
||||
// which case the angle is always positive == counter-clocwise.
|
||||
pair<float, Direction> getAngleSide(float angle, bool ccRotation = false);
|
||||
|
||||
enum class TileDamageResult {
|
||||
None = 0,
|
||||
Protected = 1,
|
||||
Normal = 2,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1245
source/game/StarHumanoid.cpp
Normal file
1245
source/game/StarHumanoid.cpp
Normal file
File diff suppressed because it is too large
Load diff
367
source/game/StarHumanoid.hpp
Normal file
367
source/game/StarHumanoid.hpp
Normal file
|
@ -0,0 +1,367 @@
|
|||
#ifndef STAR_HUMANOID_HPP
|
||||
#define STAR_HUMANOID_HPP
|
||||
|
||||
#include "StarDataStream.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarParticle.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// Required for renderDummy
|
||||
STAR_CLASS(HeadArmor);
|
||||
STAR_CLASS(ChestArmor);
|
||||
STAR_CLASS(LegsArmor);
|
||||
STAR_CLASS(BackArmor);
|
||||
|
||||
STAR_CLASS(Humanoid);
|
||||
|
||||
STAR_STRUCT(Dance);
|
||||
|
||||
enum class HumanoidEmote {
|
||||
Idle,
|
||||
Blabbering,
|
||||
Shouting,
|
||||
Happy,
|
||||
Sad,
|
||||
NEUTRAL,
|
||||
Laugh,
|
||||
Annoyed,
|
||||
Oh,
|
||||
OOOH,
|
||||
Blink,
|
||||
Wink,
|
||||
Eat,
|
||||
Sleep
|
||||
};
|
||||
extern EnumMap<HumanoidEmote> const HumanoidEmoteNames;
|
||||
size_t const EmoteSize = 14;
|
||||
|
||||
struct Personality {
|
||||
String idle;
|
||||
String armIdle;
|
||||
Vec2F headOffset;
|
||||
Vec2F armOffset;
|
||||
};
|
||||
|
||||
Personality parsePersonality(Json const& config);
|
||||
|
||||
struct HumanoidIdentity {
|
||||
explicit HumanoidIdentity(Json config = Json());
|
||||
|
||||
Json toJson() const;
|
||||
|
||||
String name;
|
||||
// Must have :idle[1-5], :sit, :duck, :walk[1-8], :run[1-8], :jump[1-4], and
|
||||
// :fall[1-4]
|
||||
String species;
|
||||
Gender gender;
|
||||
|
||||
String hairGroup;
|
||||
// Must have :normal and :climb
|
||||
String hairType;
|
||||
String hairDirectives;
|
||||
String bodyDirectives;
|
||||
String emoteDirectives;
|
||||
String facialHairGroup;
|
||||
String facialHairType;
|
||||
String facialHairDirectives;
|
||||
String facialMaskGroup;
|
||||
String facialMaskType;
|
||||
String facialMaskDirectives;
|
||||
|
||||
Personality personality;
|
||||
Vec4B color;
|
||||
|
||||
Maybe<String> imagePath;
|
||||
};
|
||||
|
||||
DataStream& operator>>(DataStream& ds, HumanoidIdentity& identity);
|
||||
DataStream& operator<<(DataStream& ds, HumanoidIdentity const& identity);
|
||||
|
||||
class Humanoid {
|
||||
public:
|
||||
enum State {
|
||||
Idle, // 1 idle frame
|
||||
Walk, // 8 walking frames
|
||||
Run, // 8 run frames
|
||||
Jump, // 4 jump frames
|
||||
Fall, // 4 fall frames
|
||||
Swim, // 7 swim frames
|
||||
SwimIdle, // 2 swim idle frame
|
||||
Duck, // 1 ducking frame
|
||||
Sit, // 1 sitting frame
|
||||
Lay, // 1 laying frame
|
||||
STATESIZE
|
||||
};
|
||||
static EnumMap<State> const StateNames;
|
||||
|
||||
Humanoid(Json const& config);
|
||||
Humanoid(HumanoidIdentity const& identity);
|
||||
|
||||
struct HumanoidTiming {
|
||||
explicit HumanoidTiming(Json config = Json());
|
||||
|
||||
static bool cyclicState(State state);
|
||||
static bool cyclicEmoteState(HumanoidEmote state);
|
||||
|
||||
int stateSeq(float timer, State state) const;
|
||||
int emoteStateSeq(float timer, HumanoidEmote state) const;
|
||||
int danceSeq(float timer, DancePtr dance) const;
|
||||
int genericSeq(float timer, float cycle, unsigned frames, bool cyclic) const;
|
||||
|
||||
Array<float, STATESIZE> stateCycle;
|
||||
Array<unsigned, STATESIZE> stateFrames;
|
||||
|
||||
Array<float, EmoteSize> emoteCycle;
|
||||
Array<unsigned, EmoteSize> emoteFrames;
|
||||
};
|
||||
|
||||
void setIdentity(HumanoidIdentity const& identity);
|
||||
HumanoidIdentity const& identity() const;
|
||||
|
||||
// All of the image identifiers here are meant to be image *base* names, with
|
||||
// a collection of frames specific to each piece. If an image is set to
|
||||
// empty string, it is disabled.
|
||||
|
||||
// Asset directives for the head armor.
|
||||
void setHeadArmorDirectives(String directives);
|
||||
// Must have :normal, climb
|
||||
void setHeadArmorFrameset(String headFrameset);
|
||||
// Asset directives for the chest, back and front arms armor.
|
||||
void setChestArmorDirectives(String directives);
|
||||
// Will have :run, :normal, and :duck
|
||||
void setChestArmorFrameset(String chest);
|
||||
// Same as back arm image frames
|
||||
void setBackSleeveFrameset(String backSleeveFrameset);
|
||||
// Same as front arm image frames
|
||||
void setFrontSleeveFrameset(String frontSleeveFrameset);
|
||||
|
||||
// Asset directives for the legs armor.
|
||||
void setLegsArmorDirectives(String directives);
|
||||
// Must have :idle, :duck, :walk[1-8], :run[1-8], :jump[1-4], :fall[1-4]
|
||||
void setLegsArmorFrameset(String legsFrameset);
|
||||
|
||||
// Asset directives for the back armor.
|
||||
void setBackArmorDirectives(String directives);
|
||||
// Must have :idle, :duck, :walk[1-8], :run[1-8], :jump[1-4], :fall[1-4]
|
||||
void setBackArmorFrameset(String backFrameset);
|
||||
|
||||
void setHelmetMaskDirectives(String helmetMaskDirectives);
|
||||
|
||||
void setBodyHidden(bool hidden);
|
||||
|
||||
void setState(State state);
|
||||
void setEmoteState(HumanoidEmote state);
|
||||
void setDance(Maybe<String> const& dance);
|
||||
void setFacingDirection(Direction facingDirection);
|
||||
void setMovingBackwards(bool movingBackwards);
|
||||
void setRotation(float rotation);
|
||||
|
||||
void setVaporTrail(bool enabled);
|
||||
|
||||
State state() const;
|
||||
HumanoidEmote emoteState() const;
|
||||
Maybe<String> dance() const;
|
||||
Direction facingDirection() const;
|
||||
bool movingBackwards() const;
|
||||
|
||||
// If not rotating, then the arms follow normal movement animation. The
|
||||
// angle parameter should be in the range [-pi/2, pi/2] (the facing direction
|
||||
// should not be included in the angle).
|
||||
void setPrimaryHandParameters(bool holdingItem, float angle, float itemAngle, bool twoHanded,
|
||||
bool recoil, bool outsideOfHand);
|
||||
void setPrimaryHandFrameOverrides(String backFrameOverride, String frontFrameOverride);
|
||||
void setPrimaryHandDrawables(List<Drawable> drawables);
|
||||
void setPrimaryHandNonRotatedDrawables(List<Drawable> drawables);
|
||||
|
||||
// Same as primary hand.
|
||||
void setAltHandParameters(bool holdingItem, float angle, float itemAngle, bool recoil,
|
||||
bool outsideOfHand);
|
||||
void setAltHandFrameOverrides(String backFrameOverride, String frontFrameOverride);
|
||||
void setAltHandDrawables(List<Drawable> drawables);
|
||||
void setAltHandNonRotatedDrawables(List<Drawable> drawables);
|
||||
|
||||
// Updates the animation based on whatever the current animation state is,
|
||||
// wrapping or clamping animation time as appropriate.
|
||||
void animate(float dt);
|
||||
|
||||
// Reset animation time to 0.0f
|
||||
void resetAnimation();
|
||||
|
||||
// Renders to centered drawables (centered on the normal image center for the
|
||||
// player graphics), (in world space, not pixels)
|
||||
List<Drawable> render();
|
||||
|
||||
// Renders to centered drawables (centered on the normal image center for the
|
||||
// player graphics), (in pixels, not world space)
|
||||
List<Drawable> renderPortrait(PortraitMode mode) const;
|
||||
|
||||
List<Drawable> renderSkull() const;
|
||||
|
||||
// Renders to centered drawables (centered on the normal image center for the
|
||||
// player graphics), (in pixels, not world space)
|
||||
static List<Drawable> renderDummy(Gender gender, HeadArmor const* head = nullptr, ChestArmor const* chest = nullptr,
|
||||
LegsArmor const* legs = nullptr, BackArmor const* back = nullptr);
|
||||
|
||||
Vec2F primaryHandPosition(Vec2F const& offset) const;
|
||||
Vec2F altHandPosition(Vec2F const& offset) const;
|
||||
|
||||
// Finds the arm position in world space if the humanoid was facing the given
|
||||
// direction and applying the given arm angle. The offset given is from the
|
||||
// rotation center of the arm.
|
||||
Vec2F primaryArmPosition(Direction facingDirection, float armAngle, Vec2F const& offset) const;
|
||||
Vec2F altArmPosition(Direction facingDirection, float armAngle, Vec2F const& offset) const;
|
||||
|
||||
// Gives the offset of the hand from the arm rotation center
|
||||
Vec2F primaryHandOffset(Direction facingDirection) const;
|
||||
Vec2F altHandOffset(Direction facingDirection) const;
|
||||
|
||||
Vec2F armAdjustment() const;
|
||||
|
||||
Vec2F mouthOffset(bool ignoreAdjustments = false) const;
|
||||
float getBobYOffset() const;
|
||||
Vec2F feetOffset() const;
|
||||
|
||||
Vec2F headArmorOffset() const;
|
||||
Vec2F chestArmorOffset() const;
|
||||
Vec2F legsArmorOffset() const;
|
||||
Vec2F backArmorOffset() const;
|
||||
|
||||
String defaultDeathParticles() const;
|
||||
List<Particle> particles(String const& name) const;
|
||||
|
||||
Json const& defaultMovementParameters() const;
|
||||
|
||||
private:
|
||||
struct HandDrawingInfo {
|
||||
List<Drawable> itemDrawables;
|
||||
List<Drawable> nonRotatedDrawables;
|
||||
bool holdingItem = false;
|
||||
float angle = 0.0f;
|
||||
float itemAngle = 0.0f;
|
||||
String backFrame;
|
||||
String frontFrame;
|
||||
float frameAngleAdjust = 0.0f;
|
||||
bool recoil = false;
|
||||
bool outsideOfHand = false;
|
||||
};
|
||||
|
||||
String frameBase(State state) const;
|
||||
String emoteFrameBase(HumanoidEmote state) const;
|
||||
|
||||
String getHeadFromIdentity() const;
|
||||
String getBodyFromIdentity() const;
|
||||
String getFacialEmotesFromIdentity() const;
|
||||
String getHairFromIdentity() const;
|
||||
String getFacialHairFromIdentity() const;
|
||||
String getFacialMaskFromIdentity() const;
|
||||
String getBackArmFromIdentity() const;
|
||||
String getFrontArmFromIdentity() const;
|
||||
String getVaporTrailFrameset() const;
|
||||
|
||||
String getBodyDirectives() const;
|
||||
String getHairDirectives() const;
|
||||
String getEmoteDirectives() const;
|
||||
String getFacialHairDirectives() const;
|
||||
String getFacialMaskDirectives() const;
|
||||
String getHelmetMaskDirectives() const;
|
||||
String getHeadDirectives() const;
|
||||
String getChestDirectives() const;
|
||||
String getLegsDirectives() const;
|
||||
String getBackDirectives() const;
|
||||
|
||||
int getEmoteStateSequence() const;
|
||||
int getArmStateSequence() const;
|
||||
int getBodyStateSequence() const;
|
||||
|
||||
Maybe<DancePtr> getDance() const;
|
||||
|
||||
Vec2F m_globalOffset;
|
||||
Vec2F m_headRunOffset;
|
||||
Vec2F m_headSwimOffset;
|
||||
Vec2F m_headDuckOffset;
|
||||
Vec2F m_headSitOffset;
|
||||
Vec2F m_headLayOffset;
|
||||
float m_runFallOffset;
|
||||
float m_duckOffset;
|
||||
float m_sitOffset;
|
||||
float m_layOffset;
|
||||
Vec2F m_recoilOffset;
|
||||
Vec2F m_mouthOffset;
|
||||
Vec2F m_feetOffset;
|
||||
|
||||
Vec2F m_headArmorOffset;
|
||||
Vec2F m_chestArmorOffset;
|
||||
Vec2F m_legsArmorOffset;
|
||||
Vec2F m_backArmorOffset;
|
||||
|
||||
bool m_bodyHidden;
|
||||
|
||||
List<int> m_armWalkSeq;
|
||||
List<int> m_armRunSeq;
|
||||
List<float> m_walkBob;
|
||||
List<float> m_runBob;
|
||||
List<float> m_swimBob;
|
||||
float m_jumpBob;
|
||||
Vec2F m_frontArmRotationCenter;
|
||||
Vec2F m_backArmRotationCenter;
|
||||
Vec2F m_frontHandPosition;
|
||||
Vec2F m_backArmOffset;
|
||||
|
||||
String m_headFrameset;
|
||||
String m_bodyFrameset;
|
||||
String m_backArmFrameset;
|
||||
String m_frontArmFrameset;
|
||||
String m_emoteFrameset;
|
||||
String m_hairFrameset;
|
||||
String m_facialHairFrameset;
|
||||
String m_facialMaskFrameset;
|
||||
|
||||
bool m_bodyFullbright;
|
||||
|
||||
String m_vaporTrailFrameset;
|
||||
unsigned m_vaporTrailFrames;
|
||||
float m_vaporTrailCycle;
|
||||
|
||||
String m_backSleeveFrameset;
|
||||
String m_frontSleeveFrameset;
|
||||
String m_headArmorFrameset;
|
||||
String m_headArmorDirectives;
|
||||
String m_chestArmorFrameset;
|
||||
String m_chestArmorDirectives;
|
||||
String m_legsArmorFrameset;
|
||||
String m_legsArmorDirectives;
|
||||
String m_backArmorFrameset;
|
||||
String m_backArmorDirectives;
|
||||
String m_helmetMaskDirectives;
|
||||
|
||||
State m_state;
|
||||
HumanoidEmote m_emoteState;
|
||||
Maybe<String> m_dance;
|
||||
Direction m_facingDirection;
|
||||
bool m_movingBackwards;
|
||||
float m_rotation;
|
||||
bool m_drawVaporTrail;
|
||||
|
||||
HandDrawingInfo m_primaryHand;
|
||||
HandDrawingInfo m_altHand;
|
||||
|
||||
bool m_twoHanded;
|
||||
|
||||
HumanoidIdentity m_identity;
|
||||
HumanoidTiming m_timing;
|
||||
|
||||
float m_animationTimer;
|
||||
float m_emoteAnimationTimer;
|
||||
float m_danceTimer;
|
||||
|
||||
Json m_particleEmitters;
|
||||
String m_defaultDeathParticles;
|
||||
|
||||
Json m_defaultMovementParameters;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
251
source/game/StarImageMetadataDatabase.cpp
Normal file
251
source/game/StarImageMetadataDatabase.cpp
Normal file
|
@ -0,0 +1,251 @@
|
|||
#include "StarImageMetadataDatabase.hpp"
|
||||
#include "StarFile.hpp"
|
||||
#include "StarImage.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
#include "StarEncode.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
Vec2U ImageMetadataDatabase::imageSize(String const& path) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
auto i = m_sizeCache.find(path);
|
||||
if (i != m_sizeCache.end())
|
||||
return i->second;
|
||||
|
||||
locker.unlock();
|
||||
Vec2U size = calculateImageSize(path);
|
||||
|
||||
locker.lock();
|
||||
m_sizeCache[path] = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
List<Vec2I> ImageMetadataDatabase::imageSpaces(String const& path, Vec2F position, float fillLimit, bool flip) const {
|
||||
SpacesEntry key = make_tuple(path, Vec2I::round(position), fillLimit, flip);
|
||||
|
||||
MutexLocker locker(m_mutex);
|
||||
auto i = m_spacesCache.find(key);
|
||||
if (i != m_spacesCache.end()) {
|
||||
return i->second;
|
||||
}
|
||||
|
||||
String filteredPath = filterProcessing(path);
|
||||
SpacesEntry filteredKey = make_tuple(filteredPath, Vec2I::round(position), fillLimit, flip);
|
||||
|
||||
auto j = m_spacesCache.find(filteredKey);
|
||||
if (j != m_spacesCache.end()) {
|
||||
auto spaces = j->second;
|
||||
m_spacesCache[key] = spaces;
|
||||
return spaces;
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
|
||||
auto image = Root::singleton().assets()->image(filteredPath);
|
||||
int imageWidth = image->width();
|
||||
int imageHeight = image->height();
|
||||
|
||||
Vec2I min((position / TilePixels).floor());
|
||||
Vec2I max(((Vec2F(imageWidth, imageHeight) + position) / TilePixels).ceil());
|
||||
|
||||
List<Vec2I> spaces;
|
||||
|
||||
for (int yspace = min[1]; yspace < max[1]; ++yspace) {
|
||||
for (int xspace = min[0]; xspace < max[0]; ++xspace) {
|
||||
float fillRatio = 0.0f;
|
||||
|
||||
for (int y = 0; y < (int)TilePixels; ++y) {
|
||||
int ypixel = round(yspace * (int)TilePixels + y - position[1]);
|
||||
if (ypixel < 0 || ypixel >= imageHeight)
|
||||
continue;
|
||||
|
||||
for (int x = 0; x < (int)TilePixels; ++x) {
|
||||
int xpixel = round(xspace * (int)TilePixels + x - position[0]);
|
||||
if (flip)
|
||||
xpixel = imageWidth - 1 - xpixel;
|
||||
|
||||
if (xpixel < 0 || xpixel >= imageWidth)
|
||||
continue;
|
||||
|
||||
if (image->get(xpixel, ypixel)[3] > 0)
|
||||
fillRatio += 1.0f / square(TilePixels);
|
||||
}
|
||||
}
|
||||
|
||||
if (fillRatio >= fillLimit)
|
||||
spaces.append(Vec2I(xspace, yspace));
|
||||
}
|
||||
}
|
||||
|
||||
locker.lock();
|
||||
m_spacesCache[key] = spaces;
|
||||
m_spacesCache[filteredKey] = spaces;
|
||||
|
||||
return spaces;
|
||||
}
|
||||
|
||||
RectU ImageMetadataDatabase::nonEmptyRegion(String const& path) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
auto i = m_regionCache.find(path);
|
||||
if (i != m_regionCache.end()) {
|
||||
return i->second;
|
||||
}
|
||||
|
||||
String filteredPath = filterProcessing(path);
|
||||
auto j = m_regionCache.find(filteredPath);
|
||||
if (j != m_regionCache.end()) {
|
||||
m_regionCache[path] = j->second;
|
||||
return j->second;
|
||||
}
|
||||
|
||||
locker.unlock();
|
||||
auto image = Root::singleton().assets()->image(filteredPath);
|
||||
RectU region = RectU::null();
|
||||
image->forEachPixel([®ion](unsigned x, unsigned y, Vec4B const& pixel) {
|
||||
if (pixel[3] > 0)
|
||||
region.combine(RectU::withSize({x, y}, {1, 1}));
|
||||
});
|
||||
|
||||
locker.lock();
|
||||
m_regionCache[path] = region;
|
||||
m_regionCache[filteredPath] = region;
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
String ImageMetadataDatabase::filterProcessing(String const& path) {
|
||||
AssetPath components = AssetPath::split(path);
|
||||
|
||||
components.directives.filter([](String const& directive) {
|
||||
ImageOperation operation;
|
||||
try {
|
||||
operation = imageOperationFromString(directive);
|
||||
} catch (StarException const&) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(operation.is<HueShiftImageOperation>() ||
|
||||
operation.is<SaturationShiftImageOperation>() ||
|
||||
operation.is<BrightnessMultiplyImageOperation>() ||
|
||||
operation.is<FadeToColorImageOperation>() ||
|
||||
operation.is<ScanLinesImageOperation>() ||
|
||||
operation.is<SetColorImageOperation>());
|
||||
});
|
||||
|
||||
return AssetPath::join(components);
|
||||
}
|
||||
|
||||
Vec2U ImageMetadataDatabase::calculateImageSize(String const& path) const {
|
||||
// Carefully calculate an image's size while trying not to actually load it.
|
||||
// In error cases, this will fall back to calling Assets::image, so that image
|
||||
// can possibly produce a missing image asset or properly report the error.
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
auto fallback = [&assets, &path]() {
|
||||
return assets->image(path)->size();
|
||||
};
|
||||
|
||||
AssetPath components = AssetPath::split(path);
|
||||
if (!assets->assetExists(components.basePath)) {
|
||||
return fallback();
|
||||
}
|
||||
|
||||
Vec2U imageSize;
|
||||
if (components.subPath) {
|
||||
auto frames = assets->imageFrames(components.basePath);
|
||||
if (!frames)
|
||||
return fallback();
|
||||
|
||||
if (auto rect = frames->getRect(*components.subPath))
|
||||
imageSize = rect->size();
|
||||
else
|
||||
return fallback();
|
||||
} else {
|
||||
// We ensure that the base image size is cached even when given directives,
|
||||
// so we don't have to call Image::readPngMetadata on the same file more
|
||||
// than once.
|
||||
MutexLocker locker(m_mutex);
|
||||
if (auto size = m_sizeCache.maybe(components.basePath)) {
|
||||
imageSize = *size;
|
||||
} else {
|
||||
locker.unlock();
|
||||
imageSize = get<0>(Image::readPngMetadata(assets->openFile(components.basePath)));
|
||||
locker.lock();
|
||||
m_sizeCache[components.basePath] = imageSize;
|
||||
}
|
||||
}
|
||||
|
||||
struct OperationSizeAdjust {
|
||||
Vec2U& imageSize;
|
||||
bool hasError;
|
||||
|
||||
OperationSizeAdjust(Vec2U& size) : imageSize(size), hasError(false) {};
|
||||
|
||||
void operator()(HueShiftImageOperation const&) {}
|
||||
|
||||
void operator()(SaturationShiftImageOperation const&) {}
|
||||
|
||||
void operator()(BrightnessMultiplyImageOperation const&) {}
|
||||
|
||||
void operator()(FadeToColorImageOperation const&) {}
|
||||
|
||||
void operator()(ScanLinesImageOperation const&) {}
|
||||
|
||||
void operator()(SetColorImageOperation const&) {}
|
||||
|
||||
void operator()(ColorReplaceImageOperation const&) {}
|
||||
|
||||
void operator()(AlphaMaskImageOperation const&) {}
|
||||
|
||||
void operator()(BlendImageOperation const&) {}
|
||||
|
||||
void operator()(MultiplyImageOperation const&) {}
|
||||
|
||||
void operator()(BorderImageOperation const& bio) {
|
||||
imageSize += Vec2U::filled(bio.pixels * 2);
|
||||
}
|
||||
|
||||
void operator()(ScaleImageOperation const& sio) {
|
||||
imageSize = Vec2U::round(vmult(Vec2F(imageSize), sio.scale));
|
||||
}
|
||||
|
||||
void operator()(CropImageOperation const& cio) {
|
||||
if (cio.subset.isEmpty() ||
|
||||
cio.subset.xMin() < 0 ||
|
||||
cio.subset.yMin() < 0 ||
|
||||
(unsigned)cio.subset.xMax() > imageSize[0] ||
|
||||
(unsigned)cio.subset.yMax() > imageSize[1]) {
|
||||
hasError = true;
|
||||
} else {
|
||||
imageSize = Vec2U(cio.subset.size());
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(FlipImageOperation const&) {}
|
||||
};
|
||||
|
||||
OperationSizeAdjust osa(imageSize);
|
||||
|
||||
for (auto const& directive : components.directives) {
|
||||
ImageOperation operation;
|
||||
try {
|
||||
operation = imageOperationFromString(directive);
|
||||
} catch (StarException const&) {
|
||||
return fallback();
|
||||
}
|
||||
|
||||
operation.call(osa);
|
||||
if (osa.hasError) {
|
||||
return fallback();
|
||||
}
|
||||
}
|
||||
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
}
|
40
source/game/StarImageMetadataDatabase.hpp
Normal file
40
source/game/StarImageMetadataDatabase.hpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#ifndef STAR_IMAGE_METADATA_DATABASE_HPP
|
||||
#define STAR_IMAGE_METADATA_DATABASE_HPP
|
||||
|
||||
#include "StarRect.hpp"
|
||||
#include "StarMap.hpp"
|
||||
#include "StarString.hpp"
|
||||
#include "StarThread.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(ImageMetadataDatabase);
|
||||
|
||||
// Caches image size, image spaces, and nonEmptyRegion completely until a
|
||||
// reload, does not expire cached values in a TTL based way like Assets,
|
||||
// because they are expensive to compute and cheap to keep around.
|
||||
class ImageMetadataDatabase {
|
||||
public:
|
||||
Vec2U imageSize(String const& path) const;
|
||||
List<Vec2I> imageSpaces(String const& path, Vec2F position, float fillLimit, bool flip) const;
|
||||
RectU nonEmptyRegion(String const& path) const;
|
||||
|
||||
private:
|
||||
// Removes image processing directives that don't affect image spaces /
|
||||
// non-empty regions.
|
||||
static String filterProcessing(String const& path);
|
||||
|
||||
Vec2U calculateImageSize(String const& path) const;
|
||||
|
||||
// Path, position, fillLimit, and flip
|
||||
typedef tuple<String, Vec2I, float, bool> SpacesEntry;
|
||||
|
||||
mutable Mutex m_mutex;
|
||||
mutable StringMap<Vec2U> m_sizeCache;
|
||||
mutable HashMap<SpacesEntry, List<Vec2I>> m_spacesCache;
|
||||
mutable StringMap<RectU> m_regionCache;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
64
source/game/StarInteractionTypes.cpp
Normal file
64
source/game/StarInteractionTypes.cpp
Normal file
|
@ -0,0 +1,64 @@
|
|||
#include "StarInteractionTypes.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DataStream& operator>>(DataStream& ds, InteractRequest& ir) {
|
||||
ds >> ir.sourceId;
|
||||
ds >> ir.sourcePosition;
|
||||
ds >> ir.targetId;
|
||||
ds >> ir.interactPosition;
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, InteractRequest const& ir) {
|
||||
ds << ir.sourceId;
|
||||
ds << ir.sourcePosition;
|
||||
ds << ir.targetId;
|
||||
ds << ir.interactPosition;
|
||||
return ds;
|
||||
}
|
||||
|
||||
EnumMap<InteractActionType> const InteractActionTypeNames{{InteractActionType::None, "None"},
|
||||
{InteractActionType::OpenContainer, "OpenContainer"},
|
||||
{InteractActionType::SitDown, "SitDown"},
|
||||
{InteractActionType::OpenCraftingInterface, "OpenCraftingInterface"},
|
||||
{InteractActionType::OpenSongbookInterface, "OpenSongbookInterface"},
|
||||
{InteractActionType::OpenNpcCraftingInterface, "OpenNpcCraftingInterface"},
|
||||
{InteractActionType::OpenMerchantInterface, "OpenMerchantInterface"},
|
||||
{InteractActionType::OpenAiInterface, "OpenAiInterface"},
|
||||
{InteractActionType::OpenTeleportDialog, "OpenTeleportDialog"},
|
||||
{InteractActionType::ShowPopup, "ShowPopup"},
|
||||
{InteractActionType::ScriptPane, "ScriptPane"},
|
||||
{InteractActionType::Message, "Message"}};
|
||||
|
||||
InteractAction::InteractAction() {
|
||||
type = InteractActionType::None;
|
||||
entityId = NullEntityId;
|
||||
}
|
||||
|
||||
InteractAction::InteractAction(InteractActionType type, EntityId entityId, Json data)
|
||||
: type(type), entityId(entityId), data(data) {}
|
||||
|
||||
InteractAction::InteractAction(String const& typeName, EntityId entityId, Json data)
|
||||
: type(InteractActionTypeNames.getLeft(typeName)), entityId(entityId), data(data) {}
|
||||
|
||||
InteractAction::operator bool() const {
|
||||
return type != InteractActionType::None;
|
||||
}
|
||||
|
||||
DataStream& operator>>(DataStream& ds, InteractAction& ir) {
|
||||
ds.read(ir.type);
|
||||
ds.read(ir.entityId);
|
||||
ds.read(ir.data);
|
||||
return ds;
|
||||
}
|
||||
|
||||
DataStream& operator<<(DataStream& ds, InteractAction const& ir) {
|
||||
ds.write(ir.type);
|
||||
ds.write(ir.entityId);
|
||||
ds.write(ir.data);
|
||||
return ds;
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue