This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

502
source/game/CMakeLists.txt Normal file
View 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})

File diff suppressed because it is too large Load diff

View 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

View 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;
}
}

View 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

View 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())}};
}
}

View 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
View 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;
}
}

View 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

View 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"}};
}

View 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

View 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));
}
}

View 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

View 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)));
}
}

View 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

View 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;
}
}

View 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
View 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
View 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

View 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;
}
}

View 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

View 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"}
};
}

View 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

View 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;
}
}

View 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

View 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));
}
}

View 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

View 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;
}
}

View 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

View 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;
}
}

View 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

View 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;
}
}

View 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

View 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();
}
}

View 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

View 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;
}
}

View 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

View 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;
}
}

View 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

View 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();
}
}

View 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
View 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
View 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

View 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 {};
}
}

View 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

View 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;
}
}

View 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

View 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"}
};
}

View 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

View 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]);
}
}

View 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

File diff suppressed because it is too large Load diff

View 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
View 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
View 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

View 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);
}
}

View 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

View 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));
}
}

View 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

View 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;
}
}

View 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

View 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});
}
}

View 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

View 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;
}
}

View 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

File diff suppressed because it is too large Load diff

View 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

View 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]);
}
}
}

View 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

View 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);
}
}
}
}

View 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

View 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")};
}));
}
}

View 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

View 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 {};
}
}

View 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

View 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;
}
}

View 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

View 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));
}
}

View 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

View 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;
}
}

View 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

View 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));
}
}
}

View 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

View 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"}
};
}

View 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

View 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;
}
}

View 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

View 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});
}
}
}

View 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

View 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);
}
}

View 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

View 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;
}
}

View 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

View 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);
}
}

View 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

File diff suppressed because it is too large Load diff

View 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

View 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([&region](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;
}
}

View 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

View 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