mse / RSOD (public) (License: CC0 and other licenses) (since 2025-03-01) (hash sha1)
Free software FPS engine

/src/fdungeon.cpp (9dc09a1ac86b8278e3d056670a5cd2c80654b1a1) (175139 bytes) (mode 100644) (type blob)

#ifndef FDUNGEON_CPP
#define FDUNGEON_CPP

#ifndef FDUNGEON_SEPARATE_COMPILATION
	// Perlin noise.
	#define STB_PERLIN_IMPLEMENTATION
	#include <stb_perlin.h>
	// glTF loader.
	#define FGLTF_IMPLEMENTATION
	// Avatar engine.
	#define AVATAR_IMPLEMENTATION
	// Terrain engine.
	#define TERRA_IMPLEMENTATION
	// Particle engine.
	#define PARTICLE_IMPLEMENTATION
	// Bullet physics.
	#define BTRAPVERTEXBUFFER_IMPLEMENTATION
#endif

// Use doubles to match the version of Bullet produced by
// build_cmake_pybullet_double.sh
#ifndef BT_USE_DOUBLE_PRECISION
	#define BT_USE_DOUBLE_PRECISION
#endif
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/Gimpact/btGImpactCollisionAlgorithm.h>
#include <btrapCopypasta.h>
#include <btrapVertexBuffer.h>

// Implementations in libs.cpp.
#include <stb_image.h>
#include <tm_json.h>

#include <fgltf.h>
#include <avatar.h>
#include <terra.h>
#include <particle.h>

#include <limits.h>
#include <stdint.h>

#include <cmath>

#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>

namespace fdungeon {

// Agent type flags.
static const int
	TYPE_AGENT = 1,
	TYPE_VEHICLE = 2,
	TYPE_DYNAMIC = 4,
	TYPE_STATIC = 8;

// An inventory instance of an item, such as a weapon or a trinket.
struct InventoryInstance {
	std::string weapon; // A string to look up the weapon if applicable.
	std::string trinket; // A string to look up the trinket if applicable.
	int64_t count; // How many of the item the agent owns.
	double clip; // The amount of "energy" contained within the instance.
};

// A global weapon definition.
struct Weapon {
	std::map<std::string,std::string> names;
	std::map<std::string,std::string> descriptions;
	ParticleTemplate particleFire;
	std::string ammo;
	std::string soundFire;
	std::string soundReload;
	std::string soundHit;
	std::string soundRicochet;
	fgl::Texture *icon;
	fgltf::Scene *model;
	linalg::vec<double,3> translation;
	linalg::vec<double,3> zoomTranslation;
	linalg::vec<float,2> animationFire;
	linalg::vec<float,2> animationIdle;
	linalg::vec<float,2> animationReload;
	linalg::vec<float,2> fireSpotCone;
	fgl::Color fireSpotColor;
	GLfloat fireSpotRange;
	double fireSpotDuration;
	double blockDamage;
	double damage;
	double impact;
	double speed;
	double range;
	double clipMax;
	double zoom;
	double recoil;
	bool recoilCompensator;
	bool thirdPersonPitch;
	int64_t price;
};

// A global trinket definition.
struct Trinket {
	std::map<std::string,std::string> names;
	std::map<std::string,std::string> descriptions;
	fgl::Texture *icon;
	fgltf::Scene *model;
	double clipMax;
	int64_t price;
};

// An instance of an agent, such as a player, character, vehicle, or item.
struct Agent {
	std::string nodeName; // The name of the node that spawned the agent. First position to dereference as a userPointer. (NOT SERIALIZED)
	std::map<std::string,std::string> names; // The human-readable localized names of the agent.
	int type; // The category of the agent's physics.
	int team; // The category of the agent's objectives.
	std::string dialogue; // The script that triggers when a player interacts with the agent.
	std::string onDeath; // The script that triggers when the agent dies.
	std::string playlist; // The playlist that will play when a player enters the vehicle.
	std::string weapon;
	std::string quickWeapon;
	std::string soundExplode;
	std::string soundCollisionDamage;
	std::string soundJump;
	std::string soundHeal;
	std::vector<InventoryInstance> inventory; // The agent's inventory.
	std::map<std::string,double> ammo; // Ammo counts (to be displayed as properties of weapons and avoid inventory clutter).
	std::map<std::string,double> skills; // "Skills" affect combat. Can also be used to store quest progress.
	std::string dungeonFile; // The last dungeon the agent was seen in.
	std::string avatarFile;
	Avatar avatar;
	std::string modelFile;
	fgltf::Scene *model;
	std::string modelDestroyedFile;
	fgltf::Scene *modelDestroyed;
	int modelLoop; // Defaults to -1 (loop infinite). Set to 0 to disable looping.
	btCollisionShape *colShape;
	btRigidBody *rigidBody;
	btSphereShape *footSphere; // Used for foot spherecasts.
	btVehicleRaycaster *vehicleRaycaster;
	btRaycastVehicle *raycastVehicle;
	linalg::mat<double,4,4> spawnMat; // Spawn-time transformation of the node that spawned the agent. (NOT SERIALIZED)
	linalg::vec<double,3> angle; // View angle. Y component affects movement.
	linalg::vec<double,3> scale; // Size of capsule or box collider.
	linalg::vec<double,3> driverEyeTranslation; // Driver eye translation for vehicles. (Serialized as current pos for non-vehicles.)
	linalg::vec<double,3> lastVel; // Last linear velocity of the agent. (NOT SERIALIZED)
	linalg::vec<float,2> lightCone; // The agent's toggleable headlamp.
	fgl::Color lightColor;
	GLfloat lightRange;
	double offsetYaw; // The rotational offset applied to upright agents.
	double health; // Health of agent. When health <= 0, agent is destroyed or transformed in some way.
	double maxHealth; // Health cannot exceed this amount.
	double waterHealthRate; // Health added per second when submerged in water.
	double mass; // Mass of agent.
	double restitution; // Restitution of agent.
	double passiveBrakeForce; // Brake force when not accelerating.
	double brakeForce; // Brake force when actively braking.
	double thrustForce; // Directional force per second for vehicles.
	double suspensionStiffness; // Wheel suspension stiffness. Defaults to 30.
	double suspensionDamping; // Wheel suspension damping. Defaults to 2.3.
	double suspensionCompression; // Wheel suspension compression. Defaults to 4.4.
	double wheelFriction; // Wheel friction.
	double wheelFloor; // Vertical offset of bottom of wheels.
	double turn; // Front wheel angle.
	double turnAcceleration; // Turn acceleration in radians/s^2.
	double maxTurn; // Maximum turning speed in radians/s.
	double maxLean; // Horizontal leaning in radians at max turn.
	double acceleration; // Acceleration in m/s^2.
	double topSpeed; // Top speed in m/s.
	double walkSpeed; // The speed of a walking agent.
	double eyeHeight; // View and weapon raycast offset from bottom of capsule.
	double footLength; // Length of foot ray from bottom of capsule.
	double jumpVel; // Vertical velocity applied to the agent when it jumps.
	double terminalVel; // Vertical terminal velocity. TODO: Don't ignore this value!
	double collisionDamageThreshold; // The minimum velocity to cause collision damage.
	double collisionDamageFactor; // The number multiplied by velocity to produce collision damage.
	double sittingCollisionDamageFactor; // The collision damage factor when sitting in/on an agent (such as a vehicle).
	double standing; // Timer indicating whether or not the foot sphere was recently intersecting an object. (NOT SERIALIZED)
	double weaponBob; // Vertical offset of weapon based on walking. (NOT SERIALIZED)
	double weaponCooldown; // Firing is possible when weaponCooldown is 0. (NOT SERIALIZED)
	double weaponFireSpotCooldown; // Fire spot is on when this is > 0. (NOT SERIALIZED)
	float weaponTime; // The animation timestamp for the weapon. (NOT SERIALIZED)
	bool lightOn; // Whether or not the toggleable headlamp is on. (NOT SERIALIZED)
	bool visible; // Whether or not the agent will be drawn.
	Agent *parentAgent, *driverAgent, *prev, *next;
};

// A map partition.
struct Partition {
	std::string mapFile;
	fgltf::Scene mapScene;
	terra::Terrain terrain;
	linalg::vec<double,3> translation;
	double friction;
	double restitution;
};

// A node as defined in JSON and by the in-game editor.
struct WorldNode {
	std::string name;
	linalg::vec<double,4> rotation;
	linalg::vec<double,3> translation;
	btRigidBody *rigidBody; // A world node's rigidbody must be null except when shared with an agent.
};

// A cubemap.
struct Cube {
	std::string paths;
	fgl::Texture texture;
};

class Dungeon {
	public:
		/**********  READY FLAG                              **********/
		bool ready = false;
		/**********  PATHS                                   **********/
		// The path to load dungeon files from.
		std::string dungeonPath = ".";
		// The path to load agent files from.
		std::string agentPath = ".";
		// The path to load avatar files from.
		std::string avatarPath = ".";
		// The path to load particle files from.
		std::string particlePath = ".";
		// The current dungeon JSON file name. Initially empty.
		std::string dungeonFile = "";
		/**********  MISC. STRINGS                           **********/
		std::string onLoad = "";
		/**********  MESHES                                  **********/
		// A mesh that is used for animated water.
		fgl::Mesh waterMesh = {};
		// An instance buffer used to draw water tiles.
		fgl::InstanceBuffer waterBuffer = {};
		/**********  WEAPONS AND TRINKETS                    **********/
		// A table of weapons for lookup by name.
		std::map<std::string,Weapon> weapons;
		// A table of trinkets for lookup by name.
		std::map<std::string,Trinket> trinkets;
		/**********  AGENTS                                  **********/
		// A doubly linked list of agents.
		Agent *agents = nullptr;
		// A pointer to the player agent.
		Agent *playerAgent = nullptr;
		// A pointer to the agent to highlight.
		Agent *targetAgent = nullptr;
		// An empty agent.
		Agent defaultAgent = Agent();
		// The JSON file containing the player agent definition.
		std::string playerAgentFile = "player_default.json";
		// If playerJSON is non-empty, the player will be spawned with
		// the properties herein rather than those of playerAgentFile.
		// To avoid bugs, do not set this directly. Use the parameter
		// to loadWorld instead.
		std::string playerJSON = "";
		// The amount the player's camera is zoomed.
		// AI agents do nothing like this.
		double playerZoom = 1.0;
		// The amount the weapon behaves as if the camera is zoomed.
		// This controls animations and works in VR.
		double playerWeaponZoom = 1.0;
		// The Euler angles to offset the player's on-screen weapon by.
		linalg::vec<double,3> playerOffsetAngle = { 0.0, 0.0, 0.0 };
		// Determines whether or not to asjust the player's weapon lighting.
		// This is set automatically on a frame-by-frame basis.
		bool playerFireSpotOn = false;
		/**********  PARTITIONS                              **********/
		std::vector<Partition> partitions = {};
		// Indices and distances of the nearest partitions to the camera.
		std::vector<std::pair<size_t,double>> nearestPartitions = {};
		// Determines the maximum distance for partitions to be drawn.
		double maxPartitionDistance = 500.0;
		/**********  WORLD NODES                             **********/
		std::vector<WorldNode> worldNodes = {};
		// The world node the player is currently interacting with.
		size_t currentWorldNode = SIZE_MAX;
		/**********  WAYPOINTS                               **********/
		// Waypoints and their distances from the current agent.
		std::vector<linalg::vec<double,4>> waypoints = {};
		/**********  PARTICLES                               **********/
		std::map<std::string,ParticleServer> particleServers = {};
		/**********  BULLET                                  **********/
		// Variables used for the Bullet physics simulation.
		btDefaultCollisionConfiguration* collisionConfiguration = nullptr;
		btCollisionDispatcher* dispatcher = nullptr;
		btBroadphaseInterface* overlappingPairCache = nullptr;
		btSequentialImpulseConstraintSolver* solver = nullptr;
		btDiscreteDynamicsWorld* dynamicsWorld = nullptr;
		/**********  GLOBAL NUMERIC PROPERTIES               **********/
		// The positional offset of the third-person camera, when enabled.
		linalg::vec<double,3> thirdPersonCameraTranslation{0.0, 0.0, 2.0};
		// The minimum and maximum pitch for the camera in third-person mode.
		double thirdPersonCameraMinPitch = -1.570796;
		double thirdPersonCameraMaxPitch = 1.570796;
		// The yaw of the camera about the player in third-person mode.
		double thirdPersonYawOffset = 0.0;
		// A good default. Let's call it 5/10 mouse sensitivity.
		// Slider factor would be 0.0003.
		double mouseSpeed = 0.0015;
		// Vertical FoV. Default is 70 degrees.
		double verticalField = 70.0 * 0.01745;
		// Vertical orthographic visible range. Default is +/- 15 meters.
		double orthoRadius = 15.0;
		// The amount of time the simulation has been running.
		// Used to control animations.
		double age = 0.0;
		// The cloud scroll position.
		double cloudScroll = 0.5;
		// Distance between the top of the tallest wave and waterLevel.
		// Automatically set and animated at runtime.
		float waterWaveHeight = 0.0f;
		// Pen thickness relative to the screen.
		float outlineThickness = 0.0015f;
		/**********  SHADOW PROPERTIES                       **********/
		// The width and height of the directional shadow map.
		unsigned int shadowResolution = 1750;
		// The width and height of the "low" directional shadow map.
		// This is used for scenes that don't benefit from high-res shadows.
		unsigned int shadowResolutionLow = 512;
		// The radius of the shadow frustum in meters.
		double shadowScale = 40.0;
		// The near and far clip of the shadow camera.
		double shadowNearClip = 1.0;
		double shadowFarClip = 400.0;
		// The offset from the player to the shadow camera (light source).
		linalg::vec<double,3> shadowOffset = { 0.0, 200.0, 0.0 };
		/**********  NUMERIC MAP CONFIGURATION PROPERTIES    **********/
		// The maximum distance at which aiFrameCallback is called.
		// TODO: Why does the max AI distance often end up as 75m?
		double maxAIDistance = 500.0;
		// Gravity in m/s^2.
		double gravity = 9.8;
		// The maximum distance at which punctual lights are drawn.
		double lightRange = 300.0;
		// Scale the brightness of cubemap illumination on lit objects.
		// This is combined with ambient_light and can be used to
		// compensate for the low dynamic range of cubemaps.
		double lightBrightness = 1.0;
		// Fog brightness. This is generally lower than illumination
		// intensity.
		double fogBrightness = 0.6;
		// Fog density from 0.0 to 1.0. Mathematically, this kind of
		// exponential fog never converges to 100% density.
		double fogDensity = 0.01;
		// Near and far clipping planes.
		double nearClip = 0.06;
		double farClip = 400.0;
		// Global water level on the Y axis.
		double waterLevel = 0.0;
		// Scale of wave amplitude.
		double waterAmplitude = 1.0;
		// Water animation speed. 1.0 is fast.
		double waterSpeed = 1.0;
		// The Y value below which everything is in shadow and nothing may pass.
		// Disabled by default until worldFloor is set in JSON.
		double worldFloor = 0.0;
		// The speed at which the cloud texture scrolls.
		double cloudSpeed = 0.011;
		/**********  BOOLEANS                                **********/
		// Third-person view.
		bool thirdPerson = false;
		// Top-down view.
		bool topDown = false;
		// High-quality rendering. TODO: Non-PBR is currently non-functional.
		bool pbr = true;
		// High-resolution shadowing.
		bool shadowHighRes = true;
		// Outline rendering.
		bool outline = true;
		// Map punctual lights always on.
		bool lights_always_on = false;
		// Draw the sky cube.
		bool draw_cube = false;
		// Draw clouds.
		bool draw_clouds = false;
		/**********  VR VARIABLES                            **********/
		// The center matrix before it is transformed by other functions.
		// The copy is made by Dungeon::updateStereoCamera().
		linalg::mat<double,4,4> viewMatCenter = linalg::identity;
		// Stereoscopic matrices generated by Dungeon::updateStereoCamera().
		linalg::mat<double,4,4> viewMatL = linalg::identity;
		linalg::mat<double,4,4> viewMatR = linalg::identity;
		// Inter-pupilary distance in meters. 63mm is Monado's default.
		double ipd = 0.063;
		// Eye convergence angle in radians. For VR this should be 0.
		// For oldschool 3D glasses, 0.02 is about 2 meters and looks good.
		double stereoAngle = 0.0;
		/**********  MATRICES                                **********/
		// viewMat and projMat are generated by Dungeon::simulate.
		linalg::mat<double,4,4> viewMat = linalg::identity;
		linalg::mat<double,4,4> projMat = linalg::identity;
		// Initial light matrix used for shadow texel snapping.
		linalg::mat<double,4,4> lightOriginMat = linalg::identity;
		/**********  EPHEMERAL PUNCTUAL LIGHT PROPERTIES     **********/
		std::vector<GLfloat> pointLights;
		std::vector<GLfloat> spotLights;
		/**********  FGL PROPERTIES                          **********/
		// Pointer to a Framebuffer struct. MUST be set from your code
		// at runtime.
		fgl::Framebuffer *frameBuf;
		// Amount to scale the framebuffer viewport for Dynamic
		// Resolution Scaling.
		float frameBufScale = 1.0f;
		// The framebuffers for variance shadow mapping. Reused to avoid
		// needless allocations.
		fgl::Framebuffer shadowBufMultisample = fgl::newFramebuffer;
		fgl::Framebuffer shadowBuf = fgl::newFramebuffer;
		fgl::Framebuffer shadowBufTemp = fgl::newFramebuffer;
		fgl::Framebuffer shadowBufLowMultisample = fgl::newFramebuffer;
		fgl::Framebuffer shadowBufLow = fgl::newFramebuffer;
		fgl::Framebuffer shadowBufLowTemp = fgl::newFramebuffer;
		// The world ambient light color.
		fgl::Color ambient_light = fgl::newColor;
		// The outline colors.
		fgl::Color outline_regular = { 0.0f, 0.0f, 0.0f, 1.0f };
		fgl::Color outline_highlight = { 1.0f, 1.0f, 1.0f, 1.0f };
		// Amount to scale [clamped] emissiveFactor by. Workaround for a
		// glTF format bug, like the elusive emissiveStrength extension:
		// https://github.com/KhronosGroup/glTF/pull/1994
		GLfloat emissive_scale = 1.0f;
		// Underwater color with alpha. Defaults to match standard
		// shadow color (dependent on choice of shaders).
		fgl::Color underwater_color = { 0.0f, 0.0f, 0.0f, 0.43f };
		// Water surface texture file.
		std::string waterTextureName;
		// Water surface texture.
		fgl::Texture waterTexture = fgl::newTexture;
		// Clouds texture file.
		std::string cloudsTextureName;
		// Clouds texture.
		fgl::Texture cloudsTexture = fgl::newTexture;
		// Default impact particle file.
		std::string defaultImpactName;
		// Default impact particle template.
		ParticleTemplate defaultImpact = ParticleTemplate();
		// Default blood particle file.
		std::string defaultBloodName;
		// Default impact particle template.
		ParticleTemplate defaultBlood = ParticleTemplate();
		// The radiance cubemap.
		fgl::Texture radiance = fgl::newTexture;
		// The diffuse irradiance cubemap generated from the radiance
		// cubemap.
		fgl::Texture irradiance = fgl::newTexture;
		// The framebuffer for the diffuse irradiance cubemap. Reused to
		// avoid needless allocations.
		fgl::Framebuffer irradianceBuf = fgl::newFramebuffer;
		/**********  FOPEN OVERRIDE                          **********/
		FILE *(*ModFopen)(const char*,const char*) = fopen;
		/**********  CALLBACK FUNCTIONS                      **********/
		// Called when playerAgent->rigidBody intersects a portal.
		void (*portalCallback)(std::string);
		// Called when an agent takes falling damage.
		void (*collisionDamageCallback)(Agent*,double);
		// Called every simulation frame when an agent is active.
		bool (*aiFrameCallback)(Agent*,double);
		/**********  MEMBER FUNCTIONS                        **********/
		unsigned char *stbiLoadModdable( std::string filePath, int *w, int *h, int *channels );
		fgl::Texture loadSprite( const std::string &filePath );
		fgl::Pipeline loadShaders( std::string vertFile, std::string fragFile, std::vector<std::string> samplers );
		void setCube( std::vector<std::string> imageFiles );
		ParticleTemplate loadParticles( std::string filePath );
		void unloadWorld();
		void initGraphics( std::string shaderPath );
		void initPhysics();
		void loadWorld(
			std::string dungeonPath,
			std::string agentPath,
			std::string avatarPath,
			std::string particlePath,
			std::string filePath,
			const std::string &playerJSON = "",
			bool append = false );
		std::string serializePartition( Partition &p );
		std::string serializeWorld();
		void appendWeapons( std::string weaponPath, std::string filePath );
		void appendTrinkets( std::string trinketPath, std::string filePath );
		fgltf::Scene *getModel( const std::string &filePath );
		void setAgentModel( Agent *a, const std::string &modelFile );
		Agent *addAgent( btVector3 position, btScalar mass, btScalar restitution, linalg::vec<double,3> scale, linalg::vec<double,3> boxTranslation = {0,0,0}, bool box = false );
		Agent *parseAgent( const std::string &text, std::string filePath, linalg::mat<double,4,4> spawnMat );
		Agent *loadAgent( std::string filePath, linalg::mat<double,4,4> spawnMat );
		void loadSavedPlayerAgent( std::string filePath );
		std::string serializeAgent( Agent *a );
		double getRotationDelta( double a, double b, double speed );
		void rotateAgentConstrained( Agent *a, linalg::vec<double,3> rotation );
		void rotateAgentUnconstrained( Agent *a, linalg::vec<double,3> rotation );
		bool moveAgent( Agent *a, linalg::vec<double,3> translation, double standingTime, double d );
		void positionAgent( Agent *a, linalg::vec<double,3> position );
		void accelerateVehicle( Agent *a, double force );
		void brakeVehicle( Agent *a, double brakeForce );
		void rightVehicle( Agent *a );
		bool isVehicleUpright( Agent *a );
		double getVehicleLean( Agent *a );
		double getVehiclePitch( Agent *a );
		double getVehicleYaw( Agent *a );
		void setAgentCollisions( Agent *a, bool enable );
		void setAgentCrouch( Agent *a, bool enable, double d );
		void unsitAgent( Agent *a );
		linalg::vec<double,3> getNodeLookAt( linalg::mat<double,4,4> m );
		btTransform getAgentPhysicsTransform( Agent *a );
		linalg::mat<double,4,4> getAgentPoseMat( Agent *a );
		std::pair<linalg::vec<double,3>,linalg::vec<double,3>> getAgentGazeRay( Agent *a, double range, bool pitch = true );
		std::pair<void*,double> getAgentGazeHit( Agent *a, double range );
		std::string getAgentLocalizedName( Agent *a, const std::string &languageCode );
		std::string getWeaponLocalizedName( Weapon *w, const std::string &languageCode );
		void removeAgent( Agent *a, bool conservative = false );
		Agent *rayImpact( Agent *a, std::pair<linalg::vec<double,3>,linalg::vec<double,3>> ray, double impact, double damage, std::string defenseSkill = "def" );
		std::pair<bool,Agent*> agentFireWeapon( Agent *a, std::string weapon = "", std::string attackSkill = "atk", std::string defenseSkill = "def" );
		void agentAddWeapon( Agent *a, const std::string &weapon );
		void agentReloadWeapon( Agent *a, const std::string &weapon );
		bool agentCanReloadCurrentWeapon( Agent *a );
		void moveMouse( double x, double y );
		void prepareLightBuffers( double d );
		void simulateWater( float wscale, double d );
		void simulate( double maxDraw, double d, std::string defenseSkill = "def" );
		void updateMonoCamera();
		void updateStereoCamera();
		fgl::Pipeline useDungeonPipeline( bool skinPass, bool shadowPass );
		void drawSolid( double maxDraw, bool shadowPass = false, int agent_types = 0xFFFFFFFF );
		void drawWater( bool shadowPass = false );
		void drawTransparent();
		void draw( double maxDraw, bool allowTransparent = true, fgl::Mesh *nodeMesh = nullptr, int agent_types = 0xFFFFFFFF );
	private:
		void addAgentGazeSpot( Agent *a, fgl::Color color, GLfloat range = 0.0f, GLfloat innerConeAngle = 0.0f, GLfloat outerConeAngle = 0.785f );
		void sceneNodeHandler( fgltf::SceneNode &node, linalg::mat<double,4,4> &m );
		void worldNodeHandler( WorldNode &node, linalg::mat<double,4,4> &m );
		void updateCallback( double d );
		void steerVehicle( Agent *a );
};

// Implementation begins here.

#ifndef FDUNGEON_SEPARATE_COMPILATION

fgl::Pipeline skyPipeline = fgl::newPipeline;

std::vector<std::string> cloudsSamplers = { "u_texture" };
fgl::Pipeline cloudsPipeline = fgl::newPipeline;

fgl::Pipeline outlinePipeline = fgl::newPipeline;

fgl::Pipeline particleShadowPipeline = fgl::newPipeline;

fgl::Pipeline shadowPipeline = fgl::newPipeline;

std::vector<std::string> shadowBlurSamplers = { "u_shadow" };
fgl::Pipeline shadowBlurPipeline = fgl::newPipeline;

std::vector<std::string> dungeonSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline dungeonPipeline = fgl::newPipeline;

std::vector<std::string> pbrDungeonSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline pbrDungeonPipeline = fgl::newPipeline;

std::vector<std::string> skinSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline skinPipeline = fgl::newPipeline;

std::vector<std::string> waterSamplers = { "u_albedo", "u_shadow", "u_radiance" };
fgl::Pipeline waterPipeline = fgl::newPipeline;

std::vector<std::string> waterInstanceSamplers = { "u_albedo", "u_shadow" };
fgl::Pipeline waterInstancePipeline = fgl::newPipeline;

std::map<std::string,fgl::Texture> sprites = {};

std::map<std::string,fgltf::Scene> models = {};

std::map<std::string,terra::Heightmap> heightmaps = {};

std::vector<Cube> cubes;

unsigned char *Dungeon::stbiLoadModdable( std::string filePath, int *w, int *h, int *channels ){
	unsigned char *imgData = nullptr;
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( file ){
		imgData = stbi_load_from_file( file, w, h, channels, 0 );
		fclose( file );
	}
	return imgData;
}

fgl::Texture Dungeon::loadSprite( const std::string &filePath ){
	fgl::Texture result = {};
	int width, height, channels;
	void *img =
		stbiLoadModdable( filePath, &width, &height, &channels );
	if( img )
		result = fgl::loadTexture( img, width, height, channels );
	else
		std::cerr << "Failed to load " << filePath << std::endl;
	if( result.success ){
		// Disable texture wrapping.
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
	}
	stbi_image_free( img );
	return result;
}

fgl::Pipeline Dungeon::loadShaders( std::string vertFile, std::string fragFile, std::vector<std::string> samplers ){
	std::string
		shader_files[2] = { vertFile, fragFile },
		shader_text[2] = {};
	for( size_t i = 0; i < 2; i++ ){
		FILE* file = ModFopen( shader_files[i].c_str(), "rb" );
		if( !file ){
			fprintf( stderr, "Failed to open %s\n", shader_files[i].c_str() );
			return fgl::newPipeline;
		}
		// TODO: Profile this against ftell/fseek.
		char buf[4096];
		while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
			shader_text[i] += std::string( buf, len );
		}
		fclose( file );
	}
	return fgl::loadPipeline( shader_text[0].c_str(), shader_text[1].c_str(), samplers );
}

// Load and set a Cube, never loading the same Cube twice in the same
// process.
void Dungeon::setCube( std::vector<std::string> imageFiles ){
	if( imageFiles.size() != 6 ){
		return;
	}
	std::string paths = "";
	for( size_t i = 0; i < imageFiles.size(); i++ ){
		// Store image names as a concatenated string.
		paths += imageFiles[i] + ";";
		// Convert image names to full paths.
		imageFiles[i] = dungeonPath + "/" + imageFiles[i];
	}
	for( Cube &cube : cubes ){
		if( cube.paths == paths ){
			radiance = cube.texture;
			irradianceBuf =
				fgl::getIrradianceFramebuffer( radiance, irradianceBuf );
			irradiance = fgl::getFramebufferTexture( irradianceBuf );
			return;
		}
	}
	// No Cube found, so load the named files.
	int width, height, channels;
	std::vector<GLvoid*> faces = {};
	bool err = false;
	for( size_t i = 0; i < 6; i++ ){
		faces.push_back( stbiLoadModdable( imageFiles[i], &width, &height, &channels ) );
		if( !faces.back() ){
			std::cerr << "Failed to load " << imageFiles[i] << std::endl;
			err = true;
			break;
		}
	}
	if( !err ){
		cubes.push_back( {
			paths,
			fgl::loadCubemap( faces, width, height, channels )
		} );
		radiance = cubes.back().texture;
		irradianceBuf =
			fgl::getIrradianceFramebuffer( radiance, irradianceBuf );
		irradiance = fgl::getFramebufferTexture( irradianceBuf );
	}
	for( GLvoid* f : faces ){
		stbi_image_free( f );
	}
}

ParticleTemplate Dungeon::loadParticles( std::string filePath ){
	std::string text = "";
	// Load the particle definition.
	filePath = particlePath + "/" + filePath;
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( file ){
		char buf[4096];
		while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
			text += std::string( buf, len );
		}
		fclose( file );
	}else{
		std::cerr << "Failed to open " << filePath << std::endl;
	}
	return ParticleTemplate( text );
}

// 
void Dungeon::unloadWorld(){
	// Safety hatch to stop an already-unloaded world from being
	// unloaded twice.
	if( !ready )
		return;
	ready = false;
	// Clear the onLoad string.
	onLoad = "";
	// Unload the partitions and associated data.
	for( auto &p : partitions ){
		fgltf::UnloadScene( p.mapScene );
		terra::FreeTerrain( p.terrain, dynamicsWorld );
	}
	partitions.clear();
	// Unload world nodes.
	worldNodes.clear();
	// Unload waypoints.
	waypoints.clear();
	// Unload the water texture.
	waterTextureName = "";
	if( waterTexture.success )
		fgl::freeTexture( waterTexture );
	// Unload the clouds texture.
	cloudsTextureName = "";
	if( cloudsTexture.success )
		fgl::freeTexture( cloudsTexture );
	// Remove the agents.
	while( agents ){
		Agent *a = agents->next;
		if( agents )
			removeAgent( agents );
		agents = a;
	}
	playerAgent = nullptr;
	// Remove the world floor.
	worldFloor = 0.0;
	// Clean up any remaining physics data.
	if( dynamicsWorld ){
		// Remove the rigidbodies from the dynamics world and delete them.
		for( int i = dynamicsWorld->getNumCollisionObjects() - 1; i >= 0; i-- ){
			btCollisionObject* obj = dynamicsWorld->getCollisionObjectArray()[i];
			btRigidBody* body = btRigidBody::upcast( obj );
			if( body && body->getMotionState() ){
				delete body->getMotionState();
			}
			dynamicsWorld->removeCollisionObject( obj );
			delete obj;
		}
		delete dynamicsWorld;
	}
	if( solver )
		delete solver;
	if( overlappingPairCache )
		delete overlappingPairCache;
	if( dispatcher )
		delete dispatcher;
	if( collisionConfiguration )
		delete collisionConfiguration;
}

// You MUST call this before calling loadWorld.
void Dungeon::initGraphics( std::string shaderPath ){
	// Shader file prefix.
	std::string pre = shaderPath + "/";
	// Load the sky pipeline.
	if( !skyPipeline.success )
		skyPipeline = loadShaders( pre + "sky.vert", pre + "sky.frag", {} );
	// Load the clouds pipeline.
	if( !cloudsPipeline.success )
		cloudsPipeline = loadShaders( pre + "clouds.vert", pre + "clouds.frag", cloudsSamplers );
	// Load the outline shader pipeline.
	if( !outlinePipeline.success )
		outlinePipeline = loadShaders( pre + "outline.vert", pre + "outline.frag", {} );
	// Load the particle shadow shader pipeline.
	if( !particleShadowPipeline.success )
		particleShadowPipeline = loadShaders( pre + "particle_shadow.vert", pre + "particle_shadow.frag", {} );
	// Load the shadow shader pipelines.
	if( !shadowPipeline.success )
		shadowPipeline = loadShaders( pre + "shadow.vert", pre + "shadow.frag", {} );
	if( !shadowBlurPipeline.success )
		shadowBlurPipeline = loadShaders( pre + "shadow_blur.vert", pre + "shadow_blur.frag", shadowBlurSamplers );
	// Load the dungeon shader pipelines.
	//if( !dungeonPipeline.success )
	//	dungeonPipeline = loadShaders( pre + "dungeon_sky_low.vert", pre + "dungeon_sky_low.frag", dungeonSamplers );
	if( !pbrDungeonPipeline.success )
		pbrDungeonPipeline = loadShaders( pre + "dungeon_sky_high.vert", pre + "dungeon_sky_high.frag", pbrDungeonSamplers );
	if( !skinPipeline.success )
		skinPipeline = loadShaders( pre + "dungeon_skin.vert", pre + "dungeon_skin.frag", skinSamplers );
	// Load the water shader pipelines.
	if( !waterPipeline.success )
		waterPipeline = loadShaders( pre + "water.vert", pre + "water.frag", waterSamplers );
	if( !waterInstancePipeline.success )
		waterInstancePipeline = loadShaders( pre + "water_instance.vert", pre + "water_instance.frag", waterInstanceSamplers );
	// Create the framebuffers for the variance shadow map.
	if( !shadowBufMultisample.success )
		shadowBufMultisample = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F, 2 );
	if( !shadowBuf.success )
		shadowBuf = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F );
	if( !shadowBufTemp.success )
		shadowBufTemp = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F );
	// Create the framebuffers for the "low" shadow map.
	if( !shadowBufLowMultisample.success )
		shadowBufLowMultisample = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F, 4 );
	if( !shadowBufLow.success )
		shadowBufLow = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F );
	if( !shadowBufLowTemp.success )
		shadowBufLowTemp = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F );
}

// Called internally.
// Initialize the Bullet physics engine. Will overwrite pointers, so
// make sure to call unloadWorld() first if initPhysics() was called
// before.
void Dungeon::initPhysics(){
	///collision configuration contains default setup for memory, collision setup. Advanced users can create their own configuration.
	collisionConfiguration = new btDefaultCollisionConfiguration();

	///use the default collision dispatcher. For parallel processing you can use a diffent dispatcher (see Extras/BulletMultiThreaded)
	dispatcher = new btCollisionDispatcher( collisionConfiguration );
	btGImpactCollisionAlgorithm::registerAlgorithm( dispatcher );

	///btDbvtBroadphase is a good general purpose broadphase. You can also try out btAxis3Sweep.
	overlappingPairCache = new btDbvtBroadphase();

	///the default constraint solver. For parallel processing you can use a different solver (see Extras/BulletMultiThreaded)
	solver = new btSequentialImpulseConstraintSolver;

	dynamicsWorld = new btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration );
}

// Load or append a world from a JSON file.
void Dungeon::loadWorld(
		std::string dungeonPath,
		std::string agentPath,
		std::string avatarPath,
		std::string particlePath,
		std::string filePath,
		const std::string &playerJSON,
		bool append ){

	if( !append ){
		// Clear the previously loaded content.
		unloadWorld();
		// Default lightRange to 300.
		lightRange = 300.0;
		// Default lights_always_on to false.
		lights_always_on = false;
		// Default shadowHighRes to true.
		shadowHighRes = true;
		// Default maxPartitionDistance to 500.
		maxPartitionDistance = 500.0;
		// Reset the third-person yaw offset.
		thirdPersonYawOffset = 0.0;
	}
	if( !ready ){
		// Start physics.
		initPhysics();
	}

	// The map file can be anywhere. All dungeon files will be loaded
	// from dungeonPath. All agent files will be loaded from agentPath.
	// All particle files will be loaded from particlePath
	this->dungeonPath = dungeonPath;
	this->agentPath = agentPath;
	this->avatarPath = avatarPath;
	this->particlePath = particlePath;
	// playerJSON is set every call.
	// Use the parameter rather than direct access to avoid bugs.
	this->playerJSON = playerJSON;

	// Load the map definition.
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( !file ){
		std::cerr << "Failed to open map definition " << filePath << std::endl;
		fgl::end();
		return;
	}

	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );

	// Parse the map definition. jsonAllocateDocument with minimal features.
	auto allocatedDocument = jsonAllocateDocument( text.c_str(), text.size(), JSON_READER_REASONABLE );

	if( allocatedDocument.document.error.type != JSON_OK ){
		std::cerr
			<< "JSON error: "
			<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
			<< allocatedDocument.document.error.line << ":"
			<< allocatedDocument.document.error.column << " of "
			<< filePath << "\n" << std::endl;
		jsonFreeDocument( &allocatedDocument );
		return;
	}

	auto &info = allocatedDocument.document.root;

	// Convert JsonStringView to std::string.
	auto viewToString = []( JsonStringView str ){
		return std::string( str.data, str.size );
	};

	// Populate the class with map parameters.
	if( info["partitions"] ){
		// Load partitions.
		for( auto part : info["partitions"].getArray() ){
			partitions.push_back( {} );
			auto &p = partitions.back();
			bool has_map = false;
			if( part["map"] ){
				has_map = true;
				p.mapFile = viewToString( part["map"].getString() );
				p.mapScene = fgltf::LoadScene( ModFopen, dungeonPath + "/" + p.mapFile );
				fgltf::AddSceneToDynamicsWorld(
					p.mapScene,
					dynamicsWorld,
					fgltf::PHYSICS_TRIHARD,
					&defaultAgent
				);
			}
			if( part["translation"] ){
				auto t = part["translation"].getArray();
				if( t.size() >= 3 ){
					p.translation = {
						t[0].getDouble(),
						t[1].getDouble(),
						t[2].getDouble()
					};
				}
			}
			p.friction = 1.0;
			if( part["friction"] ){
				p.friction = part["friction"].getDouble();
			}
			p.restitution = 0.0;
			if( part["restitution"] ){
				p.restitution = part["restitution"].getDouble();
			}
			terra::Terrain t{};
			t.mesh = fgl::newMesh;
			t.needSync = false;
			// Check if the partition has terrain.
			if( part["terrain"] ){
				JsonValue terrainDef = part["terrain"];
				// Build the terrain.
				if( terrainDef["texture"] ){
					t.textureName =
						viewToString( terrainDef["texture"].getString() );
				}
				if( terrainDef["heightColors"] ){
					t.heightColors =
						viewToString( terrainDef["heightColors"].getString() );
				}
				t.tileSizeX = 1.0;
				t.tileSizeZ = 1.0;
				if( terrainDef["tileSizeX"] ){
					t.tileSizeX = terrainDef["tileSizeX"].getDouble();
				}
				if( terrainDef["tileSizeZ"] ){
					t.tileSizeZ = terrainDef["tileSizeZ"].getDouble();
				}
				t.textureTileX = 1.0f;
				t.textureTileZ = 1.0f;
				if( terrainDef["textureTileX"] ){
					t.textureTileX = terrainDef["textureTileX"].getFloat();
				}
				if( terrainDef["textureTileZ"] ){
					t.textureTileZ = terrainDef["textureTileZ"].getFloat();
				}
				t.resX = 1;
				t.resZ = 1;
				if( terrainDef["resX"] ){
					t.resX = terrainDef["resX"].getUInt();
				}
				if( terrainDef["resZ"] ){
					t.resZ = terrainDef["resZ"].getUInt();
				}
				if( terrainDef["heights"] ){
					std::string text;
					for( auto &line : terrainDef["heights"].getArray() ){
						text += viewToString( line.getString() );
					}
					t.heights = terra::StringToHeights( text );
					size_t terrainSize = (t.resX + 1) * (t.resZ + 1);
					if( t.heights.size() < terrainSize )
						t.heights.resize( terrainSize );
				}
				t.heightmapName = "";
				t.heightmap = nullptr;
				if( terrainDef["heightmap"] ){
					JsonValue heightmapDef = terrainDef["heightmap"];
					if( heightmapDef["file"] ){
						// Get the heightmap parameters.
						float pixelScale = 1.0f;
						if( heightmapDef["pixelScale"] ){
							pixelScale = heightmapDef["pixelScale"].getFloat();
						}
						float whiteHeight = 1.0f;
						if( heightmapDef["whiteHeight"] ){
							whiteHeight = heightmapDef["whiteHeight"].getFloat();
						}
						float noisePeriod = 1.0f;
						if( heightmapDef["noisePeriod"] ){
							noisePeriod = heightmapDef["noisePeriod"].getFloat();
						}
						float noiseAmplitude = 1.0f;
						if( heightmapDef["noiseAmplitude"] ){
							noiseAmplitude = heightmapDef["noiseAmplitude"].getFloat();
						}
						// Load the heightmap.
						std::string heightmapFile = viewToString(heightmapDef["file"].getString());
						if(heightmaps.find(heightmapFile) == heightmaps.end()){
							const char *file_path = std::string(dungeonPath + "/" + heightmapFile).c_str();
							FILE *file = ModFopen(file_path, "rb");
							if(file){
								heightmaps[heightmapFile] =
									terra::LoadHeightmapFromFile(file, pixelScale, whiteHeight, noisePeriod, noiseAmplitude);
								fclose(file);
								t.heightmapName = heightmapFile;
								t.heightmap = &heightmaps[heightmapFile];
							}else{
								fprintf(stderr, "Failed to open %s\n", file_path);
								heightmaps[heightmapFile] = {};
							}
						}
					}
				}
			}
			p.terrain = t;
			if( p.terrain.resX > 0 && p.terrain.resZ > 0 ){
				// Once Bullet gets involved, it's only safe to call
				// BakeTerrainMesh in-place. Do not copy terrains with
				// physics.
				terra::BakeTerrainMesh(p.terrain, dynamicsWorld, p.translation, p.friction, p.restitution);
			}
			if( has_map ){
				auto modelMat = linalg::translation_matrix( p.translation );
				std::function<void(int,linalg::mat<double,4,4>,size_t)> runTree =
						[&]( int n, linalg::mat<double,4,4> m, size_t depth ){
					depth++;
					if( depth > fgltf::max_tree_depth ){
						fprintf( stderr, "Maximum tree depth exceeded. Possible malformed scene.\n" );
						return;
					}
					fgltf::SceneNode &node = p.mapScene.nodes[n];
					m = linalg::mul(
						m,
						linalg::translation_matrix( node.translation ),
						linalg::rotation_matrix( node.rotation ),
						linalg::scaling_matrix( node.scale )
					);
					// Call the handler function.
					sceneNodeHandler( node, m );
					for( int c : node.children ){
						runTree( c, m, depth );
					}
				};
				for( int r : p.mapScene.roots ){
					runTree( r, modelMat, 0 );
				}
				// Update limb positions and parameters in the physics world.
				fgltf::UpdateSceneDynamics(
					p.mapScene,
					modelMat,
					p.friction,
					p.restitution
				);
			}
		}
		ready = true;
	}
	if( info["nodes"] ){
		// Load world nodes.
		for( auto &node : info["nodes"].getArray() ){
			worldNodes.push_back( {} );
			auto &n = worldNodes.back();
			if( node["name"] ){
				n.name = viewToString( node["name"].getString() );
			}
			n.rotation = {0.0, 0.0, 0.0, 1.0};
			if( node["rotation"] ){
				auto r = node["rotation"].getArray();
				if( r.size() >= 4 ){
					n.rotation = {
						r[0].getDouble(),
						r[1].getDouble(),
						r[2].getDouble(),
						r[3].getDouble()
					};
				}else if( r.size() == 3 ){
					// Accept Euler radian angles as an alternative
					// representation of rotations.
					// Fragile; for quick hacks and testing only.
					n.rotation = fgl::eulerToQuat(
						r[0].getDouble(),
						r[1].getDouble(),
						r[2].getDouble()
					);
				}
			}
			if( node["translation"] ){
				auto t = node["translation"].getArray();
				if( t.size() >= 3 ){
					n.translation = {
						t[0].getDouble(),
						t[1].getDouble(),
						t[2].getDouble()
					};
				}
			}
			// Spawn any object specified by the node.
			auto m = linalg::pose_matrix( n.rotation, n.translation );
			worldNodeHandler( n, m );
		}
	}
	if( info["maxPartitionDistance"] ){
		maxPartitionDistance = info["maxPartitionDistance"].getDouble();
	}
	if( info["maxAIDistance"] ){
		maxAIDistance = info["maxAIDistance"].getDouble();
	}
	if( info["gravity"] ){
		gravity = info["gravity"].getDouble();
	}
	if( info["lightRange"] ){
		lightRange = info["lightRange"].getDouble();
	}
	if( info["lightBrightness"] ){
		lightBrightness = info["lightBrightness"].getDouble();
	}
	if( info["lightsAlwaysOn"] ){
		lights_always_on = info["lightsAlwaysOn"].getBool();
	}
	if( info["shadowHighRes"] ){
		shadowHighRes = info["shadowHighRes"].getBool();
	}
	if( info["fogBrightness"] ){
		fogBrightness = info["fogBrightness"].getDouble();
	}
	if( info["fogDensity"] ){
		fogDensity = info["fogDensity"].getDouble();
	}
	if( info["emissiveScale"] ){
		emissive_scale = info["emissiveScale"].getFloat();
	}
	if( info["nearClip"] ){
		nearClip = info["nearClip"].getDouble();
	}
	if( info["farClip"] ){
		farClip = info["farClip"].getDouble();
	}
	if( info["waterLevel"] ){
		waterLevel = info["waterLevel"].getDouble();
	}
	if( info["waterAmplitude"] ){
		waterAmplitude = info["waterAmplitude"].getDouble();
	}
	if( info["waterSpeed"] ){
		waterSpeed = info["waterSpeed"].getDouble();
	}
	if( info["underwaterColor"] ){
		auto c = info["underwaterColor"].getArray();
		if( c.size() >= 4 ){
			underwater_color = {
				c[0].getFloat(),
				c[1].getFloat(),
				c[2].getFloat(),
				c[3].getFloat()
			};
		}
	}
	if( info["waterTexture"] ){
		if( waterTexture.success ){
			// Unload existing water texture.
			fgl::freeTexture( waterTexture );
		}
		waterTextureName =
			viewToString( info["waterTexture"].getString() );
		std::string fileName = dungeonPath + "/" + waterTextureName;
		int width, height, channels;
		void *img =
			stbiLoadModdable( fileName, &width, &height, &channels );
		if( img )
			waterTexture = fgl::loadTexture( img, width, height, channels );
		else
			std::cerr << "Failed to load " << fileName << std::endl;
		stbi_image_free( img );
	}
	if( info["worldFloor"] ){
		worldFloor = info["worldFloor"].getDouble();
		// Add the world floor plane.
		Agent *a = addAgent(
			btVector3( 0.0, worldFloor, 0.0 ),
			btScalar( 0.0 ),
			btScalar( 0.0 ),
			{ 99999.0, 0.0, 99999.0 },
			{ 0.0, 0.0, 0.0 },
			true
		);
		// Work around auto-cleanup (???) TODO: FIXME
		a->health = 1.0;
		a->maxHealth = 1.0;
		// Prevent the floor plane from registering as an agent to raycasts.
		a->nodeName = "";
		a->visible = false;
		// Assign the floor plane's agent type.
		a->type = TYPE_STATIC;
		// Assign the spawn matrix.
		a->spawnMat =
			linalg::translation_matrix( linalg::vec<double,3>( 0.0, worldFloor, 0.0 ) );
		// Set up physics properties.
		btVector3 localInertia( 0, 0, 0 );
		a->colShape->calculateLocalInertia( 0.0, localInertia );
		btTransform startTransform;
		startTransform.setIdentity();
		startTransform.setOrigin( btVector3( 0.0, worldFloor, 0.0 ) );
		//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
		btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );
		btRigidBody::btRigidBodyConstructionInfo rbInfo( 0.0, myMotionState, a->colShape, localInertia );
		// Use high friction for better traction.
		rbInfo.m_friction = 1.0;
		// Prevent agents from bouncing.
		rbInfo.m_restitution = 0.0;
		// Create the rigid body.
		a->rigidBody = new btRigidBody( rbInfo );
		// Set the agent's userPointer to the struct.
		a->rigidBody->setUserPointer( a );
		dynamicsWorld->addRigidBody( a->rigidBody );
	}
	if( info["cube"] ){
		std::vector<std::string> imageFiles = {};
		for( auto &fileName : info["cube"].getArray() ){
			imageFiles.push_back( viewToString( fileName.getString() ) );
		}
		setCube( imageFiles );
	}
	if( info["drawCube"] ){
		draw_cube = info["drawCube"].getBool();
	}

	// Clouds.
	if( info["cloudsTexture"] ){
		if( cloudsTexture.success ){
			// Unload existing clouds texture.
			fgl::freeTexture( cloudsTexture );
		}
		cloudsTextureName =
			viewToString( info["cloudsTexture"].getString() );
		std::string fileName = dungeonPath + "/" + cloudsTextureName;
		int width, height, channels;
		void *img =
			stbiLoadModdable( fileName, &width, &height, &channels );
		if( img )
			cloudsTexture = fgl::loadTexture( img, width, height, channels );
		else
			std::cerr << "Failed to load " << fileName << std::endl;
		stbi_image_free( img );
	}
	if( info["cloudSpeed"] ){
		cloudSpeed = info["cloudSpeed"].getDouble();
	}
	if( info["drawClouds"] ){
		draw_clouds = info["drawClouds"].getBool();
	}

	// Particles.
	if( info["defaultImpact"] ){
		defaultImpactName = viewToString(info["defaultImpact"].getString());
		defaultImpact = loadParticles(defaultImpactName);
		particleServers[defaultImpact.texture] = ParticleServer();
	}
	if( info["defaultBlood"] ){
		defaultBloodName = viewToString(info["defaultBlood"].getString());
		defaultBlood = loadParticles(defaultBloodName);
		particleServers[defaultBlood.texture] = ParticleServer();
	}

	// onLoad (for use in user code)
	if( onLoad.length() < 1 && info["onLoad"] ){
		onLoad = viewToString(info["onLoad"].getString());
	}

	// Get appends (for pre-generated procedural content, downloads, etc.)
	std::vector<std::string> appends;
	if( info["appends"] ){
		for( auto &s : info["appends"].getArray() ){
			appends.push_back( viewToString( s.getString() ) );
		}
	}

	// Unload the JSON document.
	jsonFreeDocument( &allocatedDocument );

	if( !append ){
		dungeonFile = filePath;
		age = 0.0;
	}

	// Append the appends.
	for( std::string &f : appends ){
		loadWorld( dungeonPath, agentPath, avatarPath, particlePath, dungeonPath + "/" + f, playerJSON, true );
	}

	// Create a player if one does not already exist.
	if( !playerAgent )
		playerAgent = loadAgent( playerAgentFile, linalg::identity );
}

std::string Dungeon::serializePartition( Partition &p ){
	std::string str = "{\n";
	if( p.mapFile.length() > 0 ){
		str += "\t\t\t\"map\": \"" + p.mapFile + "\",\n";
	}
	if( p.terrain.mesh.success ){
		str += "\t\t\t\"terrain\": "
			+ terra::SerializeTerrain( p.terrain ) + ",\n";
	}
	str += "\t\t\t\"translation\": ["
		+ std::to_string(p.translation.x) + ", "
		+ std::to_string(p.translation.y) + ", "
		+ std::to_string(p.translation.z) + "],\n";
	str += "\t\t\t\"friction\": " + std::to_string(p.friction) + ",\n";
	str += "\t\t\t\"restitution\": " + std::to_string(p.restitution) + "\n";
	str += "\t\t}";
	return str;
}

std::string Dungeon::serializeWorld(){
	std::string str = "{\n";
	str += "\t\"partitions\": [\n";
	for( size_t i = 0; i < partitions.size(); i++ ){
		str += "\t\t" + serializePartition( partitions[i] );
		if( i < partitions.size() - 1 ) str += ",";
		str += "\n";
	}
	str += "\t],\n";
	str += "\t\"nodes\": [\n";
	for( size_t i = 0; i < worldNodes.size(); i++ ){
		auto &n = worldNodes[i];
		str += "\t\t{\n\t\t\t\"name\": \"" + n.name + "\",\n";
		str += "\t\t\t\"rotation\": ["
			+ std::to_string(n.rotation.x) + ", "
			+ std::to_string(n.rotation.y) + ", "
			+ std::to_string(n.rotation.z) + ", "
			+ std::to_string(n.rotation.w) + "],\n";
		str += "\t\t\t\"translation\": ["
			+ std::to_string(n.translation.x) + ", "
			+ std::to_string(n.translation.y) + ", "
			+ std::to_string(n.translation.z) + "]\n";
		str += "\t\t}";
		if( i < worldNodes.size() - 1 ) str += ",";
		str += "\n";
	}
	str += "\t],\n";
	str += "\t\"gravity\": " + std::to_string(gravity) + ",\n";
	str += "\t\"emissiveScale\": " + std::to_string(emissive_scale) + ",\n";
	str += "\t\"lightBrightness\": " + std::to_string(lightBrightness) + ",\n";
	str += "\t\"lightsAlwaysOn\": ";
	str += (lights_always_on ? "true,\n" : "false,\n");
	str += "\t\"shadowHighRes\": ";
	str += (shadowHighRes ? "true,\n" : "false,\n");
	str += "\t\"fogBrightness\": " + std::to_string(fogBrightness) + ",\n";
	str += "\t\"fogDensity\": " + std::to_string(fogDensity) + ",\n";
	str += "\t\"nearClip\": " + std::to_string(nearClip) + ",\n";
	str += "\t\"farClip\": " + std::to_string(farClip) + ",\n";
	str += "\t\"underwaterColor\": ["
		+ std::to_string(underwater_color.r) + ", "
		+ std::to_string(underwater_color.g) + ", "
		+ std::to_string(underwater_color.b) + ", "
		+ std::to_string(underwater_color.a) + "],\n";
	if( waterTextureName.length() > 0 )
		str += "\t\"waterTexture\": \"" + waterTextureName + "\",\n";
	str += "\t\"waterLevel\": " + std::to_string(waterLevel) + ",\n";
	str += "\t\"waterAmplitude\": " + std::to_string(waterAmplitude) + ",\n";
	str += "\t\"waterSpeed\": " + std::to_string(waterSpeed) + ",\n";
	str += "\t\"worldFloor\": " + std::to_string(worldFloor) + ",\n";
	str += "\t\"drawCube\": ";
	str += (draw_cube ? "true,\n" : "false,\n");
	if( cloudsTextureName.length() > 0 )
		str += "\t\"cloudsTexture\": \"" + cloudsTextureName + "\",\n";
	str += "\t\"cloudSpeed\": " + std::to_string(cloudSpeed) + ",\n";
	str += "\t\"drawClouds\": ";
	str += (draw_clouds ? "true,\n" : "false,\n");
	if( defaultImpactName.length() > 0 )
		str += "\t\"defaultImpact\": \"" + defaultImpactName + "\",\n";
	if( defaultBloodName.length() > 0 )
		str += "\t\"defaultBlood\": \"" + defaultBloodName + "\",\n";
	if( onLoad.length() > 0 )
		str += "\t\"onLoad\": \"" + onLoad + "\"\n";
	str += "}\n";
	return str;
}

void Dungeon::appendWeapons( std::string weaponPath, std::string filePath ){
	// Load the weapons file.
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( !file ){
		std::cerr << "Failed to open " << filePath << std::endl;
		return;
	}

	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );

	// Parse the map definition.
	auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );

	if( allocatedDocument.document.error.type != JSON_OK ){
		std::cerr
			<< "JSON error: "
			<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
			<< allocatedDocument.document.error.line << ":"
			<< allocatedDocument.document.error.column << " of "
			<< filePath << "\n" << std::endl;
		jsonFreeDocument( &allocatedDocument );
		return;
	}

	// Convert JsonStringView to std::string.
	auto viewToString = []( JsonStringView str ){
		return std::string( str.data, str.size );
	};

	// Loop through the weapon definitions.
	for( auto &weapDef : allocatedDocument.document.root.getObject() ){
		std::string weapName = viewToString( weapDef.name );
		// Find localized names and descriptions for the weapon,
		// starting with English.
		std::map<std::string,std::string> localizedNames, localizedBlurbs;
		localizedNames["en"] = weapName;
		localizedBlurbs["en"] = "";
		for( auto &prop : weapDef.value.getObject() ){
			std::string propName = viewToString( prop.name );
			if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
				localizedNames[propName.substr( 5 )] =
					viewToString( prop.value.getString() );
			}
			if( propName.length() > 12 && propName.substr( 0, 12 ) == "description_" ){
				localizedBlurbs[propName.substr( 12 )] =
					viewToString( prop.value.getString() );
			}
		}
		ParticleTemplate particleFire{};
		if( weapDef.value["particle_fire"] ){
			particleFire =
				loadParticles(viewToString(weapDef.value["particle_fire"].getString()));
			particleServers[particleFire.texture] = ParticleServer();
		}
		std::string ammo;
		if( weapDef.value["ammo"] ){
			ammo = viewToString( weapDef.value["ammo"].getString() );
		}
		std::string soundFire;
		if( weapDef.value["sound_fire"] ){
			soundFire =
				viewToString( weapDef.value["sound_fire"].getString() );
		}
		std::string soundReload;
		if( weapDef.value["sound_reload"] ){
			soundReload =
				viewToString( weapDef.value["sound_reload"].getString() );
		}
		std::string soundHit;
		if( weapDef.value["sound_hit"] ){
			soundHit =
				viewToString( weapDef.value["sound_hit"].getString() );
		}
		std::string soundRicochet;
		if( weapDef.value["sound_ricochet"] ){
			soundRicochet =
				viewToString( weapDef.value["sound_ricochet"].getString() );
		}
		fgl::Texture *icon = nullptr;
		if( weapDef.value["icon"] ){
			// Load the icon.
			std::string iconFile = viewToString( weapDef.value["icon"].getString() );
			if( sprites.find( iconFile ) == sprites.end() )
				sprites[iconFile] = loadSprite( weaponPath + "/" + iconFile );
			icon = &sprites[iconFile];
		}
		fgltf::Scene *model = nullptr;
		if( weapDef.value["model"] ){
			// Load the weapon model.
			std::string modelFile = viewToString( weapDef.value["model"].getString() );
			if( models.find( modelFile ) == models.end() )
				models[modelFile] = fgltf::LoadScene( ModFopen, weaponPath + "/" + modelFile );
			model = &models[modelFile];
		}
		linalg::vec<double,3> translation( 0.0, 0.0, 0.0 );
		if( weapDef.value["translation"] ){
			auto t = weapDef.value["translation"].getArray();
			if( t.size() >= 3 ){
				translation = {
					t[0].getDouble(),
					t[1].getDouble(),
					t[2].getDouble()
				};
			}
		}
		linalg::vec<double,3> zoomTranslation( 0.0, 0.0, 0.0 );
		if( weapDef.value["zoom_translation"] ){
			auto t = weapDef.value["zoom_translation"].getArray();
			if( t.size() >= 3 ){
				zoomTranslation = {
					t[0].getDouble(),
					t[1].getDouble(),
					t[2].getDouble()
				};
			}
		}
		// Animation ranges.
		linalg::vec<float,2> animationFire( 0.0f, 0.0f );
		if( weapDef.value["animation_fire"] ){
			auto a = weapDef.value["animation_fire"].getArray();
			if( a.size() >= 2 ){
				animationFire = {
					a[0].getFloat(),
					a[1].getFloat()
				};
			}
		}
		linalg::vec<float,2> animationIdle( 0.0f, 0.0f );
		if( weapDef.value["animation_idle"] ){
			auto a = weapDef.value["animation_idle"].getArray();
			if( a.size() >= 2 ){
				animationIdle = {
					a[0].getFloat(),
					a[1].getFloat()
				};
			}
		}
		linalg::vec<float,2> animationReload( 0.0f, 0.0f );
		if( weapDef.value["animation_reload"] ){
			auto a = weapDef.value["animation_reload"].getArray();
			if( a.size() >= 2 ){
				animationReload = {
					a[0].getFloat(),
					a[1].getFloat()
				};
			}
		}
		// Fire spot.
		linalg::vec<float,2> fireSpotCone( 0.0f, 0.785f );
		if( weapDef.value["fire_spot_cone"] ){
			auto a = weapDef.value["fire_spot_cone"].getArray();
			if( a.size() >= 2 ){
				fireSpotCone = {
					a[0].getFloat(),
					a[1].getFloat()
				};
			}
		}
		fgl::Color fireSpotColor = fgl::newColor;
		if( weapDef.value["fire_spot_color"] ){
			auto a = weapDef.value["fire_spot_color"].getArray();
			if( a.size() >= 4 ){
				fireSpotColor = {
					a[0].getFloat(),
					a[1].getFloat(),
					a[2].getFloat(),
					a[3].getFloat()
				};
			}
		}
		GLfloat fireSpotRange = 0.0f;
		if( weapDef.value["fire_spot_range"] ){
			fireSpotRange = weapDef.value["fire_spot_range"].getFloat();
		}
		double fireSpotDuration = 0.0;
		if( weapDef.value["fire_spot_duration"] ){
			fireSpotDuration = weapDef.value["fire_spot_duration"].getDouble();
		}
		// Stats.
		double blockDamage = 0.0;
		if( weapDef.value["block_damage"] ){
			blockDamage = weapDef.value["block_damage"].getDouble();
		}
		double damage = 0.0;
		if( weapDef.value["damage"] ){
			damage = weapDef.value["damage"].getDouble();
		}
		double impact = 0.0;
		if( weapDef.value["impact"] ){
			impact = weapDef.value["impact"].getDouble();
		}
		double speed = 1.0;
		if( weapDef.value["speed"] ){
			speed = weapDef.value["speed"].getDouble();
		}
		double range = 1.9;
		if( weapDef.value["range"] ){
			range = weapDef.value["range"].getDouble();
		}
		double clipMax = 0.0;
		if( weapDef.value["clip_max"] ){
			clipMax = weapDef.value["clip_max"].getDouble();
		}
		double zoom = 1.0;
		if( weapDef.value["zoom"] ){
			zoom = weapDef.value["zoom"].getDouble();
		}
		double recoil = 0.0;
		if( weapDef.value["recoil"] ){
			recoil = weapDef.value["recoil"].getDouble();
		}
		bool recoilCompensator = false;
		if( weapDef.value["recoil_compensator"] ){
			recoilCompensator = weapDef.value["recoil_compensator"].getBool();
		}
		bool thirdPersonPitch = false;
		if( weapDef.value["third_person_pitch"] ){
			thirdPersonPitch = weapDef.value["third_person_pitch"].getBool();
		}
		int64_t price = 0;
		if( weapDef.value["price"] ){
			price = weapDef.value["price"].getInt64();
		}
		weapons[weapName] = {
			localizedNames,
			localizedBlurbs,
			particleFire,
			ammo,
			soundFire,
			soundReload,
			soundHit,
			soundRicochet,
			icon,
			model,
			translation,
			zoomTranslation,
			animationFire,
			animationIdle,
			animationReload,
			fireSpotCone,
			fireSpotColor,
			fireSpotRange,
			fireSpotDuration,
			blockDamage,
			damage,
			impact,
			speed,
			range,
			clipMax,
			zoom,
			recoil,
			recoilCompensator,
			thirdPersonPitch,
			price
		};
	}

	jsonFreeDocument( &allocatedDocument );
}

void Dungeon::appendTrinkets( std::string trinketPath, std::string filePath ){
	// Load the trinkets file.
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( !file ){
		std::cerr << "Failed to open " << filePath << std::endl;
		return;
	}

	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );

	// Parse the map definition.
	auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );

	if( allocatedDocument.document.error.type != JSON_OK ){
		std::cerr
			<< "JSON error: "
			<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
			<< allocatedDocument.document.error.line << ":"
			<< allocatedDocument.document.error.column << " of "
			<< filePath << "\n" << std::endl;
		jsonFreeDocument( &allocatedDocument );
		return;
	}

	// Convert JsonStringView to std::string.
	auto viewToString = []( JsonStringView str ){
		return std::string( str.data, str.size );
	};

	// Loop through the trinket definitions.
	for( auto &trinketDef : allocatedDocument.document.root.getObject() ){
		std::string trinketName = viewToString( trinketDef.name );
		// Find localized names and descriptions for the trinket,
		// starting with English.
		std::map<std::string,std::string> localizedNames, localizedBlurbs;
		localizedNames["en"] = trinketName;
		localizedBlurbs["en"] = "";
		for( auto &prop : trinketDef.value.getObject() ){
			std::string propName = viewToString( prop.name );
			if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
				localizedNames[propName.substr( 5 )] =
					viewToString( prop.value.getString() );
			}
			if( propName.length() > 12 && propName.substr( 0, 12 ) == "description_" ){
				localizedBlurbs[propName.substr( 12 )] =
					viewToString( prop.value.getString() );
			}
		}
		fgl::Texture *icon = nullptr;
		if( trinketDef.value["icon"] ){
			// Load the icon.
			std::string iconFile = viewToString( trinketDef.value["icon"].getString() );
			if( sprites.find( iconFile ) == sprites.end() )
				sprites[iconFile] = loadSprite( trinketPath + "/" + iconFile );
			icon = &sprites[iconFile];
		}
		fgltf::Scene *model = nullptr;
		if( trinketDef.value["model"] ){
			// Load the weapon model.
			std::string modelFile = viewToString( trinketDef.value["model"].getString() );
			if( models.find( modelFile ) == models.end() )
				models[modelFile] = fgltf::LoadScene( ModFopen, trinketPath + "/" + modelFile );
			model = &models[modelFile];
		}
		// Stats.
		double clipMax = 0.0;
		if( trinketDef.value["clip_max"] ){
			clipMax = trinketDef.value["clip_max"].getDouble();
		}
		int64_t price = 0;
		if( trinketDef.value["price"] ){
			price = trinketDef.value["price"].getInt64();
		}
		trinkets[trinketName] = {
			localizedNames,
			localizedBlurbs,
			icon,
			model,
			clipMax,
			price
		};
	}

	jsonFreeDocument( &allocatedDocument );
}

fgltf::Scene *Dungeon::getModel( const std::string &filePath ){
	if(models.find(filePath) == models.end())
		models[filePath] =
			fgltf::LoadScene(ModFopen, agentPath + "/" + filePath);
	return &models[filePath];
}

void Dungeon::setAgentModel( Agent *a, const std::string &modelFile ){
	if(ready && a){
		a->modelFile = modelFile;
		if(dynamicsWorld && a->type == fdungeon::TYPE_STATIC && a->model){
			// Remove the rigidbodies from the dynamics world and delete them.
			for(int i = dynamicsWorld->getNumCollisionObjects() - 1; i >= 0; i--){
				btCollisionObject* obj =
					dynamicsWorld->getCollisionObjectArray()[i];
				if(obj && obj->getUserPointer() == a){
					btRigidBody* body = btRigidBody::upcast(obj);
					if(body && body->getMotionState())
						delete body->getMotionState();
					dynamicsWorld->removeCollisionObject(obj);
					delete obj;
				}
			}
		}
		a->model = getModel(modelFile);
		if(dynamicsWorld && a->type == fdungeon::TYPE_STATIC && a->model){
			// Set up the new static model.
			fgltf::AddSceneToDynamicsWorld(
				*a->model,
				dynamicsWorld,
				fgltf::PHYSICS_TRIHARD,
				&defaultAgent
			);
			// Set the user pointer for all of the agent's sub-nodes
			// that have rigid bodies.
			for(auto &n : a->model->nodes){
				if(n.rigidBody) n.rigidBody->setUserPointer(a);
			}
			// Update limb positions and parameters in the physics world.
			fgltf::UpdateSceneDynamics(
				*a->model,
				a->spawnMat,
				1.0, // TODO: friction
				a->restitution
			);
		}
	}
}

// 
Agent *Dungeon::addAgent( btVector3 position, btScalar mass, btScalar restitution, linalg::vec<double,3> scale, linalg::vec<double,3> boxTranslation, bool box ){
	// Allocate an Agent object and push it to the back of the list.
	Agent *a = new Agent;
	if( !a ) return nullptr;
	if( agents ){
		Agent *agents_ptr = agents;
		while( agents_ptr->next )
			agents_ptr = agents_ptr->next;
		agents_ptr->next = a;
		a->prev = agents_ptr;
	}else{
		agents = a;
		a->prev = nullptr;
	}
	a->next = nullptr;

	// Parent and driver start as null.
	a->parentAgent = nullptr;
	a->driverAgent = nullptr;

	// Initialize the agent's node name to "agent".
	a->nodeName = "agent";

	// The "agent" type has no special meaning.
	a->type = TYPE_AGENT;

	// Team 0 is the default. These are user-defined.
	a->team = 0;

	// Model loop -1 is the default.
	a->modelLoop = -1;

	bool isStatic = ( mass == 0.0 );

	// Add a collision shape to the agent.
	// By default, only vehicles get boxes.
	a->footSphere = nullptr;
	if( box ){
		linalg::vec<double,3> halfExtents = scale * 0.5;
		std::vector<linalg::vec<double,3>> points = {
			{  halfExtents.x,  halfExtents.y,  halfExtents.z },
			{  halfExtents.x,  halfExtents.y, -halfExtents.z },
			{  halfExtents.x, -halfExtents.y,  halfExtents.z },
			{  halfExtents.x, -halfExtents.y, -halfExtents.z },
			{ -halfExtents.x,  halfExtents.y,  halfExtents.z },
			{ -halfExtents.x,  halfExtents.y, -halfExtents.z },
			{ -halfExtents.x, -halfExtents.y,  halfExtents.z },
			{ -halfExtents.x, -halfExtents.y, -halfExtents.z }
		};
		btConvexHullShape *shape = new btConvexHullShape();
		for( auto p : points ){
			p += boxTranslation;
			shape->addPoint( btVector3( p.x, p.y, p.z ) );
		}
		a->colShape = shape;
	}else if( isStatic ){
		a->colShape = nullptr;
		a->rigidBody = nullptr;
	}else{
		// Dynamic capsule agent.
		a->colShape = new btCapsuleShape( scale.x * 0.5, scale.y - scale.x );
		a->footSphere = new btSphereShape( 0.1 ); // TODO: Customizable foot radius.
	}

	if( !isStatic ){
		btVector3 localInertia( 0, 0, 0 );
		a->colShape->calculateLocalInertia( mass, localInertia );

		btTransform startTransform;
		startTransform.setIdentity();
		startTransform.setOrigin( position );
		//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
		btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );

		btRigidBody::btRigidBodyConstructionInfo rbInfo( mass, myMotionState, a->colShape, localInertia );
		// Use high friction for better traction.
		rbInfo.m_friction = 1.0;
		rbInfo.m_restitution = restitution;
		// Create the rigid body.
		a->rigidBody = new btRigidBody( rbInfo );

		// Lock the rotation for capsule colliders.
		if( !box ) a->rigidBody->setAngularFactor( btScalar( 0 ) );

		// Set the agent's userPointer to the struct.
		a->rigidBody->setUserPointer( a );

		dynamicsWorld->addRigidBody( a->rigidBody );
	}

	a->inventory = {};
	a->ammo = {};
	a->skills = {};

	a->angle = { 0.0, 0.0, 0.0 };
	a->scale = scale;
	a->offsetYaw = 0.0;
	// Static agents are invincible unless health and maxHealth are set.
	a->health = isStatic ? 0.0 : 100.0;
	a->maxHealth = a->health;
	a->waterHealthRate = 0.0;
	a->mass = mass;
	a->restitution = restitution;
	a->turn = 0.0;
	a->walkSpeed = 3.0; // 3 m/s is standard for a human.

	a->vehicleRaycaster = nullptr;
	a->raycastVehicle = nullptr;

	// Set default properties to be used by the dungeon engine.
	a->eyeHeight = scale.y - scale.x * 0.5; // Center of top sphere.
	a->footLength = 0.08;
	a->collisionDamageThreshold = DBL_MAX;
	a->collisionDamageFactor = 0.0;
	a->lastVel = {0.0, 0.0, 0.0};
	a->standing = 0.0;
	a->weaponBob = 0.0;
	a->weaponCooldown = 0.0;
	a->weaponFireSpotCooldown = 0.0;
	a->weaponTime = 0.0f;

	a->lightOn = false;
	a->visible = true;

	a->model = nullptr;
	a->modelDestroyed = nullptr;

	return a;
}

// Parse an agent definition.
Agent *Dungeon::parseAgent( const std::string &text, std::string filePath, linalg::mat<double,4,4> spawnMat ){
	auto allocatedDocument =
		jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
	if( allocatedDocument.document.error.type != JSON_OK ){
		std::cerr
			<< "JSON error: "
			<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
			<< allocatedDocument.document.error.line << ":"
			<< allocatedDocument.document.error.column << " of "
			<< filePath << "\n" << std::endl;
		jsonFreeDocument( &allocatedDocument );
		return nullptr;
	}
	auto &info = allocatedDocument.document.root;
	// Convert JsonStringView to std::string.
	auto viewToString = []( JsonStringView str ){
		return std::string( str.data, str.size );
	};

	auto position =
		btVector3( spawnMat[3].x, spawnMat[3].y, spawnMat[3].z );

	// Determine the type of agent.
	int agent_type = TYPE_AGENT, agent_wheels = 0;
	if( info["type"] ){
		std::string t = viewToString( info["type"].getString() );
		if( t == "bike" ){
			agent_type = TYPE_VEHICLE;
			agent_wheels = 2;
		}else if( t == "plane" ){
			agent_type = TYPE_VEHICLE;
			agent_wheels = 3;
		}else if( t == "car" ){
			agent_type = TYPE_VEHICLE;
			agent_wheels = 4;
		}else if( t == "dynamic" ){
			agent_type = TYPE_DYNAMIC;
		}else if( t == "static" ){
			agent_type = TYPE_STATIC;
		}
	}
	// Get the agent's mass.
	double agent_mass = agent_type == TYPE_STATIC ? 0.0 : 50.0;
	if( agent_type != TYPE_STATIC && info["mass"] ){
		agent_mass = info["mass"].getDouble();
	}
	// Get the agent's restitution.
	double agent_restitution = 0.0;
	if( info["restitution"] ){
		agent_restitution = info["restitution"].getDouble();
	}
	// Get the agent's collision scale.
	linalg::vec<double,3> agent_scale( 0.8, 1.7, 0.8 );
	if( info["collisionScale"] ){
		auto s = info["collisionScale"].getArray();
		if( s.size() >= 3 ){
			agent_scale = {
				s[0].getDouble(),
				s[1].getDouble(),
				s[2].getDouble()
			};
		}
	}
	// Get the agent's collision box translation.
	linalg::vec<double,3> agent_box_translation( 0.0, 0.0, 0.0 );
	if( info["collisionBoxTranslation"] ){
		auto t = info["collisionBoxTranslation"].getArray();
		if( t.size() >= 3 ){
			agent_box_translation = {
				t[0].getDouble(),
				t[1].getDouble(),
				t[2].getDouble()
			};
		}
	}

	// Create the agent.
	Agent *a = addAgent(
		position,
		btScalar( agent_mass ),
		btScalar( agent_restitution ),
		agent_scale,
		agent_box_translation,
		( agent_type == TYPE_VEHICLE )
	);

	// Re-enable dynamic agent rotation and set a reasonable angular damping.
	if( agent_type == TYPE_DYNAMIC ){
		a->rigidBody->setAngularFactor(btScalar(1));
		a->rigidBody->setDamping(a->rigidBody->getLinearDamping(), 0.8);
	}

	// Assign the agent's type.
	a->type = agent_type;

	// Assign the spawn matrix.
	a->spawnMat = spawnMat;

	// Find localized names for the agent.
	for( auto &prop : info.getObject() ){
		std::string propName = viewToString( prop.name );
		if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
			a->names[propName.substr( 5 )] =
				viewToString( prop.value.getString() );
		}
	}
	// Set the agent's team.
	if( info["team"] ){
		a->team = info["team"].getInt();
	}
	// Set the agent's dialogue.
	if( info["dialogue"] ){
		a->dialogue = viewToString( info["dialogue"].getString() );
	}
	// Set the agent's death callback.
	if( info["onDeath"] ){
		a->onDeath = viewToString( info["onDeath"].getString() );
	}
	// Set the agent's playlist.
	if( info["playlist"] ){
		a->playlist = viewToString( info["playlist"].getString() );
	}
	// Set the agent's weapon.
	if( info["weapon"] ){
		a->weapon = viewToString( info["weapon"].getString() );
		// Add the weapon to the agent's inventory automatically.
		agentAddWeapon( a, a->weapon );
	}
	// Set the agent's quick attack weapon.
	// NOT added to inventory automatically. This allows quick attacks
	// to not use an inventory slot.
	if( info["quickWeapon"] ){
		a->quickWeapon = viewToString( info["quickWeapon"].getString() );
	}
	// Set the agent's explode sound.
	if( info["soundExplode"] ){
		a->soundExplode = viewToString( info["soundExplode"].getString() );
	}
	// Set the agent's falling damage sound.
	if( info["soundCollisionDamage"] ){
		a->soundCollisionDamage = viewToString( info["soundCollisionDamage"].getString() );
	}
	// Set the agent's jump sound.
	if( info["soundJump"] ){
		a->soundJump = viewToString( info["soundJump"].getString() );
	}
	// Set the agent's heal sound.
	if( info["soundHeal"] ){
		a->soundHeal = viewToString( info["soundHeal"].getString() );
	}
	// Fill the agent's inventory.
	a->inventory = {};
	if( info["inventory"] ){
		for( auto &item : info["inventory"].getArray() ){
			InventoryInstance newInstance = {};
			if( item["weapon"] ){
				newInstance.weapon = viewToString( item["weapon"].getString() );
			}
			if( item["trinket"] ){
				newInstance.trinket = viewToString( item["trinket"].getString() );
			}
			// Count defaults to 1.
			newInstance.count = 1;
			if( item["count"] ){
				newInstance.count = item["count"].getInt64();
			}
			if( item["clip"] ){
				newInstance.clip = item["clip"].getDouble();
			}
			// Add the item to the agent's inventory, overwriting if necessary.
			bool added = false;
			for( auto &instance : a->inventory ){
				if( instance.weapon == newInstance.weapon ){
					instance = newInstance;
					added = true;
				}
			}
			if( !added ) a->inventory.push_back( newInstance );
		}
	}
	// Set the agent's ammo counts.
	if( info["ammo"] ){
		for( auto &ammoType : info["ammo"].getObject() ){
			a->ammo[viewToString( ammoType.name )] = ammoType.value.getDouble();
		}
	}
	// Set the agent's skills.
	if( info["skills"] ){
		for( auto &skill : info["skills"].getObject() ){
			a->skills[viewToString( skill.name )] = skill.value.getDouble();
		}
	}
	// Load the agent's dungeon file string.
	if( info["dungeonFile"] ){
		a->dungeonFile = viewToString( info["dungeonFile"].getString() );
	}
	// Load the agent's model.
	if( info["model"] ){
		a->modelFile = viewToString( info["model"].getString() );
		if( a->modelFile.length() > 0 ){
			if( models.find( a->modelFile ) == models.end() )
				models[a->modelFile] =
					fgltf::LoadScene( ModFopen, agentPath + "/" + a->modelFile );
			a->model = &models[a->modelFile];
		}
	}
	// Load the agent's destroyed model.
	if( info["modelDestroyed"] ){
		a->modelDestroyedFile = viewToString( info["modelDestroyed"].getString() );
		if( a->modelDestroyedFile.length() > 0 ){
			if( models.find( a->modelDestroyedFile ) == models.end() )
				models[a->modelDestroyedFile] =
					fgltf::LoadScene( ModFopen, agentPath + "/" + a->modelDestroyedFile );
			a->modelDestroyed = &models[a->modelDestroyedFile];
		}
	}
	// Load the agent's model loop parameter.
	if( info["modelLoop"] ){
		a->modelLoop = info["modelLoop"].getInt();
	}
	// Load the agent's avatar (mutually exclusive to the "model").
	if( info["avatar"] ){
		a->avatarFile = viewToString( info["avatar"].getString() );
		if( a->avatarFile.length() > 0 )
			a->avatar = Avatar( ModFopen, avatarPath + "/" + a->avatarFile );
	}
	// Get the center of mass translation.
	if( info["centerOfMassTranslation"] ){
		auto t = info["centerOfMassTranslation"].getArray();
		if( t.size() >= 3 ){
			btVector3 offset(
				t[0].getDouble(),
				t[1].getDouble(),
				t[2].getDouble()
			);
			// TODO: Erwin never properly added this feature AFAIK.
			// TODO: Cleanup code for btCompoundShape.
			// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=2209
			// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=1506
			// https://pybullet.org/Bullet/BulletFull/classbtCompoundShape.html
			// compoundShape->addChildShape( localTransform, shape );
			// TODO: Or just shift a weight left and right, no muss no fuss!
			// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=11066
		}
	}
	// Set the driver eye translation.
	if( info["driverEyeTranslation"] ){
		auto t = info["driverEyeTranslation"].getArray();
		if( t.size() >= 3 ){
			a->driverEyeTranslation = {
				t[0].getDouble(),
				t[1].getDouble(),
				t[2].getDouble()
			};
		}
	}
	// Toggleable flashlight.
	a->lightCone = linalg::vec<float,2>( 0.0f, 0.785f );
	if( info["lightCone"] ){
		auto cone = info["lightCone"].getArray();
		if( cone.size() >= 2 ){
			a->lightCone = {
				cone[0].getFloat(),
				cone[1].getFloat()
			};
		}
	}
	a->lightColor = fgl::newColor;
	if( info["lightColor"] ){
		auto color = info["lightColor"].getArray();
		if( color.size() >= 4 ){
			a->lightColor = {
				color[0].getFloat(),
				color[1].getFloat(),
				color[2].getFloat(),
				color[3].getFloat()
			};
		}
	}
	a->lightRange = 0.0f;
	if( info["lightRange"] ){
		a->lightRange = info["lightRange"].getFloat();
	}
	// Get the agent's yaw offset.
	if( info["offsetYaw"] ){
		a->offsetYaw = info["offsetYaw"].getDouble();
	}
	// Get the agent's starting health, increasing maxHealth if necessary.
	if( info["health"] ){
		a->health = info["health"].getDouble();
		if( a->health > a->maxHealth ) a->maxHealth = a->health;
	}
	// Get the agent's maximum health.
	if( info["maxHealth"] ){
		a->maxHealth = info["maxHealth"].getDouble();
	}
	// Get the agent's water health rate.
	if( info["waterHealthRate"] ){
		a->waterHealthRate = info["waterHealthRate"].getDouble();
	}
	// Get the agent's walk speed.
	if( info["walkSpeed"] ){
		a->walkSpeed = info["walkSpeed"].getDouble();
	}
	// Get the agent's jump velocity.
	if( info["jumpVel"] ){
		a->jumpVel = info["jumpVel"].getDouble();
	}
	// Get the agent's collision damage threshold.
	if( info["collisionDamageThreshold"] ){
		a->collisionDamageThreshold = info["collisionDamageThreshold"].getDouble();
	}
	// Get the agent's collision damage factor.
	if( info["collisionDamageFactor"] ){
		a->collisionDamageFactor = info["collisionDamageFactor"].getDouble();
	}
	// Get the agent's sitting collision damage factor.
	if( info["sittingCollisionDamageFactor"] ){
		a->sittingCollisionDamageFactor = info["sittingCollisionDamageFactor"].getDouble();
	}
	// Get the agent's foot length.
	if( info["footLength"] ){
		a->footLength = info["footLength"].getDouble();
	}
	// Get vehicle properties.
	a->passiveBrakeForce = 0.0;
	a->brakeForce = 5.0;
	a->thrustForce = 0.0;
	a->suspensionStiffness = 30.0;
	a->suspensionDamping = 2.3;
	a->suspensionCompression = 4.4;
	a->wheelFriction = 10.0;
	a->wheelFloor = 0.0;
	a->turnAcceleration = 0.0;
	a->maxTurn = 3.0; // This can apply to non-vehicles as well.
	a->maxLean = 0.0;
	a->acceleration = 0.0;
	a->topSpeed = 0.0;
	if( info["passiveBrakeForce"] ){
		a->passiveBrakeForce = info["passiveBrakeForce"].getDouble();
	}
	if( info["brakeForce"] ){
		a->brakeForce = info["brakeForce"].getDouble();
	}
	if( info["thrustForce"] ){
		a->thrustForce = info["thrustForce"].getDouble();
	}
	if( info["suspensionStiffness"] ){
		a->suspensionStiffness = info["suspensionStiffness"].getDouble();
	}
	if( info["suspensionDamping"] ){
		a->suspensionDamping = info["suspensionDamping"].getDouble();
	}
	if( info["suspensionCompression"] ){
		a->suspensionCompression = info["suspensionCompression"].getDouble();
	}
	if( info["wheelFriction"] ){
		a->wheelFriction = info["wheelFriction"].getDouble();
	}
	if( info["wheelFloor"] ){
		a->wheelFloor = info["wheelFloor"].getDouble();
	}
	if( info["turnAcceleration"] ){
		a->turnAcceleration = info["turnAcceleration"].getDouble();
	}
	if( info["maxTurn"] ){
		a->maxTurn = info["maxTurn"].getDouble();
	}
	if( info["maxLean"] ){
		a->maxLean = info["maxLean"].getDouble();
	}
	if( info["acceleration"] ){
		a->acceleration = info["acceleration"].getDouble();
	}
	if( info["topSpeed"] ){
		a->topSpeed = info["topSpeed"].getDouble();
	}

	// Set the weapon animation to idle.
	auto it = weapons.find( a->weapon );
	if( it != weapons.end() ){
		a->weaponTime = it->second.animationIdle.x;
	}

	if( a->type == TYPE_VEHICLE ){
		// Set up the vehicle.
		// https://pybullet.org/Bullet/BulletFull/classbtRaycastVehicle.html

		btRaycastVehicle::btVehicleTuning tuning; // Default tuning for now.

		a->vehicleRaycaster = new btDefaultVehicleRaycaster( dynamicsWorld );
		a->raycastVehicle = new btRaycastVehicle( tuning, a->rigidBody, a->vehicleRaycaster );

		// Never deactivate the vehicle.
		a->rigidBody->setActivationState( DISABLE_DEACTIVATION );

		dynamicsWorld->addVehicle( a->raycastVehicle );

		// TODO: Customizable wheel parameters.
		double wheelRadius = 0.32;
		double wheelWidth = 0.07;
		double wheelY = wheelRadius + a->wheelFloor;
		btScalar suspensionRestLength( wheelRadius );
		btVector3 wheelDirectionCS0(0, -1, 0);
		btVector3 wheelAxleCS(-1, 0, 0);

		// Choose coordinate system. (Indices are: right, up, forward.)
		a->raycastVehicle->setCoordinateSystem( 0, 1, 2 );

		if( agent_wheels == 2 ){
			bool isFrontWheel = true;
			btVector3 connectionPointCS0(0.0, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			isFrontWheel = false;
			connectionPointCS0 = btVector3(0.0, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
		}else if( agent_wheels == 3 ){
			bool isFrontWheel = true;
			btVector3 connectionPointCS0(0.0, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			isFrontWheel = false;
			connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			connectionPointCS0 = btVector3(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
		}else if( agent_wheels == 4 ){
			bool isFrontWheel = true;
			btVector3 connectionPointCS0(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			isFrontWheel = false;
			connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
			connectionPointCS0 = btVector3(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
			a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
		}

		for( int i = 0; i < a->raycastVehicle->getNumWheels(); i++ ){ // TODO: Customizable wheel parameters.
			btWheelInfo& wheel = a->raycastVehicle->getWheelInfo(i);
			// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=2398
			wheel.m_maxSuspensionTravelCm = suspensionRestLength * 100.0;
			wheel.m_suspensionStiffness = a->suspensionStiffness;
			wheel.m_wheelsDampingRelaxation = a->suspensionDamping;
			wheel.m_wheelsDampingCompression = a->suspensionCompression;
			wheel.m_frictionSlip = a->wheelFriction;
			wheel.m_rollInfluence = 0.1f; //1.0f;
		}

		// Brake the vehicle.
		brakeVehicle( a, a->brakeForce );

		// Set the vehicle's orientation to its spawn orientation.
		a->rigidBody->setWorldTransform( btTransform(
			btMatrix3x3(spawnMat[0][0], spawnMat[1][0], spawnMat[2][0],
						spawnMat[0][1], spawnMat[1][1], spawnMat[2][1],
						spawnMat[0][2], spawnMat[1][2], spawnMat[2][2]),
			position
		) );
	}else if( a->type == TYPE_STATIC && a->model ){
		// Set up the static model.
		fgltf::AddSceneToDynamicsWorld(
			*a->model,
			dynamicsWorld,
			fgltf::PHYSICS_TRIHARD,
			&defaultAgent
		);
		// Set the user pointer for all of the agent's sub-nodes that
		// have rigid bodies.
		for( auto &n : a->model->nodes ){
			if( n.rigidBody ) n.rigidBody->setUserPointer( a );
		}
		// Update limb positions and parameters in the physics world.
		fgltf::UpdateSceneDynamics(
			*a->model,
			a->spawnMat,
			1.0, // TODO: friction
			agent_restitution
		);
	}else if( a->type == TYPE_STATIC && !a->colShape && !a->rigidBody ){
		// A static object without a model is presumably a static avatar.
		a->colShape =
			new btCapsuleShape( agent_scale.x * 0.5, agent_scale.y - agent_scale.x );
		btVector3 localInertia( 0, 0, 0 );
		a->colShape->calculateLocalInertia( btScalar(agent_mass), localInertia );
		// Offset the position to put the bottom of the capsule at the agent's origin.
		position.setY( position.getY() + a->scale.y * 0.5 );
		btTransform startTransform;
		startTransform.setIdentity();
		startTransform.setOrigin( position );
		btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );
		btRigidBody::btRigidBodyConstructionInfo rbInfo( btScalar(agent_mass), myMotionState, a->colShape, localInertia );
		// Half friction feels better than full for static avatars.
		rbInfo.m_friction = 0.5;
		// Prevent agents from bouncing.
		rbInfo.m_restitution = 0.0;
		// Create the rigid body.
		a->rigidBody = new btRigidBody( rbInfo );
		// Lock the rotation.
		a->rigidBody->setAngularFactor( btScalar(0) );
		// Set the agent's userPointer to the struct.
		a->rigidBody->setUserPointer( a );
		dynamicsWorld->addRigidBody( a->rigidBody );
	}

	jsonFreeDocument( &allocatedDocument );

	return a;
}

Agent *Dungeon::loadAgent( std::string filePath, linalg::mat<double,4,4> spawnMat ){
	// Discard everything after ".json".
	size_t ext_at = filePath.rfind( ".json" );
	if( ext_at == std::string::npos ){
		// No .json file extension. Add an empty agent at spawnMat.
		Agent *a = addAgent(btVector3(0,0,0), 0.0, 0.0, {1.0,1.0,1.0});
		a->type = TYPE_STATIC;
		a->rigidBody = nullptr;
		a->spawnMat = spawnMat;
		a->visible = false;
		return a;
	}else{
		// Remove excess characters after the .json file extension.
		filePath.resize( ext_at + 5 );
	}
	// Load the agent definition.
	filePath = agentPath + "/" + filePath;
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( !file ){
		std::cerr << "Failed to open " << filePath << std::endl;
		return nullptr;
	}
	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );
	return parseAgent( text, filePath, spawnMat );
}

void Dungeon::loadSavedPlayerAgent( std::string filePath ){
	// Load the agent definition.
	FILE *file = ModFopen( filePath.c_str(), "rb" );
	if( !file ){
		std::cerr << "Failed to open " << filePath << std::endl;
		return;
	}
	std::string text = "";
	char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		text += std::string( buf, len );
	}
	fclose( file );
	// JSON parsing boilerplate.
	auto allocatedDocument =
		jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
	if( allocatedDocument.document.error.type != JSON_OK ){
		std::cerr
			<< "JSON error: "
			<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
			<< allocatedDocument.document.error.line << ":"
			<< allocatedDocument.document.error.column << " of "
			<< filePath << "\n" << std::endl;
		jsonFreeDocument( &allocatedDocument );
		return;
	}
	auto &info = allocatedDocument.document.root;
	// Convert JsonStringView to std::string.
	auto viewToString = []( JsonStringView str ){
		return std::string( str.data, str.size );
	};
	// Load the agent's dungeon file string.
	std::string agentDungeon;
	if( info["dungeonFile"] ){
		agentDungeon = viewToString( info["dungeonFile"].getString() );
	}
	// Load the agent's angle.
	linalg::vec<double,3> agentAngle( 0.0, 0.0, 0.0 );
	if( info["angle"] ){
		auto v = info["angle"].getArray();
		if( v.size() >= 3 ){
			agentAngle = {
				v[0].getDouble(),
				v[1].getDouble(),
				v[2].getDouble()
			};
		}
	}
	// Free the JSON document.
	jsonFreeDocument( &allocatedDocument );
	// Load the agent in its specified dungeon.
	loadWorld(dungeonPath, agentPath, avatarPath, particlePath, agentDungeon, text);
	if(playerAgent){
		// Position and rotate the agent.
		positionAgent(playerAgent, playerAgent->driverEyeTranslation);
		playerAgent->angle = agentAngle;
	}
}

// Return a JSON string containing the agent definition.
// Vehicles currently do not serialize properly. TODO: collisionBoxTranslation
std::string Dungeon::serializeAgent( Agent *a ){
	std::string str = "{\n";
	for( const auto &it : a->names ){
		str += "\t\"name_" + it.first + "\": \"" + it.second + "\",\n";
	}
	str += "\t\"type\": \"";
	if( a->type == TYPE_VEHICLE && a->raycastVehicle ){
		if( a->raycastVehicle->getNumWheels() == 2 ) str += "bike";
		else if( a->raycastVehicle->getNumWheels() == 3 ) str += "plane";
		else if( a->raycastVehicle->getNumWheels() == 4 ) str += "car";
		else str += "vehicle";
	}else{
		if( a->type == TYPE_DYNAMIC ){
			str += "dynamic";
		}else if( a->type == TYPE_STATIC ){
			str += "static";
		}else{
			str += "agent";
		}
		// Hijack driverEyeTranslation to store current position.
		a->driverEyeTranslation = getAgentPoseMat(a)[3].xyz();
	}
	str += "\",\n";
	str += "\t\"team\": " + std::to_string(a->team) + ",\n";
	str += "\t\"dialogue\": \"" + a->dialogue + "\",\n";
	str += "\t\"onDeath\": \"" + a->onDeath + "\",\n";
	str += "\t\"playlist\": \"" + a->playlist + "\",\n";
	str += "\t\"weapon\": \"" + a->weapon + "\",\n";
	str += "\t\"quickWeapon\": \"" + a->quickWeapon + "\",\n";
	str += "\t\"soundExplode\": \"" + a->soundExplode + "\",\n";
	str += "\t\"soundCollisionDamage\": \"" + a->soundCollisionDamage + "\",\n";
	str += "\t\"soundJump\": \"" + a->soundJump + "\",\n";
	str += "\t\"soundHeal\": \"" + a->soundHeal + "\",\n";
	str += "\t\"inventory\": [\n";
	for( size_t i = 0; i < a->inventory.size(); i++ ){
		InventoryInstance &instance = a->inventory[i];
		str += "\t\t{ \"weapon\": \"" + instance.weapon + "\", ";
		str += "\"trinket\": \"" + instance.trinket + "\", ";
		str += "\"count\": " + std::to_string(instance.count) + ", ";
		str += "\"clip\": " + std::to_string(instance.clip) + " }";
		if( i < a->inventory.size() - 1 ) str += ",";
		str += "\n";
	}
	str += "\t],\n";
	str += "\t\"ammo\": { ";
	size_t ct = 0;
	for( const auto &ammo_it : a->ammo ){
		str += "\"" + ammo_it.first + "\": "
			+ std::to_string(ammo_it.second);
		ct++;
		if( ct < a->ammo.size() ) str += ", ";
	}
	str += " },\n";
	str += "\t\"skills\": { ";
	ct = 0;
	for( const auto &skills_it : a->skills ){
		str += "\"" + skills_it.first + "\": "
			+ std::to_string(skills_it.second);
		ct++;
		if( ct < a->skills.size() ) str += ", ";
	}
	str += " },\n";
	str += "\t\"dungeonFile\": \"" + dungeonFile + "\",\n"; // The current dungeon file.
	str += "\t\"model\": \"" + a->modelFile + "\",\n";
	str += "\t\"modelDestroyed\": \"" + a->modelDestroyedFile + "\",\n";
	str += "\t\"modelLoop\": \"" + std::to_string(a->modelLoop) + "\",\n";
	str += "\t\"avatar\": \"" + a->avatarFile + "\",\n";
	str += "\t\"angle\": ["
		+ std::to_string(a->angle.x) + ", "
		+ std::to_string(a->angle.y) + ", "
		+ std::to_string(a->angle.z) + "],\n";
	str += "\t\"collisionScale\": ["
		+ std::to_string(a->scale.x) + ", "
		+ std::to_string(a->scale.y) + ", "
		+ std::to_string(a->scale.z) + "],\n";
	str += "\t\"driverEyeTranslation\": ["
		+ std::to_string(a->driverEyeTranslation.x) + ", "
		+ std::to_string(a->driverEyeTranslation.y) + ", "
		+ std::to_string(a->driverEyeTranslation.z) + "],\n";
	// Toggleable flashlight.
	str += "\t\"lightCone\": ["
		+ std::to_string(a->lightCone.x) + ", "
		+ std::to_string(a->lightCone.y) + "],\n";
	str += "\t\"lightColor\": ["
		+ std::to_string(a->lightColor.r) + ", "
		+ std::to_string(a->lightColor.g) + ", "
		+ std::to_string(a->lightColor.b) + ", "
		+ std::to_string(a->lightColor.a) + "],\n";
	str += "\t\"lightRange\": " + std::to_string(a->lightRange) + ",\n";
	// General numeric properties.
	str += "\t\"offsetYaw\": " + std::to_string(a->offsetYaw) + ",\n";
	str += "\t\"health\": " + std::to_string(a->health) + ",\n";
	str += "\t\"maxHealth\": " + std::to_string(a->maxHealth) + ",\n";
	str += "\t\"waterHealthRate\": " + std::to_string(a->waterHealthRate) + ",\n";
	str += "\t\"mass\": " + std::to_string(a->mass) + ",\n";
	str += "\t\"restitution\": " + std::to_string(a->restitution) + ",\n";
	str += "\t\"walkSpeed\": " + std::to_string(a->walkSpeed) + ",\n";
	str += "\t\"jumpVel\": " + std::to_string(a->jumpVel) + ",\n";
	str += "\t\"collisionDamageThreshold\": " + std::to_string(a->collisionDamageThreshold) + ",\n";
	str += "\t\"collisionDamageFactor\": " + std::to_string(a->collisionDamageFactor) + ",\n";
	str += "\t\"sittingCollisionDamageFactor\": " + std::to_string(a->sittingCollisionDamageFactor) + ",\n";
	str += "\t\"footLength\": " + std::to_string(a->footLength) + ",\n";
	// Vehicle properties.
	str += "\t\"passiveBrakeForce\": " + std::to_string(a->passiveBrakeForce) + ",\n";
	str += "\t\"brakeForce\": " + std::to_string(a->brakeForce) + ",\n";
	str += "\t\"thrustForce\": " + std::to_string(a->thrustForce) + ",\n";
	str += "\t\"suspensionStiffness\": " + std::to_string(a->suspensionStiffness) + ",\n";
	str += "\t\"suspensionDamping\": " + std::to_string(a->suspensionDamping) + ",\n";
	str += "\t\"suspensionCompression\": " + std::to_string(a->suspensionCompression) + ",\n";
	str += "\t\"wheelFriction\": " + std::to_string(a->wheelFriction) + ",\n";
	str += "\t\"wheelFloor\": " + std::to_string(a->wheelFloor) + ",\n";
	str += "\t\"turnAcceleration\": " + std::to_string(a->turnAcceleration) + ",\n";
	str += "\t\"maxTurn\": " + std::to_string(a->maxTurn) + ",\n";
	str += "\t\"maxLean\": " + std::to_string(a->maxLean) + ",\n";
	str += "\t\"acceleration\": " + std::to_string(a->acceleration) + ",\n";
	str += "\t\"topSpeed\": " + std::to_string(a->topSpeed) + "\n";
	str += "}\n";
	return str;
}

// Rotate angle a to angle b at speed.
double Dungeon::getRotationDelta(double a, double b, double speed){
	double diff = a - b;
	double diff2 = std::atan2(std::sin(diff), std::cos(diff));
	if(diff2 > 0.0) return std::min(speed, M_PI - diff2);
	else return std::max(-speed, -M_PI - diff2);
}

// Rotate an agent with a constrained X angle, taking third-person into account.
void Dungeon::rotateAgentConstrained( Agent *a, linalg::vec<double,3> rotation ){
	if( a == playerAgent && topDown ){
		a->angle = {0.0, 0.0, 0.0};
		return;
	}
	double minX = -1.570796, maxX = 1.570796;
	a->angle.z = fgl::wrapAngle( a->angle.z + rotation.z );
	if( thirdPerson && a == playerAgent ){
		minX = thirdPersonCameraMinPitch;
		maxX = thirdPersonCameraMaxPitch;
		thirdPersonYawOffset = fgl::wrapAngle( thirdPersonYawOffset + rotation.y );
	}else{
		a->angle.y = fgl::wrapAngle( a->angle.y + rotation.y );
	}
	a->angle.x = std::min( std::max( a->angle.x + rotation.x, minX ), maxX );
	// Optional: Apply the Y angle to the rigid body. This breaks smooth
	// animations when running at > 60 FPS.
	/*
	btTransform trans = a->rigidBody->getWorldTransform();
	linalg::vec<double,4> q = fgl::eulerToQuat( 0.0, a->angle.y, 0.0 );
	trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
	// Activate the object.
	a->rigidBody->setActivationState( 1 );
	a->rigidBody->setWorldTransform( trans );
	if( a->rigidBody->getMotionState() )
		a->rigidBody->getMotionState()->setWorldTransform( trans );
	*/
}

// Rotate an agent without constraints.
// This may temporarily freeze vehicles in place.
void Dungeon::rotateAgentUnconstrained( Agent *a, linalg::vec<double,3> rotation ){
	if( a->type == TYPE_VEHICLE ){
		btTransform trans = getAgentPhysicsTransform( a );
		btQuaternion r = trans.getRotation();
		auto q = linalg::qmul(
			fgl::eulerToQuat( rotation.x, rotation.y, rotation.z ),
			linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
		);
		trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
		// Activate the vehicle.
		a->rigidBody->setActivationState( 1 );
		// Set the vehicle's transform.
		a->rigidBody->setWorldTransform( trans );
		if( a->rigidBody->getMotionState() )
			a->rigidBody->getMotionState()->setWorldTransform( trans );
	}else{
		// Rotate the agent's eye in world space.
		a->angle.x = fgl::wrapAngle( a->angle.x + rotation.x );
		a->angle.y = fgl::wrapAngle( a->angle.y + rotation.y );
		a->angle.z = fgl::wrapAngle( a->angle.z + rotation.z );
	}
}

// Move an agent by translation. Return true if agent successfully jumped.
bool Dungeon::moveAgent( Agent *a, linalg::vec<double,3> translation, double standingTime, double d ){
	if( !a->rigidBody ) return false;
	// https://gamedev.stackexchange.com/questions/37282
	// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=7202
	// Cast a foot ray.
	btVector3 from = a->rigidBody->getWorldTransform().getOrigin();
	btVector3 to(
		from.getX(),
		from.getY() - a->scale.y * 0.5 - a->footLength,
		from.getZ()
	);
	//btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
	//dynamicsWorld->rayTest( from, to, rayResult );
	//a->standing = rayResult.hasHit();
	// Spherecast the foot.
	btTransform fromTrans, toTrans;
	fromTrans.setIdentity();
	fromTrans.setOrigin( from );
	toTrans.setIdentity();
	toTrans.setOrigin( to );
	// https://pybullet.org/Bullet/BulletFull/classbtClosestNotMeConvexResultCallback.html
	// A fixed version of Bullet's official class.
	btrapClosestNotMeConvexResultCallback rayResult(
		a->rigidBody,
		from,
		to,
		(btOverlappingPairCache*)overlappingPairCache
	);
	dynamicsWorld->convexSweepTest( a->footSphere, fromTrans, toTrans, rayResult );
	if(rayResult.hasHit()){
		a->standing = standingTime;
	}else{
		a->standing = std::max(a->standing - d, 0.0);
	}
	/*
	a->standing = false;
	// Loop through spherecast hits.
	for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
		if( rayResult.m_collisionObjects[i] != a ){
			a->standing = true;
			break;
		}
	}
	*/
	// Get the Y angle of the translation vector plus the Y angle of the agent.
	double moveAngle =
		std::atan2( translation.x, translation.z ) + a->angle.y;
	// Get the length of the Y plane of the translation vector.
	double moveLength = linalg::length(
		linalg::vec<double,2>( translation.x, translation.z )
	);
	// Apply the necessary adjustments in third-person mode.
	if(thirdPerson && !topDown && a == playerAgent && moveLength > 0.0){
		moveAngle += thirdPersonYawOffset;
		// Rotate agent toward moveAngle.
		double rdelta = getRotationDelta(a->angle.y, moveAngle, a->maxTurn * d);
		a->angle.y = fgl::wrapAngle(a->angle.y + rdelta);
		thirdPersonYawOffset = fgl::wrapAngle(thirdPersonYawOffset - rdelta);
	}
	btVector3 vel = a->rigidBody->getVelocityInLocalPoint( btVector3( 0, 0, 0 ) );

#if 0
// This section is non-functional due to vel not representing distance traveled.
if( fgl::charKey( 'e' ) ){
	// Capsule radius.
	const double r = a->scale.x * 0.5;
	// Center of capsule's top sphere.
	const double center = a->scale.y - r;
	// Allow the eye height to drift vertically to smooth movement.
	a->eyeHeight = center
		+ std::min( std::max( a->eyeHeight - center - vel.getY() * d, -r ), r );
	// Push the eye height towards the imaginary center position at an
	// approximately linear rate.
	if( a->eyeHeight < center ){
		a->eyeHeight = std::min( a->eyeHeight + 5.0 * d, center );
	}else if( a->eyeHeight > center ){
		a->eyeHeight = std::max( a->eyeHeight - 5.0 * d, center );
	}
	printf( "eyeHeight: %f\n", (float)a->eyeHeight );
}
#endif

	// Activate the object.
	a->rigidBody->setActivationState( 1 );

	//a->rigidBody->applyCentralImpulse( ??? / a->rigidBody->getInvMass() );
	if(a->standing != 0.0 || waterLevel + waterWaveHeight > from.getY()){
		// TODO: Set velocity relative to velocity of intersecting point.
		// Vertical movement.
		btVector3 moveVec = btVector3(
			vel.getX(),
			translation.y ? translation.y : vel.getY(),
			vel.getZ()
		);
		// Lateral movement. Only override current velocity when desired
		// movement velocity is more than half of current velocity.
		if( ( translation.x || translation.z )
			&& moveLength > linalg::length<double,2>( { vel.getX(), vel.getZ() } ) * 0.5 ){
			moveVec = btVector3(
				std::sin( moveAngle ) * moveLength,
				moveVec.getY(),
				std::cos( moveAngle ) * moveLength
			);
			// Zero friction for smooth sliding.
			a->rigidBody->setFriction( 0.0 );
		}else{
			// Feet are touching the ground but not moving.
			a->rigidBody->setFriction( 1.0 );
		}
		// Apply the velocity.
		a->rigidBody->setLinearVelocity( moveVec );
		return translation.y != 0.0;
	}else{
		// Set friction to 0 when feet are not touching the ground.
		a->rigidBody->setFriction( 0.0 );
	}
	return false;
}

void Dungeon::positionAgent( Agent *a, linalg::vec<double,3> position ){
	if( a->type == TYPE_STATIC || !a->rigidBody ){
		a->spawnMat[3].x = position.x;
		a->spawnMat[3].y = position.y;
		a->spawnMat[3].z = position.z;
		return;
	}
	btTransform trans = getAgentPhysicsTransform( a );
	trans.setOrigin( btVector3( position.x, position.y, position.z ) );
	a->rigidBody->setActivationState( 1 );
	a->rigidBody->setWorldTransform( trans );
	if( a->rigidBody->getMotionState() )
		a->rigidBody->getMotionState()->setWorldTransform( trans );
}

// Accelerate an agent as a vehicle via its own space.
// TODO: acceleration = wheel torque / ( wheel radius * vehicle mass )
// "Engine force" seems arbitrary. What is it?
void Dungeon::accelerateVehicle( Agent *a, double force ){
	int wheels = a->raycastVehicle->getNumWheels();
	// Set appropriate brake force.
	for( int i = 0; i < wheels; i++ ){
		a->raycastVehicle->setBrake(
			(force == 0.0) ? a->passiveBrakeForce : 0.0,
			i
		);
	}
	double speed = a->raycastVehicle->getCurrentSpeedKmHour();
	// If accelerating in the same direction and speed exceeds top
	// speed, set force to 0.0.
	if( speed * force > 0.0 && std::abs(speed) > a->topSpeed )
		force = 0.0;
	// Thrust.
	double thrust = (force == 0.0)
		? 0.0
		: ( (force > 0.0) ? a->thrustForce : -a->thrustForce );
	btTransform trans = a->rigidBody->getWorldTransform();
	btQuaternion r = trans.getRotation();
	auto rotationMat = linalg::rotation_matrix(
		linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
	);
	auto thrustVec = linalg::mul(
		rotationMat,
		linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
	)[3].xyz() * thrust;
	a->rigidBody->applyCentralForce( btVector3( thrustVec.x, thrustVec.y, thrustVec.z ) );
	// Spin wheels.
	if( wheels == 2 ){
		a->raycastVehicle->applyEngineForce( force, 1 );
	}else if( wheels == 3 ){
		a->raycastVehicle->applyEngineForce( force, 1 );
		a->raycastVehicle->applyEngineForce( force, 2 );
	}else if( wheels == 4 ){
		a->raycastVehicle->applyEngineForce( force, 2 );
		a->raycastVehicle->applyEngineForce( force, 3 );
	}
}

void Dungeon::brakeVehicle( Agent *a, double brakeForce ){
	for( int i = 0; i < a->raycastVehicle->getNumWheels(); i++ ){
		a->raycastVehicle->applyEngineForce( 0.0, i );
		a->raycastVehicle->setBrake( brakeForce, i );
	}
}

void Dungeon::rightVehicle( Agent *a ){
	auto q = fgl::eulerToQuat( 0.0, getVehicleYaw( a ), 0.0 );
	//btTransform trans = getAgentPhysicsTransform( a );
	btTransform trans = a->rigidBody->getWorldTransform();
	trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
	a->rigidBody->setWorldTransform( trans );
	if( a->rigidBody->getMotionState() )
		a->rigidBody->getMotionState()->setWorldTransform( trans );
	a->rigidBody->setAngularVelocity( btVector3( 0.0, 0.0, 0.0 ) );
}

bool Dungeon::isVehicleUpright( Agent *a ){
	btTransform trans = getAgentPhysicsTransform( a );
	btQuaternion r = trans.getRotation();
	auto rotationMat = linalg::rotation_matrix(
		linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
	);
	return ( linalg::mul(
		rotationMat,
		linalg::translation_matrix( linalg::vec<double,3>( 0.0, 1.0, 0.0 ) )
	)[3].y > 0.3 ); // This threshold is meant to make the "upright cone" smaller.
}

double Dungeon::getVehicleLean( Agent *a ){
	btTransform trans = getAgentPhysicsTransform( a );
	btQuaternion r = trans.getRotation();
	auto rotationMat = linalg::rotation_matrix(
		linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
	);
	auto side = linalg::mul(
		rotationMat,
		linalg::translation_matrix( linalg::vec<double,3>( 1.0, 0.0, 0.0 ) )
	)[3].xyz();
	return atan2(
		side.y,
		linalg::length<double,2>( { side.x, side.z } )
	);
}

double Dungeon::getVehiclePitch( Agent *a ){
	btTransform trans = getAgentPhysicsTransform( a );
	btQuaternion r = trans.getRotation();
	auto rotationMat = linalg::rotation_matrix(
		linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
	);
	auto end = linalg::mul(
		rotationMat,
		linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, -1.0 ) )
	)[3].xyz();
	return atan2(
		end.y,
		linalg::length<double,2>( { end.x, end.z } )
	);
}

double Dungeon::getVehicleYaw( Agent *a ){
	btTransform trans = getAgentPhysicsTransform( a );
	btQuaternion r = trans.getRotation();
	auto rotationMat = linalg::rotation_matrix(
		linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
	);
	auto end = linalg::mul(
		rotationMat,
		linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
	)[3].xyz();
	return std::atan2( end.x, end.z );
}

void Dungeon::setAgentCollisions( Agent *a, bool enable ){
	if( !a->rigidBody ) return;
	// https://pybullet.org/Bullet/BulletFull/classbtCollisionObject.html
	auto flags = a->rigidBody->getCollisionFlags();
	if( enable ){
		a->rigidBody->setCollisionFlags( flags
			& ~btCollisionObject::CF_NO_CONTACT_RESPONSE );
	}else{
		a->rigidBody->setCollisionFlags( flags
			| btCollisionObject::CF_NO_CONTACT_RESPONSE );
	}
}

void Dungeon::setAgentCrouch( Agent *a, bool enable, double d ){
	if( !a->colShape ) return;
	// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=13057
	// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=6325
	double s = enable ? 0.6 : 1.0;
	//a->colShape->setLocalScaling( btVector3( 0.0, s, 0.0 ) );
	double eye = a->scale.y * s - a->scale.x * 0.5;
	if( a->eyeHeight < eye ){
		a->eyeHeight += 6.0 * d;
		if( a->eyeHeight > eye ) a->eyeHeight = eye;
	}else if( a->eyeHeight > eye ){
		a->eyeHeight -= 6.0 * d;
		if( a->eyeHeight < eye ) a->eyeHeight = eye;
	}
}

void Dungeon::unsitAgent( Agent *a ){
	auto ray = getAgentGazeRay( a, 1.0 );
	auto dir = ray.first - ray.second;
	a->angle.y = std::atan2( dir.x, dir.z );
	a->angle.z = 0.0;
	setAgentCollisions( a, true );
	if( a->parentAgent && a->parentAgent->driverAgent == a ){
		a->parentAgent->turn = 0.0;
		a->parentAgent->driverAgent = nullptr;
	}
	a->parentAgent = nullptr;
}

linalg::vec<double,3> Dungeon::getNodeLookAt( linalg::mat<double,4,4> m ){
	// https://gamedev.stackexchange.com/questions/119702/fastest-way-to-neutralize-scale-in-the-transform-matrix
	// Get a rotation quaternion from the top-left part of the matrix.
	auto q = linalg::rotation_quat<double>( {
		linalg::normalize<double,3>( { m[0][0], m[0][1], m[0][2] } ),
		linalg::normalize<double,3>( { m[1][0], m[1][1], m[1][2] } ),
		linalg::normalize<double,3>( { m[2][0], m[2][1], m[2][2] } )
	} );
	// Get a look-at vector from the rotation quaternion.
	// This is inverse of the node's Blender orientation but points in
	// the same direction as the base of a cone empty.
	return linalg::qzdir( q ) * linalg::vec<double,3>( -1.0, 1.0, -1.0 );
}

btTransform Dungeon::getAgentPhysicsTransform( Agent *a ){
	if( a->rigidBody->getMotionState() ){
		btTransform trans;
		a->rigidBody->getMotionState()->getWorldTransform( trans );
		return trans;
	}else{
		return a->rigidBody->getWorldTransform();
	}
}

// 
linalg::mat<double,4,4> Dungeon::getAgentPoseMat( Agent *a ){
	if( a->type == TYPE_STATIC || !a->rigidBody ) return a->spawnMat;
	btTransform trans = getAgentPhysicsTransform( a );
	btVector3 &o = trans.getOrigin();
	// Return physical rotations for vehicles and axis-aligned rotations
	// for other agents.
	if( a->type == TYPE_VEHICLE || a->type == TYPE_DYNAMIC ){
		btQuaternion r = trans.getRotation();
		return linalg::pose_matrix(
			linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
			linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
		);
	}else{
		return linalg::pose_matrix(
			fgl::eulerToQuat( 0.0, a->angle.y + a->offsetYaw, 0.0 ),
			linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
		);
	}
}

std::pair<linalg::vec<double,3>,linalg::vec<double,3>> Dungeon::getAgentGazeRay( Agent *a, double range, bool pitch ){
	if( !a->rigidBody )
		return { linalg::vec<double,3>(0,0,0), linalg::vec<double,3>(0,0,0) };
	linalg::vec<double,3> from, to;
	if( a->parentAgent && a->parentAgent->driverAgent == a ){
		// Cast a ray from the offset driver eye position.
		auto eyeMat = linalg::mul(
			getAgentPoseMat( a->parentAgent ),
			linalg::pose_matrix(
				fgl::eulerToQuat(
					pitch ? a->angle.x : 0.0,
					a->angle.y,
					a->angle.z ),
				a->parentAgent->driverEyeTranslation
			)
		);
		from = eyeMat[3].xyz();
		auto targetMat = linalg::mul(
			eyeMat,
			linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, -range ) )
		);
		to = targetMat[3].xyz();
	}else{
		// Cast a ray from the agent eye position in world space.
		btTransform trans = getAgentPhysicsTransform( a );
		btVector3 &o = trans.getOrigin();
		from = {
			o.getX(),
			o.getY() - a->scale.y * 0.5 + a->eyeHeight,
			o.getZ()
		};
		auto offset = linalg::normalize( linalg::vec<double,3>(
			-std::sin( a->angle.y ),
			std::tan( pitch ? a->angle.x : 0.0 ),
			-std::cos( a->angle.y )
		) ) * range;
		to = from + offset;
	}
	return { from, to };
}

std::pair<void*,double> Dungeon::getAgentGazeHit( Agent *a, double range ){
	// https://gamedev.stackexchange.com/questions/37282
	// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=7202
	// https://pybullet.org/Bullet/BulletFull/structbtCollisionWorld_1_1ClosestRayResultCallback.html
	auto ray = getAgentGazeRay( a, range );
	btVector3
		from( ray.first.x, ray.first.y, ray.first.z ),
		to( ray.second.x, ray.second.y, ray.second.z );
	/*
	btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
	dynamicsWorld->rayTest( from, to, rayResult );
	if( rayResult.hasHit() )
		return rayResult.m_collisionObject->getUserPointer();
	*/
	// Find the closest ray hit that is not the agent's parent.
	btCollisionWorld::AllHitsRayResultCallback rayResult( from, to );
	dynamicsWorld->rayTest( from, to, rayResult );
	bool has_hit = false;
	size_t closest;
	double frac = 1.0;
	for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
		void *userptr = rayResult.m_collisionObjects[i]->getUserPointer();
		if( userptr != a && userptr != a->parentAgent
			&& rayResult.m_hitFractions[i] < frac ){
			has_hit = true;
			closest = i;
			frac = rayResult.m_hitFractions[i];
		}
	}
	if( has_hit )
		return { rayResult.m_collisionObjects[closest]->getUserPointer(), frac * range };
	return { nullptr, 0.0 };
}

// Get the agent's localized name, falling back to English.
// Fill out name_en at minimum if you want a name to display.
std::string Dungeon::getAgentLocalizedName( Agent *a, const std::string &languageCode ){
	if( a->names.size() < 1 ) return "";
	auto it1 = a->names.find( languageCode );
	if( it1 == a->names.end() ){
		auto it2 = a->names.find( "en" );
		if( it2 == a->names.end() ){
			return "";
		}else{
			return it2->second;
		}
	}else{
		return it1->second;
	}
}

// Get the weapon's localized name, first falling back to English, then
// a default name.
std::string Dungeon::getWeaponLocalizedName( Weapon *w, const std::string &languageCode ){
	auto it1 = w->names.find( languageCode );
	if( it1 == w->names.end() ){
		auto it2 = w->names.find( "en" );
		if( it2 == w->names.end() ){
			return "Weapon";
		}else{
			return it2->second;
		}
	}else{
		return it1->second;
	}
}

// 
void Dungeon::removeAgent( Agent *a, bool conservative ){ // TODO: Safely remove static objects. Make this not crash the program!
	// Detach the agent from other agents.
	if( a->parentAgent && a->parentAgent->driverAgent == a )
		a->parentAgent->driverAgent = nullptr;
	if( a->driverAgent && a->driverAgent->parentAgent == a )
		unsitAgent( a->driverAgent );
	if( conservative ){
		// Disable the agent without removing it.
		if( a->rigidBody ){
			a->rigidBody->setUserPointer( &defaultAgent );
			a->rigidBody->setFriction( 1.0 );
			if( a->raycastVehicle ){
				for(int i = 0; i < a->raycastVehicle->getNumWheels(); i++)
					a->raycastVehicle->applyEngineForce( 0.0, i );
			}
		}
	}else{
		// Fully remove the agent.
		if( a->prev )
			a->prev->next = a->next;
		if( a->next )
			a->next->prev = a->prev;
		if( a->colShape )
			delete a->colShape;
		if( a->rigidBody ){
			if( a->rigidBody->getMotionState() )
				delete a->rigidBody->getMotionState();
			if( dynamicsWorld )
				dynamicsWorld->removeRigidBody( a->rigidBody );
			delete a->rigidBody;
		}
		if( a->footSphere )
			delete a->footSphere;
		if( a->vehicleRaycaster )
			delete a->vehicleRaycaster;
		if( a->raycastVehicle ){
			if( dynamicsWorld )
				dynamicsWorld->removeAction( a->raycastVehicle );
			delete a->raycastVehicle;
		}
		delete a;
	}
}

// Apply an impulse to the physics world with the ray and return the
// agent that it hits.
Agent *Dungeon::rayImpact( Agent *a, std::pair<linalg::vec<double,3>,linalg::vec<double,3>> ray, double impact, double damage, std::string defenseSkill ){
	// https://pybullet.org/Bullet/BulletFull/classbtRigidBody.html
	btVector3
		from( ray.first.x, ray.first.y, ray.first.z ),
		to( ray.second.x, ray.second.y, ray.second.z );
	// Upcast any btCollisionObject to btRigidBody. No crashes from this yet.
	btRigidBody *rb = nullptr;
	void *userptr = &defaultAgent;
	btVector3 hit;
	if( a ){
		// Find the closest ray hit that is not the agent's parent.
		btCollisionWorld::AllHitsRayResultCallback rayResult( from, to );
		dynamicsWorld->rayTest( from, to, rayResult );
		bool has_hit = false;
		size_t closest;
		double frac = 1.0;
		for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
			auto cob = rayResult.m_collisionObjects[i];
			auto optr = cob->getUserPointer();
			if( optr != a && (!optr || optr != a->parentAgent)
				&& rayResult.m_hitFractions[i] < frac
				&& ((optr && optr != &defaultAgent && ((Agent*)optr)->parentAgent) // Allow ray hits while driving.
				|| !(cob->getCollisionFlags() & btCollisionObject::CF_NO_CONTACT_RESPONSE)) ){
				has_hit = true;
				closest = i;
				frac = rayResult.m_hitFractions[i];
			}
		}
		if( !has_hit ) return nullptr;
		rb = (btRigidBody*)rayResult.m_collisionObjects[closest];
		userptr = rb->getUserPointer();
		hit = rayResult.m_hitPointWorld[closest];
	}else{
		// Agent is null. Find the closest ray hit. TODO: Actually test this use case!
		btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
		dynamicsWorld->rayTest( from, to, rayResult );
		if( !rayResult.hasHit() ) return nullptr;
		userptr = rayResult.m_collisionObject->getUserPointer();
		hit = rayResult.m_hitPointWorld;
	}
	std::string atype =
		userptr ? (*(std::string*)userptr).substr( 0, 5 ) : "";
	Agent *hitAgent = (Agent*)userptr;
	if( atype == "agent" || atype == "spawn" ){
		// Ray hit an agent.
		// TODO: Headshots, special damage zones, blood vertices, destroying static objects, etc.
		if( hitAgent != &defaultAgent ){
			if( defenseSkill.length() > 0 && hitAgent->skills.size() > 0 ){
				auto defense_skill_it = hitAgent->skills.find( defenseSkill );
				if( defense_skill_it != hitAgent->skills.end() ){
					// Defense skill affects damage.
					// If skill == 10.0, damage is reduced by about 10%.
					// If skill == 100.0, damage is reduced by about 52%.
					// If skill == 500.0, damage is reduced by about 85%.
					if( defense_skill_it->second > 1.0 )
						damage /= ( 0.011 * defense_skill_it->second + 1.0 );
				}
			}
			if(hitAgent->maxHealth > 0.0) hitAgent->health -= damage;
		}
		// TODO: Agent-specific particles.
		if( hitAgent != &defaultAgent && !hitAgent->model ){
			// Avatar = blood
			auto ps_it = particleServers.find(defaultBlood.texture);
			if(ps_it != particleServers.end()){
				ps_it->second.addOmniBurst(
					defaultBlood.num, defaultBlood.vel,
					{hit.getX(), hit.getY(), hit.getZ()},
					defaultBlood.startScale, defaultBlood.endScale,
					defaultBlood.startMass, defaultBlood.endMass, defaultBlood.maxAge,
					defaultBlood.damage, (void*)&defaultAgent );
			}
		}else{
			// No avatar = default impact
			auto ps_it = particleServers.find(defaultImpact.texture);
			if(ps_it != particleServers.end()){
				ps_it->second.addOmniBurst(
					defaultImpact.num, defaultImpact.vel,
					{hit.getX(), hit.getY(), hit.getZ()},
					defaultImpact.startScale, defaultImpact.endScale,
					defaultImpact.startMass, defaultImpact.endMass, defaultImpact.maxAge,
					defaultImpact.damage, (void*)&defaultAgent );
			}
		}
	}else{
		// Ray hit a non-agent.
		// Apply default impact particles.
		// Old behavior: return null.
		// New behavior: return &defaultAgent.
		hitAgent = &defaultAgent;
		auto ps_it = particleServers.find(defaultImpact.texture);
		if(ps_it != particleServers.end()){
			ps_it->second.addOmniBurst(
				defaultImpact.num, defaultImpact.vel,
				{hit.getX(), hit.getY(), hit.getZ()},
				defaultImpact.startScale, defaultImpact.endScale,
				defaultImpact.startMass, defaultImpact.endMass, defaultImpact.maxAge,
				defaultImpact.damage, (void*)&defaultAgent );
		}
	}
	if( rb->getInvMass() != 0.0 ){
		// Apply a physical impulse to the agent.
		btTransform trans = rb->getWorldTransform();
		btQuaternion r = trans.getRotation();
		btVector3 o = trans.getOrigin();
		auto localHit = linalg::mul(
			linalg::inverse( linalg::pose_matrix(
				linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
				linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
			) ),
			linalg::translation_matrix( linalg::vec<double,3>( hit.x(), hit.y(), hit.z() ) )
		)[3].xyz();
		rb->setActivationState( 1 );
		auto impulse = linalg::normalize( ray.second - ray.first ) * impact;
		rb->applyImpulse( btVector3( impulse.x, impulse.y, impulse.z ),
			btVector3( localHit.x, localHit.y, localHit.z ) );
	}
	return hitAgent;
}

// Fire the agent's weapon. TODO: pellets, ammo cost, crouch_fire animation.
// Return { success, ray impact }
std::pair<bool,Agent*> Dungeon::agentFireWeapon( Agent *a, std::string weapon, std::string attackSkill, std::string defenseSkill ){
	if( !a ) return { false, nullptr };
	if( weapon.empty() ) weapon = a->weapon;
	auto it = weapons.find( weapon );
	if( it == weapons.end() ) return { false, nullptr };
	auto &weap = it->second;
	if( weap.clipMax > 0.0 ){
		// The weapon requires a clip and consumes ammo.
		double cost = 1.0;
		// Find the weapon in the agent's inventory.
		InventoryInstance *instance = nullptr;
		for( auto &instance_ref : a->inventory ){
			if( instance_ref.weapon == a->weapon ){
				instance = &instance_ref;
				break;
			}
		}
		if( !instance || instance->count == 0 || instance->clip < cost )
			return { false, nullptr };
		instance->clip = std::max( instance->clip - cost, 0.0 );
	}
	// Offset agent angle BEFORE the shot, accuracy/recoil depending on stats.
	double recoil = weap.recoil;
	if( attackSkill.length() > 0 ){
		auto attack_skill_it = a->skills.find( attackSkill );
		if( attack_skill_it != a->skills.end() ){
			// Attack skill affects recoil.
			// Simplified from the quadratic regression provided by this tool:
			// https://planetcalc.com/8735/
			// If skill <= 1.0, recoil is the weapon's base recoil.
			// If skill == 10.0, recoil is about half.
			// If skill == 100.0, recoil is about one fourth.
			// If skill == 500.0, recoil is about one thirteenth.
			if( attack_skill_it->second > 1.0 )
				recoil /= ( 0.0228 * attack_skill_it->second + 1.4 );
		}
	}
	double angle = (std::rand() % 1024) / 1024.0
		* (weap.recoilCompensator ? 6.28318 : 3.14159);
	rotateAgentConstrained(
		a,
		{std::sin(angle) * recoil, std::cos(angle) * recoil, 0.0}
	);
	// Fire. TODO: crouch_fire
	a->weaponCooldown = 1.0 / weap.speed;
	a->weaponFireSpotCooldown = weap.fireSpotDuration;
	a->weaponTime = weap.animationFire.x;
	auto fire_it = a->avatar.bodyAnimations.find("stand_fire");
	if( fire_it != a->avatar.bodyAnimations.end() )
		a->avatar.bodyFrame = fire_it->second.x;
	bool pitch = a != playerAgent || !thirdPerson || weap.thirdPersonPitch;
	auto ray = getAgentGazeRay(a, weap.range, pitch);
	// Emit particles.
	auto ps_it = particleServers.find(weap.particleFire.texture);
	if(ps_it != particleServers.end()){
		double startScale = weap.particleFire.startScale,
			endScale = weap.particleFire.endScale,
			startMass = weap.particleFire.startMass,
			endMass = weap.particleFire.endMass,
			maxAge = weap.particleFire.maxAge;
		double svel = (endScale - startScale) / maxAge;
		double mvel = (endMass - startMass) / maxAge;
		// TODO: Randomly emit weap.particleFire.num particles.
		for(size_t i = 0; i < 1; ++i){
			Particle part{};
			part.tvel = linalg::normalize(ray.second - ray.first)
				* weap.particleFire.vel + a->lastVel;
			part.translation = ray.first;
			part.svel = svel;
			part.scale = startScale;
			part.mvel = mvel;
			part.mass = startMass;
			part.maxAge = maxAge;
			part.damage = weap.particleFire.damage;
			part.userptr = (void*)a;
			ps_it->second.particles.push_back(part);
		}
	}
	return {
		true,
		rayImpact(a, ray, weap.impact, weap.damage, defenseSkill)
	};
}

// Add a weapon to the agent's inventory.
void Dungeon::agentAddWeapon( Agent *a, const std::string &weapon ){
	for( auto &instance_ref : a->inventory ){
		if( instance_ref.weapon == weapon ){
			instance_ref.count++;
			return;
		}
	}
	InventoryInstance instance = {};
	instance.weapon = weapon;
	instance.count = 1;
	a->inventory.push_back( instance );
}

// Reload the clip for the specified weapon.
void Dungeon::agentReloadWeapon( Agent *a, const std::string &weapon ){
	// Find the global weapon definition.
	auto weap_it = weapons.find( weapon );
	if( weap_it == weapons.end() ) return;
	Weapon &weap = weap_it->second;
	// Find the agent's backup ammo.
	auto backup_it = a->ammo.find( weap.ammo );
	if( backup_it == a->ammo.end() ) return;
	double &backup = backup_it->second;
	// Transfer from backup to clip.
	for( auto &instance_ref : a->inventory ){
		if( instance_ref.weapon == weapon ){
			double transfer =
				std::min( weap.clipMax - instance_ref.clip, backup );
			if( transfer > 0.0 ){
				backup -= transfer;
				instance_ref.clip += transfer;
			}
			return;
		}
	}
}

// Returns whether or not reloading is possible (or advisable).
bool Dungeon::agentCanReloadCurrentWeapon( Agent *a ){
	// Find the global weapon definition.
	auto weap_it = weapons.find( a->weapon );
	if( weap_it == weapons.end() ) return false;
	// Find the agent's backup ammo.
	auto backup_it = a->ammo.find( weap_it->second.ammo );
	if( backup_it == a->ammo.end() ) return false;
	// Find the agent's weapon instance.
	for( auto &instance_ref : a->inventory ){
		if( instance_ref.weapon == a->weapon ){
			// Return true if there is backup ammo to fill the clip.
			return ( backup_it->second > 0.0
				&& weap_it->second.clipMax > instance_ref.clip );
		}
	}
	return false;
}

// Use mouse input to rotate the playerAgent with a constrained X angle.
void Dungeon::moveMouse( double x, double y ){
	if( playerAgent ){
		double f = -mouseSpeed / std::max( playerZoom, 1.0 );
		rotateAgentConstrained( playerAgent, { y * f, x * f, 0.0 } );
	}
}

// Prepare the light buffers.
void Dungeon::prepareLightBuffers( double d ){
	pointLights.clear();
	spotLights.clear();
	// Add partition lights at night only. Looks better and runs faster.
	// Consider them part of the power grid.
	// Draw them anyways if lightsAlwaysOn is true. (Underground areas etc.)
	if( shadowOffset.y < 0.0 || lights_always_on ){
		for( size_t i = 0; i < nearestPartitions.size(); i++ ){
			if( nearestPartitions[i].first >= partitions.size() )
				continue;
			auto &p = partitions[nearestPartitions[i].first];
			fgltf::AddToWorldLights(
				p.mapScene,
				linalg::translation_matrix( p.translation ),
				viewMat[3].xyz(),
				lightRange,
				pointLights,
				spotLights
			);
		}
	}
	// Add agent lights.
	Agent *agent_ptr = agents;
	while( agent_ptr ){
		if( agent_ptr->visible && (agent_ptr->health > 0.0 || agent_ptr->maxHealth == 0.0) && agent_ptr->model
			&& (agent_ptr->type != TYPE_VEHICLE || agent_ptr->driverAgent) ){
			auto agentMat = getAgentPoseMat( agent_ptr );
			if( agent_ptr->type == TYPE_AGENT ){
				// Origin at bottom of collider.
				agentMat[3].y -= agent_ptr->scale.y * 0.5;
			}
			// Add the model's lights to the world.
			fgltf::AddToWorldLights(
				*agent_ptr->model,
				agentMat,
				viewMat[3].xyz(),
				lightRange,
				pointLights,
				spotLights
			);
		}
		if( agent_ptr->lightOn ){
			// Add the agent's toggleable gaze spot.
			addAgentGazeSpot(
				agent_ptr,
				agent_ptr->lightColor,
				agent_ptr->lightRange,
				agent_ptr->lightCone.x,
				agent_ptr->lightCone.y
			);
		}
		if( agent_ptr != playerAgent
			&& agent_ptr->weaponFireSpotCooldown > 0.0 ){
			// Add the agent's weapon fire spot.
			Weapon *weap = nullptr;
			auto it = weapons.find( agent_ptr->weapon );
			if( it != weapons.end() ) weap = &it->second;
			if( weap ){
				addAgentGazeSpot(
					agent_ptr,
					weap->fireSpotColor,
					weap->fireSpotRange,
					weap->fireSpotCone.x,
					weap->fireSpotCone.y
				);
			}
			// Cool down the weapon fire spot.
			agent_ptr->weaponFireSpotCooldown -= d;
		}
		agent_ptr = agent_ptr->next;
	}
	playerFireSpotOn = false;
	if( playerAgent && playerAgent->weaponFireSpotCooldown > 0.0 ){
		// Add the player's weapon fire spot.
		Weapon *weap = nullptr;
		auto it = weapons.find( playerAgent->weapon );
		if( it != weapons.end() ) weap = &it->second;
		if( weap ){
			addAgentGazeSpot(
				playerAgent,
				weap->fireSpotColor,
				weap->fireSpotRange,
				weap->fireSpotCone.x,
				weap->fireSpotCone.y
			);
			playerFireSpotOn = true;
		}
		// Cool down the weapon fire spot.
		playerAgent->weaponFireSpotCooldown -= d;
	}
}

// Animate the water.
void Dungeon::simulateWater( float wscale, double d ){
	auto GetElevation = [&]( float u, float y, float v ){
		return
			( stb_perlin_noise3_seed( u * 4, y * 4, v * 4, 4, 4, 4, 0 )
			* 0.25f
			+ stb_perlin_noise3_seed( u * 8, y * 8, v * 8, 8, 8, 8, 1 )
			* 0.125f )
			* waterAmplitude;
	};

	float yscale = 1.0f;
	int wsize = pbr ? 16 : 8;
	static float perlinY;
	perlinY = std::fmod<float>( perlinY + waterSpeed * d, wsize );

	// Ease waterWaveHeight back down.
	waterWaveHeight -= 0.25 * waterAmplitude * waterSpeed * d;

	size_t vert_pos = 0, idx_pos = 0;

	// Generate the mesh.
	for( int x = 0; x <= wsize; x++ ){
		for( int z = 0; z <= wsize; z++ ){
			// Calculate UV coordinates.
			float
				u = x / (float)wsize,
				v = z / (float)wsize;
			float elev_left = 0.0f, elev_up = 0.0f;
			// TODO: Add STB_PERLIN_INCLUDED to stb_perlin.h.
			float elev = GetElevation( u, perlinY, v );
			if( elev > waterWaveHeight )
				waterWaveHeight = elev;
			// Steal elevations from previous vertices where possible.
			if( x == 0 )
				elev_left = GetElevation( -1.0f / wsize, perlinY, v );
			if( z == 0 )
				elev_up = GetElevation( u, perlinY, -1.0f / wsize );
			size_t idx = ( x - 1 ) * ( wsize + 1 ) + z;
			if( x && idx < waterMesh.vertices.size() )
				elev_left = waterMesh.vertices[idx].Position[1] / yscale;
			idx = x * ( wsize + 1 ) + z - 1;
			if( z && idx < waterMesh.vertices.size() )
				elev_up = waterMesh.vertices[idx].Position[1] / yscale;

			// Generate a normal vector based on the elevation of the
			// current vertex and the elevations of 2 adjacent vertices.
			/*
			auto vec_a = linalg::vec<float,3>( wscale / wsize, 0.0f, ( elev - elev_left ) * yscale );
			auto vec_b = linalg::vec<float,3>( 0.0f, wscale / wsize, ( elev - elev_up ) * yscale );
			auto norm = linalg::cross(
				linalg::normalize( vec_a ),
				linalg::normalize( vec_b )
			);
			*/
			auto norm = linalg::normalize( linalg::vec<float,3>(
				(elev_left - elev) / wscale * wsize,
				yscale,
				(elev_up - elev) / wscale * wsize
			) );
			// Add the vertex to the mesh.
			if( vert_pos >= waterMesh.vertices.size() ){
				waterMesh.vertices.resize( vert_pos + 1 );
			}
			waterMesh.vertices[vert_pos] = {
				{ x / (float)wsize * wscale, elev * yscale, z / (float)wsize * wscale },
				{ norm.x, norm.y, norm.z },
				{ 1.0f, 0.0f, 0.0f }, // Vertex shaders can correct tangents.
				{ u, v }
			};
			vert_pos++;

			if( x && z ){
				// Add the triangles to the vertex and neighbors.
				// Determine the indices from the x and z coordinates.
				if( idx_pos >= waterMesh.indices.size() ){
					waterMesh.indices.resize( idx_pos + 6 );
				}
				auto &inds = waterMesh.indices;
				inds[idx_pos    ] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z - 1 ); // Up and left by 1.
				inds[idx_pos + 1] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z ); // Left by 1.
				inds[idx_pos + 2] = (fgl::Index)( x * ( wsize + 1 ) + z ); // Current position.
				inds[idx_pos + 3] = (fgl::Index)( x * ( wsize + 1 ) + z );
				inds[idx_pos + 4] = (fgl::Index)( x * ( wsize + 1 ) + z - 1 ); // Up by 1.
				inds[idx_pos + 5] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z - 1 ); // Up and left by 1.
				idx_pos += 6;
			}
		}
	}

	// Resize the vectors if the new mesh is smaller than the old mesh.
	if( vert_pos < waterMesh.vertices.size() )
		waterMesh.vertices.resize( vert_pos );
	if( idx_pos < waterMesh.indices.size() )
		waterMesh.indices.resize( idx_pos );

	// Upload the mesh to graphics memory.
	if( waterMesh.success ){
		fgl::updateMesh( waterMesh, waterMesh.vertices, waterMesh.indices, true );
	}else{
		waterMesh = fgl::loadMesh( waterMesh.vertices, waterMesh.indices, true );
	}

	// Buffer water tiles.
	waterBuffer.clear();
	// Move the water with the camera.
	linalg::vec<double,3> player_offset = {
		std::floor( viewMat[3].x / wscale ) * wscale,
		0.0,
		std::floor( viewMat[3].z / wscale ) * wscale
	};
	/*
	//   * * *
	// * * * * *
	// * * * * *
	// * * * * *
	//   * * *
	for( int x = -2; x < 3; x++ ){
		for( int y = -2; y < 3; y++ ){
			// Skip corners.
			if( ( x == -2 || x == 2 ) && ( y == -2 || y == 2 ) )
				continue;
			linalg::vec<double,3> o = {
				x * wscale,
				waterLevel,
				y * wscale
			};
			waterBuffer.push(
				{ fgl::fogColor.r, fgl::fogColor.g, fgl::fogColor.b, 1.0f },
				linalg::translation_matrix<double>( o + player_offset )
			);
		}
	}
	*/
	/*
	for( int x = -4; x < 5; x++ ){
		for( int y = -4; y < 5; y++ ){
			linalg::vec<double,3> o = {
				x * wscale,
				waterLevel,
				y * wscale
			};
			waterBuffer.push(
				{ fgl::fogColor.r, fgl::fogColor.g, fgl::fogColor.b, 1.0f },
				linalg::translation_matrix<double>( o + player_offset )
			);
		}
	}
	*/
	// Send the fog color to the water instance pipeline.
	fgl::Color waterColor = fgl::getFog();
	waterColor.a = 1.0f;
	// Spiral out from the player.
	int distance = 1, direction = 0, x = 0, y = 0;
	// 100 seems to be the magic number on old NVidia cards.
	// However, overdraw can cause lag, so we'll use a conservative 64.
	for( int i = 0; i < 64; i++ ){
		linalg::vec<double,3> o = {
			x * wscale,
			waterLevel,
			y * wscale
		};
		waterBuffer.push(
			waterColor,
			linalg::translation_matrix<double>( o + player_offset ),
			linalg::identity
		);
		// Move the offset coordinates.
		if( direction == 0 ){
			x++;
			if( x >= distance ){
				direction = 1;
			}
		}else if( direction == 1 ){
			y++;
			if( y >= distance ){
				direction = 2;
			}
		}else if( direction == 2 ){
			x--;
			if( x <= -distance ){
				direction = 3;
			}
		}else{
			y--;
			if( y <= -distance ){
				direction = 0;
				distance++;
			}
		}
	}
	// Add larger water planes around the center area.
	for( int i = 0; i < 8; i++ ){
		static int offset_x[8] = {  5,  5,   -3, -11, -11, -11, -3, 5 };
		static int offset_y[8] = { -3, -11, -11, -11,  -3,   5,  5, 5 };
		linalg::vec<double,3> o = {
			offset_x[i] * wscale,
			waterLevel - waterWaveHeight * 1.5,
			offset_y[i] * wscale
		};
		waterBuffer.push(
			waterColor,
			linalg::mul(
				linalg::translation_matrix<double>( o + player_offset ),
				linalg::scaling_matrix<double>( { 8.0, 1.0, 8.0 } )
			),
			linalg::identity
		);
	}
	// Add the outermost water planes leagues away on the horizon.
	for( int i = 0; i < 8; i++ ){
		static int offset_x[8] = {  13,  13, -11, -35, -35, -35, -11, 13 };
		static int offset_y[8] = { -11, -35, -35, -35, -11,  13,  13, 13 };
		linalg::vec<double,3> o = {
			offset_x[i] * wscale,
			waterLevel - waterWaveHeight * 4.5,
			offset_y[i] * wscale
		};
		waterBuffer.push(
			waterColor,
			linalg::mul(
				linalg::translation_matrix<double>( o + player_offset ),
				linalg::scaling_matrix<double>( { 24.0, 2.0, 24.0 } )
			),
			linalg::identity
		);
	}
	// Upload attributes.
	waterBuffer.upload();
}

template<class T> linalg::mat<T,4,4> ortho(
		T left,
		T right,
		T bottom,
		T top,
		T near,
		T far ){
	return {
		{ 2 / (right - left), 0, 0, 0 },
		{ 0, 2 / (top - bottom), 0, 0 },
		// Oldschool OpenGL clip space.
		//{ 0, 0, 2 / (near - far), 0 },
		//{ -(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1 }
		// glClipControl 0-1 depth clip space.
		{ 0, 0, 1 / (near - far), 0 },
		{ -(right + left) / (right - left), -(top + bottom) / (top - bottom), near / (near - far), 1 }
	};
}

// Physics, shadow, and camera position handling.
void Dungeon::simulate( double maxDraw, double d, std::string defenseSkill ){
	if( !dynamicsWorld )
		return;
	simulateWater( 16.0f, d );
	dynamicsWorld->setGravity( btVector3( 0.0, gravity * -1.0, 0.0 ) );
	age += d;
	cloudScroll += cloudSpeed * d;
	// Play in-range partition animations.
	for( size_t i = 0; i < nearestPartitions.size(); i++ ){
		if( nearestPartitions[i].first >= partitions.size() )
			continue;
		auto &p = partitions[nearestPartitions[i].first];
		// Loop the glTF scene's animations.
		for( size_t j = 0; j < p.mapScene.animations.size(); j++ ){
			auto &anim = p.mapScene.animations[j];
			float animLength = 0.0f;
			for( auto &chan : anim.channels ){
				animLength = std::max( animLength, chan.timeline.back() );
			}
			float animTime = (float)std::fmod( age, (double)animLength );
			fgltf::SetSceneAnimation( p.mapScene, i, animTime );
		}
		if( p.mapScene.animations.size() > 0 ){
			// Update the physics data.
			fgltf::UpdateSceneDynamics(
				p.mapScene,
				linalg::translation_matrix( p.translation ),
				p.friction,
				p.restitution
			);
		}
	}
	if( d > 0.0 ){
		int iterations = 5;
		double physicsDelta = std::min( d, 0.1 ) / iterations;
		for( int i = 0; i < iterations; i++ ){
			if( dynamicsWorld->stepSimulation( physicsDelta, 0 ) > 0 ){
				updateCallback( physicsDelta );
			}
		}
	}else{
		dynamicsWorld->stepSimulation( 0.0, 0 );
	}

	if( playerAgent ){
		// The player agent exists. May be in vehicle fwiw.
		// Check for playerAgent/portal collisions.
		int numManifolds = dispatcher->getNumManifolds();
		for( int i = 0; i < numManifolds; i++ ){
			btPersistentManifold *mf =
				dispatcher->getManifoldByIndexInternal( i );
			btCollisionObject *ob0 = (btCollisionObject*)mf->getBody0();
			btCollisionObject *ob1 = (btCollisionObject*)mf->getBody1();
			std::string *name;
			// Get the name of any object colliding with playerAgent.
			if( ( ob0 == playerAgent->rigidBody
				&& ( name = (std::string*)ob1->getUserPointer() ) )
				|| ( ob1 == playerAgent->rigidBody
				&& ( name = (std::string*)ob0->getUserPointer() ) ) ){
				// Portals.
				if( portalCallback && name->length() >= 6
					&& name->substr( 0, 6 ) == "portal" ){
					std::string param =
						name->length() >= 8 ? name->substr( 7 ) : "";
					if(param == "kill")
						playerAgent->health = 0.0;
					else
						(*portalCallback)(param);
				}
			}
		}
		if( !playerAgent )
			return;
	}
	if( playerAgent ) updateMonoCamera();

	// Partition sort-em-up without reallocating nearestPartitions.
	linalg::vec<double,3> cameraPosition = viewMat[3].xyz();
	nearestPartitions.resize( 0 );
	for( size_t i = 0; i < partitions.size(); i++ ){
		nearestPartitions.push_back( {
			i,
			linalg::length( cameraPosition - partitions[i].translation )
		} );
	}
	std::sort( nearestPartitions.begin(), nearestPartitions.end(),
		[]( const std::pair<size_t,double> &a, const std::pair<size_t,double> &b ){
		return a.second < b.second;
	} );
	for( size_t i = 0; i < nearestPartitions.size(); i++ ){
		if( nearestPartitions[i].second > std::min(maxPartitionDistance, maxDraw) ){
			nearestPartitions.resize( i );
			break;
		}
	}

	// Simulate agents.
	Agent *agent_ptr = agents;
	while( agent_ptr ){
		// Falling damage.
		if( agent_ptr->type == TYPE_AGENT && agent_ptr->rigidBody
			&& agent_ptr->rigidBody->getUserPointer() != &defaultAgent ){
			auto v = agent_ptr->rigidBody->getLinearVelocity();
			linalg::vec<double,3> vel(v.getX(), v.getY(), v.getZ());
			double dist = linalg::length( vel - agent_ptr->lastVel );
			agent_ptr->lastVel = vel;
			if( dist > agent_ptr->collisionDamageThreshold ){
				double damage = dist * ( agent_ptr->parentAgent
					? agent_ptr->sittingCollisionDamageFactor
					: agent_ptr->collisionDamageFactor );
				agent_ptr->health -= damage;
				if( collisionDamageCallback )
					(*collisionDamageCallback)( agent_ptr, damage );
			}
		}
		// AI.
		if( aiFrameCallback && agent_ptr->rigidBody
			&& agent_ptr->rigidBody->getUserPointer() != &defaultAgent ){
			btVector3 &o =
				agent_ptr->rigidBody->getWorldTransform().getOrigin();
			linalg::vec<double,3> pos( o.getX(), o.getY(), o.getZ() );
			if( linalg::length( cameraPosition - pos ) > maxAIDistance ){
				// TODO: Out-of-range behavior.
			}else{
				// Run AI. Remove agent if aiFrameCallback returns true.
				if( (*aiFrameCallback)( agent_ptr, d ) ){
					// Flag the agent for removal and skip to the next agent.
					agent_ptr->health = 0.0;
					agent_ptr = agent_ptr->next;
					continue;
				}
			}
		}
		// Animations.
		if( agent_ptr->model ){
			fgltf::Scene &scn = *agent_ptr->model;
			// Play all agent model animations.
			float animLength = 0.0f;
			for( size_t i = 0; i < scn.animations.size(); i++ ){
				auto &anim = scn.animations[i];
				for( auto &chan : anim.channels ){
					animLength = std::max( animLength, chan.timeline.back() );
				}
			}
			float animTime = agent_ptr->modelLoop
				? (float)std::fmod( age, (double)animLength ) : (float)age;
			for( size_t i = 0; i < scn.animations.size(); i++ ){
				fgltf::SetSceneAnimation( scn, i, animTime );
			}
			//printf( "%f / %f\n", animTime, animLength );
		}else{
			// Simulate the avatar. TODO: Melee, non-physical walking.
			btVector3 velVec =
				agent_ptr->rigidBody ? agent_ptr->rigidBody->getLinearVelocity() : btVector3(0,0,0);
			double velHoriz = linalg::length(linalg::vec<double,2>(velVec.x(), velVec.z()));
			bool jumping = agent_ptr->type == TYPE_AGENT && agent_ptr->standing == 0.0;
			if( agent_ptr->health > 0.0 ){
				auto fire_it = agent_ptr->avatar.bodyAnimations.find("stand_fire");
				if( fire_it != agent_ptr->avatar.bodyAnimations.end()
					&& agent_ptr->avatar.bodyFrame >= fire_it->second.x
					&& agent_ptr->avatar.bodyFrame < fire_it->second.y ){
					// Standing fire animation.
					agent_ptr->avatar.simulate( "stand_fire", d, false );
				}else if( agent_ptr->eyeHeight < agent_ptr->scale.y - agent_ptr->scale.x * 0.5 ){
					// Crouching.
					if( velHoriz > 0.1 && !jumping ){
						// Crawl animation.
						agent_ptr->avatar.simulate( "crawl", d );
					}else{
						// Crouch idle animation.
						agent_ptr->avatar.simulate( "crouch_idle", d );
					}
				}else{
					// Not crouching.
					if( jumping ){
						// Jump animation.
						agent_ptr->avatar.simulate( "jump", d );
					}else if( velHoriz > 0.1 ){
						if( velHoriz < agent_ptr->walkSpeed * 1.5 ){
							// Walk animation.
							agent_ptr->avatar.simulate( "walk", d );
						}else{
							// Sprint animation.
							agent_ptr->avatar.simulate( "sprint", d );
						}
					}else{
						// Idle animation.
						agent_ptr->avatar.simulate( "idle", d );
					}
				}
			}else{
				// Die animation.
				agent_ptr->avatar.simulate( "die", d, false );
				if(agent_ptr->rigidBody){
					// Zero mass if standing, otherwise fall infinitely.
					if(agent_ptr->standing != 0.0){
						btVector3 btzero(0,0,0);
						agent_ptr->mass = 0.0;
						agent_ptr->rigidBody->setMassProps(0.0, btzero);
						// Zero velocity.
						agent_ptr->rigidBody->setLinearVelocity(btzero);
						agent_ptr->rigidBody->setAngularVelocity(btzero);
					}
					// Disable collisions.
					setAgentCollisions(agent_ptr, false);
				}
			}
		}
		agent_ptr = agent_ptr->next;
	}

	// Clean up dead agents.
	agent_ptr = agents;
	while( agent_ptr ){
		Agent *a = agent_ptr->next;
		if(agent_ptr && agent_ptr != playerAgent
			&& agent_ptr->health <= 0.0 && agent_ptr->maxHealth > 0.0){
			// Prevent this from triggering again by setting maxHealth negative.
			agent_ptr->maxHealth = -1.0;
			// Run the agent's death callback.
			if(portalCallback && agent_ptr->onDeath.length() > 0)
				(*portalCallback)( agent_ptr->onDeath );
			// Special case for static agents: Replace the physics model.
			if(agent_ptr->type == fdungeon::TYPE_STATIC)
				setAgentModel(agent_ptr, agent_ptr->modelDestroyedFile);
			// Remove the agent from the world (though probably not completely from memory until next level load).
			removeAgent( agent_ptr, true );
		}
		agent_ptr = a;
	}

	// Simulate particles.
	for( auto &ps : particleServers ){
		ps.second.simulate( d );
		// Simulate collisions and damage for damaging particles.
		for( auto &p : ps.second.particles ){
			if( p.damage != 0.0 ){
				// Cast a ray from the particle's previous position to its current position.
				// Cap mass to 10 kg because it may be set very high in order to cheat drag.
				// Ultra-high mass = ultra-high impact = objects clip through walls
				auto tprev = p.translation - p.tvel * d;
				if(rayImpact((Agent*)p.userptr, {tprev, p.translation},
					linalg::length(p.tvel) * std::min(p.mass, 10.0), p.damage, defenseSkill)){
					p.age = DBL_MAX;
				}
			}
		}
	}

	// Configure the shadow map.
	//shadowViewMat = linalg::pose_matrix( fgl::eulerToQuat( -0.7, 0.0, 0.0 ), { 0.0, 10.0, 20.0 } );
	/*
	fgl::setLightMatrix( linalg::mul(
		ortho<double>( -15.0, 15.0, -15.0, 15.0, 2.0, 40.0 ),
		linalg::lookat_matrix<double>( { 10.0, 7.2, 12.0 }, { 10.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0 } )
	) );
	*/
	// Avoid gimbal-locking the sun.
	if( shadowOffset.x == 0.0 && shadowOffset.z == 0.0 )
		shadowOffset.x = 0.0001;
	// Rotate the shadow map by about 45 degrees.
	// The calculation isn't perfect, but it avoids popping artifacts.
	lightOriginMat = linalg::mul(
		ortho<double>( -shadowScale, shadowScale, -shadowScale, shadowScale, shadowNearClip, shadowFarClip ),
		linalg::lookat_matrix<double>( shadowOffset, { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 1.0 } )
	);
	// Shoot the shadow in the gun direction 2-dimensionally. TODO: Verify this is working correctly.
	auto dir = linalg::normalize( linalg::transpose( viewMat )[2].xyz() ) * linalg::vec<double,3>( -1.0, 0.0, 1.0 );
	linalg::mat<double,4,4> lightMat = linalg::mul(
		lightOriginMat,
		linalg::translation_matrix( dir * shadowScale - viewMat[3].xyz() )
	);
	// Snap the light position to the nearest texel along X and Y in light space.
	double snapSize = 2.0 / shadowResolution;
	lightMat[3].x = std::floor( lightMat[3].x / snapSize ) * snapSize;
	lightMat[3].y = std::floor( lightMat[3].y / snapSize ) * snapSize;
	fgl::setLightMatrix( lightMat );

	if( shadowOffset.y >= 0.0 ){
		// Render to the multisample shadow map. Fill the shadow buffer.
		fgl::setFramebuffer(shadowHighRes ? shadowBufMultisample : shadowBufLowMultisample);
		glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
		drawSolid( maxDraw, true );
		fgl::setPipeline( particleShadowPipeline );
		fgl::Texture tex = fgl::getBlankTexture();
		for(auto &ps : particleServers)
			ps.second.draw(tex, linalg::identity, linalg::identity, true);

		// Blit the multisample shadow map to the non-multisample shadow map.
		fgl::setFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow);
		fgl::drawFramebuffer(shadowHighRes ? shadowBufMultisample : shadowBufLowMultisample);

		// Blur the shadow.
		if( !shadowBlurPipeline.success ) return;
		fgl::setPipeline( shadowBlurPipeline );
		// Reset the texture matrix.
		fgl::setTextureMatrix( linalg::identity );
		auto u_blurScale =
			glGetUniformLocation( shadowBlurPipeline.programObject, "u_blurScale" );
		float s = 1.4 / (shadowHighRes ? shadowResolution : shadowResolutionLow);
		// Shadow map blur (first pass)
		glUniform2f(
			u_blurScale,
			s,
			0.0
		);
		fgl::setFramebuffer(shadowHighRes ? shadowBufTemp : shadowBufLowTemp); // <-- to
		fgl::drawFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow); // from -->
		// Shadow map blur (second pass)
		glUniform2f(
			u_blurScale,
			0.0,
			s
		);
		fgl::setFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow); // <-- to
		fgl::drawFramebuffer(shadowHighRes ? shadowBufTemp : shadowBufLowTemp); // from -->
	}

	// Switch to the main framebuffer.
	fgl::setFramebuffer( *frameBuf, frameBufScale );

	// Prepare the light buffers.
	prepareLightBuffers( d );

	// Update viewMatL and viewMatR.
	updateStereoCamera();
}

// Create view and projection matrices based on the player's transform.
void Dungeon::updateMonoCamera(){
	if( playerAgent ){
		if( playerAgent->parentAgent
			&& playerAgent->parentAgent->driverAgent == playerAgent ){
			// In vehicle.
			// Parent the camera to the parent agent.
			viewMat = linalg::mul(
				getAgentPoseMat( playerAgent->parentAgent ),
				linalg::pose_matrix(
					fgl::eulerToQuat(
						playerAgent->angle.x,
						playerAgent->angle.y,
						playerAgent->angle.z ),
					playerAgent->parentAgent->driverEyeTranslation
				)
			);
		}else{
			// Not in vehicle.
			linalg::mat<double,4,4> poseMat = getAgentPoseMat( playerAgent );
			// Turn camera eulers into matrices.
			viewMat = linalg::pose_matrix(
				topDown
					? fgl::eulerToQuat(-1.570796, playerAgent->angle.y, 0.0)
					: fgl::eulerToQuat(playerAgent->angle.x,
						playerAgent->angle.y + (thirdPerson ? thirdPersonYawOffset : 0.0),
						playerAgent->angle.z),
				linalg::vec<double,3>(
					poseMat[3].x,
					topDown
						? farClip * 0.5
						: poseMat[3].y - playerAgent->scale.y * 0.5 + playerAgent->eyeHeight,
					poseMat[3].z
				)
			);
		}
	}
	// Offset the view matrix for third-person mode.
	if( playerAgent && thirdPerson && !topDown ){
		// Multiply in reverse order to move mat4 relative to its own orientation.
		auto destMat = linalg::mul(
			viewMat,
			linalg::translation_matrix(thirdPersonCameraTranslation)
		);
		// Use the foot sphere to check for intersections with physical geometry.
		btVector3 from(viewMat[3].x, viewMat[3].y, viewMat[3].z);
		btVector3 to(destMat[3].x, destMat[3].y, destMat[3].z);
		btTransform fromTrans, toTrans;
		fromTrans.setIdentity();
		fromTrans.setOrigin(from);
		toTrans.setIdentity();
		toTrans.setOrigin(to);
		// https://pybullet.org/Bullet/BulletFull/classbtClosestNotMeConvexResultCallback.html
		// A fixed version of Bullet's official class.
		btrapClosestNotMeConvexResultCallback rayResult(
			playerAgent->rigidBody,
			from,
			to,
			(btOverlappingPairCache*)overlappingPairCache
		);
		dynamicsWorld->convexSweepTest(playerAgent->footSphere, fromTrans, toTrans, rayResult);
		if(rayResult.hasHit()){
			viewMat[3].xyz() += (destMat[3].xyz() - viewMat[3].xyz())
				* rayResult.m_closestHitFraction;
		}else{
			viewMat = destMat;
		}
	}
	// Create the projection matrix.
	double aspect = (double)frameBuf->width / (double)frameBuf->height;
	projMat = topDown
		? ortho( -orthoRadius * aspect, orthoRadius * aspect, -orthoRadius, orthoRadius, nearClip, farClip )
		: linalg::perspective_matrix( verticalField / playerZoom, aspect, nearClip, farClip );
}

// Update viewMatL and viewMatR based on viewMat and ipd.
void Dungeon::updateStereoCamera(){
	// Make a copy of the original view matrix.
	viewMatCenter = viewMat;
	// Multiply in reverse order to move mat4 relative to its own orientation.
	// Cheat the IPD when zooming. (This should not affect VR unless you zoom in VR. o.O)
	viewMatL = linalg::mul(
		viewMat,
		linalg::pose_matrix(
			fgl::eulerToQuat(0.0, -stereoAngle / playerZoom, 0.0),
			linalg::vec<double,3>((ipd / playerZoom) * -0.5, 0.0, 0.0))
	);
	viewMatR = linalg::mul(
		viewMat,
		linalg::pose_matrix(
			fgl::eulerToQuat(0.0, stereoAngle / playerZoom, 0.0),
			linalg::vec<double,3>((ipd / playerZoom) * 0.5, 0.0, 0.0))
	);
}

// Switch to a dungeon rendering pipeline for non-instanced objects.
fgl::Pipeline Dungeon::useDungeonPipeline( bool skinPass, bool shadowPass ){
	if(!skinPipeline.success) skinPass = false;
	auto &pipeline = skinPass
		? skinPipeline
		: ( pbr ? pbrDungeonPipeline : dungeonPipeline );
	if( skinPass && shadowPass ){
		// Less optimal than a skin shadow pipeline, but better than nothing.
		fgl::setPipeline( skinPipeline );
		return skinPipeline;
	}else if( pipeline.success && !shadowPass ){
		fgl::setPipeline( pipeline );
		fgl::setTexture( irradiance, 4 );
		// Pass the shadow map texture to the shader.
		glActiveTexture( GL_TEXTURE3 );
		glBindTexture( GL_TEXTURE_2D, shadowHighRes ? shadowBuf.texture : shadowBufLow.texture );
		// TODO: Get rid of glGet* at render time as they can cost performance.
		glUniform1f(
			glGetUniformLocation( pipeline.programObject, "u_emissiveScale" ),
			emissive_scale
		);
		glUniform4f(
			glGetUniformLocation( pipeline.programObject, "u_ambient" ),
			ambient_light.r * lightBrightness,
			ambient_light.g * lightBrightness,
			ambient_light.b * lightBrightness,
			ambient_light.a
		);
		glUniform4f(
			glGetUniformLocation( pipeline.programObject, "u_underwater" ),
			underwater_color.r,
			underwater_color.g,
			underwater_color.b,
			underwater_color.a
		);
		glUniform2f(
			glGetUniformLocation( pipeline.programObject, "u_waterHeights" ),
			(float)waterLevel,
			waterWaveHeight
		);
		// Send the lights to the shader.
		glUniform1i(
			glGetUniformLocation( pipeline.programObject, "u_numPointLights" ),
			(GLint)( pointLights.size() / 8 )
		);
		glUniform4fv(
			glGetUniformLocation( pipeline.programObject, "u_pointLights" ),
			(GLsizei)( pointLights.size() / 4 ),
			pointLights.data()
		);
		glUniform1i(
			glGetUniformLocation( pipeline.programObject, "u_numSpotLights" ),
			(GLint)( spotLights.size() / 16 )
		);
		glUniform4fv(
			glGetUniformLocation( pipeline.programObject, "u_spotLights" ),
			(GLsizei)( spotLights.size() / 4 ),
			spotLights.data()
		);
		return pipeline;
	}else if( shadowPipeline.success && shadowPass ){
		fgl::setPipeline( shadowPipeline );
		return shadowPipeline;
	}else{
		fgl::Pipeline unlitPipeline = fgl::getUnlitPipeline();
		fgl::setPipeline( unlitPipeline );
		return unlitPipeline;
	}
}

// Draw solid world geometry and agents.
void Dungeon::drawSolid( double maxDraw, bool shadowPass, int agent_types ){
	glEnable( GL_DEPTH_TEST );
	glDisable( GL_BLEND );

	GLint u_outlineThickness = 0, u_outlineColor = 0;
	if(outline && !shadowPass && outlinePipeline.success){
		u_outlineThickness = glGetUniformLocation(outlinePipeline.programObject, "u_outlineThickness");
		u_outlineColor = glGetUniformLocation(outlinePipeline.programObject, "u_outlineColor");
	}

	if( outline && !topDown && !shadowPass && outlinePipeline.success ){
		// Draw outline hulls.
		// These are cheap enough that in theory, their occlusion of
		// more expensive faces behind them saves render time.
		fgl::setPipeline( outlinePipeline );
		glUniform1f(
			u_outlineThickness,
			outlineThickness / playerZoom
		);
		glUniform4f(
			u_outlineColor,
			outline_regular.r,
			outline_regular.g,
			outline_regular.b,
			outline_regular.a
		);
		glCullFace( GL_FRONT );
		size_t maxParts = nearestPartitions.size();
		// Hard-coded limit of 9 world partitons drawn in shadow maps.
		// This is currently not enabled because it breaks immersion.
		//if( maxParts > 9 && shadowPass ) maxParts = 9;
		for( size_t i = 0; i < maxParts; i++ ){
			if( nearestPartitions[i].first >= partitions.size() )
				continue;
			auto &p = partitions[nearestPartitions[i].first];
			// Draw glTF outline.
			fgltf::DrawScene(
				p.mapScene,
				linalg::translation_matrix( p.translation ),
				viewMat,
				projMat,
				farClip,
				true, // Disable material switching.
				fgltf::ALPHA_OPAQUE,
				fgltf::MESH_RIGID
			);
			if( p.terrain.mesh.success ){
				// Draw terrain outline.
				terra::DrawTerrain(
					p.terrain,
					p.translation,
					viewMat,
					projMat
				);
			}
		}
	}

	// Set a shader pipeline.
	useDungeonPipeline( false, shadowPass );

	fgl::Texture blankTexture = fgl::getBlankTexture();

	if( shadowPass && worldFloor != 0.0 ){
		// Draw the world floor (important to avoid VSM glitches).
		fgl::Mesh cube = fgl::getCubeMesh();
		double s = std::max( shadowScale, shadowFarClip ) * 20.0;
		auto oldFog = fgl::getFog();
		fgl::setFog({oldFog.r, oldFog.g, oldFog.b, 9999.0f});
		//fgl::setTexture(blankTexture, 0);
		fgl::drawMesh(
			cube,
			linalg::mul(
				linalg::translation_matrix<double>( { viewMat[3].x, worldFloor, viewMat[3].z } ),
				linalg::scaling_matrix<double>( { s, 0.0, s } )
			),
			viewMat,
			projMat
		);
		fgl::setFog(oldFog);
	}

	// Draw the opaque parts of the GLTF map.
	//glCullFace( shadowPass ? GL_FRONT : GL_BACK );
	glCullFace( GL_BACK );
	for( size_t i = 0; i < nearestPartitions.size(); i++ ){
		if( nearestPartitions[i].first >= partitions.size() )
			continue;
		auto &p = partitions[nearestPartitions[i].first];
		// Draw glTF.
		fgltf::DrawScene(
			p.mapScene,
			linalg::translation_matrix( p.translation ),
			viewMat,
			projMat,
			farClip,
			shadowPass,
			( fgltf::ALPHA_OPAQUE | fgltf::ALPHA_MASK ),
			fgltf::MESH_RIGID
		);
		if( p.terrain.mesh.success ){
			// Draw terrain.
			if( !shadowPass ){
				// Set diffuse texture. TODO: More efficient terrain texture access.
				fgl::setTexture(
					fgltf::images[fgltf::LoadImage(ModFopen, dungeonPath + "/", p.terrain.textureName)].texture,
					0
				);
				// Set metallic/roughness texture.
				fgl::setTexture( blankTexture, 1 );
				// Set metallic and roughness factors.
				fgl::setMetallicFactor( 0.0f );
				fgl::setRoughnessFactor( 0.8f );
				// Set base color and emissive factors.
				fgl::setBaseColorFactor( fgl::newColor );
				fgl::setEmissiveFactor( 0.0f, 0.0f, 0.0f );
			}
			terra::DrawTerrain(
				p.terrain,
				p.translation,
				viewMat,
				projMat
			);
		}
	}

	// Draw agents. TODO: Draw agent models with independently controlled animations.
	int passes = (outline && !topDown && !shadowPass && outlinePipeline.success) ? 3 : 2;
	for( int pass = 0; pass < passes; pass++ ){
		// The outline occlusion optimization above probably isn't that useful here.
		// Pass 0: Regular pipeline. Pass 1: Skins. Pass 2: Outlines.
		if( pass == 1 ){
			useDungeonPipeline( true, shadowPass );
		}else if( pass == 2 ){
			fgl::setPipeline( outlinePipeline );
			glUniform1f(
				u_outlineThickness,
				outlineThickness / playerZoom
			);
			glUniform4f(
				u_outlineColor,
				outline_regular.r,
				outline_regular.g,
				outline_regular.b,
				outline_regular.a
			);
			glCullFace( GL_FRONT );
		}
		Agent *agent_ptr = agents;
		while( agent_ptr ){
			if( (agent_ptr->type & agent_types)
				&& agent_ptr->visible && !agent_ptr->parentAgent
				&& (agent_ptr != playerAgent || thirdPerson) ){
				auto agentMat = getAgentPoseMat( agent_ptr );
				if( agent_ptr->type == TYPE_AGENT ){
					// Origin at bottom of collider.
					agentMat[3].y -= agent_ptr->scale.y * 0.5;
				}
				// Check if agent is within draw distance.
				if(linalg::length(viewMat[3].xyz() - agentMat[3].xyz()) < maxDraw){
					// Check for agent target match.
					if(pass == 2 && agent_ptr == targetAgent){
						glUniform4f(
							u_outlineColor,
							outline_highlight.r,
							outline_highlight.g,
							outline_highlight.b,
							outline_highlight.a
						);
					}
					if( agent_ptr->model ){
						// Draw the agent model.
						fgltf::DrawScene(
							(agent_ptr->health > 0.0 || agent_ptr->maxHealth == 0.0 || !agent_ptr->modelDestroyed)
								? *agent_ptr->model : *agent_ptr->modelDestroyed,
							agentMat,
							viewMat,
							projMat,
							farClip,
							shadowPass || ( pass == 2 ),
							( fgltf::ALPHA_OPAQUE | ( pass == 2 ? 0 : fgltf::ALPHA_MASK ) ),
							( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
						);
					}else{
						// Set default material settings.
						fgl::setBaseColorFactor( fgl::newColor );
						fgl::setMetallicFactor( 0.0f );
						fgl::setRoughnessFactor( 0.5f );
						fgl::setEmissiveFactor( 0.0f, 0.0f, 0.0f );
						// Set the metallic roughness texture.
						fgl::setTexture( blankTexture, 1 );
						// Set the emissive texture.
						fgl::setTexture( blankTexture, 2 );
						// Draw the agent's avatar.
						agent_ptr->avatar.draw(
							agentMat,
							viewMat,
							projMat,
							farClip,
							agent_ptr->weapon
						);
					}
					/*
					}else if( pass == 0 ){
						// Draw a generic box.
						fgl::drawMesh(
							fgl::cubeMesh,
							linalg::mul(
								getAgentPoseMat( agent_ptr ),
								linalg::scaling_matrix( agent_ptr->scale )
							),
							viewMat,
							projMat
						);
					}
					*/
					if(pass == 2 && agent_ptr == targetAgent){
						glUniform4f(
							u_outlineColor,
							outline_regular.r,
							outline_regular.g,
							outline_regular.b,
							outline_regular.a
						);
					}
				}
			}
			agent_ptr = agent_ptr->next;
		}
	}

	// Switch to normal face culling.
	glCullFace( GL_BACK );
}

void Dungeon::drawWater( bool shadowPass ){
	bool have_pipeline =
		waterInstancePipeline.success && waterTexture.success;
	if( have_pipeline && !shadowPass ){
		glBindFramebuffer( GL_FRAMEBUFFER, frameBuf->fbo );
		// Switch to the water instance pipeline.
		fgl::setPipeline( waterInstancePipeline );
		// Use the water texture for base color and alpha.
		fgl::setTexture( waterTexture, 0 );
		if( radiance.success && false ){
			// TODO: Radiance cubemap textures.
			// Use the radiance cubemap for reflective lighting.
			fgl::setTexture( radiance, 1 );
		}
		// Use the shadow map.
		glActiveTexture( GL_TEXTURE1 );
		glBindTexture( GL_TEXTURE_2D, shadowHighRes ? shadowBuf.texture : shadowBufLow.texture );
		// Send the underwater color to the shader.
		glUniform4f(
			glGetUniformLocation( waterInstancePipeline.programObject, "u_underwater" ),
			underwater_color.r,
			underwater_color.g,
			underwater_color.b,
			underwater_color.a
		);
		// Send the near and far clip distances to the shader.
		glUniform1f(
			glGetUniformLocation( waterInstancePipeline.programObject, "u_near" ),
			(float)nearClip
		);
		glUniform1f(
			glGetUniformLocation( waterInstancePipeline.programObject, "u_far" ),
			(float)farClip
		);
		// Send the lights to the shader.
		glUniform1i(
			glGetUniformLocation( waterInstancePipeline.programObject, "u_numSpotLights" ),
			(GLint)( spotLights.size() / 16 )
		);
		glUniform4fv(
			glGetUniformLocation( waterInstancePipeline.programObject, "u_spotLights" ),
			(GLsizei)( spotLights.size() / 4 ),
			spotLights.data()
		);
	}
	if( !shadowPass ){
		// Disable depth writes because water shaders read depth.
		glDepthMask( GL_FALSE );
		// Disable backface culling to view the surface from underwater.
		glDisable( GL_CULL_FACE );
		glEnable( GL_BLEND );
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	}
	// Draw the instance buffer.
	waterBuffer.draw( waterMesh, viewMat, projMat, fgl::getLightMatrix() );
	if( shadowPass ) return;
	glDepthMask( GL_TRUE );
	glEnable( GL_CULL_FACE );
}

// Draw the transparent objects.
void Dungeon::drawTransparent(){
	glEnable( GL_BLEND );
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	// Draw particles. TODO: Adjust drawing order.
	for( auto &ps : particleServers ){
		fgl::Texture *tex = getCacheTexture(ModFopen, particlePath + "/" + ps.first);
		ps.second.draw( *tex, viewMat, projMat );
	}

	// Set a shader pipeline.
	useDungeonPipeline( false, false );

	// Draw the transparent parts of the GLTF map. (Partition by partition back-to-front.)
	for( int i = (int)nearestPartitions.size() - 1; i >= 0; i-- ){
		if( nearestPartitions[i].first >= partitions.size() )
			continue;
		auto &p = partitions[nearestPartitions[i].first];
		fgltf::DrawScene(
			p.mapScene,
			linalg::translation_matrix( p.translation ),
			viewMat,
			projMat,
			farClip,
			false,
			fgltf::ALPHA_BLEND,
			fgltf::MESH_RIGID
		);
	}
}

// Draw everything. You should call simulate( d ) before calling draw().
void Dungeon::draw( double maxDraw, bool allowTransparent, fgl::Mesh *nodeMesh, int agent_types ){
	Weapon *weap = nullptr;
	linalg::mat<double,4,4> weapModelMat;
	useDungeonPipeline( false, false );
	auto depthCrunch =
		glGetUniformLocation( fgl::getPipeline().programObject, "u_depthCrunch" );
	if( playerAgent && !thirdPerson && !topDown ){
		auto it = weapons.find( playerAgent->weapon );
		if( it != weapons.end() ) weap = &it->second;
		if( weap ){
			// Get a factor from unzoomed to fully zoomed.
			double f = ( weap->zoom > 1.0 )
				? ( playerWeaponZoom - 1.0 ) / ( weap->zoom - 1.0 )
				: 0.0;
			// Calculate a model matrix.
			linalg::vec<double,3> b( 0.0, playerAgent->weaponBob, 0.0 );
			weapModelMat = linalg::mul(
				viewMatCenter,
				linalg::rotation_matrix(fgl::eulerToQuat(playerOffsetAngle.x, playerOffsetAngle.y, playerOffsetAngle.z)),
				linalg::translation_matrix(linalg::lerp(weap->translation + b, weap->zoomTranslation + b, f))
			);
			// Set the weapon animation timestamp.
			for( size_t i = 0; i < weap->model->animations.size(); i++ ){
				fgltf::SetSceneAnimation( *weap->model, i, playerAgent->weaponTime );
			}
		}
		// Hide the player's weapon spotlight.
		if( playerFireSpotOn ){
			glUniform1i(
				glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
				(GLint)std::max( (int)spotLights.size() / 16 - 1, 0 )
			);
		}
		glEnable( GL_DEPTH_TEST );
		int passes = ( weap && weap->model ) ? 3 : 0;
		GLint u_outlineThickness = (passes == 3)
			? glGetUniformLocation(outlinePipeline.programObject, "u_outlineThickness")
			: 0;
		for( int pass = 0; pass < passes; pass++ ){
			// Pass 0: Normal pipeline. Pass 1: Skins. Pass 2: Outlines.
			if( pass == 1 ){
				//fgl::setPipeline( skinPipeline );
				useDungeonPipeline( true, false );
			}else if( pass == 2 ){
				fgl::setPipeline( outlinePipeline );
				glUniform1f(
					u_outlineThickness,
					outlineThickness / playerZoom
				);
				glCullFace( GL_FRONT );
			}
			if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.5f );
			// Draw the weapon model.
			fgltf::DrawScene(
				*weap->model,
				weapModelMat,
				viewMat,
				projMat,
				farClip,
				( pass == 2 ),
				( fgltf::ALPHA_OPAQUE | ( pass == 2 ? 0 : fgltf::ALPHA_MASK ) ),
				( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
			);
			if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.0f );
		}
	}
	// Show the player's weapon spotlight.
	if( playerFireSpotOn ){
		glUniform1i(
			glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
			(GLint)( spotLights.size() / 16 )
		);
	}
	drawSolid( maxDraw, false, agent_types );
	// Draw the sky after drawing solid geometry.
	if( draw_cube && !topDown ){
		setPipeline( skyPipeline );
		glDisable( GL_CULL_FACE );
		fgl::Mesh cube = fgl::getCubeMesh();
		// Extract the 3x3 rotation matrix from the view matrix.
		// The camera is effectively at <0,0,0> with the skybox.
		drawMesh(
			cube,
			linalg::identity,
			linalg::mat<double,4,4>(
				{ viewMat[0][0], viewMat[0][1], viewMat[0][2], 0.0 },
				{ viewMat[1][0], viewMat[1][1], viewMat[1][2], 0.0 },
				{ viewMat[2][0], viewMat[2][1], viewMat[2][2], 0.0 },
				{ 0.0,           0.0,           0.0,           1.0 }
			),
			projMat
		);
		glEnable( GL_CULL_FACE );
		// Skybox.
		//fgl::drawSkybox( radiance, viewMat, projMat, ambient_light );
	}else{
		// Solid color.
		fgl::cls( fgl::getFog(), false );
	}
	// Draw clouds.
	if( draw_clouds ){
		fgl::setPipeline( cloudsPipeline );
		glDepthMask( GL_FALSE );
		glEnable( GL_BLEND );
		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
		fgl::setTexture( cloudsTexture, 0 );
		auto old_fog = fgl::getFog();
		fgl::setFog( { 0.0f, 0.0f, 0.0f, 0.01f } );
		auto planeMesh = fgl::getPlaneMesh();
		for( int i = 1; i <= 2; i++ ){
			fgl::setTextureMatrix( linalg::mul(
				linalg::translation_matrix(linalg::vec<double,3>(cloudScroll * i, 0.0, 0.0)),
				linalg::scaling_matrix(linalg::vec<double,3>(6.5 / i, 6.5 / i, 6.5 / i))
			) );
			fgl::drawMesh(
				planeMesh,
				linalg::mul(
					//linalg::translation_matrix(linalg::vec<double,3>(viewMat[3].x, viewMat[3].y + 20.0 - i * 0.1, viewMat[3].z)),
					linalg::rotation_matrix(fgl::eulerToQuat(1.570796327, 0.0, 0.0)),
					linalg::scaling_matrix(linalg::vec<double,3>(1200.0, 1200.0, 1200.0))
				),
				linalg::mat<double,4,4>(
					{ viewMat[0][0], viewMat[0][1], viewMat[0][2], 0.0 },
					{ viewMat[1][0], viewMat[1][1], viewMat[1][2], 0.0 },
					{ viewMat[2][0], viewMat[2][1], viewMat[2][2], 0.0 },
					{ 0.0,          i * 0.1 - 20.0, 0.0,           1.0 }
				),
				projMat
			);
		}
		fgl::setFog( old_fog );
		fgl::setTextureMatrix( linalg::identity );
		glDepthMask( GL_TRUE );
	}
	if( nodeMesh ){
		useDungeonPipeline( false, false );
		// Set default material settings.
		fgl::setBaseColorFactor( {0.0f, 0.0f, 0.9f, 0.4f} );
		fgl::setMetallicFactor( 0.0f );
		fgl::setRoughnessFactor( 0.5f );
		fgl::setEmissiveFactor( 0.0f, 0.0f, 0.1f );
		fgl::Texture blankTexture = fgl::getBlankTexture();
		// Set the diffuse texture.
		fgl::setTexture( blankTexture, 0 );
		// Set the metallic roughness texture.
		fgl::setTexture( blankTexture, 1 );
		// Set the emissive texture.
		fgl::setTexture( blankTexture, 2 );
		glCullFace( GL_BACK );
		glDisable( GL_BLEND );
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		for( auto &n : worldNodes ){
			auto m = linalg::pose_matrix( n.rotation, n.translation );
			fgl::drawMesh( *nodeMesh, m, viewMat, projMat );
			if( topDown && depthCrunch != -1 ){
				// Draw a transparent version of the mesh over the top.
				glUniform1f( depthCrunch, 0.9f );
				glEnable( GL_BLEND );
				fgl::drawMesh( *nodeMesh, m, viewMat, projMat );
				glDisable( GL_BLEND );
				glUniform1f( depthCrunch, 0.0f );
			}
		}
	}
	if( allowTransparent ){
		drawWater();
		drawTransparent();
		int passes = ( weap && weap->model && !topDown ) ? 2 : 0;
		for( int pass = 0; pass < passes; pass++ ){
			// Pass 0: Normal pipeline. Pass 1: Skins.
			if( pass == 1 ){
				//fgl::setPipeline( skinPipeline );
				useDungeonPipeline( true, false );
			}
			if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.5f );
			// Hide the player's weapon spotlight.
			if( playerFireSpotOn ){
				glUniform1i(
					glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
					(GLint)std::max( (int)spotLights.size() / 16 - 1, 0 )
				);
			}
			// Draw the weapon model.
			fgltf::DrawScene(
				*weap->model,
				weapModelMat,
				viewMat,
				projMat,
				farClip,
				false,
				fgltf::ALPHA_BLEND,
				( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
			);
			if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.0f );
		}
	}
	// glGetError can cause a full pipeline sync, so disable for performance.
	// TODO: Make this toggleable.
	if( false ){
		GLenum glerr;
		while( ( glerr = glGetError() ) != GL_NO_ERROR ){
			printf( "OpenGL error: 0x%.4X\n", glerr );
		}
	}
}

// Add a spotlight to the buffer without sending it to the shader.
void Dungeon::addAgentGazeSpot( Agent *a, fgl::Color color, GLfloat range, GLfloat innerConeAngle, GLfloat outerConeAngle ){
	if( !a ) return;
	auto ray = getAgentGazeRay( a, 1.0 );
	auto dir = ray.first - ray.second;
	// Offset the light from the eye to mitigate light glitches.
	ray.first += dir * 0.05;
	GLfloat lightAngleScale =
		1.0f / std::max(0.001f, std::cos(innerConeAngle) - std::cos(outerConeAngle));
	GLfloat lightAngleOffset = -std::cos(outerConeAngle) * lightAngleScale;
	spotLights.insert( spotLights.end(), {
		(GLfloat)ray.first.x, (GLfloat)ray.first.y + 0.03f, (GLfloat)ray.first.z, range,
		color.r, color.g, color.b, color.a,
		(GLfloat)dir.x, (GLfloat)dir.y, (GLfloat)dir.z, 0.0f,
		lightAngleScale, lightAngleOffset, 0.0f, 0.0f
	} );
}

// Spawn agents where specified.
void Dungeon::sceneNodeHandler( fgltf::SceneNode &node, linalg::mat<double,4,4> &m ){
	// Get a look-at vector.
	auto look = getNodeLookAt( m );
	// Get an FPS-style Euler X angle from the look-at vector.
	double angle_x = atan2(
		look.y,
		linalg::length<double,2>( { look.x, look.z } )
	);
	// Get an FPS-style Euler Y angle from the look-at vector.
	double angle_y = std::atan2( look.x, look.z );
	// Set the object's userPointer to the node name since that
	// currently contains the most useful information.
	if( node.rigidBody ){
		node.rigidBody->setUserPointer( &node.name );
	}
	if( node.name == "spawn" ){
		// Reposition the player agent or create a new one.
		if( playerAgent ){
			positionAgent( playerAgent, m[3].xyz() );
			playerAgent->angle.x = angle_x;
			playerAgent->angle.y = angle_y;
		}else{
			// Create an agent centered at the node's coordinates.
			if( playerJSON.length() > 0 ){
				playerAgent = parseAgent( playerJSON, "playerJSON", m );
			}else{
				playerAgent = loadAgent( playerAgentFile, m );
			}
			// Assign properties.
			if( playerAgent ){
				playerAgent->nodeName = node.name;
				playerAgent->angle.x = angle_x;
				playerAgent->angle.y = angle_y;
			}
		}
	}else if( node.name.length() > 6
		&& node.name.substr( 0, 5 ) == "agent" ){
		// Create an arbitrary agent controlled by script.
		Agent *agent = loadAgent( node.name.substr( 6 ), m );
		// Assign additional properties.
		if( agent ){
			agent->nodeName = node.name;
			agent->angle.x = angle_x;
			agent->angle.y = angle_y;
		}
	}else if( node.name.length() >= 8
		&& node.name.substr( 0, 8 ) == "waypoint" ){
		// Add a waypoint.
		waypoints.push_back( m[3] );
	}else if( ( node.name.length() >= 5
		&& node.name.substr( 0, 5 ) == "ghost" )
		|| ( node.name.length() >= 6
		&& node.name.substr( 0, 6 ) == "portal" ) ){
		// This only runs if the node has a rigid body.
		// You must make portals out of regular geometry.
		if( node.rigidBody ){
			// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=3997
			// Disable collisions.
			node.rigidBody->setCollisionFlags(
				node.rigidBody->getCollisionFlags()
				| btCollisionObject::CF_NO_CONTACT_RESPONSE
			);
		}
	}
}

void Dungeon::worldNodeHandler( WorldNode &node, linalg::mat<double,4,4> &m ){
	fgltf::SceneNode snode = {};
	snode.name = node.name;
	snode.rotation = node.rotation;
	snode.translation = node.translation;
	snode.rigidBody = node.rigidBody;
	sceneNodeHandler( snode, m );
}

// Physics update callback. This only influences the physics engine and
// tries to converge the simulation rather than move objects over time.
// tl;dr don't put animation code in this function!
void Dungeon::updateCallback( double d ){
	Agent *a = agents;
	while( a ){
		if( a->type != TYPE_STATIC && a->rigidBody ){
			// Agent buoyancy.
			double buoyancy = 20.0;
			auto trans = a->rigidBody->getWorldTransform();
			btVector3 o = trans.getOrigin();
			double y = o.getY();
			if( a->type == TYPE_VEHICLE ){
				buoyancy = 12.0;
				auto center =
					linalg::vec<double,3>( 0.0, a->scale.y * 0.5 + a->wheelFloor, 0.0 );
				btQuaternion r = trans.getRotation();
				y = linalg::mul(
					linalg::pose_matrix(
						linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
						linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
					),
					linalg::translation_matrix( center )
				)[3].y;
			}
			double f = std::min(
				std::max( waterLevel + waterWaveHeight * 0.25 - y, 0.0 )
				/ waterWaveHeight, 1.0 );
			a->rigidBody->applyCentralForce( btVector3( 0.0, f * a->mass * buoyancy, 0.0 ) );
			a->rigidBody->setDamping( f * 0.3, f * 0.15 );
			if( a->waterHealthRate != 0.0 ){
				// Handle water health effects.
				a->health += f * a->waterHealthRate * d;
			}
		}
		if( a->type == TYPE_VEHICLE && a->driverAgent ){
			steerVehicle( a );
		}
		a = a->next;
	}
}

// Steer a vehicle. This should only be called in updateCallback.
void Dungeon::steerVehicle( Agent *a ){
	if( a->raycastVehicle->getNumWheels() == 2 ){
		btTransform trans = a->rigidBody->getWorldTransform();
		btQuaternion r = trans.getRotation();
		auto rotationMat = linalg::rotation_matrix(
			linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
		);
		auto end = linalg::mul(
			rotationMat,
			linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
		)[3].xyz();
		double pitch = atan2(
			end.y,
			linalg::length<double,2>( { end.x, end.z } )
		);
		auto side = linalg::mul(
			rotationMat,
			linalg::translation_matrix( linalg::vec<double,3>( 1.0, 0.0, 0.0 ) )
		)[3].xyz();
		double lean = atan2(
			side.y,
			linalg::length<double,2>( { side.x, side.z } )
		);
		double maxLean =
			std::max( a->maxLean - std::max( std::abs( pitch ) - 0.2, 0.0 ), 0.0 );
		rotationMat = linalg::mul(
			linalg::inverse( linalg::rotation_matrix( fgl::eulerToQuat( 0.0, 0.0, -a->turn / a->maxTurn * maxLean - lean ) ) ),
			linalg::inverse( rotationMat )
		);
		a->rigidBody->setWorldTransform( btTransform(
			btMatrix3x3(rotationMat[0][0], rotationMat[0][1], rotationMat[0][2],
						rotationMat[1][0], rotationMat[1][1], rotationMat[1][2],
						rotationMat[2][0], rotationMat[2][1], rotationMat[2][2]),
			trans.getOrigin()
		) );
		a->rigidBody->setDamping(
			a->rigidBody->getLinearDamping(),
			a->turn == 0.0 ? 0.8 : 0.5
		);
		//if( a->rigidBody->getMotionState() )
		//	a->rigidBody->getMotionState()->setWorldTransform( trans );
	}
	// Set the front wheel angle to the turn value.
	a->raycastVehicle->setSteeringValue( a->turn, 0 );
	if( a->raycastVehicle->getNumWheels() == 4 )
		a->raycastVehicle->setSteeringValue( a->turn, 1 );
}

#endif // ifndef FDUNGEON_SEPARATE_COMPILATION

} // namespace fdungeon

#endif // ifndef FDUNGEON_CPP


Mode Type Size Ref File
100644 blob 170 42b08f467f099371d96fc6f35757c4510bfe7987 .gitignore
100644 blob 13711 25dbc408cf6dce13f6aac062d31879240d7f4f5e Makefile
100644 blob 2396 fc392e2bdb9b9ac4abb5fded56a32df18102c407 README.md
040000 tree - 56f2c438287fb1f800a0493d6b0d6907dc648241 base
100644 blob 487269 29cfd3578eb40b1f039e271bcaa81af49d1b7f3c gamecontrollerdb.txt
040000 tree - 99c807d76953d139e1c10f9f5c053455b9a79d94 include
100755 blob 257 5da83586fddf2984c55ed40e03f43c504552e8aa makeanib
100755 blob 677 e3ce6d4069311dcbb89f1d1166a00a2f9d2934b5 package-steam
100644 blob 879 1aa6cc46749b8ad10c792dd501a23d62886ae951 package-steam-build-demo.vdf
100644 blob 887 accdaa60338652380422b5c2caa0651cc8b0ea7e package-steam-build-playtest.vdf
100644 blob 879 0b12a202f0ef30f781c82678a68c9a5093365e7e package-steam-build.vdf
100644 blob 1182 81610ae8ed0d39b7b1412ef22eff86289fe2adb2 rsod.cbp
040000 tree - 43b2388b0f36e437807a966f6cf9a0dfae5fef7a src
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/mse/RSOD

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/mse/RSOD

Clone this repository using git:
git clone git://git.rocketgit.com/user/mse/RSOD

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main