init
This commit is contained in:
commit
87bff164f1
416 files changed
+279512
No files matched your search
@@ -0,0 +1,406 @@
|
||||
// Generated by tools/gen_api_names.py from lua_api_documentation.txt. Do not edit.
|
||||
"EntityLoad",
|
||||
"EntityLoadEndGameItem",
|
||||
"EntityLoadCameraBound",
|
||||
"EntityLoadToEntity",
|
||||
"EntitySave",
|
||||
"EntityCreateNew",
|
||||
"EntityKill",
|
||||
"EntityGetIsAlive",
|
||||
"EntityAddComponent",
|
||||
"EntityRemoveComponent",
|
||||
"EntityGetAllComponents",
|
||||
"EntityGetComponent",
|
||||
"EntityGetFirstComponent",
|
||||
"EntityGetComponentIncludingDisabled",
|
||||
"EntityGetFirstComponentIncludingDisabled",
|
||||
"EntitySetTransform",
|
||||
"EntityApplyTransform",
|
||||
"EntityGetTransform",
|
||||
"EntityAddChild",
|
||||
"EntityGetAllChildren",
|
||||
"EntityGetParent",
|
||||
"EntityGetRootEntity",
|
||||
"EntityRemoveFromParent",
|
||||
"EntitySetComponentsWithTagEnabled",
|
||||
"EntitySetComponentIsEnabled",
|
||||
"EntityGetName",
|
||||
"EntitySetName",
|
||||
"EntityGetTags",
|
||||
"EntityGetWithTag",
|
||||
"EntityGetInRadius",
|
||||
"EntityGetInRadiusWithTag",
|
||||
"EntityGetClosest",
|
||||
"EntityGetClosestWithTag",
|
||||
"EntityGetWithName",
|
||||
"EntityAddTag",
|
||||
"EntityRemoveTag",
|
||||
"EntityHasTag",
|
||||
"EntityGetFilename",
|
||||
"EntitiesGetMaxID",
|
||||
"ComponentGetValue",
|
||||
"ComponentGetValueBool",
|
||||
"ComponentGetValueInt",
|
||||
"ComponentGetValueFloat",
|
||||
"ComponentGetValueVector2",
|
||||
"ComponentSetValue",
|
||||
"ComponentSetValueVector2",
|
||||
"ComponentSetValueValueRange",
|
||||
"ComponentSetValueValueRangeInt",
|
||||
"ComponentSetMetaCustom",
|
||||
"ComponentGetMetaCustom",
|
||||
"ComponentObjectGetValue",
|
||||
"ComponentObjectSetValue",
|
||||
"ComponentAddTag",
|
||||
"ComponentRemoveTag",
|
||||
"ComponentGetTags",
|
||||
"ComponentHasTag",
|
||||
"ComponentGetValue2",
|
||||
"ComponentSetValue2",
|
||||
"ComponentObjectGetValue2",
|
||||
"ComponentObjectSetValue2",
|
||||
"EntityAddComponent2",
|
||||
"ComponentGetVectorSize",
|
||||
"ComponentGetVectorValue",
|
||||
"ComponentGetVector",
|
||||
"ComponentGetIsEnabled",
|
||||
"ComponentGetEntity",
|
||||
"ComponentGetMembers",
|
||||
"ComponentObjectGetMembers",
|
||||
"ComponentGetTypeName",
|
||||
"GetUpdatedEntityID",
|
||||
"GetUpdatedComponentID",
|
||||
"SetTimeOut",
|
||||
"RegisterSpawnFunction",
|
||||
"SpawnActionItem",
|
||||
"SpawnStash",
|
||||
"SpawnApparition",
|
||||
"LoadEntityToStash",
|
||||
"AddMaterialInventoryMaterial",
|
||||
"RemoveMaterialInventoryMaterial",
|
||||
"GetMaterialInventoryMainMaterial",
|
||||
"GameScreenshake",
|
||||
"GameOnCompleted",
|
||||
"GameGiveAchievement",
|
||||
"GameDoEnding2",
|
||||
"GetParallelWorldPosition",
|
||||
"BiomeMapLoad_KeepPlayer",
|
||||
"BiomeMapLoad",
|
||||
"BiomeSetValue",
|
||||
"BiomeGetValue",
|
||||
"BiomeObjectSetValue",
|
||||
"BiomeVegetationSetValue",
|
||||
"BiomeMaterialSetValue",
|
||||
"BiomeMaterialGetValue",
|
||||
"GameIsIntroPlaying",
|
||||
"GameGetIsGamepadConnected",
|
||||
"GameGetWorldStateEntity",
|
||||
"GameGetPlayerStatsEntity",
|
||||
"GameGetOrbCountAllTime",
|
||||
"GameGetOrbCountThisRun",
|
||||
"GameGetOrbCollectedThisRun",
|
||||
"GameGetOrbCollectedAllTime",
|
||||
"GameClearOrbsFoundThisRun",
|
||||
"GameGetOrbCountTotal",
|
||||
"CellFactory_GetName",
|
||||
"CellFactory_GetType",
|
||||
"CellFactory_GetUIName",
|
||||
"CellFactory_GetAllLiquids",
|
||||
"CellFactory_GetAllSands",
|
||||
"CellFactory_GetAllGases",
|
||||
"CellFactory_GetAllFires",
|
||||
"CellFactory_GetAllSolids",
|
||||
"CellFactory_GetTags",
|
||||
"CellFactory_HasTag",
|
||||
"GameGetCameraPos",
|
||||
"GameSetCameraPos",
|
||||
"GameSetCameraFree",
|
||||
"GameGetCameraBounds",
|
||||
"GameRegenItemAction",
|
||||
"GameRegenItemActionsInContainer",
|
||||
"GameRegenItemActionsInPlayer",
|
||||
"GameKillInventoryItem",
|
||||
"GamePickUpInventoryItem",
|
||||
"GameGetAllInventoryItems",
|
||||
"GameDropAllItems",
|
||||
"GameDropPlayerInventoryItems",
|
||||
"GameDestroyInventoryItems",
|
||||
"GameIsInventoryOpen",
|
||||
"GameTriggerGameOver",
|
||||
"LoadPixelScene",
|
||||
"LoadBackgroundSprite",
|
||||
"RemovePixelSceneBackgroundSprite",
|
||||
"RemovePixelSceneBackgroundSprites",
|
||||
"GameCreateCosmeticParticle",
|
||||
"GameCreateParticle",
|
||||
"GameCreateSpriteForXFrames",
|
||||
"GameShootProjectile",
|
||||
"EntityInflictDamage",
|
||||
"EntityIngestMaterial",
|
||||
"EntityRemoveIngestionStatusEffect",
|
||||
"EntityRemoveStainStatusEffect",
|
||||
"EntityAddRandomStains",
|
||||
"EntitySetDamageFromMaterial",
|
||||
"EntityRefreshSprite",
|
||||
"EntityGetWandCapacity",
|
||||
"EntityGetHotspot",
|
||||
"GamePlayAnimation",
|
||||
"GameGetVelocityCompVelocity",
|
||||
"GameGetGameEffect",
|
||||
"GameGetGameEffectCount",
|
||||
"LoadGameEffectEntityTo",
|
||||
"GetGameEffectLoadTo",
|
||||
"PolymorphTableAddEntity",
|
||||
"PolymorphTableRemoveEntity",
|
||||
"PolymorphTableGet",
|
||||
"PolymorphTableSet",
|
||||
"SetPlayerSpawnLocation",
|
||||
"UnlockItem",
|
||||
"GameGetPotionColorUint",
|
||||
"EntityGetFirstHitboxCenter",
|
||||
"Raytrace",
|
||||
"RaytraceSurfaces",
|
||||
"RaytraceSurfacesAndLiquiform",
|
||||
"RaytracePlatforms",
|
||||
"FindFreePositionForBody",
|
||||
"GetSurfaceNormal",
|
||||
"GameGetSkyVisibility",
|
||||
"GameGetFogOfWar",
|
||||
"GameGetFogOfWarBilinear",
|
||||
"GameSetFogOfWar",
|
||||
"DoesWorldExistAt",
|
||||
"StringToHerdId",
|
||||
"HerdIdToString",
|
||||
"GetHerdRelation",
|
||||
"EntityGetHerdRelation",
|
||||
"EntityGetHerdRelationSafe",
|
||||
"GenomeSetHerdId",
|
||||
"EntityGetClosestWormAttractor",
|
||||
"EntityGetClosestWormDetractor",
|
||||
"GamePrint",
|
||||
"GamePrintImportant",
|
||||
"DEBUG_GetMouseWorld",
|
||||
"DEBUG_MARK",
|
||||
"GameGetFrameNum",
|
||||
"GameGetRealWorldTimeSinceStarted",
|
||||
"InputIsKeyDown",
|
||||
"InputIsKeyJustDown",
|
||||
"InputIsKeyJustUp",
|
||||
"InputGetMousePosOnScreen",
|
||||
"InputIsMouseButtonDown",
|
||||
"InputIsMouseButtonJustDown",
|
||||
"InputIsMouseButtonJustUp",
|
||||
"InputIsJoystickButtonDown",
|
||||
"InputIsJoystickButtonJustDown",
|
||||
"InputGetJoystickAnalogButton",
|
||||
"InputIsJoystickConnected",
|
||||
"InputGetJoystickAnalogStick",
|
||||
"IsPlayer",
|
||||
"IsInvisible",
|
||||
"GameIsDailyRun",
|
||||
"GameIsDailyRunOrDailyPracticeRun",
|
||||
"GameIsModeFullyDeterministic",
|
||||
"GlobalsSetValue",
|
||||
"GlobalsGetValue",
|
||||
"MagicNumbersGetValue",
|
||||
"SetWorldSeed",
|
||||
"SessionNumbersGetValue",
|
||||
"SessionNumbersSetValue",
|
||||
"SessionNumbersSave",
|
||||
"AutosaveDisable",
|
||||
"StatsGetValue",
|
||||
"StatsGlobalGetValue",
|
||||
"StatsBiomeGetValue",
|
||||
"StatsBiomeReset",
|
||||
"StatsLogPlayerKill",
|
||||
"CreateItemActionEntity",
|
||||
"GetRandomActionWithType",
|
||||
"GetRandomAction",
|
||||
"GameGetDateAndTimeUTC",
|
||||
"GameGetDateAndTimeLocal",
|
||||
"GameEmitRainParticles",
|
||||
"GameCutThroughWorldVertical",
|
||||
"BiomeMapSetSize",
|
||||
"BiomeMapGetSize",
|
||||
"BiomeMapSetPixel",
|
||||
"BiomeMapGetPixel",
|
||||
"BiomeMapConvertPixelFromUintToInt",
|
||||
"BiomeMapLoadImage",
|
||||
"BiomeMapLoadImageCropped",
|
||||
"BiomeMapGetVerticalPositionInsideBiome",
|
||||
"BiomeMapGetName",
|
||||
"SetRandomSeed",
|
||||
"Random",
|
||||
"Randomf",
|
||||
"RandomDistribution",
|
||||
"RandomDistributionf",
|
||||
"ProceduralRandom",
|
||||
"ProceduralRandomf",
|
||||
"ProceduralRandomi",
|
||||
"PhysicsAddBodyImage",
|
||||
"PhysicsAddBodyCreateBox",
|
||||
"PhysicsAddJoint",
|
||||
"PhysicsApplyForce",
|
||||
"PhysicsApplyTorque",
|
||||
"PhysicsApplyTorqueToComponent",
|
||||
"PhysicsApplyForceOnArea",
|
||||
"PhysicsRemoveJoints",
|
||||
"PhysicsSetStatic",
|
||||
"PhysicsGetComponentVelocity",
|
||||
"PhysicsGetComponentAngularVelocity",
|
||||
"PhysicsComponentGetTransform",
|
||||
"PhysicsComponentSetTransform",
|
||||
"PhysicsBodyIDGetFromEntity",
|
||||
"PhysicsBodyIDQueryBodies",
|
||||
"PhysicsBodyIDGetTransform",
|
||||
"PhysicsBodyIDSetTransform",
|
||||
"PhysicsBodyIDApplyForce",
|
||||
"PhysicsBodyIDApplyLinearImpulse",
|
||||
"PhysicsBodyIDApplyTorque",
|
||||
"PhysicsBodyIDGetWorldCenter",
|
||||
"PhysicsBodyIDGetDamping",
|
||||
"PhysicsBodyIDSetDamping",
|
||||
"PhysicsBodyIDGetGravityScale",
|
||||
"PhysicsBodyIDSetGravityScale",
|
||||
"PhysicsBodyIDGetBodyAABB",
|
||||
"PhysicsBody2InitFromComponents",
|
||||
"PhysicsPosToGamePos",
|
||||
"GamePosToPhysicsPos",
|
||||
"PhysicsVecToGameVec",
|
||||
"GameVecToPhysicsVec",
|
||||
"LooseChunk",
|
||||
"VerletApplyCircularForce",
|
||||
"VerletApplyDirectionalForce",
|
||||
"AddFlagPersistent",
|
||||
"RemoveFlagPersistent",
|
||||
"HasFlagPersistent",
|
||||
"GameAddFlagRun",
|
||||
"GameRemoveFlagRun",
|
||||
"GameHasFlagRun",
|
||||
"GameTriggerMusicEvent",
|
||||
"GameTriggerMusicCue",
|
||||
"GameTriggerMusicFadeOutAndDequeueAll",
|
||||
"GamePlaySound",
|
||||
"GameEntityPlaySound",
|
||||
"GameEntityPlaySoundLoop",
|
||||
"GameSetPostFxParameter",
|
||||
"GameUnsetPostFxParameter",
|
||||
"GameSetPostFxTextureParameter",
|
||||
"GameUnsetPostFxTextureParameter",
|
||||
"GameTextGetTranslatedOrNot",
|
||||
"GameTextGet",
|
||||
"GuiCreate",
|
||||
"GuiDestroy",
|
||||
"GuiStartFrame",
|
||||
"GuiOptionsAdd",
|
||||
"GuiOptionsRemove",
|
||||
"GuiOptionsClear",
|
||||
"GuiOptionsAddForNextWidget",
|
||||
"GuiColorSetForNextWidget",
|
||||
"GuiZSet",
|
||||
"GuiZSetForNextWidget",
|
||||
"GuiIdPush",
|
||||
"GuiIdPushString",
|
||||
"GuiIdPop",
|
||||
"GuiAnimateBegin",
|
||||
"GuiAnimateEnd",
|
||||
"GuiAnimateAlphaFadeIn",
|
||||
"GuiAnimateScaleIn",
|
||||
"GuiText",
|
||||
"GuiTextCentered",
|
||||
"GuiImage",
|
||||
"GuiImageNinePiece",
|
||||
"GuiButton",
|
||||
"GuiImageButton",
|
||||
"GuiSlider",
|
||||
"GuiTextInput",
|
||||
"GuiBeginAutoBox",
|
||||
"GuiEndAutoBoxNinePiece",
|
||||
"GuiTooltip",
|
||||
"GuiBeginScrollContainer",
|
||||
"GuiEndScrollContainer",
|
||||
"GuiLayoutBeginHorizontal",
|
||||
"GuiLayoutBeginVertical",
|
||||
"GuiLayoutAddHorizontalSpacing",
|
||||
"GuiLayoutAddVerticalSpacing",
|
||||
"GuiLayoutEnd",
|
||||
"GuiLayoutBeginLayer",
|
||||
"GuiLayoutEndLayer",
|
||||
"GuiGetScreenDimensions",
|
||||
"GuiGetTextDimensions",
|
||||
"GuiGetImageDimensions",
|
||||
"GuiGetPreviousWidgetInfo",
|
||||
"GameIsBetaBuild",
|
||||
"DebugGetIsDevBuild",
|
||||
"DebugEnableTrailerMode",
|
||||
"GameGetIsTrailerModeEnabled",
|
||||
"Debug_SaveTestPlayer",
|
||||
"DebugBiomeMapGetFilename",
|
||||
"EntityConvertToMaterial",
|
||||
"ConvertEverythingToGold",
|
||||
"ConvertMaterialEverywhere",
|
||||
"ConvertMaterialOnAreaInstantly",
|
||||
"LoadRagdoll",
|
||||
"GetDailyPracticeRunSeed",
|
||||
"ModIsEnabled",
|
||||
"ModGetActiveModIDs",
|
||||
"ModGetAPIVersion",
|
||||
"ModDoesFileExist",
|
||||
"ModMaterialFilesGet",
|
||||
"ModSettingGet",
|
||||
"ModSettingSet",
|
||||
"ModSettingGetNextValue",
|
||||
"ModSettingSetNextValue",
|
||||
"ModSettingRemove",
|
||||
"ModSettingGetCount",
|
||||
"ModSettingGetAtIndex",
|
||||
"StreamingGetIsConnected",
|
||||
"StreamingGetConnectedChannelName",
|
||||
"StreamingGetVotingCycleDurationFrames",
|
||||
"StreamingGetRandomViewerName",
|
||||
"StreamingGetSettingsGhostsNamedAfterViewers",
|
||||
"StreamingSetCustomPhaseDurations",
|
||||
"StreamingForceNewVoting",
|
||||
"StreamingSetVotingEnabled",
|
||||
"ModLuaFileAppend",
|
||||
"ModLuaFileGetAppends",
|
||||
"ModLuaFileSetAppends",
|
||||
"ModTextFileGetContent",
|
||||
"ModTextFileSetContent",
|
||||
"ModTextFileWhoSetContent",
|
||||
"ModImageMakeEditable",
|
||||
"ModImageIdFromFilename",
|
||||
"ModImageGetPixel",
|
||||
"ModImageSetPixel",
|
||||
"ModImageWhoSetContent",
|
||||
"ModImageDoesExist",
|
||||
"ModMagicNumbersFileAdd",
|
||||
"ModMaterialsFileAdd",
|
||||
"ModRegisterAudioEventMappings",
|
||||
"ModRegisterMusicBank",
|
||||
"ModDevGenerateSpriteUVsForDirectory",
|
||||
"RegisterProjectile",
|
||||
"RegisterGunAction",
|
||||
"RegisterGunShotEffects",
|
||||
"BeginProjectile",
|
||||
"EndProjectile",
|
||||
"BeginTriggerTimer",
|
||||
"BeginTriggerHitWorld",
|
||||
"BeginTriggerDeath",
|
||||
"EndTrigger",
|
||||
"SetProjectileConfigs",
|
||||
"StartReload",
|
||||
"ActionUsesRemainingChanged",
|
||||
"ActionUsed",
|
||||
"LogAction",
|
||||
"OnActionPlayed",
|
||||
"OnNotEnoughManaForAction",
|
||||
"BaabInstruction",
|
||||
"SetValueNumber",
|
||||
"GetValueNumber",
|
||||
"SetValueInteger",
|
||||
"GetValueInteger",
|
||||
"SetValueBool",
|
||||
"GetValueBool",
|
||||
"dofile",
|
||||
"dofile_once",
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
// The entity/component part of Noita's Lua API, expressed as an interface the game world implements.
|
||||
// LuaHost forwards EntityGetFirstComponent, ComponentGetValue2, EntityLoad, EntityKill, ... to it.
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "entity/spawn_comp.h"
|
||||
|
||||
namespace ng {
|
||||
|
||||
// A Lua value crossing the boundary.
|
||||
struct LVal {
|
||||
enum class T { Nil, Bool, Num, Str } t = T::Nil;
|
||||
bool b = false;
|
||||
double n = 0;
|
||||
std::string s;
|
||||
static LVal nil() { return {}; }
|
||||
static LVal boolean(bool v) { LVal r; r.t = T::Bool; r.b = v; return r; }
|
||||
static LVal number(double v) { LVal r; r.t = T::Num; r.n = v; return r; }
|
||||
static LVal string(std::string v) { LVal r; r.t = T::Str; r.s = std::move(v); return r; }
|
||||
};
|
||||
|
||||
class IEntityWorld {
|
||||
public:
|
||||
virtual ~IEntityWorld() = default;
|
||||
|
||||
// ---- entities ----
|
||||
virtual int load(const std::string& path, double x, double y) = 0; // 0 on failure
|
||||
virtual int load_with(const std::string& path, double x, double y, const std::vector<SpawnComp>& extra) { (void)extra; return load(path, x, y); } // the same with components merged in at the spawn ("=Type" merges into the first of that type)
|
||||
virtual void load_to_entity(const std::string& path, int entity) = 0; // EntityLoadToEntity: the file's components are added to the entity (not its tags, not its children)
|
||||
virtual void kill(int id) = 0;
|
||||
virtual void physics_init(int /*id*/) {} // PhysicsBody2InitFromComponents
|
||||
virtual bool alive(int id) = 0;
|
||||
virtual bool get_transform(int id, double& x, double& y, double& rot, double& sx, double& sy) = 0;
|
||||
virtual void set_transform(int id, double x, double y, double rot, double sx, double sy) = 0;
|
||||
virtual std::string get_name(int id) = 0;
|
||||
virtual void set_name(int id, const std::string& name) = 0;
|
||||
virtual std::string get_filename(int id) = 0;
|
||||
virtual bool has_tag(int id, const std::string& tag) = 0;
|
||||
virtual void add_tag(int id, const std::string& tag) = 0;
|
||||
virtual void remove_tag(int id, const std::string& tag) = 0;
|
||||
virtual std::string get_tags(int id) = 0; // comma separated
|
||||
virtual std::vector<int> with_tag(const std::string& tag) = 0; // empty tag = all
|
||||
virtual std::vector<int> in_radius(double x, double y, double r, const std::string& tag) = 0;
|
||||
virtual void script_query(bool on) { (void)on; } // a Lua query follows (with_tag / in_radius): the GPU-only projectiles get their twins first
|
||||
virtual int parent(int id) = 0;
|
||||
virtual std::vector<int> children(int id) = 0;
|
||||
virtual void add_child(int parent, int child) = 0;
|
||||
// A spell card inside a wand (AddGunAction / AddGunActionPermanent): a hidden child entity carrying the action id.
|
||||
virtual int add_gun_action(int wand, const std::string& action_id, bool permanent) = 0;
|
||||
|
||||
// ---- components ----
|
||||
virtual std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) = 0;
|
||||
virtual int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) = 0;
|
||||
virtual void remove_component(int comp) = 0;
|
||||
virtual void set_component_enabled(int comp, bool enabled) = 0;
|
||||
virtual bool component_enabled(int comp) = 0;
|
||||
virtual std::string component_type(int comp) = 0;
|
||||
virtual int component_entity(int comp) = 0;
|
||||
virtual bool comp_has_tag(int comp, const std::string& tag) = 0;
|
||||
virtual LVal get_value(int comp, const std::string& key, bool typed) = 0; // nil if the key is unknown
|
||||
virtual void set_value(int comp, const std::string& key, const LVal& v) = 0;
|
||||
|
||||
// ---- context of the running script ----
|
||||
virtual int updated_entity() = 0;
|
||||
virtual int updated_component() = 0;
|
||||
virtual int frame() = 0;
|
||||
virtual std::vector<int> game_effect_components(int entity, const std::string& effect) { (void)entity; (void)effect; return {}; } // GameEffectComponents of the entity and its children (empty name = all)
|
||||
virtual int load_game_effect(int entity, const std::string& effect, bool always_new, int& effect_entity) { (void)entity; (void)effect; (void)always_new; effect_entity = 0; return 0; } // GetGameEffectLoadTo: the component (and entity) of the effect on the entity, loaded when missing
|
||||
virtual int load_game_effect_file(int entity, const std::string& file) { (void)entity; (void)file; return 0; } // LoadGameEffectEntityTo
|
||||
virtual void shoot_projectile(int shooter, double x, double y, double tx, double ty, int entity, bool send_message = true) { (void)shooter; (void)x; (void)y; (void)tx; (void)ty; (void)entity; (void)send_message; } // GameShootProjectile
|
||||
virtual void play_sound(const std::string& bank, const std::string& event, double x, double y) { (void)bank; (void)event; (void)x; (void)y; } // GamePlaySound
|
||||
virtual void physics_remove_joints(double x0, double y0, double x1, double y1) { (void)x0; (void)y0; (void)x1; (void)y1; } // PhysicsRemoveJoints
|
||||
virtual void screenshake(double amount, double x, double y) { (void)amount; (void)x; (void)y; } // GameScreenshake
|
||||
virtual void convert_material_everywhere(int from, int to) { (void)from; (void)to; } // ConvertMaterialEverywhere (fungal shifts)
|
||||
virtual void entity_play_sound(int entity, const std::string& name) { (void)entity; (void)name; } // GameEntityPlaySound: through every AudioComponent of the entity
|
||||
virtual void entity_play_sound_loop(int entity, const std::string& tag, double intensity, double intensity2) { (void)entity; (void)tag; (void)intensity; (void)intensity2; } // GameEntityPlaySoundLoop
|
||||
// EntityInflictDamage (007b4030 -> 00ab9740 -> TakeDamage_Impl 00ab9a90): damage of a type (a DAMAGE_* name) through the entity's enabled DamageModelComponent; the world position defaults to the entity's
|
||||
virtual void inflict_damage(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double impulse_x, double impulse_y, int responsible, double world_x, double world_y, double knockback) { (void)entity; (void)amount; (void)type; (void)description; (void)ragdoll_fx; (void)impulse_x; (void)impulse_y; (void)responsible; (void)world_x; (void)world_y; (void)knockback; }
|
||||
virtual std::string biome_name(double x, double y) { (void)x; (void)y; return "_EMPTY_"; } // BiomeMapGetName: "$biome_coalmine"...
|
||||
virtual int world_state_entity() = 0;
|
||||
virtual void remove_from_parent(int id) { (void)id; } // EntityRemoveFromParent
|
||||
virtual bool camera_bounds(double& x, double& y, double& w, double& h) { (void)x; (void)y; (void)w; (void)h; return false; } // GameGetCameraBounds / GameGetCameraPos
|
||||
virtual void regen_item_actions(int player) { (void)player; } // GameRegenItemActionsInPlayer: every spell the player carries has its uses back
|
||||
virtual int create_entity(const std::string& name) { (void)name; return 0; } // EntityCreateNew: an entity without components
|
||||
virtual void comp_add_tag(int comp, const std::string& tag) { (void)comp; (void)tag; } // ComponentAddTag
|
||||
virtual int herd_relation(int a, int b) { (void)a; (void)b; return 0; } // EntityGetHerdRelation
|
||||
virtual int wand_capacity(int wand) { (void)wand; return 0; } // EntityGetWandCapacity: the slots without the always-cast spells
|
||||
virtual void print(const std::string& title, const std::string& text, bool important) = 0;
|
||||
virtual void physics_force(int entity, double fx, double fy) { (void)entity; (void)fx; (void)fy; } // PhysicsApplyForce
|
||||
struct BodyInfo { int entity = 0; double mass = 1, x = 0, y = 0, vx = 0, vy = 0, angular = 0; }; // a rigid body: mass (kg), position (pixels), velocity (pixels per second)
|
||||
virtual std::vector<BodyInfo> bodies_in_area(double x0, double y0, double x1, double y1) { (void)x0; (void)y0; (void)x1; (void)y1; return {}; } // PhysicsApplyForceOnArea
|
||||
virtual void physics_area_force(int entity, double fx, double fy) { (void)entity; (void)fx; (void)fy; } // the force of PhysicsApplyForceOnArea's callback (newtons on the body)
|
||||
};
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,514 @@
|
||||
// Lua natives of the entity / component API; they forward to LuaHost::world (IEntityWorld).
|
||||
#include <lua.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include "entity/material_mix.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "lua/entity_api.h"
|
||||
#include "lua/lua_host.h"
|
||||
|
||||
namespace ng {
|
||||
|
||||
namespace {
|
||||
|
||||
double nn(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : def; }
|
||||
std::string ss(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
|
||||
IEntityWorld* W(LuaHost& h) { return h.world; }
|
||||
|
||||
void push_val(lua_State* L, const LVal& v) {
|
||||
switch (v.t) {
|
||||
case LVal::T::Nil: lua_pushnil(L); break;
|
||||
case LVal::T::Bool: lua_pushboolean(L, v.b); break;
|
||||
case LVal::T::Num: lua_pushnumber(L, v.n); break;
|
||||
case LVal::T::Str: lua_pushstring(L, v.s.c_str()); break;
|
||||
}
|
||||
}
|
||||
|
||||
LVal read_val(lua_State* L, int i) {
|
||||
if (lua_isboolean(L, i)) return LVal::boolean(lua_toboolean(L, i) != 0);
|
||||
if (lua_type(L, i) == LUA_TNUMBER) return LVal::number(lua_tonumber(L, i));
|
||||
if (lua_isstring(L, i)) return LVal::string(lua_tostring(L, i));
|
||||
return LVal::nil();
|
||||
}
|
||||
|
||||
void push_list(lua_State* L, const std::vector<int>& v) {
|
||||
lua_createtable(L, (int)v.size(), 0);
|
||||
for (size_t i = 0; i < v.size(); ++i) { lua_pushnumber(L, v[i]); lua_rawseti(L, -2, (int)i + 1); }
|
||||
}
|
||||
|
||||
// ---- entities ----
|
||||
int n_EntityLoad(LuaHost& h, lua_State* L) {
|
||||
if (!W(h)) {
|
||||
HostEvent e; e.kind = HostEvent::Kind::EntityLoad; e.path = ss(L, 1); e.x = nn(L, 2); e.y = nn(L, 3);
|
||||
e.id = h.next_entity_id++;
|
||||
lua_pushnumber(L, e.id);
|
||||
h.add_event(std::move(e));
|
||||
return 1;
|
||||
}
|
||||
lua_pushnumber(L, W(h)->load(ss(L, 1), nn(L, 2), nn(L, 3)));
|
||||
return 1;
|
||||
}
|
||||
int n_PhysicsBody2InitFromComponents(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_init((int)nn(L, 1)); return 0; }
|
||||
int n_EntityKill(LuaHost& h, lua_State* L) { if (W(h)) W(h)->kill((int)nn(L, 1)); return 0; }
|
||||
int n_EntityGetIsAlive(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->alive((int)nn(L, 1))); return 1; }
|
||||
int n_EntityGetTransform(LuaHost& h, lua_State* L) {
|
||||
double x = 0, y = 0, r = 0, sx = 1, sy = 1;
|
||||
if (W(h)) W(h)->get_transform((int)nn(L, 1), x, y, r, sx, sy);
|
||||
lua_pushnumber(L, x); lua_pushnumber(L, y); lua_pushnumber(L, r); lua_pushnumber(L, sx); lua_pushnumber(L, sy);
|
||||
return 5;
|
||||
}
|
||||
int n_EntitySetTransform(LuaHost& h, lua_State* L) {
|
||||
if (!W(h)) return 0;
|
||||
double x, y, r, sx, sy;
|
||||
const int id = (int)nn(L, 1);
|
||||
if (!W(h)->get_transform(id, x, y, r, sx, sy)) return 0;
|
||||
W(h)->set_transform(id, nn(L, 2, x), nn(L, 3, y), nn(L, 4, r), nn(L, 5, sx), nn(L, 6, sy));
|
||||
return 0;
|
||||
}
|
||||
int n_EntityGetName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_name((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||
int n_EntitySetName(LuaHost& h, lua_State* L) { if (W(h)) W(h)->set_name((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||
int n_EntityGetFilename(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_filename((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||
int n_EntityHasTag(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->has_tag((int)nn(L, 1), ss(L, 2))); return 1; }
|
||||
int n_EntityAddTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||
int n_EntityRemoveTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||
int n_EntityGetTags(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->get_tags((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||
struct ScriptQuery { // (the query is a script's: the lazy twins of the GPU projectiles are made for it)
|
||||
IEntityWorld* w; explicit ScriptQuery(IEntityWorld* a) : w(a) { if (w) w->script_query(true); } ~ScriptQuery() { if (w) w->script_query(false); }
|
||||
};
|
||||
int n_EntityGetWithTag(LuaHost& h, lua_State* L) { ScriptQuery q(W(h)); push_list(L, W(h) ? W(h)->with_tag(ss(L, 1)) : std::vector<int>{}); return 1; }
|
||||
int n_EntityGetInRadiusWithTag(LuaHost& h, lua_State* L) {
|
||||
ScriptQuery q(W(h));
|
||||
push_list(L, W(h) ? W(h)->in_radius(nn(L, 1), nn(L, 2), nn(L, 3), ss(L, 4)) : std::vector<int>{});
|
||||
return 1;
|
||||
}
|
||||
int n_EntityGetInRadius(LuaHost& h, lua_State* L) {
|
||||
ScriptQuery q(W(h));
|
||||
push_list(L, W(h) ? W(h)->in_radius(nn(L, 1), nn(L, 2), nn(L, 3), "") : std::vector<int>{});
|
||||
return 1;
|
||||
}
|
||||
int n_EntityGetClosestWithTag(LuaHost& h, lua_State* L) {
|
||||
int best = 0;
|
||||
if (W(h)) {
|
||||
double bd = 1e300;
|
||||
ScriptQuery q(W(h));
|
||||
for (int id : W(h)->with_tag(ss(L, 3))) {
|
||||
double x, y, r, sx, sy;
|
||||
if (!W(h)->get_transform(id, x, y, r, sx, sy)) continue;
|
||||
const double d = (x - nn(L, 1)) * (x - nn(L, 1)) + (y - nn(L, 2)) * (y - nn(L, 2));
|
||||
if (d < bd) { bd = d; best = id; }
|
||||
}
|
||||
}
|
||||
lua_pushnumber(L, best);
|
||||
return 1;
|
||||
}
|
||||
int n_EntityGetParent(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->parent((int)nn(L, 1)) : 0); return 1; }
|
||||
int n_EntityGetRootEntity(LuaHost& h, lua_State* L) { // the topmost parent (the entity itself when it has none)
|
||||
int id = (int)nn(L, 1);
|
||||
for (int guard = 0; W(h) && guard < 64; ++guard) { const int p = W(h)->parent(id); if (p <= 0) break; id = p; }
|
||||
lua_pushnumber(L, id);
|
||||
return 1;
|
||||
}
|
||||
int n_EntityGetAllChildren(LuaHost& h, lua_State* L) {
|
||||
if (!W(h)) { lua_pushnil(L); return 1; }
|
||||
auto c = W(h)->children((int)nn(L, 1));
|
||||
if (c.empty()) lua_pushnil(L); else push_list(L, c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- components ----
|
||||
// The value of a component field, through the entity runtime or, during world generation, the components of a spawn (LuaHost::gen_get)
|
||||
LVal gv(LuaHost& h, int cid, const std::string& key, bool typed) {
|
||||
if (W(h)) return W(h)->get_value(cid, key, typed);
|
||||
std::string v;
|
||||
if (!h.gen_get(cid, key, v)) return LVal::nil();
|
||||
if (typed) { char* end = nullptr; const double d = std::strtod(v.c_str(), &end); if (!v.empty() && end && *end == 0) return LVal::number(d); }
|
||||
return LVal::string(v);
|
||||
}
|
||||
void sv(LuaHost& h, int cid, const std::string& key, const LVal& v) {
|
||||
if (W(h)) { W(h)->set_value(cid, key, v); return; }
|
||||
std::string s2;
|
||||
if (v.t == LVal::T::Num) { char b[64]; std::snprintf(b, sizeof b, "%.17g", v.n); s2 = b; }
|
||||
else if (v.t == LVal::T::Bool) s2 = v.b ? "1" : "0";
|
||||
else s2 = v.s;
|
||||
h.gen_set(cid, key, s2);
|
||||
}
|
||||
int comps(LuaHost& h, lua_State* L, bool first, bool include_disabled) {
|
||||
std::vector<int> v;
|
||||
if (W(h)) v = W(h)->components((int)nn(L, 1), ss(L, 2), ss(L, 3), include_disabled);
|
||||
else v = h.gen_components((int)nn(L, 1), ss(L, 2), ss(L, 3));
|
||||
if (first) { if (v.empty()) lua_pushnil(L); else lua_pushnumber(L, v.front()); return 1; }
|
||||
if (v.empty()) lua_pushnil(L); else push_list(L, v);
|
||||
return 1;
|
||||
}
|
||||
int n_EntityGetFirstComponent(LuaHost& h, lua_State* L) { return comps(h, L, true, false); }
|
||||
int n_EntityGetFirstComponentIncludingDisabled(LuaHost& h, lua_State* L) { return comps(h, L, true, true); }
|
||||
int n_EntityGetComponent(LuaHost& h, lua_State* L) { return comps(h, L, false, false); }
|
||||
int n_EntityGetComponentIncludingDisabled(LuaHost& h, lua_State* L) { return comps(h, L, false, true); }
|
||||
|
||||
int add_component(LuaHost& h, lua_State* L) {
|
||||
std::map<std::string, std::string> attrs;
|
||||
if (lua_istable(L, 3)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 3)) {
|
||||
if (lua_type(L, -2) == LUA_TSTRING) {
|
||||
std::string v;
|
||||
if (lua_isboolean(L, -1)) v = lua_toboolean(L, -1) ? "1" : "0";
|
||||
else if (lua_type(L, -1) == LUA_TNUMBER) { char b[64]; std::snprintf(b, sizeof b, "%g", lua_tonumber(L, -1)); v = b; }
|
||||
else if (lua_isstring(L, -1)) v = lua_tostring(L, -1);
|
||||
attrs[lua_tostring(L, -2)] = v;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
if (!W(h)) { // world generation: the component goes with the spawn of the entity
|
||||
SpawnComp sc; sc.type = ss(L, 2); sc.attrs = attrs;
|
||||
if (h.add_spawn_comp((int)nn(L, 1), std::move(sc))) lua_pushnumber(L, h.next_entity_id++); else lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
lua_pushnumber(L, W(h)->add_component((int)nn(L, 1), ss(L, 2), attrs));
|
||||
return 1;
|
||||
}
|
||||
int n_EntityAddComponent(LuaHost& h, lua_State* L) { return add_component(h, L); }
|
||||
int n_EntityAddComponent2(LuaHost& h, lua_State* L) { return add_component(h, L); }
|
||||
int n_EntityRemoveComponent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_component((int)nn(L, 2)); return 0; }
|
||||
int n_EntitySetComponentIsEnabled(LuaHost& h, lua_State* L) { if (W(h)) W(h)->set_component_enabled((int)nn(L, 2), lua_toboolean(L, 3) != 0); return 0; }
|
||||
int n_ComponentGetIsEnabled(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->component_enabled((int)nn(L, 1))); return 1; }
|
||||
int n_ComponentGetTypeName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->component_type((int)nn(L, 1)).c_str() : ""); return 1; }
|
||||
int n_ComponentGetEntity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->component_entity((int)nn(L, 1)) : 0); return 1; }
|
||||
int n_ComponentHasTag(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->comp_has_tag((int)nn(L, 1), ss(L, 2))); return 1; }
|
||||
|
||||
// ComponentGetValue: always a string ("" when unknown)
|
||||
int n_ComponentGetValue(LuaHost& h, lua_State* L) {
|
||||
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||
lua_pushstring(L, v.t == LVal::T::Str ? v.s.c_str() : "");
|
||||
return 1;
|
||||
}
|
||||
// ComponentGetValue2: a vector field (vec2 x / y, ValueRange min / max: flattened as "field.x" ...) gives two values, as the engine's typed getter does
|
||||
static const char* const kVecSuffix[2][2] = {{".x", ".y"}, {".min", ".max"}};
|
||||
int n_ComponentGetValue2(LuaHost& h, lua_State* L) {
|
||||
const int cid = (int)nn(L, 1);
|
||||
const std::string key = ss(L, 2);
|
||||
LVal v = gv(h, cid, key, true);
|
||||
if (v.t == LVal::T::Nil) {
|
||||
for (const auto& pr : kVecSuffix) {
|
||||
LVal a = gv(h, cid, key + pr[0], true);
|
||||
if (a.t == LVal::T::Nil) continue;
|
||||
push_val(L, a); push_val(L, gv(h, cid, key + pr[1], true));
|
||||
return 2;
|
||||
}
|
||||
if (key == "mVelocity") { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } // (not written yet: at rest)
|
||||
}
|
||||
push_val(L, v);
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentGetValueInt(LuaHost& h, lua_State* L) {
|
||||
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||
lua_pushnumber(L, v.t == LVal::T::Str ? std::floor(std::atof(v.s.c_str())) : 0);
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentGetValueFloat(LuaHost& h, lua_State* L) {
|
||||
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||
lua_pushnumber(L, v.t == LVal::T::Str ? std::atof(v.s.c_str()) : 0);
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentGetValueBool(LuaHost& h, lua_State* L) {
|
||||
LVal v = gv(h, (int)nn(L, 1), ss(L, 2), false);
|
||||
lua_pushboolean(L, v.t == LVal::T::Str && (v.s == "1" || v.s == "true"));
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentSetValue(LuaHost& h, lua_State* L) {
|
||||
sv(h, (int)nn(L, 1), ss(L, 2), LVal::string(ss(L, 3)));
|
||||
return 0;
|
||||
}
|
||||
int n_ComponentSetValue2(LuaHost& h, lua_State* L) {
|
||||
const int cid = (int)nn(L, 1);
|
||||
const std::string key = ss(L, 2);
|
||||
if (lua_gettop(L) >= 4 && gv(h, cid, key, true).t == LVal::T::Nil) { // a vector field: two values (x / y, or min / max of a ValueRange)
|
||||
const bool range = gv(h, cid, key + ".min", true).t != LVal::T::Nil;
|
||||
sv(h, cid, key + (range ? ".min" : ".x"), read_val(L, 3));
|
||||
sv(h, cid, key + (range ? ".max" : ".y"), read_val(L, 4));
|
||||
return 0;
|
||||
}
|
||||
sv(h, cid, key, read_val(L, 3));
|
||||
return 0;
|
||||
}
|
||||
// ComponentObjectGetValue(comp, object, field): objects are flattened as "object.field"
|
||||
int n_ComponentObjectGetValue(LuaHost& h, lua_State* L) {
|
||||
LVal v = W(h) ? W(h)->get_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), false) : LVal::nil();
|
||||
lua_pushstring(L, v.t == LVal::T::Str ? v.s.c_str() : "");
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentObjectGetValue2(LuaHost& h, lua_State* L) {
|
||||
push_val(L, W(h) ? W(h)->get_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), true) : LVal::nil());
|
||||
return 1;
|
||||
}
|
||||
int n_ComponentObjectSetValue(LuaHost& h, lua_State* L) {
|
||||
if (W(h)) W(h)->set_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), LVal::string(ss(L, 4)));
|
||||
return 0;
|
||||
}
|
||||
int n_ComponentObjectSetValue2(LuaHost& h, lua_State* L) {
|
||||
if (W(h)) W(h)->set_value((int)nn(L, 1), ss(L, 2) + "." + ss(L, 3), read_val(L, 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n_EntitySetComponentsWithTagEnabled(LuaHost& h, lua_State* L) {
|
||||
if (!W(h)) return 0;
|
||||
for (int c : W(h)->components((int)nn(L, 1), "", ss(L, 2), true)) W(h)->set_component_enabled(c, lua_toboolean(L, 3) != 0);
|
||||
return 0;
|
||||
}
|
||||
// vectors are stored as "x,y"
|
||||
int n_ComponentSetValueVector2(LuaHost& h, lua_State* L) { // the field's x / y
|
||||
sv(h, (int)nn(L, 1), ss(L, 2) + ".x", LVal::number(nn(L, 3))); sv(h, (int)nn(L, 1), ss(L, 2) + ".y", LVal::number(nn(L, 4)));
|
||||
return 0;
|
||||
}
|
||||
int n_ComponentGetValueVector2(LuaHost& h, lua_State* L) {
|
||||
double a = 0, b = 0;
|
||||
if (W(h)) {
|
||||
const LVal x = W(h)->get_value((int)nn(L, 1), ss(L, 2) + ".x", true), y = W(h)->get_value((int)nn(L, 1), ss(L, 2) + ".y", true);
|
||||
if (x.t == LVal::T::Num) a = x.n;
|
||||
if (y.t == LVal::T::Num) b = y.n;
|
||||
}
|
||||
lua_pushnumber(L, a); lua_pushnumber(L, b);
|
||||
return 2;
|
||||
}
|
||||
|
||||
int n_EntityAddChild(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_child((int)nn(L, 1), (int)nn(L, 2)); return 0; }
|
||||
int n_AddGunAction(LuaHost& h, lua_State* L) { if (std::getenv("NG_WANDDBG")) std::printf("AddGunAction(%d, %s)%c", (int)nn(L, 1), lua_isstring(L, 2) ? lua_tostring(L, 2) : "<nil>", 10); if (W(h)) W(h)->add_gun_action((int)nn(L, 1), ss(L, 2), false); return 0; }
|
||||
int n_AddGunActionPermanent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->add_gun_action((int)nn(L, 1), ss(L, 2), true); return 0; }
|
||||
int n_IsPlayer(LuaHost& h, lua_State* L) { lua_pushboolean(L, W(h) && W(h)->has_tag((int)nn(L, 1), "player_unit")); return 1; }
|
||||
int n_GameGetPotionColorUint(LuaHost&, lua_State* L) { lua_pushnumber(L, 0xFF4080C0); return 1; }
|
||||
// The contents of a flask or barrel: the first MaterialInventoryComponent holds one main material (count_per_material_type.Material.material / .count).
|
||||
int n_AddMaterialInventoryMaterial(LuaHost& h, lua_State* L) {
|
||||
if (!W(h)) return 0;
|
||||
auto c = W(h)->components((int)nn(L, 1), "MaterialInventoryComponent", "", true);
|
||||
if (c.empty()) return 0;
|
||||
auto parts = mix_get(*W(h), c.front());
|
||||
mix_add(parts, ss(L, 2), nn(L, 3));
|
||||
mix_set(*W(h), c.front(), parts);
|
||||
return 0;
|
||||
}
|
||||
int n_GetMaterialInventoryMainMaterial(LuaHost& h, lua_State* L) { // (entity, ignore_box2d_materials = true) -> the id of the largest part of the first MaterialInventoryComponent, 0 if none
|
||||
int best = 0;
|
||||
if (W(h) && h.material_type) {
|
||||
const bool skipBox2d = lua_gettop(L) < 2 || lua_toboolean(L, 2);
|
||||
auto c = W(h)->components((int)nn(L, 1), "MaterialInventoryComponent", "", true);
|
||||
if (!c.empty()) {
|
||||
const auto list = h.material_list ? h.material_list() : decltype(h.material_list()){};
|
||||
double most = 0;
|
||||
for (const MixPart& pt : mix_get(*W(h), c.front())) {
|
||||
const int id = h.material_type(pt.mat);
|
||||
if (id <= 0 || pt.n <= most) continue;
|
||||
if (skipBox2d && id < (int)list.size() && std::find(list[id].tags.begin(), list[id].tags.end(), "box2d") != list[id].tags.end()) continue;
|
||||
most = pt.n; best = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
lua_pushinteger(L, best);
|
||||
return 1;
|
||||
}
|
||||
int n_StatsGlobalGetValue(LuaHost&, lua_State* L) { const char* k = lua_tostring(L, 1); lua_pushstring(L, k && std::string(k) == "death_count" ? std::to_string(lua_death_count()).c_str() : "0"); return 1; }
|
||||
int n_PhysicsApplyForce(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_force((int)nn(L, 1), nn(L, 2), nn(L, 3)); return 0; }
|
||||
int n_GameVecToPhysicsVec(LuaHost&, lua_State* L) { lua_pushnumber(L, nn(L, 1) * 0.1); lua_pushnumber(L, nn(L, 2) * 0.1); return 2; } // 10 pixels per physics unit
|
||||
// the action's custom_xml_file (gun_actions.lua): the engine loads that card file instead of the generic one (it has the spell's sprite and its extras)
|
||||
static std::string action_card_file(lua_State* L, const std::string& id) {
|
||||
std::string file;
|
||||
lua_getglobal(L, "actions");
|
||||
if (lua_istable(L, -1)) {
|
||||
const size_t n = lua_objlen(L, -1);
|
||||
for (size_t i = 1; i <= n && file.empty(); ++i) {
|
||||
lua_rawgeti(L, -1, (int)i);
|
||||
lua_getfield(L, -1, "id");
|
||||
const bool match = lua_isstring(L, -1) && id == lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (match) { lua_getfield(L, -1, "custom_xml_file"); if (lua_isstring(L, -1)) file = lua_tostring(L, -1); lua_pop(L, 1); }
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return file.empty() ? std::string("data/entities/misc/custom_cards/action.xml") : file;
|
||||
}
|
||||
|
||||
// A spell card lying in the world: the action's card file (custom_xml_file, else the generic card) carrying the action id.
|
||||
int n_CreateItemActionEntity(LuaHost& h, lua_State* L) {
|
||||
const std::string cardFile = action_card_file(L, ss(L, 1));
|
||||
if (!W(h)) { // world generation (the shop spells of a holy mountain): the card is loaded with its action, merged into the card's ItemActionComponent ("=" spawn component)
|
||||
HostEvent e; e.kind = HostEvent::Kind::EntityLoad; e.path = cardFile; e.x = nn(L, 2); e.y = nn(L, 3);
|
||||
e.id = h.next_entity_id++;
|
||||
const int id = e.id;
|
||||
h.add_event(std::move(e));
|
||||
SpawnComp sc; sc.type = "=ItemActionComponent"; sc.attrs["action_id"] = ss(L, 1);
|
||||
h.add_spawn_comp(id, std::move(sc));
|
||||
lua_pushnumber(L, id);
|
||||
return 1;
|
||||
}
|
||||
std::vector<SpawnComp> extra(1); // (the action is the card's from its spawn on: its sprite is chosen then)
|
||||
extra[0].type = "=ItemActionComponent"; extra[0].attrs["action_id"] = ss(L, 1);
|
||||
const int id = W(h)->load_with(cardFile, nn(L, 2), nn(L, 3), extra);
|
||||
if (id) {
|
||||
auto c = W(h)->components(id, "ItemActionComponent", "", true);
|
||||
const int comp = c.empty() ? W(h)->add_component(id, "ItemActionComponent", {}) : c.front();
|
||||
W(h)->set_value(comp, "action_id", LVal::string(ss(L, 1)));
|
||||
W(h)->set_name(id, ss(L, 1));
|
||||
}
|
||||
lua_pushnumber(L, id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- context ----
|
||||
int n_GetUpdatedEntityID(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->updated_entity() : 0); return 1; }
|
||||
int n_GetUpdatedComponentID(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->updated_component() : 0); return 1; }
|
||||
int n_GameGetFrameNum(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->frame() : 0); return 1; }
|
||||
int n_BiomeMapGetName(LuaHost& h, lua_State* L) { lua_pushstring(L, W(h) ? W(h)->biome_name(lua_tonumber(L, 1), lua_tonumber(L, 2)).c_str() : "_EMPTY_"); return 1; }
|
||||
// 007b3c30: send_message (default true) - false: no Message_Shot, the shooter's script_shot scripts do not see it; the 8th argument (verlet_parent_entity) is looked up and not used
|
||||
int n_GameShootProjectile(LuaHost& h, lua_State* L) { if (W(h)) W(h)->shoot_projectile((int)lua_tonumber(L, 1), lua_tonumber(L, 2), lua_tonumber(L, 3), lua_tonumber(L, 4), lua_tonumber(L, 5), (int)lua_tonumber(L, 6), lua_gettop(L) < 7 || lua_toboolean(L, 7) != 0); return 0; }
|
||||
int n_GetGameEffectLoadTo(LuaHost& h, lua_State* L) { // ( entity, effect name, always_load_new ) -> component id, entity id
|
||||
int fx_entity = 0;
|
||||
const int cid = W(h) ? W(h)->load_game_effect((int)nn(L, 1), ss(L, 2), lua_toboolean(L, 3) != 0, fx_entity) : 0;
|
||||
if (cid) lua_pushnumber(L, cid); else lua_pushnil(L);
|
||||
lua_pushnumber(L, fx_entity);
|
||||
return 2;
|
||||
}
|
||||
int n_LoadGameEffectEntityTo(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->load_game_effect_file((int)nn(L, 1), ss(L, 2)) : 0); return 1; }
|
||||
int n_GameGetGameEffect(LuaHost& h, lua_State* L) { // ( entity, effect name ) -> component id or 0
|
||||
const std::vector<int> v = W(h) ? W(h)->game_effect_components((int)nn(L, 1), ss(L, 2)) : std::vector<int>();
|
||||
lua_pushnumber(L, v.empty() ? 0 : v.front());
|
||||
return 1;
|
||||
}
|
||||
int n_GameGetWorldStateEntity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->world_state_entity() : 0); return 1; }
|
||||
int n_PhysicsRemoveJoints(LuaHost& h, lua_State* L) { if (W(h)) W(h)->physics_remove_joints(nn(L, 1), nn(L, 2), nn(L, 3), nn(L, 4)); return 0; } // ( world_pos_min_x, world_pos_min_y, world_pos_max_x, world_pos_max_y )
|
||||
int n_ConvertMaterialEverywhere(LuaHost& h, lua_State* L) { if (W(h)) W(h)->convert_material_everywhere((int)nn(L, 1), (int)nn(L, 2)); return 0; } // ( material_from_type, material_to_type )
|
||||
int n_GameScreenshake(LuaHost& h, lua_State* L) { if (W(h)) W(h)->screenshake(nn(L, 1), lua_isnumber(L, 2) ? lua_tonumber(L, 2) : 0.0, lua_isnumber(L, 3) ? lua_tonumber(L, 3) : 0.0); return 0; } // ( strength, x = 0, y = 0 )
|
||||
int n_GamePlaySound(LuaHost& h, lua_State* L) { if (W(h)) W(h)->play_sound(ss(L, 1), ss(L, 2), lua_tonumber(L, 3), lua_tonumber(L, 4)); return 0; }
|
||||
int n_GameEntityPlaySound(LuaHost& h, lua_State* L) { if (W(h)) W(h)->entity_play_sound((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||
int n_GameEntityPlaySoundLoop(LuaHost& h, lua_State* L) { if (W(h)) W(h)->entity_play_sound_loop((int)nn(L, 1), ss(L, 2), lua_tonumber(L, 3), lua_isnumber(L, 4) ? lua_tonumber(L, 4) : 0.0); return 0; }
|
||||
// EntityInflictDamage( entity, amount, damage_type, description, ragdoll_fx, impulse_x, impulse_y, entity_who_is_responsible = 0, world_pos_x = entity_x, world_pos_y = entity_y, knockback_force = 0 )
|
||||
// (007b4030: fewer than 7 arguments is an error and does nothing; the optional ones count by lua_gettop)
|
||||
int n_EntityInflictDamage(LuaHost& h, lua_State* L) {
|
||||
const int n = lua_gettop(L);
|
||||
if (n < 7 || !W(h)) return 0;
|
||||
W(h)->inflict_damage((int)nn(L, 1), lua_tonumber(L, 2), ss(L, 3), ss(L, 4), ss(L, 5), lua_tonumber(L, 6), lua_tonumber(L, 7), n >= 8 ? (int)lua_tointeger(L, 8) : 0,
|
||||
n >= 9 ? lua_tonumber(L, 9) : 0.0, n >= 10 ? lua_tonumber(L, 10) : 0.0, n >= 11 ? lua_tonumber(L, 11) : 0.0);
|
||||
return 0;
|
||||
}
|
||||
int n_GamePrint(LuaHost& h, lua_State* L) { if (W(h)) W(h)->print(ss(L, 1), "", false); return 0; }
|
||||
int n_GamePrintImportant(LuaHost& h, lua_State* L) { if (W(h)) W(h)->print(ss(L, 1), ss(L, 2), true); return 0; }
|
||||
int n_GameGetGameEffectCount(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? (double)W(h)->game_effect_components((int)nn(L, 1), ss(L, 2)).size() : 0.0); return 1; }
|
||||
// GameTextGet("$key", args...): no translation files in data.wak, so the key itself (readable form) is returned.
|
||||
int n_GameTextGet(LuaHost& h, lua_State* L) { // the English text of the key; $0.. $9 are the arguments (translated themselves when they are keys)
|
||||
const std::string key = ss(L, 1);
|
||||
std::string t = key.empty() || key[0] != '$' ? key : h.translate(key);
|
||||
if (t.empty()) t = key;
|
||||
for (int a = 0; a < 10; ++a) {
|
||||
const std::string tag = "$" + std::to_string(a);
|
||||
std::string arg = ss(L, 2 + a);
|
||||
if (!arg.empty() && arg[0] == '$') { const std::string& tr = h.translate(arg); if (!tr.empty()) arg = tr; }
|
||||
for (size_t p = 0; (p = t.find(tag, p)) != std::string::npos;) { t.replace(p, tag.size(), arg); p += arg.size(); }
|
||||
}
|
||||
lua_pushstring(L, t.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// the centre of the first hitbox of an entity (its position when it has none)
|
||||
int n_EntityGetFirstHitboxCenter(LuaHost& h, lua_State* L) {
|
||||
double x = 0, y = 0, r = 0, sx = 1, sy = 1;
|
||||
const int id = (int)nn(L, 1);
|
||||
if (!W(h) || !W(h)->get_transform(id, x, y, r, sx, sy)) { lua_pushnil(L); return 1; }
|
||||
for (int c : W(h)->components(id, "HitboxComponent", "", false)) {
|
||||
auto g = [&](const char* k) { const LVal v = W(h)->get_value(c, k, false); return v.t == LVal::T::Str ? std::atof(v.s.c_str()) : 0.0; };
|
||||
lua_pushnumber(L, x + (g("aabb_min_x") + g("aabb_max_x")) * 0.5 * sx);
|
||||
lua_pushnumber(L, y + (g("aabb_min_y") + g("aabb_max_y")) * 0.5 * sy);
|
||||
return 2;
|
||||
}
|
||||
lua_pushnumber(L, x); lua_pushnumber(L, y);
|
||||
return 2;
|
||||
}
|
||||
int n_EntityGetAllComponents(LuaHost& h, lua_State* L) { push_list(L, W(h) ? W(h)->components((int)nn(L, 1), "", "", true) : std::vector<int>()); return 1; }
|
||||
int n_EntityRemoveFromParent(LuaHost& h, lua_State* L) { if (W(h)) W(h)->remove_from_parent((int)nn(L, 1)); return 0; }
|
||||
int n_GameGetCameraBounds(LuaHost& h, lua_State* L) {
|
||||
double x = 0, y = 0, w = 0, ht = 0;
|
||||
if (W(h)) W(h)->camera_bounds(x, y, w, ht);
|
||||
lua_pushnumber(L, x); lua_pushnumber(L, y); lua_pushnumber(L, w); lua_pushnumber(L, ht); // GameGetCameraBounds() -> x, y, w, h (007aea80)
|
||||
return 4;
|
||||
}
|
||||
// GameGetVelocityCompVelocity( entity ) -> vx, vy: VelocityComponent.mVelocity of the entity (pixels per second)
|
||||
int n_GameGetVelocityCompVelocity(LuaHost& h, lua_State* L) {
|
||||
double vx = 0, vy = 0;
|
||||
if (W(h)) for (int c : W(h)->components((int)nn(L, 1), "VelocityComponent", "", true)) {
|
||||
const LVal x = W(h)->get_value(c, "mVelocity.x", true), y = W(h)->get_value(c, "mVelocity.y", true);
|
||||
if (x.t == LVal::T::Num) vx = x.n;
|
||||
if (y.t == LVal::T::Num) vy = y.n;
|
||||
break;
|
||||
}
|
||||
lua_pushnumber(L, vx); lua_pushnumber(L, vy);
|
||||
return 2;
|
||||
}
|
||||
// PhysicsApplyForceOnArea( calculate_force_for_body, entity_id, x0, y0, x1, y1 ): the callback gets (entity, mass, x, y, vel_x, vel_y, vel_angular) for every rigid body in the area and
|
||||
// returns force_pos_x, force_pos_y, force_x, force_y, force_angular; the force is applied to the body (black_hole_gravity.lua)
|
||||
int n_PhysicsApplyForceOnArea(LuaHost& h, lua_State* L) {
|
||||
if (!W(h) || !lua_isfunction(L, 1)) return 0;
|
||||
const double x0 = nn(L, 3), y0 = nn(L, 4), x1 = nn(L, 5), y1 = nn(L, 6);
|
||||
for (const IEntityWorld::BodyInfo& b : W(h)->bodies_in_area(x0, y0, x1, y1)) {
|
||||
lua_pushvalue(L, 1);
|
||||
lua_pushnumber(L, b.entity); lua_pushnumber(L, b.mass); lua_pushnumber(L, b.x); lua_pushnumber(L, b.y); lua_pushnumber(L, b.vx); lua_pushnumber(L, b.vy); lua_pushnumber(L, b.angular);
|
||||
if (lua_pcall(L, 7, 5, 0) != 0) { lua_pop(L, 1); continue; }
|
||||
const double fx = lua_isnumber(L, -3) ? lua_tonumber(L, -3) : 0.0, fy = lua_isnumber(L, -2) ? lua_tonumber(L, -2) : 0.0;
|
||||
lua_pop(L, 5);
|
||||
if (fx != 0.0 || fy != 0.0) W(h)->physics_area_force(b.entity, fx, fy);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int n_GameGetCameraPos(LuaHost& h, lua_State* L) {
|
||||
double x = 0, y = 0, w = 0, ht = 0;
|
||||
if (W(h)) W(h)->camera_bounds(x, y, w, ht);
|
||||
lua_pushnumber(L, x + w * 0.5); lua_pushnumber(L, y + ht * 0.5);
|
||||
return 2;
|
||||
}
|
||||
int n_GameRegenItemActionsInPlayer(LuaHost& h, lua_State* L) { if (W(h)) W(h)->regen_item_actions((int)nn(L, 1)); return 0; }
|
||||
int n_PhysicsGetComponentVelocity(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; } // (the velocity of a body lives on the GPU)
|
||||
int n_EntityCreateNew(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->create_entity(ss(L, 1)) : 0); return 1; }
|
||||
int n_ComponentAddTag(LuaHost& h, lua_State* L) { if (W(h)) W(h)->comp_add_tag((int)nn(L, 1), ss(L, 2)); return 0; }
|
||||
int n_EntityGetHerdRelation(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->herd_relation((int)nn(L, 1), (int)nn(L, 2)) : 0); return 1; }
|
||||
int n_EntityGetWandCapacity(LuaHost& h, lua_State* L) { lua_pushnumber(L, W(h) ? W(h)->wand_capacity((int)nn(L, 1)) : 0); return 1; }
|
||||
int n_GameGetIsTrailerModeEnabled(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; }
|
||||
int n_EntityLoadToEntity(LuaHost& h, lua_State* L) { // the file's components are added to the entity (its tags are not)
|
||||
if (W(h)) W(h)->load_to_entity(ss(L, 1), (int)nn(L, 2));
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void register_entity_natives(LuaHost& h) {
|
||||
struct { const char* name; NativeFn fn; } natives[] = {
|
||||
{"EntityLoad", n_EntityLoad}, {"EntityLoadCameraBound", n_EntityLoad}, {"EntityLoadEndGameItem", n_EntityLoad},
|
||||
{"GameGetVelocityCompVelocity", n_GameGetVelocityCompVelocity}, {"PhysicsApplyForceOnArea", n_PhysicsApplyForceOnArea}, {"EntityKill", n_EntityKill}, {"PhysicsBody2InitFromComponents", n_PhysicsBody2InitFromComponents}, {"EntityGetIsAlive", n_EntityGetIsAlive},
|
||||
{"EntityGetTransform", n_EntityGetTransform}, {"EntitySetTransform", n_EntitySetTransform}, {"EntityApplyTransform", n_EntitySetTransform},
|
||||
{"EntityGetName", n_EntityGetName}, {"EntitySetName", n_EntitySetName}, {"EntityGetFilename", n_EntityGetFilename},
|
||||
{"EntityHasTag", n_EntityHasTag}, {"EntityAddTag", n_EntityAddTag}, {"EntityRemoveTag", n_EntityRemoveTag}, {"EntityGetTags", n_EntityGetTags},
|
||||
{"EntityGetWithTag", n_EntityGetWithTag}, {"EntityGetInRadiusWithTag", n_EntityGetInRadiusWithTag}, {"EntityGetInRadius", n_EntityGetInRadius},
|
||||
{"EntityGetClosestWithTag", n_EntityGetClosestWithTag}, {"EntityGetParent", n_EntityGetParent}, {"EntityGetRootEntity", n_EntityGetRootEntity},
|
||||
{"EntityGetAllChildren", n_EntityGetAllChildren},
|
||||
{"EntityGetFirstComponent", n_EntityGetFirstComponent}, {"EntityGetFirstComponentIncludingDisabled", n_EntityGetFirstComponentIncludingDisabled},
|
||||
{"EntityGetComponent", n_EntityGetComponent}, {"EntityGetComponentIncludingDisabled", n_EntityGetComponentIncludingDisabled},
|
||||
{"EntityAddComponent", n_EntityAddComponent}, {"EntityAddComponent2", n_EntityAddComponent2}, {"EntityRemoveComponent", n_EntityRemoveComponent},
|
||||
{"EntitySetComponentIsEnabled", n_EntitySetComponentIsEnabled}, {"ComponentGetIsEnabled", n_ComponentGetIsEnabled},
|
||||
{"ComponentGetTypeName", n_ComponentGetTypeName}, {"ComponentGetEntity", n_ComponentGetEntity}, {"ComponentHasTag", n_ComponentHasTag},
|
||||
{"ComponentGetValue", n_ComponentGetValue}, {"ComponentGetValue2", n_ComponentGetValue2}, {"ComponentGetValueInt", n_ComponentGetValueInt},
|
||||
{"ComponentGetValueFloat", n_ComponentGetValueFloat}, {"ComponentGetValueBool", n_ComponentGetValueBool},
|
||||
{"ComponentSetValue", n_ComponentSetValue}, {"ComponentSetValue2", n_ComponentSetValue2},
|
||||
{"ComponentObjectGetValue", n_ComponentObjectGetValue}, {"ComponentObjectGetValue2", n_ComponentObjectGetValue2},
|
||||
{"ComponentObjectSetValue", n_ComponentObjectSetValue}, {"ComponentObjectSetValue2", n_ComponentObjectSetValue2},
|
||||
{"AddGunAction", n_AddGunAction}, {"AddGunActionPermanent", n_AddGunActionPermanent}, {"EntityAddChild", n_EntityAddChild}, {"IsPlayer", n_IsPlayer}, {"GameGetPotionColorUint", n_GameGetPotionColorUint},
|
||||
{"CreateItemActionEntity", n_CreateItemActionEntity}, {"AddMaterialInventoryMaterial", n_AddMaterialInventoryMaterial}, {"GetMaterialInventoryMainMaterial", n_GetMaterialInventoryMainMaterial}, {"StatsGlobalGetValue", n_StatsGlobalGetValue}, {"PhysicsApplyForce", n_PhysicsApplyForce}, {"GameVecToPhysicsVec", n_GameVecToPhysicsVec},
|
||||
{"EntitySetComponentsWithTagEnabled", n_EntitySetComponentsWithTagEnabled}, {"ComponentSetValueVector2", n_ComponentSetValueVector2},
|
||||
{"ComponentGetValueVector2", n_ComponentGetValueVector2},
|
||||
{"GetUpdatedEntityID", n_GetUpdatedEntityID}, {"GetUpdatedComponentID", n_GetUpdatedComponentID}, {"GameGetFrameNum", n_GameGetFrameNum},
|
||||
{"GameGetWorldStateEntity", n_GameGetWorldStateEntity}, {"BiomeMapGetName", n_BiomeMapGetName}, {"GameShootProjectile", n_GameShootProjectile}, {"GetGameEffectLoadTo", n_GetGameEffectLoadTo}, {"LoadGameEffectEntityTo", n_LoadGameEffectEntityTo}, {"GameGetGameEffect", n_GameGetGameEffect}, {"GamePrint", n_GamePrint}, {"GamePlaySound", n_GamePlaySound}, {"PhysicsRemoveJoints", n_PhysicsRemoveJoints}, {"GameScreenshake", n_GameScreenshake}, {"ConvertMaterialEverywhere", n_ConvertMaterialEverywhere}, {"GameEntityPlaySound", n_GameEntityPlaySound}, {"GameEntityPlaySoundLoop", n_GameEntityPlaySoundLoop}, {"EntityInflictDamage", n_EntityInflictDamage}, {"GamePrintImportant", n_GamePrintImportant},
|
||||
{"EntityGetFirstHitboxCenter", n_EntityGetFirstHitboxCenter}, {"EntityGetAllComponents", n_EntityGetAllComponents}, {"GameRegenItemActionsInPlayer", n_GameRegenItemActionsInPlayer}, {"PhysicsGetComponentVelocity", n_PhysicsGetComponentVelocity}, {"EntityRemoveFromParent", n_EntityRemoveFromParent}, {"GameGetCameraBounds", n_GameGetCameraBounds}, {"GameGetCameraPos", n_GameGetCameraPos}, {"EntityCreateNew", n_EntityCreateNew}, {"ComponentAddTag", n_ComponentAddTag}, {"EntityGetHerdRelation", n_EntityGetHerdRelation}, {"EntityGetWandCapacity", n_EntityGetWandCapacity}, {"GameGetIsTrailerModeEnabled", n_GameGetIsTrailerModeEnabled}, {"EntityLoadToEntity", n_EntityLoadToEntity}, {"ComponentGetMetaCustom", n_ComponentGetValue}, {"ComponentSetMetaCustom", n_ComponentSetValue},
|
||||
{"GameGetGameEffectCount", n_GameGetGameEffectCount}, {"GameTextGet", n_GameTextGet}, {"GameTextGetTranslatedOrNot", n_GameTextGet},
|
||||
};
|
||||
for (auto& n : natives) h.register_native(n.name, n.fn);
|
||||
}
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,456 @@
|
||||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include "lua/gpu_lua.h"
|
||||
#include "lua/gpu_lua_profiles.h"
|
||||
#include "gpu_lua_sources.h"
|
||||
#include <shaderc/shaderc.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
// This target deliberately uses the exact vendored LuaJIT compiler/bytecode.
|
||||
// No source is evaluated: luaL_loadbuffer only parses and creates a prototype.
|
||||
extern "C" {
|
||||
#include "lauxlib.h"
|
||||
#include "lua.h"
|
||||
}
|
||||
|
||||
#include "lua/gpu_lua_proto.h"
|
||||
|
||||
namespace ng::gpu_lua {
|
||||
uint64_t fingerprint(const unsigned char* data, std::size_t size) {
|
||||
uint64_t h=14695981039346656037ull;
|
||||
for(std::size_t i=0;i<size;++i)if(data[i]!=13)h=(h^data[i])*1099511628211ull;
|
||||
return h;
|
||||
}
|
||||
namespace {
|
||||
const char* const library="data/scripts/lib/utilities.lua";
|
||||
const std::map<std::string,unsigned> apis={
|
||||
{"GetUpdatedEntityID",1},{"EntityGetTransform",2},{"EntityGetFirstComponent",3},
|
||||
{"ComponentGetValue2",4},{"ComponentSetValue2",5},{"dofile_once",6},{"dofile",7},
|
||||
{"RaytraceSurfaces",8},{"EntityApplyTransform",9},
|
||||
{"ComponentGetValueVector2",4},{"ComponentSetValueVector2",5}};
|
||||
const std::map<std::string,unsigned> math_apis={
|
||||
{"abs",10},{"sqrt",11},{"atan2",12},{"cos",13},{"sin",14},{"max",15}};
|
||||
using GCproto=NgLuaProto;
|
||||
using BCIns=NgLuaInstruction;
|
||||
static NgLuaOp bc_op(BCIns i){return i.op;}
|
||||
static unsigned bc_a(BCIns i){return i.a;}
|
||||
static unsigned bc_b(BCIns i){return i.b;}
|
||||
static unsigned bc_c(BCIns i){return i.c;}
|
||||
static unsigned bc_d(BCIns i){return i.d;}
|
||||
static int bc_j(BCIns i){return i.jump;}
|
||||
struct Instructions {GCproto* p;BCIns operator[](unsigned i){return ng_lua_instruction(p,i);}};
|
||||
static Instructions proto_bc(GCproto* p){return {p};}
|
||||
std::string number(double n) {
|
||||
const float f=static_cast<float>(n);uint32_t bits;std::memcpy(&bits,&f,4);
|
||||
std::ostringstream out;out<<"uintBitsToFloat(0x"<<std::hex<<bits<<"u)";return out.str();
|
||||
}
|
||||
struct Parsed {
|
||||
std::unique_ptr<lua_State,decltype(&lua_close)> state{luaL_newstate(),lua_close};
|
||||
GCproto view{};GCproto* pt=&view;
|
||||
explicit Parsed(const std::string& source,const std::string& path) {
|
||||
if(!state)throw std::runtime_error("Lua GPU: cannot allocate parser");
|
||||
if(source.size()>1024*1024)throw std::runtime_error(path+": Lua GPU source limit exceeded");
|
||||
if(!source.empty()&&static_cast<unsigned char>(source[0])==0x1b)throw std::runtime_error(path+": Lua source required, bytecode input rejected");
|
||||
if(luaL_loadbuffer(state.get(),source.data(),source.size(),path.c_str())!=0)
|
||||
throw std::runtime_error(lua_tostring(state.get(),-1));
|
||||
// Check the linked VM's frame ABI before dereferencing private structures.
|
||||
std::vector<unsigned char> dump;
|
||||
lua_dump(state.get(),[](lua_State*,const void* p,size_t n,void* dst)->int {
|
||||
auto& b=*static_cast<std::vector<unsigned char>*>(dst);
|
||||
b.insert(b.end(),static_cast<const unsigned char*>(p),static_cast<const unsigned char*>(p)+n);return 0;
|
||||
},&dump);
|
||||
if(dump.size()<5||dump[3]!=2||bool(dump[4]&8)!=bool(ng_lua_frame_shift()))throw std::runtime_error("Lua GPU: linked LuaJIT bytecode ABI mismatch");
|
||||
if(!ng_lua_view(state.get(),pt))throw std::runtime_error("Lua GPU: expected Lua prototype");
|
||||
}
|
||||
};
|
||||
uint64_t compiled_fingerprint(Parsed& parsed) {
|
||||
uint64_t digest=14695981039346656037ull;
|
||||
const int status=ng_lua_dump_stripped(parsed.state.get(),parsed.pt,
|
||||
[](lua_State*,const void* bytes,size_t size,void* context)->int {
|
||||
auto& hash=*static_cast<uint64_t*>(context);
|
||||
const auto* data=static_cast<const unsigned char*>(bytes);
|
||||
// Bytecode is binary: CR bytes must participate in this digest.
|
||||
for(size_t i=0;i<size;++i)hash=(hash^data[i])*1099511628211ull;
|
||||
return 0;
|
||||
},&digest);
|
||||
if(status)throw std::runtime_error("Lua GPU: cannot fingerprint compiled library");
|
||||
return digest;
|
||||
}
|
||||
unsigned line(GCproto* pt,unsigned ip){return ng_lua_instruction(pt,ip).line;}
|
||||
std::string str(GCproto* pt,unsigned d) {
|
||||
size_t length=0;const char* bytes=ng_lua_string(pt,d,&length);
|
||||
if(!bytes)throw std::runtime_error("Lua GPU: invalid string constant");
|
||||
std::string result(bytes,length);
|
||||
if(std::any_of(result.begin(),result.end(),[](unsigned char c){return c>=128;}))throw std::runtime_error("Lua GPU: ASCII strings required");
|
||||
return result;
|
||||
}
|
||||
double numeric(GCproto* pt,unsigned d) {
|
||||
if(d>=pt->sizekn)throw std::runtime_error("Lua GPU: invalid numeric constant");
|
||||
return ng_lua_number(pt,d);
|
||||
}
|
||||
struct Generator {
|
||||
std::string path;
|
||||
Parsed root;
|
||||
std::map<std::string,unsigned> symbols;
|
||||
std::map<std::string,std::string> exports;
|
||||
std::map<std::string,unsigned> globals;
|
||||
uint64_t library_semantic = 0;
|
||||
explicit Generator(const std::string& source,const std::string& file):path(file),root(source,file){}
|
||||
unsigned intern(const std::string& s) {
|
||||
auto [it,added]=symbols.emplace(s,static_cast<unsigned>(symbols.size()+1));return it->second;
|
||||
}
|
||||
std::string literal(GCproto* p,BCIns ins) {
|
||||
const auto op=bc_op(ins);const unsigned d=bc_d(ins);
|
||||
if(op==NG_BC_KSHORT)return "ngp_num("+number(static_cast<int16_t>(d))+")";
|
||||
if(op==NG_BC_KNUM)return "ngp_num("+number(numeric(p,d))+")";
|
||||
if(op==NG_BC_KSTR)return "ngp_string("+std::to_string(intern(str(p,d)))+"u)";
|
||||
if(op==NG_BC_KPRI&&d<=2)return d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";
|
||||
throw std::runtime_error("Lua GPU: unsupported literal");
|
||||
}
|
||||
void read_exports(const std::string& source) {
|
||||
// Reviewed utilities: top-level constants precede function/table values.
|
||||
// Those unread inert definitions are omitted; function bodies never run.
|
||||
Parsed lib(source,library);library_semantic=compiled_fingerprint(lib);
|
||||
// Both reviewed distributions have the same constants / edit_component
|
||||
// behavior. Steam additionally defines unused debug/vector/color/texture
|
||||
// helpers. Comments, source paths and line information are not behavior.
|
||||
if(library_semantic!=6066579123591393726ull && library_semantic!=13023294535491161211ull)
|
||||
throw std::runtime_error(std::string(library)+": unreviewed compiled library (fingerprint "+std::to_string(library_semantic)+")");
|
||||
std::map<unsigned,std::string> values;
|
||||
for(unsigned i=1;i<lib.pt->sizebc;++i){
|
||||
BCIns ins=proto_bc(lib.pt)[i];auto op=bc_op(ins);unsigned a=bc_a(ins),d=bc_d(ins);
|
||||
if(op==NG_BC_FNEW)break;
|
||||
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)values[a]=literal(lib.pt,ins);
|
||||
else if(op==NG_BC_GSET&&values.count(a))exports[str(lib.pt,d)]=values.at(a);
|
||||
else throw std::runtime_error(std::string(library)+": unsupported library initializer");
|
||||
}
|
||||
}
|
||||
std::string body() {
|
||||
GCproto* p=root.pt;
|
||||
if(p->sizebc>1024||p->framesize>64||p->sizeuv)throw std::runtime_error("Lua GPU: target limit exceeded (1024 instructions, 64 registers, no upvalues)");
|
||||
auto reg=[&](unsigned r){if(r>=p->framesize)throw std::runtime_error("Lua GPU: register range exceeded");return "ngp_r["+std::to_string(r)+"]";};
|
||||
std::ostringstream cases;
|
||||
for(unsigned ip=1;ip<p->sizebc;++ip){
|
||||
const BCIns ins=proto_bc(p)[ip];const auto op=bc_op(ins);
|
||||
const unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
|
||||
auto fail=[&](const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": "+why+" ("+ins.name+")");};
|
||||
cases<<"case "<<ip<<": {ngp_line="<<line(p,ip)<<"u;ngp_ip="<<ip+1<<";";
|
||||
auto test=[&](const std::string& cond){
|
||||
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail("expected conditional jump");
|
||||
cases<<"if(!("<<cond<<"))ngp_ip++;";
|
||||
};
|
||||
if(op==NG_BC_KSHORT||op==NG_BC_KNUM||op==NG_BC_KPRI||op==NG_BC_KSTR)cases<<reg(a)<<"="<<literal(p,ins)<<";";
|
||||
else if(op==NG_BC_MOV)cases<<reg(a)<<"="<<reg(d)<<";";
|
||||
else if(op==NG_BC_KNIL){reg(d);for(unsigned k=a;k<=d;k++)cases<<reg(k)<<"=ngp_nil();";}
|
||||
else if(op==NG_BC_NOT)cases<<reg(a)<<"=ngp_bool(!ngp_truth("<<reg(d)<<"));";
|
||||
else if(op==NG_BC_UNM)cases<<reg(a)<<"=ngp_num(-ngp_number("<<reg(d)<<"));";
|
||||
else if(op==NG_BC_GGET){
|
||||
const std::string name=str(p,d);const auto api=apis.find(name);
|
||||
if(api!=apis.end())cases<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
|
||||
else if(name=="math")cases<<reg(a)<<"=NgpValue(5u,0.0,1u);";
|
||||
else if(exports.count(name)){
|
||||
auto [it,added]=globals.emplace(name,static_cast<unsigned>(globals.size()));
|
||||
cases<<reg(a)<<"=ngp_g["<<it->second<<"];";
|
||||
}else fail("unsupported global/API '"+name+"'");
|
||||
}else if(op==NG_BC_TGETS){
|
||||
const auto field=str(p,c);const auto api=math_apis.find(field);
|
||||
if(api==math_apis.end())fail("unsupported math field '"+field+"'");
|
||||
cases<<"if("<<reg(b)<<".tag!=5u)ngp_fault(3u);"<<reg(a)<<"=NgpValue(4u,0.0,"<<api->second<<"u);";
|
||||
}else if(op==NG_BC_CALL){
|
||||
if(b==0||c==0||b>9||c>9)fail("only fixed calls with up to eight arguments/results are supported");
|
||||
cases<<"NgpTuple args=ngp_empty();args.count="<<c-1<<";";
|
||||
for(unsigned k=0;k<c-1;k++)cases<<"args.v["<<k<<"]="<<reg(a+1+ng_lua_frame_shift()+k)<<";";
|
||||
cases<<"NgpTuple result=ngp_call("<<reg(a)<<",args);";
|
||||
for(unsigned k=0;k<b-1;k++)cases<<reg(a+k)<<"=result.v["<<k<<"];";
|
||||
}else if(op==NG_BC_JMP||op==NG_BC_UCLO){
|
||||
const int target=static_cast<int>(ip)+1+static_cast<int>(bc_j(ins));
|
||||
if(target<1||target>=static_cast<int>(p->sizebc))fail("jump target out of range");
|
||||
cases<<"ngp_ip="<<target<<";";
|
||||
}else if(op==NG_BC_RET0||op==NG_BC_RET1||op==NG_BC_RET)cases<<"projectile=ngp_proj;return ngp_error;";
|
||||
else if(op==NG_BC_LOOP){} // Marker only; backedges are ordinary JMP.
|
||||
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT){
|
||||
std::string cond="ngp_less("+reg(a)+","+reg(d)+","+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"true":"false")+")";
|
||||
// LuaJIT GE/GT are the complement tests, including unordered NaN.
|
||||
if(op==NG_BC_ISGE||op==NG_BC_ISGT)cond="!"+cond;
|
||||
test(cond);
|
||||
}else if(op>=NG_BC_ISEQV&&op<=NG_BC_ISNEP){
|
||||
std::string rhs;
|
||||
if(op<=NG_BC_ISNEV)rhs=reg(d);
|
||||
else if(op<=NG_BC_ISNES)rhs="ngp_string("+std::to_string(intern(str(p,d)))+"u)";
|
||||
else if(op<=NG_BC_ISNEN)rhs="ngp_num("+number(numeric(p,d))+")";
|
||||
else {if(d>2)fail("invalid primitive");rhs=d==0?"ngp_nil()":d==1?"ngp_bool(false)":"ngp_bool(true)";}
|
||||
const bool unequal=(static_cast<int>(op)-NG_BC_ISEQV)%2!=0;
|
||||
test(std::string(unequal?"!":"")+"ngp_equal("+reg(a)+","+rhs+")");
|
||||
}else if(op==NG_BC_IST||op==NG_BC_ISF||op==NG_BC_ISTC||op==NG_BC_ISFC){
|
||||
const bool truth=op==NG_BC_IST||op==NG_BC_ISTC;
|
||||
std::string cond=std::string(truth?"":"!")+"ngp_truth("+reg(d)+")";
|
||||
if(op==NG_BC_ISTC||op==NG_BC_ISFC)cases<<"if("<<cond<<")"<<reg(a)<<"="<<reg(d)<<";";
|
||||
test(cond);
|
||||
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW){
|
||||
const int index=static_cast<int>(op)-NG_BC_ADDVN;
|
||||
const int arithmetic=op==NG_BC_POW?5:index%5;
|
||||
std::string left,right;
|
||||
if(index<5){left=reg(b);right="ngp_num("+number(numeric(p,c))+")";}
|
||||
else if(index<10){left="ngp_num("+number(numeric(p,c))+")";right=reg(b);}
|
||||
else {left=reg(b);right=reg(c);}
|
||||
cases<<reg(a)<<"=ngp_arithmetic("<<arithmetic<<","<<left<<","<<right<<");";
|
||||
}else fail("unsupported Lua GPU instruction");
|
||||
cases<<"break;}\n";
|
||||
}
|
||||
std::ostringstream out;
|
||||
out<<"NgpValue ngp_g["<<std::max<std::size_t>(1,globals.size())<<"];\nvoid ngp_module_init(){";
|
||||
for(const auto& [name,index]:globals)out<<"ngp_g["<<index<<"]="<<exports.at(name)<<";";
|
||||
out<<"}\nuvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n"
|
||||
"ngp_proj=projectile;ngp_owner=owner;ngp_owner_id=owner_id;ngp_failed=false;ngp_loaded=false;ngp_error=uvec4(0u);ngp_line=0u;\n"
|
||||
"for(int i=0;i<ngp_g.length();i++)ngp_g[i]=ngp_nil();\nNgpValue ngp_r["<<unsigned(p->framesize)<<"];\n"
|
||||
"for(int i=0;i<ngp_r.length();i++)ngp_r[i]=ngp_nil();int ngp_ip=1;\n"
|
||||
"for(int budget=0;budget<4096&&!ngp_failed;budget++){switch(ngp_ip){\n"<<cases.str()<<
|
||||
"default:ngp_fault(3u);break;}}\nif(!ngp_failed)ngp_fault(4u);projectile=ngp_proj;return ngp_error;}\n";
|
||||
return out.str();
|
||||
}
|
||||
std::string preamble() {
|
||||
std::ostringstream out;
|
||||
for(const auto& [label,value]:std::map<std::string,std::string>{{"PROJECTILE","ProjectileComponent"},{"VELOCITY","VelocityComponent"},{"WHO","mWhoShot"},{"GRAVITY","gravity_y"},{"MOTION","mVelocity"},{"UTIL",library}})
|
||||
out<<"const uint NGP_"<<label<<"="<<intern(value)<<"u;\n";
|
||||
out<<"uint ngp_rank(uint s){switch(s){";
|
||||
unsigned rank=0;for(const auto& [s,id]:symbols)out<<"case "<<id<<"u:return "<<rank++<<"u;";
|
||||
out<<"default:return 0u;}}\nbool ngp_parse_number(uint s,out float n){n=0.0;switch(s){";
|
||||
for(const auto& [s,id]:symbols){
|
||||
lua_pushlstring(root.state.get(),s.data(),s.size());
|
||||
if(lua_isnumber(root.state.get(),-1))out<<"case "<<id<<"u:n="<<number(lua_tonumber(root.state.get(),-1))<<";return true;";
|
||||
lua_pop(root.state.get(),1);
|
||||
}
|
||||
out<<"default:return false;}}\nvoid ngp_module_init();\n";return out.str();
|
||||
}
|
||||
std::string scalar_body() {
|
||||
// Typed, acyclic SSA lowering for fused additive neighbor bodies. There
|
||||
// is no register interpreter or dynamic API dispatch in the inner loop.
|
||||
struct Value {int kind=0;std::string expr;unsigned api=0;double constant=NAN;};
|
||||
enum {Number=1,Self,Velocity,Function,String,Math};
|
||||
auto* p=root.pt;std::vector<Value> r(p->framesize);std::ostringstream out;
|
||||
out<<"uvec4 ngp_execute(inout FProj projectile,vec3 owner,float owner_id){\n";
|
||||
auto fail=[&](unsigned ip,const std::string& why){throw std::runtime_error(path+":"+std::to_string(line(p,ip))+": scalar neighbor lowering: "+why);};
|
||||
auto numeric_value=[&](const Value& v,unsigned ip){if(v.kind!=Number)fail(ip,"numeric value required");return v.expr;};
|
||||
auto temp=[&](unsigned reg,unsigned ip,const std::string& expr){
|
||||
const auto name="v"+std::to_string(ip)+"_"+std::to_string(reg);
|
||||
out<<"float "<<name<<"="<<expr<<";\n";r.at(reg)={Number,name};
|
||||
};
|
||||
for(unsigned ip=1;ip<p->sizebc;++ip) {
|
||||
const auto ins=proto_bc(p)[ip];const auto op=bc_op(ins);unsigned a=bc_a(ins),b=bc_b(ins),c=bc_c(ins),d=bc_d(ins);
|
||||
if(op==NG_BC_KNUM||op==NG_BC_KSHORT){double n=op==NG_BC_KNUM?numeric(p,d):static_cast<int16_t>(d);r.at(a)={Number,number(n),0,n};}
|
||||
else if(op==NG_BC_KSTR)r.at(a)={String,str(p,d)};
|
||||
else if(op==NG_BC_MOV)r.at(a)=r.at(d);
|
||||
else if(op==NG_BC_GGET) {
|
||||
auto name=str(p,d);auto api=apis.find(name);
|
||||
if(name=="math")r.at(a)={Math};
|
||||
else if(api!=apis.end())r.at(a)={Function,"",api->second};
|
||||
else fail(ip,"unknown global "+name);
|
||||
}else if(op==NG_BC_TGETS) {
|
||||
auto api=math_apis.find(str(p,c));
|
||||
if(r.at(b).kind!=Math||api==math_apis.end())fail(ip,"unsupported table access");
|
||||
r.at(a)={Function,"",api->second};
|
||||
}else if(op>=NG_BC_ADDVN&&op<=NG_BC_POW) {
|
||||
int index=int(op)-NG_BC_ADDVN, arithmetic=op==NG_BC_POW?5:index%5;
|
||||
Value left,right;
|
||||
if(index<5){left=r.at(b);double n=numeric(p,c);right={Number,number(n),0,n};}
|
||||
else if(index<10){double n=numeric(p,c);left={Number,number(n),0,n};right=r.at(b);}
|
||||
else{left=r.at(b);right=r.at(c);}
|
||||
auto x=numeric_value(left,ip),y=numeric_value(right,ip);
|
||||
if(arithmetic==5){if(right.constant!=2.0)fail(ip,"only square powers are reviewed");temp(a,ip,"("+x+"*"+x+")");}
|
||||
else if(arithmetic<4)temp(a,ip,"("+x+std::string(1,"+-*/"[arithmetic])+y+")");
|
||||
else fail(ip,"unreviewed arithmetic");
|
||||
}else if(op==NG_BC_UNM)temp(a,ip,"(-"+numeric_value(r.at(d),ip)+")");
|
||||
else if(op>=NG_BC_ISLT&&op<=NG_BC_ISGT) {
|
||||
if(ip+1>=p->sizebc||bc_op(proto_bc(p)[ip+1])!=NG_BC_JMP)fail(ip,"conditional jump required");
|
||||
auto jump=proto_bc(p)[ip+1];int target=int(ip)+2+bc_j(jump);
|
||||
if(target<=int(ip)||target>=int(p->sizebc)||bc_op(proto_bc(p)[target])!=NG_BC_RET0)fail(ip,"only terminal forward guards are reviewed");
|
||||
auto x=numeric_value(r.at(a),ip),y=numeric_value(r.at(d),ip);
|
||||
std::string condition="("+x+((op==NG_BC_ISLE||op==NG_BC_ISGT)?"<=":"<")+y+")";
|
||||
if(op==NG_BC_ISGE||op==NG_BC_ISGT)condition="!"+condition;
|
||||
out<<"if("<<condition<<")return uvec4(0u);\n";++ip;
|
||||
}else if(op==NG_BC_CALL) {
|
||||
if(r.at(a).kind!=Function||b==0||c==0)fail(ip,"fixed native call required");
|
||||
std::vector<Value> args;for(unsigned k=0;k<c-1;++k)args.push_back(r.at(a+1+ng_lua_frame_shift()+k));
|
||||
auto count=[&](unsigned n){if(args.size()!=n)fail(ip,"argument count mismatch");};
|
||||
auto num=[&](unsigned n){return numeric_value(args.at(n),ip);};
|
||||
auto api=r.at(a).api;
|
||||
if(api==1){count(0);r.at(a)={Self};}
|
||||
else if(api==2){
|
||||
count(1);std::string source;
|
||||
if(args[0].kind==Self)source="projectile.a";
|
||||
else if(args[0].kind==Number&&args[0].constant==1.0)source="owner";
|
||||
else fail(ip,"unreviewed entity handle");
|
||||
if(b!=3)fail(ip,"two transform results required");
|
||||
temp(a,ip,source+".x");temp(a+1,ip,source+".y");
|
||||
}else if(api==3){count(2);if(args[0].kind!=Self||args[1].kind!=String||args[1].expr!="VelocityComponent")fail(ip,"velocity component required");r.at(a)={Velocity};}
|
||||
else if(api==4||api==5){
|
||||
count(api==4?2:4);
|
||||
if(args[0].kind!=Velocity||args[1].kind!=String||args[1].expr!="mVelocity")fail(ip,"velocity vector required");
|
||||
if(api==4){if(b!=3)fail(ip,"two velocity results required");temp(a,ip,"projectile.a.z*60.0");temp(a+1,ip,"projectile.a.w*60.0");}
|
||||
else {if(b!=1)fail(ip,"write must have no results");out<<"projectile.a.zw=vec2("<<num(2)<<","<<num(3)<<")/60.0;\n";}
|
||||
}else if(api>=10&&api<=15){
|
||||
count(api==12||api==15?2:1);if(b!=2)fail(ip,"one math result required");
|
||||
const char* names[]={"abs","sqrt","ngp_scalar_atan","cos","sin","max"};
|
||||
temp(a,ip,std::string(names[api-10])+"("+num(0)+(args.size()==2?","+num(1):"")+")");
|
||||
}else fail(ip,"unreviewed native API");
|
||||
}else if(op==NG_BC_RET0)out<<"return uvec4(0u);\n";
|
||||
else fail(ip,std::string("unsupported instruction ")+ins.name);
|
||||
}
|
||||
out<<"}\n";
|
||||
return "float ngp_scalar_atan(float y,float x){float a;if(y==0.0)a=(floatBitsToUint(x)&0x80000000u)!=0u?3.141592653589793:0.0;else if(x==0.0)a=1.5707963267948966;else a=atan(abs(y),x);return (floatBitsToUint(y)&0x80000000u)!=0u?-a:a;}\n"+out.str();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
uint64_t library_fingerprint(const std::string& source) {
|
||||
Parsed parsed(source,library);return compiled_fingerprint(parsed);
|
||||
}
|
||||
bool dependency_matches(const Dependency& dep,const unsigned char* data,std::size_t size) {
|
||||
const auto raw=fingerprint(data,size);
|
||||
if(raw==dep.fingerprint||(dep.semantic!=0&&raw==dep.validated_fingerprint))return true;
|
||||
if(dep.semantic==0)return false;
|
||||
try {
|
||||
if(library_fingerprint(std::string(reinterpret_cast<const char*>(data),size))!=dep.semantic)return false;
|
||||
dep.validated_fingerprint=raw;return true;
|
||||
}catch(const std::exception&){return false;}
|
||||
}
|
||||
|
||||
Program translate(const std::string& source,const Reader& read,const std::string& path) {
|
||||
Generator gen(source,path);const std::string lib=read(library);gen.read_exports(lib);
|
||||
Program p;
|
||||
if(program_id(path)>=first_neighbor_program)p.function=gen.scalar_body();
|
||||
else {
|
||||
p.function=gen.body(); // Discover symbols/globals before constructing helpers.
|
||||
p.function=gen.preamble()+sources::runtime+p.function;
|
||||
}
|
||||
p.shader=std::string(sources::layout)+sources::terrain+p.function+
|
||||
"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){if(program!=1u)return uvec4(14u,0u,0u,0u);return ngp_execute(p,owner,owner_id);}\n"
|
||||
"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){return uvec4(14u,0u,0u,0u);}\n"+sources::dispatch;
|
||||
p.instructions=gen.root.pt->sizebc-1;p.registers=gen.root.pt->framesize;
|
||||
for(const auto& [path,bytes]:std::map<std::string,std::string>{{path,source},{library,lib}})
|
||||
p.dependencies.push_back({path,fingerprint(reinterpret_cast<const unsigned char*>(bytes.data()),bytes.size())});
|
||||
for(auto& dep:p.dependencies)if(dep.path==library)dep.semantic=gen.library_semantic;
|
||||
return p;
|
||||
}
|
||||
|
||||
Program translate_reviewed(const Reader& read) {
|
||||
Program bundle;std::map<std::string,Dependency> dependencies;
|
||||
std::ostringstream dispatch,neighbors;
|
||||
dispatch<<"uvec4 ngp_dispatch(uint program,inout FProj p,vec3 owner,float owner_id){switch(program){";
|
||||
neighbors<<"uvec4 ngp_neighbor_dispatch(uint program,inout FProj p,vec3 owner){switch(program){";
|
||||
unsigned id=0;
|
||||
for(const auto& profile:profiles) {
|
||||
++id;const auto source=read(profile.path);
|
||||
if(fingerprint(reinterpret_cast<const unsigned char*>(source.data()),source.size())!=profile.fingerprint)
|
||||
throw std::runtime_error(std::string(profile.path)+": modified projectile needs engine binding review");
|
||||
std::string lowered=source;
|
||||
if(id>=first_neighbor_program) {
|
||||
// Fuse this reviewed additive neighbor loop into a per-target kernel.
|
||||
// Preserve the original arithmetic, both distance guards and constants.
|
||||
// Query/iteration/edit_component plumbing becomes GPU gather dispatch.
|
||||
std::vector<std::string> lines;std::istringstream input(source);std::string line;
|
||||
while(std::getline(input,line)) {
|
||||
line.erase(std::remove(line.begin(),line.end(),'\r'),line.end());
|
||||
auto start=line.find_first_not_of(" \t");
|
||||
if(start!=std::string::npos)lines.push_back(line.substr(start));
|
||||
}
|
||||
auto find=[&](const std::string& prefix) {
|
||||
std::string result;unsigned count=0;
|
||||
for(const auto& candidate:lines)if(candidate.rfind(prefix,0)==0){result=candidate;++count;}
|
||||
if(count!=1)throw std::runtime_error(std::string(profile.path)+": neighbor lowering shape changed: "+prefix);
|
||||
return result;
|
||||
};
|
||||
std::ostringstream pair;
|
||||
if(std::string(profile.path)=="data/scripts/projectiles/effect_slow_bullets.lua") {
|
||||
pair<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
|
||||
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
|
||||
<<find("vel_x = vel_x *")<<'\n'<<find("vel_y = vel_y *")<<'\n'
|
||||
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\n";
|
||||
} else {
|
||||
pair<<"local px,py=EntityGetTransform((GetUpdatedEntityID()))\nlocal x,y=EntityGetTransform(1)\n"
|
||||
<<find("local distance =")<<'\n'<<find("local distance_full =")<<'\n';
|
||||
auto diamond=find("if ( distance < distance_full *");auto end=diamond.find(" and ");
|
||||
if(end==std::string::npos)throw std::runtime_error("neighbor diamond guard missing");
|
||||
pair<<diamond.substr(0,end)<<" then\n"<<find("distance = math.sqrt")<<"\nlocal "<<find("direction =")<<'\n'
|
||||
<<find("if ( distance < distance_full )")<<'\n'
|
||||
<<find("local gravity_percent =")<<'\n'<<find("local gravity_coeff =")<<'\n'
|
||||
<<"local comp=EntityGetFirstComponent(GetUpdatedEntityID(), 'VelocityComponent')\n"
|
||||
"local vel_x,vel_y=ComponentGetValueVector2(comp,'mVelocity')\n"
|
||||
<<find("local offset_x =")<<'\n'<<find("local offset_y =")<<'\n'
|
||||
<<find("vel_x = vel_x +")<<'\n'<<find("vel_y = vel_y +")<<'\n'
|
||||
<<"ComponentSetValueVector2(comp,'mVelocity',vel_x,vel_y)\nend\nend\n";
|
||||
}
|
||||
lowered=pair.str();
|
||||
}
|
||||
auto part=translate(lowered,read,profile.path);
|
||||
// The dependency is always the original asset, never the fused fragment.
|
||||
for(auto& dep:part.dependencies)if(dep.path==profile.path)dep.fingerprint=profile.fingerprint;
|
||||
// Each module owns its constants, registers and dofile_once globals.
|
||||
// Only the terrain query is shared; it carries no invocation state.
|
||||
for(const std::string prefix:{"ngp_","NGP_"}) {
|
||||
const std::string replacement=prefix+std::to_string(id)+"_";
|
||||
size_t at=0;
|
||||
while((at=part.function.find(prefix,at))!=std::string::npos) {
|
||||
if(part.function.compare(at,11,"ngp_surface")==0){at+=11;continue;}
|
||||
part.function.replace(at,prefix.size(),replacement);at+=replacement.size();
|
||||
}
|
||||
}
|
||||
for(const std::string type:{"NgpValue","NgpTuple"}) {
|
||||
size_t at=0;const auto replacement=type+std::to_string(id);
|
||||
while((at=part.function.find(type,at))!=std::string::npos) {
|
||||
part.function.replace(at,type.size(),replacement);at+=replacement.size();
|
||||
}
|
||||
}
|
||||
bundle.function+=part.function;
|
||||
bundle.instructions+=part.instructions;bundle.registers=std::max(bundle.registers,part.registers);
|
||||
for(const auto& dep:part.dependencies)dependencies[dep.path]=dep;
|
||||
dispatch<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,owner_id);";
|
||||
if(id>=first_neighbor_program)neighbors<<"case "<<id<<"u:return ngp_"<<id<<"_execute(p,owner,1.0);";
|
||||
}
|
||||
dispatch<<"default:return uvec4(14u,0u,0u,0u);}}\n";
|
||||
neighbors<<"default:return uvec4(14u,0u,0u,0u);}}\n";
|
||||
bundle.function+=dispatch.str()+neighbors.str();
|
||||
bundle.shader=std::string(sources::layout)+sources::terrain+bundle.function+sources::dispatch;
|
||||
for(const auto& [path,dep]:dependencies)bundle.dependencies.push_back(dep);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> compile_spirv(const std::string& source,const std::filesystem::path& compiler_dll) {
|
||||
struct Dll {HMODULE module;~Dll(){if(module)FreeLibrary(module);}} dll{
|
||||
LoadLibraryExW(std::filesystem::absolute(compiler_dll).c_str(),nullptr,LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)};
|
||||
if(!dll.module)throw std::runtime_error("Lua GPU: cannot load native compiler "+compiler_dll.string()+" (Windows error "+std::to_string(GetLastError())+")");
|
||||
auto get=[&](const char* name){auto f=GetProcAddress(dll.module,name);if(!f)throw std::runtime_error(std::string("Lua GPU: missing compiler symbol ")+name);return f;};
|
||||
#define NG_SHADERC(name) const auto name##_fn=reinterpret_cast<decltype(&name)>(get(#name))
|
||||
NG_SHADERC(shaderc_compiler_initialize);NG_SHADERC(shaderc_compiler_release);
|
||||
NG_SHADERC(shaderc_compile_options_initialize);NG_SHADERC(shaderc_compile_options_release);
|
||||
NG_SHADERC(shaderc_compile_options_set_target_env);NG_SHADERC(shaderc_compile_options_set_optimization_level);
|
||||
NG_SHADERC(shaderc_compile_into_spv);NG_SHADERC(shaderc_result_release);
|
||||
NG_SHADERC(shaderc_result_get_compilation_status);NG_SHADERC(shaderc_result_get_error_message);
|
||||
NG_SHADERC(shaderc_result_get_length);NG_SHADERC(shaderc_result_get_bytes);
|
||||
#undef NG_SHADERC
|
||||
std::unique_ptr<shaderc_compiler,decltype(shaderc_compiler_release_fn)> compiler(shaderc_compiler_initialize_fn(),shaderc_compiler_release_fn);
|
||||
std::unique_ptr<shaderc_compile_options,decltype(shaderc_compile_options_release_fn)> options(shaderc_compile_options_initialize_fn(),shaderc_compile_options_release_fn);
|
||||
if(!compiler||!options)throw std::runtime_error("Lua GPU: native compiler allocation failed");
|
||||
shaderc_compile_options_set_target_env_fn(options.get(),shaderc_target_env_vulkan,shaderc_env_version_vulkan_1_1);
|
||||
// Keep optimization in the Vulkan driver. The SDK optimizer expands this
|
||||
// bounded-register control flow heavily and crashes for larger VM bundles.
|
||||
// Straight-line neighbor kernels avoid the VM entirely.
|
||||
shaderc_compile_options_set_optimization_level_fn(options.get(),shaderc_optimization_level_zero);
|
||||
std::unique_ptr<shaderc_compilation_result,decltype(shaderc_result_release_fn)> result(
|
||||
shaderc_compile_into_spv_fn(compiler.get(),source.data(),source.size(),shaderc_compute_shader,script,"main",options.get()),shaderc_result_release_fn);
|
||||
if(!result)throw std::runtime_error("Lua GPU: shader compiler returned no result");
|
||||
if(shaderc_result_get_compilation_status_fn(result.get())!=shaderc_compilation_status_success)
|
||||
throw std::runtime_error(std::string("Lua GPU GLSL: ")+shaderc_result_get_error_message_fn(result.get()));
|
||||
const auto bytes=shaderc_result_get_length_fn(result.get());
|
||||
if(bytes%4||bytes<20)throw std::runtime_error("Lua GPU: invalid SPIR-V output");
|
||||
std::vector<uint32_t> code(bytes/4);std::memcpy(code.data(),shaderc_result_get_bytes_fn(result.get()),bytes);return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ng::gpu_lua {
|
||||
inline constexpr const char* script = "data/scripts/projectiles/grenade_anti_gravity.lua";
|
||||
struct Dependency {
|
||||
std::string path; uint64_t fingerprint;
|
||||
uint64_t semantic = 0;
|
||||
mutable uint64_t validated_fingerprint = 0;
|
||||
};
|
||||
struct Program {
|
||||
std::string shader, function;
|
||||
std::vector<Dependency> dependencies;
|
||||
unsigned instructions = 0, registers = 0;
|
||||
};
|
||||
using Reader = std::function<std::string(const std::string&)>;
|
||||
uint64_t fingerprint(const unsigned char* data, std::size_t size);
|
||||
// Hash stripped LuaJIT bytecode: includes behavior, excludes source/debug formatting.
|
||||
uint64_t library_fingerprint(const std::string& source);
|
||||
bool dependency_matches(const Dependency& dependency, const unsigned char* data, std::size_t size);
|
||||
// Parses with the embedded LuaJIT compiler; never executes the source on the CPU.
|
||||
// Throws a source-located diagnostic for unsupported code. Current target:
|
||||
// stateless self/owner queries, gravity/position writes and terrain raycasts.
|
||||
Program translate(const std::string& source, const Reader& read, const std::string& path = script);
|
||||
// Compiles independently namespaced programs and guards their binding contracts.
|
||||
Program translate_reviewed(const Reader& read);
|
||||
// Native shader compiler loaded in-process, only when GPU Lua is requested.
|
||||
// No Python, subprocess, writable shader directory or Vulkan SDK at runtime.
|
||||
std::vector<uint32_t> compile_spirv(const std::string& shader, const std::filesystem::path& compiler_dll);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ng::gpu_lua {
|
||||
struct Profile { const char* path; uint64_t fingerprint; bool exclude_self = false; };
|
||||
inline constexpr Profile profiles[] = {
|
||||
{"data/scripts/projectiles/grenade_anti_gravity.lua", 10044156309872634747ull},
|
||||
{"data/scripts/projectiles/cloud_position.lua", 8974354436317066029ull},
|
||||
{"data/scripts/projectiles/projectile_gravity.lua", 303292418746935276ull,true},
|
||||
{"data/scripts/projectiles/projectile_gravity_small.lua", 618348864316311556ull,true},
|
||||
{"data/scripts/projectiles/projectile_gravity_strong.lua", 8324536538978705398ull,true},
|
||||
{"data/scripts/projectiles/effect_slow_bullets.lua", 3766600218779469821ull},
|
||||
};
|
||||
// Zero is never a program. IDs are stable across CPU task submission and GLSL.
|
||||
inline unsigned program_id(const std::string& path) {
|
||||
for (unsigned i=0;i<sizeof(profiles)/sizeof(profiles[0]);++i)
|
||||
if (path==profiles[i].path) return i+1;
|
||||
return 0;
|
||||
}
|
||||
inline constexpr unsigned first_neighbor_program = 3, max_neighbor_fields = 512;
|
||||
inline constexpr unsigned neighbor_header = 32768;
|
||||
inline constexpr unsigned task_words = neighbor_header + 1 + max_neighbor_fields * 2;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "lj_obj.h"
|
||||
#include "lj_bc.h"
|
||||
#include "lj_bcdump.h"
|
||||
#include "gpu_lua_proto.h"
|
||||
static const char* const names[]={
|
||||
#define NG_NAME(n,a,b,c,d) #n,
|
||||
BCDEF(NG_NAME)
|
||||
#undef NG_NAME
|
||||
};
|
||||
int ng_lua_frame_shift(void){return LJ_FR2;}
|
||||
int ng_lua_dump_stripped(lua_State* state,const NgLuaProto* view,NgLuaWriter writer,void* context){
|
||||
return lj_bcwrite(state,(GCproto*)view->proto,writer,context,1);
|
||||
}
|
||||
int ng_lua_view(lua_State* state, NgLuaProto* out){
|
||||
const GCfunc* fn=(const GCfunc*)lua_topointer(state,-1);
|
||||
GCproto* p;
|
||||
if(!fn||!isluafunc(fn))return 0;
|
||||
p=funcproto(fn);out->proto=p;out->sizebc=p->sizebc;out->framesize=p->framesize;
|
||||
out->sizeuv=p->sizeuv;out->sizekgc=p->sizekgc;out->sizekn=p->sizekn;return 1;
|
||||
}
|
||||
NgLuaInstruction ng_lua_instruction(const NgLuaProto* view,unsigned position){
|
||||
GCproto* p=(GCproto*)view->proto;
|
||||
BCIns bc=proto_bc(p)[position];
|
||||
NgLuaInstruction out;
|
||||
out.op=NG_BC_UNKNOWN;
|
||||
switch(bc_op(bc)){
|
||||
#define NG_CASE(n) case BC_##n:out.op=NG_BC_##n;break;
|
||||
NG_LUA_OPS(NG_CASE)
|
||||
#undef NG_CASE
|
||||
default:break;
|
||||
}
|
||||
out.name=names[bc_op(bc)];out.a=bc_a(bc);out.b=bc_b(bc);out.c=bc_c(bc);out.d=bc_d(bc);out.jump=(int)bc_j(bc);
|
||||
out.line=p->firstline;
|
||||
if(position>0&&position<p->sizebc&&proto_lineinfo(p)){
|
||||
if(p->numline<256)out.line+=mref(p->lineinfo,const uint8_t)[position-1];
|
||||
else if(p->numline<65536)out.line+=mref(p->lineinfo,const uint16_t)[position-1];
|
||||
else out.line+=mref(p->lineinfo,const uint32_t)[position-1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const char* ng_lua_string(const NgLuaProto* view,unsigned index,size_t* length){
|
||||
GCproto* p=(GCproto*)view->proto;GCobj* obj;
|
||||
if(index>=p->sizekgc)return NULL;
|
||||
obj=proto_kgc(p,~(ptrdiff_t)index);
|
||||
if(obj->gch.gct!=~LJ_TSTR)return NULL;
|
||||
*length=obj->str.len;return strdata(&obj->str);
|
||||
}
|
||||
double ng_lua_number(const NgLuaProto* view,unsigned index){return numberVnum(proto_knumtv((GCproto*)view->proto,index));}
|
||||
@@ -0,0 +1,35 @@
|
||||
// C bridge to the exact vendored LuaJIT internals. Keep its old C headers out
|
||||
// of C++ translation units (they predate MSVC's standard integer typedefs).
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
typedef struct lua_State lua_State;
|
||||
#define NG_LUA_OPS(X) \
|
||||
X(ISLT) X(ISGE) X(ISLE) X(ISGT) \
|
||||
X(ISEQV) X(ISNEV) X(ISEQS) X(ISNES) X(ISEQN) X(ISNEN) X(ISEQP) X(ISNEP) \
|
||||
X(ISTC) X(ISFC) X(IST) X(ISF) X(MOV) X(NOT) X(UNM) \
|
||||
X(ADDVN) X(SUBVN) X(MULVN) X(DIVVN) X(MODVN) \
|
||||
X(ADDNV) X(SUBNV) X(MULNV) X(DIVNV) X(MODNV) \
|
||||
X(ADDVV) X(SUBVV) X(MULVV) X(DIVVV) X(MODVV) X(POW) \
|
||||
X(KSTR) X(KSHORT) X(KNUM) X(KPRI) X(KNIL) X(GGET) X(GSET) X(FNEW) \
|
||||
X(CALL) X(JMP) X(UCLO) X(RET0) X(RET1) X(RET) X(LOOP) X(TGETS)
|
||||
typedef enum {
|
||||
#define NG_LUA_ENUM(name) NG_BC_##name,
|
||||
NG_LUA_OPS(NG_LUA_ENUM)
|
||||
#undef NG_LUA_ENUM
|
||||
NG_BC_UNKNOWN
|
||||
} NgLuaOp;
|
||||
typedef struct { void* proto; unsigned sizebc, framesize, sizeuv, sizekgc, sizekn; } NgLuaProto;
|
||||
typedef struct { NgLuaOp op; const char* name; unsigned a,b,c,d,line; int jump; } NgLuaInstruction;
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int ng_lua_frame_shift(void);
|
||||
int ng_lua_view(lua_State* state, NgLuaProto* view);
|
||||
typedef int (*NgLuaWriter)(lua_State*, const void*, size_t, void*);
|
||||
int ng_lua_dump_stripped(lua_State* state, const NgLuaProto* view, NgLuaWriter writer, void* context);
|
||||
NgLuaInstruction ng_lua_instruction(const NgLuaProto* p, unsigned position);
|
||||
const char* ng_lua_string(const NgLuaProto* p, unsigned index, size_t* length);
|
||||
double ng_lua_number(const NgLuaProto* p, unsigned index);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
// Embedded by CMake from the actual engine shader declarations. No runtime files.
|
||||
#pragma once
|
||||
namespace ng::gpu_lua::sources {
|
||||
inline constexpr const char* layout = R"NGGLSL(#version 450
|
||||
layout(local_size_x=64) in;
|
||||
@NG_LUA_PC@
|
||||
@NG_LUA_ENT@
|
||||
@NG_LUA_PROJ@
|
||||
layout(std430,binding=22) readonly buffer Ents { Ent ents[]; };
|
||||
layout(std430,binding=23) readonly buffer Ctrl { ivec4 inp; } ctrl;
|
||||
layout(std430,binding=36) buffer FxState { uint fcnt[4]; FProj fprojs[32768]; };
|
||||
layout(std430,binding=33) readonly buffer FxTables { uvec4 ft[]; };
|
||||
layout(std430,binding=0) readonly buffer LuaCells { uint cells[]; };
|
||||
layout(std430,binding=1) readonly buffer LuaMaterials { uvec4 mats[]; };
|
||||
bool ngp_solid_at(vec2 position) {
|
||||
ivec2 q=ivec2(floor(position))-ivec2(pc.view_x,pc.view_y);
|
||||
if(q.x<0||q.y<0||q.x>=pc.gw||q.y>=pc.gh)return true;
|
||||
uint material=cells[q.y*pc.gw+q.x]&65535u;
|
||||
return material!=0u&&(mats[material*3u].y&9u)!=0u;
|
||||
}
|
||||
)NGGLSL";
|
||||
inline constexpr const char* terrain = R"NGGLSL(@NG_LUA_TERRAIN@)NGGLSL";
|
||||
inline constexpr const char* runtime = R"NGGLSL(@NG_LUA_RUNTIME@)NGGLSL";
|
||||
inline constexpr const char* dispatch = R"NGGLSL(@NG_LUA_DISPATCH@)NGGLSL";
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Generated by tools.lua_gpu.engine; source guards for the embedded program.
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace ng::gpu_lua {
|
||||
inline constexpr const char* script="data/scripts/projectiles/grenade_anti_gravity.lua";
|
||||
struct Dependency { const char* path; uint64_t fingerprint; };
|
||||
inline constexpr Dependency dependencies[] = {
|
||||
{"data/scripts/lib/utilities.lua",16046592235890916284ull},
|
||||
{"data/scripts/projectiles/grenade_anti_gravity.lua",10044156309872634747ull},
|
||||
};
|
||||
inline uint64_t fingerprint(const unsigned char* data, std::size_t size) {
|
||||
uint64_t h=14695981039346656037ull;
|
||||
for(std::size_t i=0;i<size;++i)if(data[i]!=13)h=(h^data[i])*1099511628211ull;
|
||||
return h;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// Lua natives that need no game world: persistent flags, session and magic numbers, mod settings and mod files, herds, the material
|
||||
// lists, the polymorph tables, dates and the build constants. Every VM (game, wand, world generation) gets them.
|
||||
#include <lua.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "core/random.h"
|
||||
#include "core/wak.h"
|
||||
#include "core/xml.h"
|
||||
#include "entity/herds.h"
|
||||
#include "lua/lua_host.h"
|
||||
|
||||
namespace ng {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string s_(lua_State* L, int i, const char* d = "") { return lua_isstring(L, i) ? lua_tostring(L, i) : d; }
|
||||
double n_(lua_State* L, int i, double d = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : d; }
|
||||
bool b_(lua_State* L, int i, bool d) { return lua_isnoneornil(L, i) ? d : lua_toboolean(L, i) != 0; }
|
||||
void push_strings(lua_State* L, const std::vector<std::string>& v) {
|
||||
lua_createtable(L, (int)v.size(), 0);
|
||||
for (size_t i = 0; i < v.size(); ++i) { lua_pushstring(L, v[i].c_str()); lua_rawseti(L, -2, (int)i + 1); }
|
||||
}
|
||||
void push_any(lua_State* L, const std::string& v) { // a stored setting: a number when it reads as one, a boolean for true/false
|
||||
if (v == "true" || v == "false") { lua_pushboolean(L, v == "true"); return; }
|
||||
char* end = nullptr;
|
||||
const double d = std::strtod(v.c_str(), &end);
|
||||
if (!v.empty() && end && *end == 0) lua_pushnumber(L, d); else lua_pushstring(L, v.c_str());
|
||||
}
|
||||
std::string read_any(lua_State* L, int i) {
|
||||
if (lua_isboolean(L, i)) return lua_toboolean(L, i) ? "true" : "false";
|
||||
if (lua_type(L, i) == LUA_TNUMBER) { char b[64]; std::snprintf(b, sizeof b, "%.17g", lua_tonumber(L, i)); return b; }
|
||||
return s_(L, i);
|
||||
}
|
||||
|
||||
// ---- persistent flags, unlocks ----
|
||||
int n_HasFlagPersistent(LuaHost& h, lua_State* L) { lua_pushboolean(L, LuaHost::persistent_flags().count(s_(L, 1)) != 0 || (h.assume_all_unlocked && !LuaHost::profile_flags)); return 1; } // (the engine: a file of that name in save00/persistent/flags)
|
||||
int n_AddFlagPersistent(LuaHost&, lua_State* L) { // -> true when the flag was new
|
||||
const bool added = LuaHost::persistent_flags().insert(s_(L, 1)).second;
|
||||
if (added) LuaHost::save_persistent_flags();
|
||||
lua_pushboolean(L, added);
|
||||
return 1;
|
||||
}
|
||||
int n_RemoveFlagPersistent(LuaHost&, lua_State* L) { if (LuaHost::persistent_flags().erase(s_(L, 1))) LuaHost::save_persistent_flags(); return 0; }
|
||||
int n_UnlockItem(LuaHost&, lua_State* L) { LuaHost::unlocked_items().insert(s_(L, 1)); return 0; } // (007b94e0 -> 00b83f00: the item joins the unlocked list and the "new item" notice shows; no persistent flag)
|
||||
|
||||
// ---- session numbers, magic numbers ----
|
||||
int n_SessionNumbersGetValue(LuaHost&, lua_State* L) { const auto it = LuaHost::session_numbers().find(s_(L, 1)); lua_pushstring(L, it == LuaHost::session_numbers().end() ? "0" : it->second.c_str()); return 1; }
|
||||
int n_SessionNumbersSetValue(LuaHost&, lua_State* L) { LuaHost::session_numbers()[s_(L, 1)] = read_any(L, 2); return 0; }
|
||||
int n_Nop(LuaHost&, lua_State*) { return 0; }
|
||||
int n_False(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; }
|
||||
void load_magic(const Wak& wak, const std::string& file, bool overwrite) { // the attributes of the file's root element (and its children)
|
||||
std::vector<uint8_t> raw;
|
||||
if (!wak.read(file, raw)) return;
|
||||
const auto root = parse_xml(std::string_view((const char*)raw.data(), raw.size()));
|
||||
if (!root) return;
|
||||
auto put = [&](const std::string& k, const std::string& v) { if (overwrite) LuaHost::magic_numbers()[k] = v; else LuaHost::magic_numbers().emplace(k, v); };
|
||||
for (const auto& kv : root->attrs) put(kv.first, kv.second);
|
||||
for (const auto& c : root->children) for (const auto& kv : c->attrs) put(kv.first, kv.second);
|
||||
}
|
||||
void ensure_magic(const Wak& wak) { static bool done = false; if (!done) { done = true; load_magic(wak, "data/magic_numbers.xml", false); } }
|
||||
} // namespace
|
||||
std::string LuaHost::magic_value(const Wak& wak, const std::string& key) { ensure_magic(wak); const auto it = magic_numbers().find(key); return it == magic_numbers().end() ? std::string() : it->second; }
|
||||
namespace {
|
||||
int n_MagicNumbersGetValue(LuaHost& h, lua_State* L) {
|
||||
ensure_magic(h.wak());
|
||||
const auto it = LuaHost::magic_numbers().find(s_(L, 1));
|
||||
lua_pushstring(L, it == LuaHost::magic_numbers().end() ? "" : it->second.c_str());
|
||||
return 1;
|
||||
}
|
||||
int n_ModMagicNumbersFileAdd(LuaHost& h, lua_State* L) { ensure_magic(h.wak()); load_magic(h.wak(), s_(L, 1), true); return 0; } // the mod's values override the game's
|
||||
|
||||
// ---- dates, build ----
|
||||
int n_GameGetDateAndTimeUTC(LuaHost&, lua_State* L) {
|
||||
const std::time_t t = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
gmtime_s(&tm, &t);
|
||||
const int v[6] = {tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec};
|
||||
for (int x : v) lua_pushnumber(L, x);
|
||||
return 6;
|
||||
}
|
||||
int n_GameGetRealWorldTimeSinceStarted(LuaHost&, lua_State* L) {
|
||||
static const auto t0 = std::chrono::steady_clock::now();
|
||||
lua_pushnumber(L, std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
|
||||
return 1;
|
||||
}
|
||||
int n_GetDailyPracticeRunSeed(LuaHost&, lua_State* L) { // the day's seed: the UTC date
|
||||
const std::time_t t = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
gmtime_s(&tm, &t);
|
||||
lua_pushnumber(L, (tm.tm_year + 1900) * 10000 + (tm.tm_mon + 1) * 100 + tm.tm_mday);
|
||||
return 1;
|
||||
}
|
||||
int n_DebugBiomeMapGetFilename(LuaHost&, lua_State* L) { lua_pushstring(L, "data/biome_impl/biome_map.png"); return 1; }
|
||||
|
||||
// ---- herds ----
|
||||
int n_StringToHerdId(LuaHost& h, lua_State* L) { herds_load(h.wak()); lua_pushnumber(L, herd_of(s_(L, 1))); return 1; }
|
||||
int n_HerdIdToString(LuaHost& h, lua_State* L) { herds_load(h.wak()); lua_pushstring(L, herd_name((int)n_(L, 1)).c_str()); return 1; }
|
||||
int n_GetHerdRelation(LuaHost& h, lua_State* L) { herds_load(h.wak()); lua_pushnumber(L, herd_relation((int)n_(L, 1), (int)n_(L, 2))); return 1; }
|
||||
|
||||
// ---- materials ----
|
||||
int cell_list(LuaHost& h, lua_State* L, int kind) {
|
||||
const bool statics = b_(L, 1, true), fx = b_(L, 2, false);
|
||||
std::vector<std::string> out;
|
||||
if (h.material_list)
|
||||
for (const LuaHost::MaterialBrief& m : h.material_list()) {
|
||||
if (m.kind != kind || (!statics && m.is_static)) continue;
|
||||
if (m.particle_fx && !fx) continue; // (0078d8d0)
|
||||
out.push_back(m.name);
|
||||
}
|
||||
push_strings(L, out);
|
||||
return 1;
|
||||
}
|
||||
int n_CellFactory_GetAllLiquids(LuaHost& h, lua_State* L) { return cell_list(h, L, 0); }
|
||||
int n_CellFactory_GetAllSands(LuaHost& h, lua_State* L) { return cell_list(h, L, 1); }
|
||||
int n_CellFactory_GetAllGases(LuaHost& h, lua_State* L) { return cell_list(h, L, 2); }
|
||||
int n_CellFactory_GetAllFires(LuaHost& h, lua_State* L) { return cell_list(h, L, 3); }
|
||||
int n_CellFactory_GetAllSolids(LuaHost& h, lua_State* L) { return cell_list(h, L, 4); }
|
||||
const LuaHost::MaterialBrief* brief(LuaHost& h, int id) {
|
||||
static std::vector<LuaHost::MaterialBrief> cache;
|
||||
if (cache.empty() && h.material_list) cache = h.material_list();
|
||||
return id >= 0 && id < (int)cache.size() ? &cache[id] : nullptr;
|
||||
}
|
||||
std::string bracket(const std::string& t) { return !t.empty() && t[0] == '[' ? t : "[" + t + "]"; } // (the game's tags: "[liquid]")
|
||||
int n_CellFactory_GetTags(LuaHost& h, lua_State* L) { const auto* m = brief(h, (int)n_(L, 1)); std::vector<std::string> v; if (m) for (const std::string& t : m->tags) v.push_back(bracket(t)); push_strings(L, v); return 1; }
|
||||
int n_CellFactory_HasTag(LuaHost& h, lua_State* L) {
|
||||
const auto* m = brief(h, (int)n_(L, 1));
|
||||
const std::string t = bracket(s_(L, 2));
|
||||
bool has = false;
|
||||
if (m) for (const std::string& x : m->tags) has = has || bracket(x) == t;
|
||||
lua_pushboolean(L, has);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- polymorph tables ----
|
||||
int n_PolymorphTableGet(LuaHost& h, lua_State* L) { LuaHost::build_poly_table(h.wak()); push_strings(L, LuaHost::poly_table(b_(L, 1, false))); return 1; }
|
||||
int n_PolymorphTableSet(LuaHost& h, lua_State* L) {
|
||||
LuaHost::build_poly_table(h.wak());
|
||||
std::vector<std::string>& t = LuaHost::poly_table(b_(L, 2, false));
|
||||
t.clear();
|
||||
if (lua_istable(L, 1)) for (int i = 1;; ++i) { lua_rawgeti(L, 1, i); if (lua_isnil(L, -1)) { lua_pop(L, 1); break; } t.push_back(s_(L, -1)); lua_pop(L, 1); }
|
||||
return 0;
|
||||
}
|
||||
int n_PolymorphTableAddEntity(LuaHost& h, lua_State* L) {
|
||||
LuaHost::build_poly_table(h.wak());
|
||||
std::vector<std::string>& t = LuaHost::poly_table(b_(L, 2, false));
|
||||
const std::string f = s_(L, 1);
|
||||
if (!b_(L, 3, true) || std::find(t.begin(), t.end(), f) == t.end()) t.push_back(f);
|
||||
return 0;
|
||||
}
|
||||
int n_PolymorphTableRemoveEntity(LuaHost& h, lua_State* L) {
|
||||
LuaHost::build_poly_table(h.wak());
|
||||
const std::string f = s_(L, 1);
|
||||
for (bool rare : {false, true}) {
|
||||
if ((rare && !b_(L, 3, true)) || (!rare && !b_(L, 2, true))) continue;
|
||||
auto& t = LuaHost::poly_table(rare);
|
||||
t.erase(std::remove(t.begin(), t.end(), f), t.end());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- mods ----
|
||||
int n_ModIsEnabled(LuaHost&, lua_State* L) { const auto& m = LuaHost::active_mods(); lua_pushboolean(L, std::find(m.begin(), m.end(), s_(L, 1)) != m.end()); return 1; }
|
||||
int n_ModGetActiveModIDs(LuaHost&, lua_State* L) { push_strings(L, LuaHost::active_mods()); return 1; }
|
||||
int n_ModGetAPIVersion(LuaHost&, lua_State* L) { lua_pushnumber(L, 1); return 1; }
|
||||
int n_ModDoesFileExist(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.wak().exists(s_(L, 1))); return 1; }
|
||||
int n_ModSettingGet(LuaHost&, lua_State* L) { const auto it = LuaHost::mod_settings().find(s_(L, 1)); if (it == LuaHost::mod_settings().end()) lua_pushnil(L); else push_any(L, it->second); return 1; }
|
||||
int n_ModSettingSet(LuaHost&, lua_State* L) { LuaHost::mod_settings()[s_(L, 1)] = read_any(L, 2); return 0; }
|
||||
int n_ModSettingGetNextValue(LuaHost&, lua_State* L) {
|
||||
auto it = LuaHost::mod_settings_next().find(s_(L, 1));
|
||||
if (it == LuaHost::mod_settings_next().end()) { it = LuaHost::mod_settings().find(s_(L, 1)); if (it == LuaHost::mod_settings().end()) { lua_pushnil(L); return 1; } }
|
||||
push_any(L, it->second);
|
||||
return 1;
|
||||
}
|
||||
int n_ModSettingSetNextValue(LuaHost&, lua_State* L) { LuaHost::mod_settings_next()[s_(L, 1)] = read_any(L, 2); return 0; }
|
||||
int n_ModSettingRemove(LuaHost&, lua_State* L) { const bool had = LuaHost::mod_settings().erase(s_(L, 1)) != 0; LuaHost::mod_settings_next().erase(s_(L, 1)); lua_pushboolean(L, had); return 1; }
|
||||
int n_ModSettingGetCount(LuaHost&, lua_State* L) { lua_pushnumber(L, (double)LuaHost::mod_settings().size()); return 1; }
|
||||
int n_ModSettingGetAtIndex(LuaHost&, lua_State* L) { // -> name, value, value_next
|
||||
const int i = (int)n_(L, 1);
|
||||
auto it = LuaHost::mod_settings().begin();
|
||||
for (int k = 0; k < i && it != LuaHost::mod_settings().end(); ++k) ++it;
|
||||
if (i < 0 || it == LuaHost::mod_settings().end()) { lua_pushnil(L); return 1; }
|
||||
lua_pushstring(L, it->first.c_str());
|
||||
push_any(L, it->second);
|
||||
const auto nx = LuaHost::mod_settings_next().find(it->first);
|
||||
if (nx == LuaHost::mod_settings_next().end()) lua_pushnil(L); else push_any(L, nx->second);
|
||||
return 3;
|
||||
}
|
||||
int n_ModLuaFileAppend(LuaHost&, lua_State* L) { LuaHost::mod_appends()[s_(L, 1)].push_back(s_(L, 2)); LuaHost::scripts_changed = true; return 0; }
|
||||
int n_ModLuaFileGetAppends(LuaHost&, lua_State* L) { const auto it = LuaHost::mod_appends().find(s_(L, 1)); push_strings(L, it == LuaHost::mod_appends().end() ? std::vector<std::string>() : it->second); return 1; }
|
||||
int n_ModLuaFileSetAppends(LuaHost&, lua_State* L) {
|
||||
std::vector<std::string> v;
|
||||
if (lua_istable(L, 2)) for (int i = 1;; ++i) { lua_rawgeti(L, 2, i); if (lua_isnil(L, -1)) { lua_pop(L, 1); break; } v.push_back(s_(L, -1)); lua_pop(L, 1); }
|
||||
LuaHost::mod_appends()[s_(L, 1)] = v;
|
||||
LuaHost::scripts_changed = true;
|
||||
return 0;
|
||||
}
|
||||
std::map<std::string, std::string>& who_set() { static std::map<std::string, std::string> m; return m; }
|
||||
int n_ModTextFileGetContent(LuaHost& h, lua_State* L) {
|
||||
std::vector<uint8_t> raw;
|
||||
if (!h.wak().read(s_(L, 1), raw)) { lua_pushnil(L); return 1; }
|
||||
lua_pushlstring(L, (const char*)raw.data(), raw.size());
|
||||
return 1;
|
||||
}
|
||||
int n_ModTextFileSetContent(LuaHost& h, lua_State* L) {
|
||||
size_t n = 0;
|
||||
const char* p = lua_tolstring(L, 2, &n);
|
||||
h.wak().set_override(s_(L, 1), std::vector<uint8_t>(p, p + (p ? n : 0)));
|
||||
who_set()[s_(L, 1)] = h.current_mod;
|
||||
LuaHost::scripts_changed = true;
|
||||
return 0;
|
||||
}
|
||||
int n_ModTextFileWhoSetContent(LuaHost&, lua_State* L) { const auto it = who_set().find(s_(L, 1)); lua_pushstring(L, it == who_set().end() ? "" : it->second.c_str()); return 1; }
|
||||
int n_ModMaterialFilesGet(LuaHost&, lua_State* L) { auto v = LuaHost::mod_material_files(); v.insert(v.begin(), "data/materials.xml"); push_strings(L, v); return 1; }
|
||||
int n_ModMaterialsFileAdd(LuaHost&, lua_State* L) { LuaHost::mod_material_files().push_back(s_(L, 1)); return 0; }
|
||||
int n_ModRegisterAudioEventMappings(LuaHost&, lua_State* L) { LuaHost::mod_audio_files().push_back(s_(L, 1)); return 0; }
|
||||
int n_ModRegisterMusicBank(LuaHost&, lua_State* L) { LuaHost::mod_audio_files().push_back(s_(L, 1)); return 0; }
|
||||
|
||||
// ModImage*: images a mod edits (made editable from the file, or blank of the given size); the id is an index
|
||||
struct ModImage { std::string file; int w = 0, h = 0; std::vector<uint32_t> px; std::string who; };
|
||||
std::vector<ModImage>& images() { static std::vector<ModImage> v; return v; }
|
||||
int n_ModImageMakeEditable(LuaHost& h, lua_State* L) { // (filename, width, height) -> id, w, h
|
||||
const std::string f = s_(L, 1);
|
||||
for (size_t i = 0; i < images().size(); ++i) if (images()[i].file == f) { lua_pushnumber(L, (double)i); lua_pushnumber(L, images()[i].w); lua_pushnumber(L, images()[i].h); return 3; }
|
||||
ModImage im; im.file = f; im.who = h.current_mod;
|
||||
if (!h.decode_png || !h.decode_png(f, im.w, im.h, im.px)) { im.w = (int)n_(L, 2); im.h = (int)n_(L, 3); im.px.assign((size_t)std::max(0, im.w * im.h), 0u); }
|
||||
images().push_back(std::move(im));
|
||||
lua_pushnumber(L, (double)images().size() - 1); lua_pushnumber(L, images().back().w); lua_pushnumber(L, images().back().h);
|
||||
return 3;
|
||||
}
|
||||
int n_ModImageIdFromFilename(LuaHost&, lua_State* L) { for (size_t i = 0; i < images().size(); ++i) if (images()[i].file == s_(L, 1)) { lua_pushnumber(L, (double)i); lua_pushnumber(L, images()[i].w); lua_pushnumber(L, images()[i].h); return 3; } lua_pushnil(L); return 1; }
|
||||
int n_ModImageGetPixel(LuaHost&, lua_State* L) { // ABGR as the game returns it
|
||||
const int id = (int)n_(L, 1), x = (int)n_(L, 2), y = (int)n_(L, 3);
|
||||
if (id < 0 || id >= (int)images().size() || x < 0 || y < 0 || x >= images()[id].w || y >= images()[id].h) { lua_pushnumber(L, 0); return 1; }
|
||||
const uint32_t c = images()[id].px[(size_t)y * images()[id].w + x]; // (ARGB) -> ABGR
|
||||
lua_pushnumber(L, (double)((c & 0xFF00FF00u) | ((c >> 16) & 0xFFu) | ((c & 0xFFu) << 16)));
|
||||
return 1;
|
||||
}
|
||||
int n_ModImageSetPixel(LuaHost& h, lua_State* L) {
|
||||
const int id = (int)n_(L, 1), x = (int)n_(L, 2), y = (int)n_(L, 3);
|
||||
if (id < 0 || id >= (int)images().size() || x < 0 || y < 0 || x >= images()[id].w || y >= images()[id].h) return 0;
|
||||
const uint32_t c = (uint32_t)(int64_t)n_(L, 4);
|
||||
images()[id].px[(size_t)y * images()[id].w + x] = (c & 0xFF00FF00u) | ((c >> 16) & 0xFFu) | ((c & 0xFFu) << 16);
|
||||
if (h.on_image_edited) h.on_image_edited(images()[id].file, images()[id].w, images()[id].h, images()[id].px);
|
||||
return 0;
|
||||
}
|
||||
int n_ModImageWhoSetContent(LuaHost&, lua_State* L) { for (const ModImage& im : images()) if (im.file == s_(L, 1)) { lua_pushstring(L, im.who.c_str()); return 1; } lua_pushstring(L, ""); return 1; }
|
||||
int n_ModImageDoesExist(LuaHost& h, lua_State* L) { for (const ModImage& im : images()) if (im.file == s_(L, 1)) { lua_pushboolean(L, 1); return 1; } lua_pushboolean(L, h.wak().exists(s_(L, 1))); return 1; }
|
||||
|
||||
// ---- the biome map of a biome-map script ----
|
||||
int n_BiomeMapSetSize(LuaHost& h, lua_State* L) { h.biome_image.w = std::max(0, (int)n_(L, 1)); h.biome_image.h = std::max(0, (int)n_(L, 2)); h.biome_image.px.assign((size_t)h.biome_image.w * h.biome_image.h, 0u); return 0; }
|
||||
uint32_t swap_rb(uint32_t c) { return (c & 0xFF00FF00u) | ((c >> 16) & 0xFFu) | ((c & 0xFFu) << 16); }
|
||||
int n_BiomeMapSetPixel(LuaHost& h, lua_State* L) {
|
||||
const int x = (int)n_(L, 1), y = (int)n_(L, 2);
|
||||
if (x >= 0 && y >= 0 && x < h.biome_image.w && y < h.biome_image.h) h.biome_image.px[(size_t)y * h.biome_image.w + x] = (uint32_t)(int64_t)n_(L, 3);
|
||||
return 0;
|
||||
}
|
||||
int n_BiomeMapGetPixel(LuaHost& h, lua_State* L) { // the stored ARGB with red and blue swapped (BiomeMapConvertPixelFromUintToInt makes a constant comparable)
|
||||
const int x = (int)n_(L, 1), y = (int)n_(L, 2);
|
||||
const uint32_t c = x >= 0 && y >= 0 && x < h.biome_image.w && y < h.biome_image.h ? h.biome_image.px[(size_t)y * h.biome_image.w + x] : 0u;
|
||||
lua_pushnumber(L, (double)(int32_t)swap_rb(c));
|
||||
return 1;
|
||||
}
|
||||
int n_BiomeMapConvertPixelFromUintToInt(LuaHost&, lua_State* L) { lua_pushnumber(L, (double)(int32_t)swap_rb((uint32_t)(int64_t)n_(L, 1))); return 1; }
|
||||
int load_into_map(LuaHost& h, lua_State* L, bool cropped) { // (x, y, file [, image_x, image_y, w, h])
|
||||
int iw = 0, ih = 0;
|
||||
std::vector<uint32_t> px;
|
||||
if (!LuaHost::png_decoder() || !LuaHost::png_decoder()(s_(L, 3), iw, ih, px)) return 0;
|
||||
const int dx = (int)n_(L, 1), dy = (int)n_(L, 2);
|
||||
const int sx = cropped ? (int)n_(L, 4) : 0, sy = cropped ? (int)n_(L, 5) : 0, sw = cropped ? (int)n_(L, 6) : iw, sh = cropped ? (int)n_(L, 7) : ih;
|
||||
for (int y = 0; y < sh; ++y) for (int x = 0; x < sw; ++x) {
|
||||
const int ix = sx + x, iy = sy + y, ox = dx + x, oy = dy + y;
|
||||
if (ix < 0 || iy < 0 || ix >= iw || iy >= ih || ox < 0 || oy < 0 || ox >= h.biome_image.w || oy >= h.biome_image.h) continue;
|
||||
h.biome_image.px[(size_t)oy * h.biome_image.w + ox] = px[(size_t)iy * iw + ix];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int n_BiomeMapLoadImage(LuaHost& h, lua_State* L) { return load_into_map(h, L, false); }
|
||||
int n_BiomeMapLoadImageCropped(LuaHost& h, lua_State* L) { return load_into_map(h, L, true); }
|
||||
|
||||
// ---- not in the API list: the error print, the reflection registrars (the engine runs perk_reflect.lua / status_reflect.lua /
|
||||
// event_utilities.lua to list perks, status effects and streaming events for its menus; the lists are kept), the debug build's reset
|
||||
int n_print_error(LuaHost&, lua_State* L) {
|
||||
std::string line;
|
||||
for (int i = 1; i <= lua_gettop(L); ++i) { if (i > 1) line += " "; line += lua_isstring(L, i) ? lua_tostring(L, i) : luaL_typename(L, i); }
|
||||
std::fprintf(stderr, "lua print_error: %s%c", line.c_str(), 10);
|
||||
return 0;
|
||||
}
|
||||
int n_Register(LuaHost& h, lua_State* L) { h.reflected.push_back(s_(L, 1)); return 0; }
|
||||
|
||||
// ---- streaming (not connected) ----
|
||||
int n_StreamingGetVotingCycleDurationFrames(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; }
|
||||
int n_StreamingGetConnectedChannelName(LuaHost&, lua_State* L) { lua_pushstring(L, ""); return 1; }
|
||||
int n_StreamingGetRandomViewerName(LuaHost&, lua_State* L) { lua_pushstring(L, ""); return 1; }
|
||||
|
||||
} // namespace
|
||||
|
||||
void LuaHost::build_poly_table(const Wak& wak) {
|
||||
static bool built = false;
|
||||
if (built) return;
|
||||
built = true;
|
||||
static const char* const excl[] = {"pebble", "_test_walk", "_physics", "boss_dragon", "miner_santa", "wand_ghost", "ghost", "darkghost", "ethereal_being", "nibbana"};
|
||||
static const char* const rare[] = {"boss_dragon", "miner_santa", "wand_ghost", "chest_leggy", "ultimate_killer", "necrobot", "necrobot_super", "friend"};
|
||||
const std::string pre = "data/entities/animals/";
|
||||
std::vector<std::string> common, rares;
|
||||
for (const WakEntry& e : wak.entries()) {
|
||||
if (e.path.rfind(pre, 0) != 0 || e.path.find('/', pre.size()) != std::string::npos || e.path.size() < 4 || e.path.compare(e.path.size() - 4, 4, ".xml") != 0) continue;
|
||||
const std::string name = e.path.substr(pre.size());
|
||||
bool ex = false, rr = false;
|
||||
for (const char* w : excl) ex = ex || name.find(w) != std::string::npos;
|
||||
for (const char* w : rare) rr = rr || name.find(w) != std::string::npos;
|
||||
if (rr) rares.push_back(e.path);
|
||||
if (!ex && !rr) common.push_back(e.path);
|
||||
}
|
||||
poly_table(false).insert(poly_table(false).begin(), common.begin(), common.end()); // (entries a mod added before the table existed stay)
|
||||
poly_table(true).insert(poly_table(true).begin(), rares.begin(), rares.end());
|
||||
}
|
||||
|
||||
bool LuaHost::run_biome_map_script(const Wak& wak, uint32_t world_seed, const std::string& file, BiomeImage& out, std::string* err) {
|
||||
GameRandom rng;
|
||||
rng.set_world_seed(world_seed);
|
||||
LuaHost host(wak, rng);
|
||||
if (!host.run_file(file, err)) return false;
|
||||
if (host.biome_image.w <= 0 || host.biome_image.h <= 0) return false;
|
||||
out = std::move(host.biome_image);
|
||||
return true;
|
||||
}
|
||||
|
||||
void LuaHost::save_persistent_flags() {
|
||||
if (persistent_file().empty()) return;
|
||||
std::ofstream out(persistent_file(), std::ios::trunc);
|
||||
for (const std::string& f : persistent_flags()) out << f << "\n";
|
||||
}
|
||||
|
||||
void register_host_natives(LuaHost& h) {
|
||||
struct { const char* name; NativeFn fn; } natives[] = {
|
||||
{"HasFlagPersistent", n_HasFlagPersistent}, {"AddFlagPersistent", n_AddFlagPersistent}, {"RemoveFlagPersistent", n_RemoveFlagPersistent}, {"UnlockItem", n_UnlockItem},
|
||||
{"SessionNumbersGetValue", n_SessionNumbersGetValue}, {"SessionNumbersSetValue", n_SessionNumbersSetValue}, {"SessionNumbersSave", n_Nop},
|
||||
{"MagicNumbersGetValue", n_MagicNumbersGetValue}, {"ModMagicNumbersFileAdd", n_ModMagicNumbersFileAdd},
|
||||
{"GameGetDateAndTimeUTC", n_GameGetDateAndTimeUTC}, {"GameGetRealWorldTimeSinceStarted", n_GameGetRealWorldTimeSinceStarted}, {"GetDailyPracticeRunSeed", n_GetDailyPracticeRunSeed},
|
||||
{"GameIsDailyRun", n_False}, {"GameIsDailyRunOrDailyPracticeRun", n_False}, {"GameIsBetaBuild", n_False}, {"DebugGetIsDevBuild", n_False}, {"GameIsIntroPlaying", n_False},
|
||||
{"DebugBiomeMapGetFilename", n_DebugBiomeMapGetFilename},
|
||||
// release-build behaviour: these do nothing outside the developers' builds (EntitySave, trailer mode, the test player) or report to Steam
|
||||
{"EntitySave", n_Nop}, {"DebugEnableTrailerMode", n_Nop}, {"Debug_SaveTestPlayer", n_Nop}, {"GameGiveAchievement", n_Nop}, {"AutosaveDisable", n_Nop},
|
||||
{"StringToHerdId", n_StringToHerdId}, {"HerdIdToString", n_HerdIdToString}, {"GetHerdRelation", n_GetHerdRelation},
|
||||
{"CellFactory_GetAllLiquids", n_CellFactory_GetAllLiquids}, {"CellFactory_GetAllSands", n_CellFactory_GetAllSands}, {"CellFactory_GetAllGases", n_CellFactory_GetAllGases},
|
||||
{"CellFactory_GetAllFires", n_CellFactory_GetAllFires}, {"CellFactory_GetAllSolids", n_CellFactory_GetAllSolids}, {"CellFactory_GetTags", n_CellFactory_GetTags}, {"CellFactory_HasTag", n_CellFactory_HasTag},
|
||||
{"PolymorphTableGet", n_PolymorphTableGet}, {"PolymorphTableSet", n_PolymorphTableSet}, {"PolymorphTableAddEntity", n_PolymorphTableAddEntity}, {"PolymorphTableRemoveEntity", n_PolymorphTableRemoveEntity},
|
||||
{"ModIsEnabled", n_ModIsEnabled}, {"ModGetActiveModIDs", n_ModGetActiveModIDs}, {"ModGetAPIVersion", n_ModGetAPIVersion}, {"ModDoesFileExist", n_ModDoesFileExist},
|
||||
{"ModSettingGet", n_ModSettingGet}, {"ModSettingSet", n_ModSettingSet}, {"ModSettingGetNextValue", n_ModSettingGetNextValue}, {"ModSettingSetNextValue", n_ModSettingSetNextValue},
|
||||
{"ModSettingRemove", n_ModSettingRemove}, {"ModSettingGetCount", n_ModSettingGetCount}, {"ModSettingGetAtIndex", n_ModSettingGetAtIndex},
|
||||
{"ModLuaFileAppend", n_ModLuaFileAppend}, {"ModLuaFileGetAppends", n_ModLuaFileGetAppends}, {"ModLuaFileSetAppends", n_ModLuaFileSetAppends},
|
||||
{"ModTextFileGetContent", n_ModTextFileGetContent}, {"ModTextFileSetContent", n_ModTextFileSetContent}, {"ModTextFileWhoSetContent", n_ModTextFileWhoSetContent},
|
||||
{"ModMaterialFilesGet", n_ModMaterialFilesGet}, {"ModMaterialsFileAdd", n_ModMaterialsFileAdd}, {"ModRegisterAudioEventMappings", n_ModRegisterAudioEventMappings}, {"ModRegisterMusicBank", n_ModRegisterMusicBank},
|
||||
{"ModImageMakeEditable", n_ModImageMakeEditable}, {"ModImageIdFromFilename", n_ModImageIdFromFilename}, {"ModImageGetPixel", n_ModImageGetPixel}, {"ModImageSetPixel", n_ModImageSetPixel},
|
||||
{"ModImageWhoSetContent", n_ModImageWhoSetContent}, {"ModImageDoesExist", n_ModImageDoesExist}, {"ModDevGenerateSpriteUVsForDirectory", n_Nop},
|
||||
{"BiomeMapSetSize", n_BiomeMapSetSize}, {"BiomeMapSetPixel", n_BiomeMapSetPixel}, {"BiomeMapGetPixel", n_BiomeMapGetPixel},
|
||||
{"BiomeMapConvertPixelFromUintToInt", n_BiomeMapConvertPixelFromUintToInt}, {"BiomeMapLoadImage", n_BiomeMapLoadImage}, {"BiomeMapLoadImageCropped", n_BiomeMapLoadImageCropped},
|
||||
{"print_error", n_print_error}, {"RegisterPerk", n_Register}, {"GameRegisterStatusEffect", n_Register}, {"RegisterStreamingEvent", n_Register}, {"DEBUG_GameReset", n_Nop},
|
||||
{"StreamingGetIsConnected", n_False}, {"StreamingGetConnectedChannelName", n_StreamingGetConnectedChannelName}, {"StreamingGetVotingCycleDurationFrames", n_StreamingGetVotingCycleDurationFrames},
|
||||
{"StreamingGetRandomViewerName", n_StreamingGetRandomViewerName}, {"StreamingGetSettingsGhostsNamedAfterViewers", n_False}, {"StreamingSetCustomPhaseDurations", n_Nop},
|
||||
{"StreamingForceNewVoting", n_Nop}, {"StreamingSetVotingEnabled", n_Nop},
|
||||
};
|
||||
for (auto& n : natives) h.register_native(n.name, n.fn);
|
||||
}
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,791 @@
|
||||
#include <cstdio>
|
||||
#include <unordered_map>
|
||||
#include "lua/lua_host.h"
|
||||
#include "core/xml.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
extern "C" {
|
||||
#include "lauxlib.h"
|
||||
#include "lua.h"
|
||||
#include "lualib.h"
|
||||
}
|
||||
|
||||
namespace ng {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kApiNames[] = {
|
||||
#include "lua/api_names.inc"
|
||||
};
|
||||
|
||||
LuaHost& host_of(lua_State* L) { return *static_cast<LuaHost*>(lua_touserdata(L, lua_upvalueindex(1))); }
|
||||
|
||||
using Native = int (*)(LuaHost&, lua_State*);
|
||||
|
||||
int thunk(lua_State* L) {
|
||||
auto fn = reinterpret_cast<Native>(lua_touserdata(L, lua_upvalueindex(2)));
|
||||
return fn(host_of(L), L);
|
||||
}
|
||||
|
||||
void reg(lua_State* L, LuaHost& h, const char* name, Native fn) {
|
||||
lua_pushlightuserdata(L, &h);
|
||||
lua_pushlightuserdata(L, reinterpret_cast<void*>(fn));
|
||||
lua_pushcclosure(L, thunk, 2);
|
||||
lua_setglobal(L, name);
|
||||
}
|
||||
|
||||
// Documented-but-unimplemented engine function: count the call, return nothing.
|
||||
int stub_thunk(lua_State* L) {
|
||||
host_of(L).note_missing(lua_tostring(L, lua_upvalueindex(2)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> LuaHost::stub_names() {
|
||||
std::vector<std::string> r;
|
||||
for (const char* n : kApiNames) {
|
||||
lua_getglobal(L_, n);
|
||||
if (lua_tocfunction(L_, -1) == stub_thunk) r.push_back(n);
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int traceback(lua_State* L) {
|
||||
const char* msg = lua_tostring(L, 1);
|
||||
luaL_traceback(L, L, msg ? msg : "(non-string error)", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool pcall_traced(lua_State* L, int nargs, int nresults, std::string* err) {
|
||||
const int base = lua_gettop(L) - nargs;
|
||||
lua_pushcfunction(L, traceback);
|
||||
lua_insert(L, base);
|
||||
const int rc = lua_pcall(L, nargs, nresults, base);
|
||||
lua_remove(L, base);
|
||||
if (rc != 0) {
|
||||
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "unknown error";
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double num(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lua_tonumber(L, i) : def; }
|
||||
std::string str(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
|
||||
|
||||
// ---- file loading ----
|
||||
|
||||
bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::string* err) {
|
||||
static std::unordered_map<std::string, std::vector<uint8_t>> cache; // the scripts of the entities run again and again: read the archive once
|
||||
if (LuaHost::scripts_changed) { cache.clear(); LuaHost::scripts_changed = false; } // (a mod replaced a file)
|
||||
auto it = cache.find(path);
|
||||
if (it == cache.end()) {
|
||||
std::vector<uint8_t> raw;
|
||||
if (!h.wak().read(path, raw)) {
|
||||
if (err) *err = "script not found in archive: " + path;
|
||||
return false;
|
||||
}
|
||||
it = cache.emplace(path, std::move(raw)).first;
|
||||
}
|
||||
const std::vector<uint8_t>& raw0 = it->second;
|
||||
// ModLuaFileAppend: the appended files run after the file, in its environment, every time it is loaded (the file's own return values are kept)
|
||||
std::vector<uint8_t> combined;
|
||||
const auto ap = LuaHost::mod_appends().find(path);
|
||||
if (ap != LuaHost::mod_appends().end() && !ap->second.empty()) {
|
||||
std::string s = "local __ng_r = {(function(...) ";
|
||||
s.append((const char*)raw0.data(), raw0.size());
|
||||
s += "\nend)(...)}";
|
||||
for (const std::string& f : ap->second) s += " dofile_always(\"" + f + "\")";
|
||||
s += " return unpack(__ng_r)";
|
||||
combined.assign(s.begin(), s.end());
|
||||
}
|
||||
const std::vector<uint8_t>& raw = combined.empty() ? raw0 : combined;
|
||||
if (luaL_loadbuffer(L, (const char*)raw.data(), raw.size(), ("@" + path).c_str()) != 0) {
|
||||
if (err) *err = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int n_dofile_impl(LuaHost& h, lua_State* L, bool once) {
|
||||
const std::string path = str(L, 1);
|
||||
// the chunk runs in the environment of the function that called dofile (each Lua VM of the engine loads its own copy)
|
||||
lua_Debug ar;
|
||||
if (lua_getstack(L, 1, &ar) && lua_getinfo(L, "f", &ar)) { lua_getfenv(L, -1); lua_remove(L, -2); }
|
||||
else lua_pushvalue(L, LUA_GLOBALSINDEX);
|
||||
char key[32];
|
||||
std::snprintf(key, sizeof key, "|%p", lua_topointer(L, -1));
|
||||
if (once && !h.load_once(path + key)) { lua_pop(L, 1); return 0; }
|
||||
std::string err;
|
||||
if (!load_from_wak(h, L, path, &err)) { lua_pop(L, 1); return luaL_error(L, "%s", err.c_str()); }
|
||||
lua_pushvalue(L, -2);
|
||||
lua_setfenv(L, -2);
|
||||
lua_remove(L, -2);
|
||||
const int top = lua_gettop(L) - 1; // stack height before the loaded chunk was pushed
|
||||
lua_call(L, 0, LUA_MULTRET);
|
||||
return lua_gettop(L) - top; // whatever the chunk returned
|
||||
}
|
||||
int n_dofile(LuaHost& h, lua_State* L) { return n_dofile_impl(h, L, false); }
|
||||
int n_dofile_once(LuaHost& h, lua_State* L) { return n_dofile_impl(h, L, true); }
|
||||
|
||||
// ---- randomness ----
|
||||
|
||||
int n_SetRandomSeed(LuaHost& h, lua_State* L) {
|
||||
h.rng().set_random_seed(num(L, 1), num(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int n_SetWorldSeed(LuaHost& h, lua_State* L) {
|
||||
h.rng().set_world_seed((uint32_t)num(L, 1));
|
||||
return 0;
|
||||
}
|
||||
int n_GetWorldSeed(LuaHost& h, lua_State* L) {
|
||||
lua_pushnumber(L, h.rng().world_seed());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Random(): float 0..1; Random(a): int 0..a; Random(a,b): int a..b (banker's rounding on args)
|
||||
int n_Random(LuaHost& h, lua_State* L) {
|
||||
const int n = lua_gettop(L);
|
||||
if (n == 0) { lua_pushnumber(L, h.rng().randomf()); return 1; }
|
||||
const int32_t a = n == 1 ? 0 : round_half_even_i32(num(L, 1));
|
||||
const int32_t b = round_half_even_i32(num(L, n == 1 ? 1 : 2));
|
||||
lua_pushnumber(L, h.rng().random(a, b));
|
||||
return 1;
|
||||
}
|
||||
int n_Randomf(LuaHost& h, lua_State* L) {
|
||||
const int n = lua_gettop(L);
|
||||
const double r = h.rng().prng().next();
|
||||
if (n == 0) lua_pushnumber(L, (float)r);
|
||||
else if (n == 1) lua_pushnumber(L, (float)(num(L, 1) * r));
|
||||
else lua_pushnumber(L, (float)(num(L, 1) + (num(L, 2) - num(L, 1)) * r));
|
||||
return 1;
|
||||
}
|
||||
// ProceduralRandomf(x,y[,a[,b]]): 2 args 0..1, 3 args 0..a, 4 args a..b
|
||||
int n_ProceduralRandomf(LuaHost& h, lua_State* L) {
|
||||
const int n = lua_gettop(L);
|
||||
double lo = 0, hi = 1;
|
||||
if (n == 3) hi = num(L, 3);
|
||||
else if (n >= 4) { lo = num(L, 3); hi = num(L, 4); }
|
||||
lua_pushnumber(L, h.rng().procedural_randomf(num(L, 1), num(L, 2), lo, hi));
|
||||
return 1;
|
||||
}
|
||||
// ProceduralRandomi(x,y[,a[,b]]): 2 args 0..1, 3 args 0..a, 4 args a..b (ints)
|
||||
int n_ProceduralRandomi(LuaHost& h, lua_State* L) {
|
||||
const int n = lua_gettop(L);
|
||||
double lo = 0, hi = 1;
|
||||
if (n == 3) hi = num(L, 3);
|
||||
else if (n >= 4) { lo = num(L, 3); hi = num(L, 4); }
|
||||
lua_pushnumber(L, h.rng().procedural_randomi(num(L, 1), num(L, 2), lo, hi));
|
||||
return 1;
|
||||
}
|
||||
// ProceduralRandom(x,y[,a[,b]]): 2 args float 0..1, 3 args int 0..a, 4 args float a..b
|
||||
int n_ProceduralRandom(LuaHost& h, lua_State* L) {
|
||||
return lua_gettop(L) == 3 ? n_ProceduralRandomi(h, L) : n_ProceduralRandomf(h, L);
|
||||
}
|
||||
|
||||
float get_distribution(NollaPrng& rng, float mean, int sharpness, float baseline) {
|
||||
const float pi = 3.1415f;
|
||||
const float mean_offset = 0.5f - mean;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
const float r1 = (float)rng.next();
|
||||
const float r2 = (float)rng.next();
|
||||
const float div = std::fabs(r1 - mean);
|
||||
if (r2 < (1.0f - div) * baseline) return r1;
|
||||
if (div < 0.5f) {
|
||||
const float wave = (float)std::sin((double)((mean_offset + r1) * pi));
|
||||
const float shaped = (float)std::pow((double)wave, (double)sharpness);
|
||||
if (shaped > r2) return r1;
|
||||
}
|
||||
}
|
||||
return (float)rng.next();
|
||||
}
|
||||
int n_RandomDistribution(LuaHost& h, lua_State* L) {
|
||||
const int32_t lo = (int32_t)num(L, 1), hi = (int32_t)num(L, 2), mean = (int32_t)num(L, 3);
|
||||
const int sharp = (int)num(L, 4, 1);
|
||||
if (sharp == 0) { lua_pushnumber(L, h.rng().random(lo, hi)); return 1; }
|
||||
const float adj = (float)(mean - lo) / (float)(hi - lo);
|
||||
const float d = get_distribution(h.rng().prng(), adj, sharp, (float)num(L, 5, 0.005));
|
||||
lua_pushnumber(L, lo + (int32_t)std::round((float)(hi - lo) * d));
|
||||
return 1;
|
||||
}
|
||||
int n_RandomDistributionf(LuaHost& h, lua_State* L) {
|
||||
const float lo = (float)num(L, 1), hi = (float)num(L, 2), mean = (float)num(L, 3);
|
||||
const int sharp = (int)num(L, 4, 1);
|
||||
if (sharp == 0) { lua_pushnumber(L, (float)h.rng().prng().next() * (hi - lo) + lo); return 1; }
|
||||
const float adj = (mean - lo) / (hi - lo);
|
||||
lua_pushnumber(L, lo + (hi - lo) * get_distribution(h.rng().prng(), adj, sharp, (float)num(L, 5, 0.005)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- spells ----
|
||||
|
||||
double seeded_random(uint32_t seed, double x, double y) {
|
||||
NollaPrng p;
|
||||
p.set_random_seed(seed, (double)(float)x, (double)(float)y);
|
||||
return p.next();
|
||||
}
|
||||
|
||||
int n_GetRandomAction(LuaHost& h, lua_State* L) {
|
||||
const double x = num(L, 1), y = num(L, 2);
|
||||
const int level = (int)num(L, 3);
|
||||
const uint32_t seed = h.rng().world_seed() + (uint32_t)(int)num(L, 4);
|
||||
const auto& rows = h.actions();
|
||||
if (rows.empty()) return luaL_error(L, "GetRandomAction: global 'actions' is not loaded");
|
||||
auto prob = [&](const LuaHost::ActionRow& r) {
|
||||
auto it = r.prob.find(level);
|
||||
return it == r.prob.end() ? 0.0 : it->second;
|
||||
};
|
||||
double sum = 0;
|
||||
for (auto& r : rows) sum += prob(r);
|
||||
double acc = sum * seeded_random(seed, x, y);
|
||||
for (auto& r : rows) {
|
||||
const double p = prob(r);
|
||||
if (p == 0) continue;
|
||||
if (p >= acc) { lua_pushstring(L, r.id.c_str()); return 1; }
|
||||
acc -= p;
|
||||
}
|
||||
lua_pushstring(L, rows[0].id.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int n_GetRandomActionWithType(LuaHost& h, lua_State* L) {
|
||||
const double x = num(L, 1), y = num(L, 2);
|
||||
const int level = (int)num(L, 3), type = (int)num(L, 4);
|
||||
const uint32_t seed = h.rng().world_seed() + (uint32_t)(int)num(L, 5);
|
||||
const auto& rows = h.actions();
|
||||
if (rows.empty()) return luaL_error(L, "GetRandomActionWithType: global 'actions' is not loaded");
|
||||
auto prob = [&](const LuaHost::ActionRow& r) {
|
||||
auto it = r.prob.find(level);
|
||||
return it == r.prob.end() ? 0.0 : it->second;
|
||||
};
|
||||
double sum = 0;
|
||||
for (auto& r : rows) if (r.type == type) sum += prob(r);
|
||||
double acc = sum * seeded_random(seed, x, y);
|
||||
for (auto& r : rows) {
|
||||
if (r.type != type) continue;
|
||||
const double p = prob(r);
|
||||
if (p > 0 && p >= acc) { lua_pushstring(L, r.id.c_str()); return 1; }
|
||||
acc -= p;
|
||||
}
|
||||
// fallback: walk from a random start for the first matching spell
|
||||
const size_t start = (size_t)(seeded_random(seed, x, y) * (double)rows.size());
|
||||
for (size_t j = 0; j < rows.size(); ++j) {
|
||||
const auto& r = rows[(j + start) % rows.size()];
|
||||
if (r.type == type && prob(r) > 0) { lua_pushstring(L, r.id.c_str()); return 1; }
|
||||
}
|
||||
lua_pushstring(L, rows[0].id.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- spawning ----
|
||||
|
||||
int n_RegisterSpawnFunction(LuaHost& h, lua_State* L) {
|
||||
h.spawn_functions_[(uint32_t)(int64_t)num(L, 1)] = str(L, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int load_entity(LuaHost& h, lua_State* L, HostEvent::Kind k) {
|
||||
HostEvent e;
|
||||
e.kind = k;
|
||||
e.path = str(L, 1);
|
||||
e.x = num(L, 2);
|
||||
e.y = num(L, 3);
|
||||
e.id = h.next_entity_id++;
|
||||
lua_pushnumber(L, e.id);
|
||||
h.add_event(std::move(e));
|
||||
return 1;
|
||||
}
|
||||
int n_EntityLoad(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoad); }
|
||||
int n_EntityLoadCameraBound(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoadCameraBound); }
|
||||
int n_EntityLoadEndGameItem(LuaHost& h, lua_State* L) { return load_entity(h, L, HostEvent::Kind::EntityLoadEndGameItem); }
|
||||
|
||||
int n_LoadPixelScene(LuaHost& h, lua_State* L) {
|
||||
HostEvent e;
|
||||
e.kind = HostEvent::Kind::PixelScene;
|
||||
e.path = str(L, 1);
|
||||
e.path2 = str(L, 2);
|
||||
e.x = num(L, 3);
|
||||
e.y = num(L, 4);
|
||||
e.background = str(L, 5);
|
||||
h.add_event(std::move(e));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n_LoadBackgroundSprite(LuaHost& h, lua_State* L) { // (file, x, y, z_index, check_biome_corners): world generation records it as a scene with a background only
|
||||
HostEvent e;
|
||||
e.kind = HostEvent::Kind::PixelScene;
|
||||
e.background = str(L, 1);
|
||||
e.x = num(L, 2);
|
||||
e.y = num(L, 3);
|
||||
h.add_event(std::move(e));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- run flags / globals / misc ----
|
||||
|
||||
int n_GameHasFlagRun(LuaHost& h, lua_State* L) {
|
||||
const std::string k = str(L, 1);
|
||||
bool has;
|
||||
if (h.buffer_world_writes && (h.run_flags.count(k) || h.removed_run_flags.count(k))) has = h.run_flags.count(k) != 0;
|
||||
else { std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); has = h.world_run_flags().count(k) != 0; }
|
||||
lua_pushboolean(L, has);
|
||||
return 1;
|
||||
}
|
||||
int n_GameAddFlagRun(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.run_flags.insert(str(L, 1)); h.removed_run_flags.erase(str(L, 1)); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_run_flags().insert(str(L, 1)); return 0; }
|
||||
int n_GameRemoveFlagRun(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.run_flags.erase(str(L, 1)); h.removed_run_flags.insert(str(L, 1)); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_run_flags().erase(str(L, 1)); return 0; }
|
||||
int n_GameIsModeFullyDeterministic(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; } // (a normal run)
|
||||
int n_HasFlagPersistent(LuaHost& h, lua_State* L) { lua_pushboolean(L, h.assume_all_unlocked); return 1; }
|
||||
int n_GlobalsGetValue(LuaHost& h, lua_State* L) {
|
||||
std::string v;
|
||||
if (const auto own = h.globals.find(str(L, 1)); h.buffer_world_writes && own != h.globals.end()) v = own->second;
|
||||
else {
|
||||
std::lock_guard<std::mutex> lk(LuaHost::shared_mutex);
|
||||
const auto& g = h.world_globals();
|
||||
const auto it = g.find(str(L, 1));
|
||||
v = it == g.end() ? str(L, 2) : it->second;
|
||||
}
|
||||
lua_pushstring(L, v.c_str());
|
||||
return 1;
|
||||
}
|
||||
int n_StatsGetValue(LuaHost& h, lua_State* L) { const auto it = h.stats.find(str(L, 1)); lua_pushnumber(L, it == h.stats.end() ? 0.0 : it->second); return 1; }
|
||||
int n_StatsBiomeGetValue(LuaHost& h, lua_State* L) {
|
||||
const std::string k = str(L, 1);
|
||||
const auto it = h.stats.find(k); const auto ib = h.biome_stats_base.find(k);
|
||||
lua_pushnumber(L, (it == h.stats.end() ? 0.0 : it->second) - (ib == h.biome_stats_base.end() ? 0.0 : ib->second));
|
||||
return 1;
|
||||
}
|
||||
int n_StatsResetBiome(LuaHost& h, lua_State*) { h.biome_stats_base = h.stats; return 0; }
|
||||
int n_GlobalsSetValue(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.globals[str(L, 1)] = str(L, 2); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[str(L, 1)] = str(L, 2); return 0; }
|
||||
// session numbers (new game plus count etc.): a fresh run has everything at 0
|
||||
int n_SessionNumbersGetValue(LuaHost&, lua_State* L) { lua_pushstring(L, "0"); return 1; }
|
||||
int n_GameGetFrameNum(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; }
|
||||
int n_GameGetIsGamepadConnected(LuaHost&, lua_State* L) { lua_pushboolean(L, 0); return 1; }
|
||||
|
||||
// World geometry: the biome map is `biome_map_w` chunks wide; the main world spans
|
||||
// [-w*256, w*256) and parallel worlds repeat every w*512 cells.
|
||||
int n_BiomeMapGetSize(LuaHost& h, lua_State* L) {
|
||||
lua_pushnumber(L, h.biome_map_w);
|
||||
lua_pushnumber(L, h.biome_map_h);
|
||||
return 2;
|
||||
}
|
||||
int n_BiomeMapGetVerticalPositionInsideBiome(LuaHost& h, lua_State* L) {
|
||||
lua_pushnumber(L, h.vertical_position ? h.vertical_position(num(L, 1), num(L, 2)) : 0.0);
|
||||
return 1;
|
||||
}
|
||||
int n_GetParallelWorldPosition(LuaHost& h, lua_State* L) {
|
||||
const double width = h.biome_map_w * 512.0;
|
||||
lua_pushnumber(L, std::floor((num(L, 1) + width / 2) / width));
|
||||
lua_pushnumber(L, 0);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// RaytracePlatforms(x1,y1,x2,y2) -> hit, hit_x, hit_y. Needs terrain; delegated to a hook.
|
||||
// Without one, nothing is hit and the ray end point is returned.
|
||||
int n_RaytracePlatforms(LuaHost& h, lua_State* L) {
|
||||
const double x1 = num(L, 1), y1 = num(L, 2), x2 = num(L, 3), y2 = num(L, 4);
|
||||
double hx = x2, hy = y2;
|
||||
bool hit = false;
|
||||
if (h.raytrace) hit = h.raytrace(x1, y1, x2, y2, hx, hy);
|
||||
lua_pushboolean(L, hit);
|
||||
lua_pushnumber(L, hx);
|
||||
lua_pushnumber(L, hy);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// CellFactory_GetType(name) -> the material's id (-1: none); CellFactory_GetName(id) -> its name
|
||||
int n_CellFactory_GetType(LuaHost& h, lua_State* L) {
|
||||
if (!h.material_type) { h.note_missing("CellFactory_GetType"); return 0; }
|
||||
lua_pushnumber(L, h.material_type(luaL_optstring(L, 1, "")));
|
||||
return 1;
|
||||
}
|
||||
int n_CellFactory_GetUIName(LuaHost& h, lua_State* L) {
|
||||
if (!h.material_ui_name) { h.note_missing("CellFactory_GetUIName"); lua_pushstring(L, ""); return 1; }
|
||||
lua_pushstring(L, h.material_ui_name((int)num(L, 1)).c_str());
|
||||
return 1;
|
||||
}
|
||||
int n_CellFactory_GetName(LuaHost& h, lua_State* L) {
|
||||
if (!h.material_name) { h.note_missing("CellFactory_GetName"); return 0; }
|
||||
lua_pushstring(L, h.material_name((int)num(L, 1)).c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// GameGetDateAndTimeLocal() -> year, month, day, hour, minute, second (overridable for tests)
|
||||
int n_GameGetDateAndTimeLocal(LuaHost& h, lua_State* L) {
|
||||
int v[6];
|
||||
if (h.fixed_date[0] != 0) {
|
||||
for (int i = 0; i < 6; ++i) v[i] = h.fixed_date[i];
|
||||
} else {
|
||||
const std::time_t t = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
localtime_s(&tm, &t);
|
||||
v[0] = tm.tm_year + 1900; v[1] = tm.tm_mon + 1; v[2] = tm.tm_mday; v[3] = tm.tm_hour; v[4] = tm.tm_min; v[5] = tm.tm_sec;
|
||||
}
|
||||
for (int i = 0; i < 6; ++i) lua_pushnumber(L, v[i]);
|
||||
return 6;
|
||||
}
|
||||
int n_print(LuaHost& h, lua_State* L) {
|
||||
std::string line;
|
||||
for (int i = 1; i <= lua_gettop(L); ++i) {
|
||||
size_t n = 0;
|
||||
lua_getglobal(L, "tostring");
|
||||
lua_pushvalue(L, i);
|
||||
lua_call(L, 1, 1);
|
||||
const char* s = lua_tolstring(L, -1, &n);
|
||||
if (i > 1) line += '\t';
|
||||
if (s) line.append(s, n);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
static const bool echo = std::getenv("NG_LUAPRINT") != nullptr; // (the game writes them to logger.txt; here to stdout on request)
|
||||
if (echo) std::printf("lua print: %s%c", line.c_str(), 10);
|
||||
if (h.printed_.size() >= 1000) h.printed_.erase(h.printed_.begin(), h.printed_.begin() + 500); // (kept for tests; bounded)
|
||||
h.printed_.push_back(std::move(line));
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LuaHost::LuaHost(const Wak& wak, GameRandom& rng) : wak_(wak), rng_(rng) {
|
||||
L_ = luaL_newstate();
|
||||
luaL_openlibs(L_);
|
||||
register_api();
|
||||
// Spawn functions the engine registers for every biome before its script runs (the script may override colours).
|
||||
// Colours as RGBA hex, the table common to all biomes of the reference data (noita-tools maps.json).
|
||||
static const struct { uint32_t rgba; const char* fn; } kDefaults[] = {
|
||||
{0xff0000ff, "spawn_small_enemies"}, {0x800000ff, "spawn_big_enemies"}, {0x00ff00ff, "spawn_items"},
|
||||
{0xc88d1aff, "spawn_props"}, {0xc88000ff, "spawn_props2"}, {0xc80040ff, "spawn_props3"}, {0xffff00ff, "spawn_lamp"},
|
||||
{0xff0affff, "load_pixel_scene"}, {0xff0080ff, "load_pixel_scene2"}, {0xff8000ff, "spawn_unique_enemy"},
|
||||
{0xc84040ff, "spawn_unique_enemy2"}, {0x804040ff, "spawn_unique_enemy3"}, {0x96c850ff, "spawn_ghostlamp"},
|
||||
{0x60a064ff, "spawn_candles"}, {0x50a000ff, "spawn_potion_altar"}, {0xbca0f0ff, "spawn_potions"},
|
||||
{0x00ff5aff, "spawn_apparition"}, {0x78ffffff, "spawn_heart"}, {0x50a0f0ff, "spawn_wands"}, {0xbf26a6ff, "spawn_portal"},
|
||||
{0x04a977ff, "spawn_end_portal"}, {0xffd171ff, "spawn_orb"}, {0xffd181ff, "spawn_perk"}, {0xffff81ff, "spawn_all_perks"},
|
||||
{0xc7eb28ff, "spawn_wand_trap"}, {0xe8ff80ff, "spawn_wand_trap_ignite"}, {0x2768deff, "spawn_wand_trap_electricity_source"},
|
||||
{0x2768dfff, "spawn_wand_trap_electricity"}, {0x6b4f9bff, "spawn_moon"}, {0xd7b3e8ff, "spawn_collapse"},
|
||||
};
|
||||
if (!std::getenv("NG_NO_DEFAULT_SPAWNS"))
|
||||
for (const auto& d : kDefaults) spawn_functions_[(d.rgba >> 8) | (d.rgba << 24)] = d.fn; // RGBA -> ARGB
|
||||
}
|
||||
|
||||
LuaHost::~LuaHost() {
|
||||
if (L_) lua_close(L_);
|
||||
}
|
||||
|
||||
const std::string& LuaHost::translate(const std::string& key) {
|
||||
static const std::string none;
|
||||
if (!texts_loaded_) {
|
||||
texts_loaded_ = true;
|
||||
std::vector<uint8_t> raw;
|
||||
if (!wak_.read("data/translations/common.csv", raw) && !loose_dir.empty()) {
|
||||
std::ifstream in(loose_dir + "/data/translations/common.csv", std::ios::binary);
|
||||
raw.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
}
|
||||
if (!raw.empty()) {
|
||||
const std::string t((const char*)raw.data(), raw.size());
|
||||
std::vector<std::string> row;
|
||||
std::string f;
|
||||
bool q = false;
|
||||
auto end_row = [&] {
|
||||
row.push_back(f); f.clear();
|
||||
if (row.size() > 1 && !row[0].empty() && !row[1].empty()) {
|
||||
std::string v = row[1];
|
||||
for (size_t p = 0; (p = v.find("\\n", p)) != std::string::npos;) { v.replace(p, 2, "\n"); ++p; }
|
||||
texts_[row[0]] = v;
|
||||
}
|
||||
row.clear();
|
||||
};
|
||||
for (size_t i = 0; i < t.size(); ++i) {
|
||||
const char c = t[i];
|
||||
if (q) { if (c == '"') { if (i + 1 < t.size() && t[i + 1] == '"') { f += '"'; ++i; } else q = false; } else f += c; }
|
||||
else if (c == '"') q = true;
|
||||
else if (c == ',') { row.push_back(f); f.clear(); }
|
||||
else if (c == '\n') { if (!f.empty() && f.back() == '\r') f.pop_back(); end_row(); }
|
||||
else f += c;
|
||||
}
|
||||
if (!f.empty() || !row.empty()) end_row();
|
||||
}
|
||||
}
|
||||
const auto it = texts_.find(!key.empty() && key[0] == '$' ? key.substr(1) : key);
|
||||
return it == texts_.end() ? none : it->second;
|
||||
}
|
||||
|
||||
void LuaHost::register_api() {
|
||||
// 1. every documented engine function starts as a counting no-op stub
|
||||
for (const char* n : kApiNames) {
|
||||
lua_pushlightuserdata(L_, this);
|
||||
lua_pushstring(L_, n);
|
||||
lua_pushcclosure(L_, stub_thunk, 2);
|
||||
lua_setglobal(L_, n);
|
||||
}
|
||||
// 2. real implementations override the stubs
|
||||
struct { const char* name; Native fn; } natives[] = {
|
||||
{"dofile", n_dofile}, {"dofile_once", n_dofile_once}, {"dofile_always", n_dofile},
|
||||
{"print", n_print},
|
||||
{"SetRandomSeed", n_SetRandomSeed}, {"SetWorldSeed", n_SetWorldSeed}, {"GetWorldSeed", n_GetWorldSeed},
|
||||
{"Random", n_Random}, {"Randomf", n_Randomf},
|
||||
{"ProceduralRandom", n_ProceduralRandom}, {"ProceduralRandomf", n_ProceduralRandomf}, {"ProceduralRandomi", n_ProceduralRandomi},
|
||||
{"RandomDistribution", n_RandomDistribution}, {"RandomDistributionf", n_RandomDistributionf},
|
||||
{"GetRandomAction", n_GetRandomAction}, {"GetRandomActionWithType", n_GetRandomActionWithType},
|
||||
{"RegisterSpawnFunction", n_RegisterSpawnFunction},
|
||||
{"EntityLoad", n_EntityLoad}, {"EntityLoadCameraBound", n_EntityLoadCameraBound}, {"EntityLoadEndGameItem", n_EntityLoadEndGameItem},
|
||||
{"LoadPixelScene", n_LoadPixelScene}, {"LoadBackgroundSprite", n_LoadBackgroundSprite},
|
||||
{"GameHasFlagRun", n_GameHasFlagRun}, {"GameAddFlagRun", n_GameAddFlagRun}, {"GameRemoveFlagRun", n_GameRemoveFlagRun},
|
||||
{"HasFlagPersistent", n_HasFlagPersistent}, {"GameIsModeFullyDeterministic", n_GameIsModeFullyDeterministic},
|
||||
{"StatsGetValue", n_StatsGetValue}, {"StatsBiomeGetValue", n_StatsBiomeGetValue}, {"StatsResetBiome", n_StatsResetBiome},
|
||||
{"GlobalsGetValue", n_GlobalsGetValue}, {"GlobalsSetValue", n_GlobalsSetValue},
|
||||
{"GameGetFrameNum", n_GameGetFrameNum}, {"SessionNumbersGetValue", n_SessionNumbersGetValue}, {"GameGetIsGamepadConnected", n_GameGetIsGamepadConnected},
|
||||
{"BiomeMapGetSize", n_BiomeMapGetSize}, {"BiomeMapGetVerticalPositionInsideBiome", n_BiomeMapGetVerticalPositionInsideBiome}, {"GetParallelWorldPosition", n_GetParallelWorldPosition},
|
||||
{"RaytracePlatforms", n_RaytracePlatforms}, {"RaytraceSurfaces", n_RaytracePlatforms}, {"Raytrace", n_RaytracePlatforms}, {"GameGetDateAndTimeLocal", n_GameGetDateAndTimeLocal},
|
||||
{"CellFactory_GetType", n_CellFactory_GetType}, {"CellFactory_GetName", n_CellFactory_GetName}, {"CellFactory_GetUIName", n_CellFactory_GetUIName},
|
||||
};
|
||||
for (auto& n : natives) reg(L_, *this, n.name, n.fn);
|
||||
register_entity_natives(*this);
|
||||
register_host_natives(*this);
|
||||
}
|
||||
|
||||
void LuaHost::register_native(const char* name, NativeFn fn) { reg(L_, *this, name, fn); }
|
||||
|
||||
// Pushes the current environment table (the globals when env is empty), made on first use: a table whose metatable sends missing names to the globals
|
||||
static void push_env_table(lua_State* L, const std::string& key) {
|
||||
if (key.empty()) { lua_pushvalue(L, LUA_GLOBALSINDEX); return; }
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "ng_envs");
|
||||
if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, LUA_REGISTRYINDEX, "ng_envs"); }
|
||||
lua_getfield(L, -1, key.c_str());
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
lua_newtable(L); lua_pushvalue(L, LUA_GLOBALSINDEX); lua_setfield(L, -2, "__index"); lua_setmetatable(L, -2);
|
||||
lua_pushvalue(L, -1); lua_setfield(L, -3, key.c_str());
|
||||
}
|
||||
lua_remove(L, -2);
|
||||
}
|
||||
|
||||
static void get_fn(lua_State* L, const std::string& env, const std::string& name, bool own = false) {
|
||||
if (env.empty() && !own) { lua_getglobal(L, name.c_str()); return; }
|
||||
push_env_table(L, env);
|
||||
lua_pushstring(L, name.c_str());
|
||||
if (own) lua_rawget(L, -2); else lua_gettable(L, -2);
|
||||
lua_remove(L, -2);
|
||||
}
|
||||
|
||||
void LuaHost::drop_env(const std::string& key) {
|
||||
if (key.empty()) return;
|
||||
lua_getfield(L_, LUA_REGISTRYINDEX, "ng_envs");
|
||||
if (lua_istable(L_, -1)) { lua_pushnil(L_); lua_setfield(L_, -2, key.c_str()); }
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
|
||||
bool LuaHost::has_function(const std::string& name, bool own) {
|
||||
get_fn(L_, env, name, own);
|
||||
const bool f = lua_isfunction(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
return f;
|
||||
}
|
||||
|
||||
bool LuaHost::call_function(const std::string& name, const std::vector<LVal>& args, std::string* err) {
|
||||
get_fn(L_, env, name);
|
||||
if (!lua_isfunction(L_, -1)) {
|
||||
lua_pop(L_, 1);
|
||||
if (err) *err = "global function not defined: " + name;
|
||||
return false;
|
||||
}
|
||||
for (const LVal& a : args) {
|
||||
switch (a.t) {
|
||||
case LVal::T::Nil: lua_pushnil(L_); break;
|
||||
case LVal::T::Bool: lua_pushboolean(L_, a.b); break;
|
||||
case LVal::T::Num: lua_pushnumber(L_, a.n); break;
|
||||
case LVal::T::Str: lua_pushstring(L_, a.s.c_str()); break;
|
||||
}
|
||||
}
|
||||
return pcall_traced(L_, (int)args.size(), 0, err);
|
||||
}
|
||||
|
||||
bool LuaHost::call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err) {
|
||||
results.assign(nresults, 0.0);
|
||||
get_fn(L_, env, name); // (not a function: the pcall fails, as the engine's does)
|
||||
for (const LVal& a : args) {
|
||||
switch (a.t) {
|
||||
case LVal::T::Nil: lua_pushnil(L_); break;
|
||||
case LVal::T::Bool: lua_pushboolean(L_, a.b); break;
|
||||
case LVal::T::Num: lua_pushnumber(L_, a.n); break;
|
||||
case LVal::T::Str: lua_pushstring(L_, a.s.c_str()); break;
|
||||
}
|
||||
}
|
||||
if (!pcall_traced(L_, (int)args.size(), nresults, err)) return false;
|
||||
for (int k = 0; k < nresults; ++k) results[k] = lua_tonumber(L_, k - nresults);
|
||||
lua_pop(L_, nresults);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- components of the world generation's spawns ----
|
||||
const std::vector<SpawnComp>& LuaHost::file_components(const std::string& path) {
|
||||
auto it = file_comps_.find(path);
|
||||
if (it != file_comps_.end()) return it->second;
|
||||
std::vector<SpawnComp>& out = file_comps_[path];
|
||||
std::vector<uint8_t> raw;
|
||||
if (path.empty() || !wak_.read(path, raw)) return out;
|
||||
std::unique_ptr<XmlNode> root;
|
||||
try { root = parse_xml(std::string_view((const char*)raw.data(), raw.size())); } catch (...) { return out; }
|
||||
if (!root) return out;
|
||||
std::vector<SpawnComp> mine;
|
||||
for (const auto& c : root->children) {
|
||||
if (c->name == "Base") { const std::vector<SpawnComp> base = file_components(c->attr_or("file")); out.insert(out.end(), base.begin(), base.end()); continue; } // (the base file's components first)
|
||||
if (c->name == "Entity") continue;
|
||||
SpawnComp sc; sc.type = c->name;
|
||||
for (const auto& a : c->attrs) sc.attrs[a.first] = a.second;
|
||||
for (const auto& sub : c->children) for (const auto& a : sub->attrs) sc.attrs[sub->name + "." + a.first] = a.second; // (objects: config_explosion.damage ...)
|
||||
mine.push_back(std::move(sc));
|
||||
}
|
||||
std::vector<SpawnComp>& out2 = file_comps_[path];
|
||||
out2.insert(out2.end(), mine.begin(), mine.end());
|
||||
return out2;
|
||||
}
|
||||
|
||||
std::vector<int> LuaHost::gen_components(int ent, const std::string& type, const std::string& tag) {
|
||||
std::vector<int> ids;
|
||||
const HostEvent* ev = nullptr;
|
||||
for (size_t i = events_.size(); i-- > 0;) if (events_[i].id == ent) { ev = &events_[i]; break; }
|
||||
if (!ev) return ids;
|
||||
auto has_tag = [&](const std::map<std::string, std::string>& a) {
|
||||
if (tag.empty()) return true;
|
||||
auto t = a.find("_tags");
|
||||
return t != a.end() && ("," + t->second + ",").find("," + tag + ",") != std::string::npos;
|
||||
};
|
||||
auto handle = [&](int index, const std::string& ty) {
|
||||
for (const auto& [id, g] : gen_comps_) if (g.ent == ent && g.index == index) return id;
|
||||
return gen_handle(ent, index, ty);
|
||||
};
|
||||
const std::vector<SpawnComp> fc = file_components(ev->path); // (a copy: handle() may not touch it, but the cache may grow)
|
||||
for (size_t k = 0; k < fc.size(); ++k) if ((type.empty() || fc[k].type == type) && has_tag(fc[k].attrs)) ids.push_back(handle(-1 - (int)k, fc[k].type));
|
||||
for (size_t k = 0; k < ev->comps.size(); ++k) if ((type.empty() || ev->comps[k].type == type) && has_tag(ev->comps[k].attrs)) ids.push_back(handle((int)k, ev->comps[k].type));
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool LuaHost::gen_get(int cid, const std::string& key, std::string& out) {
|
||||
auto g = gen_comps_.find(cid);
|
||||
if (g == gen_comps_.end()) return false;
|
||||
for (size_t i = events_.size(); i-- > 0;) {
|
||||
HostEvent& ev = events_[i];
|
||||
if (ev.id != g->second.ent) continue;
|
||||
if (g->second.index >= 0) {
|
||||
if (g->second.index >= (int)ev.comps.size()) return false;
|
||||
auto a = ev.comps[g->second.index].attrs.find(key);
|
||||
if (a == ev.comps[g->second.index].attrs.end()) return false;
|
||||
out = a->second; return true;
|
||||
}
|
||||
for (size_t k = ev.comps.size(); k-- > 0;) if (ev.comps[k].type == "=" + g->second.type) { auto a = ev.comps[k].attrs.find(key); if (a != ev.comps[k].attrs.end()) { out = a->second; return true; } }
|
||||
const std::vector<SpawnComp>& fc = file_components(ev.path);
|
||||
const int k = -1 - g->second.index;
|
||||
if (k < 0 || k >= (int)fc.size()) return false;
|
||||
auto a = fc[k].attrs.find(key);
|
||||
if (a == fc[k].attrs.end()) return false;
|
||||
out = a->second; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LuaHost::gen_set(int cid, const std::string& key, const std::string& value) {
|
||||
auto g = gen_comps_.find(cid);
|
||||
if (g == gen_comps_.end()) return false;
|
||||
for (size_t i = events_.size(); i-- > 0;) {
|
||||
HostEvent& ev = events_[i];
|
||||
if (ev.id != g->second.ent) continue;
|
||||
if (g->second.index >= 0) { if (g->second.index < (int)ev.comps.size()) ev.comps[g->second.index].attrs[key] = value; return true; }
|
||||
for (SpawnComp& sc : ev.comps) if (sc.type == "=" + g->second.type) { sc.attrs[key] = value; return true; }
|
||||
SpawnComp sc; sc.type = "=" + g->second.type; sc.attrs[key] = value;
|
||||
ev.comps.push_back(std::move(sc));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LuaHost::load_once(const std::string& path) { return loaded_once_.insert(path).second; }
|
||||
|
||||
bool LuaHost::run_file(const std::string& path, std::string* err) {
|
||||
if (!load_from_wak(*this, L_, path, err)) return false;
|
||||
if (!env.empty()) { push_env_table(L_, env); lua_setfenv(L_, -2); }
|
||||
return pcall_traced(L_, 0, 0, err);
|
||||
}
|
||||
|
||||
bool LuaHost::run_string(const std::string& code, const std::string& chunk_name, std::string* err) {
|
||||
if (luaL_loadbuffer(L_, code.data(), code.size(), chunk_name.c_str()) != 0) {
|
||||
if (err) *err = lua_tostring(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
return false;
|
||||
}
|
||||
return pcall_traced(L_, 0, 0, err);
|
||||
}
|
||||
|
||||
bool LuaHost::call_global(const std::string& name, const std::vector<double>& args, std::string* err) {
|
||||
lua_getglobal(L_, name.c_str());
|
||||
if (!lua_isfunction(L_, -1)) {
|
||||
lua_pop(L_, 1);
|
||||
if (err) *err = "global function not defined: " + name;
|
||||
return false;
|
||||
}
|
||||
for (double a : args) lua_pushnumber(L_, a);
|
||||
return pcall_traced(L_, (int)args.size(), 0, err);
|
||||
}
|
||||
|
||||
const std::vector<LuaHost::ActionRow>& LuaHost::actions() {
|
||||
lua_getglobal(L_, "actions");
|
||||
if (!lua_istable(L_, -1) && !tried_actions_) { // the engine knows the spell list natively: load the game's table on demand
|
||||
tried_actions_ = true;
|
||||
lua_pop(L_, 1);
|
||||
std::string e;
|
||||
const std::string saved = env; // (into the globals, whatever environment the script that asked runs in)
|
||||
env.clear();
|
||||
if (load_once("data/scripts/gun/gun_actions.lua")) run_file("data/scripts/gun/gun_actions.lua", &e);
|
||||
env = saved;
|
||||
lua_getglobal(L_, "actions");
|
||||
}
|
||||
if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return actions_; } // (the list read before stays)
|
||||
const size_t len = lua_objlen(L_, -1);
|
||||
if (len != actions_len_) {
|
||||
actions_.clear();
|
||||
for (size_t i = 1; i <= len; ++i) {
|
||||
lua_rawgeti(L_, -1, (int)i);
|
||||
ActionRow r;
|
||||
lua_getfield(L_, -1, "id");
|
||||
r.id = str(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
lua_getfield(L_, -1, "type");
|
||||
r.type = (int)num(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
lua_getfield(L_, -1, "spawn_level");
|
||||
const std::string lv = str(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
lua_getfield(L_, -1, "spawn_probability");
|
||||
const std::string pr = str(L_, -1);
|
||||
lua_pop(L_, 1);
|
||||
std::stringstream a(lv), b(pr);
|
||||
std::string x, y;
|
||||
while (std::getline(a, x, ',') && std::getline(b, y, ',')) r.prob[std::atoi(x.c_str())] = std::atof(y.c_str());
|
||||
actions_.push_back(std::move(r));
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
actions_len_ = len;
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
return actions_;
|
||||
}
|
||||
|
||||
static int g_profileDeaths = 0;
|
||||
void set_profile_death_count(int deaths) { g_profileDeaths = deaths; }
|
||||
int lua_death_count() {
|
||||
if (const char* d = std::getenv("NG_DEATHS")) return std::atoi(d);
|
||||
return g_profileDeaths;
|
||||
}
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,194 @@
|
||||
#pragma once
|
||||
// Embeds LuaJIT and exposes the subset of Noita's Lua API that biome / item scripts
|
||||
// need, so the original game scripts can run unmodified from data.wak.
|
||||
#include <cstdint>
|
||||
#include "entity/spawn_comp.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/random.h"
|
||||
#include "lua/entity_api.h"
|
||||
#include "core/wak.h"
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace ng {
|
||||
|
||||
class LuaHost;
|
||||
using NativeFn = int (*)(LuaHost&, lua_State*);
|
||||
void register_entity_natives(LuaHost& h); // lua/entity_natives.cpp
|
||||
void register_host_natives(LuaHost& h);
|
||||
void register_runtime_natives(LuaHost& h); // entity/runtime_natives.cpp: the world's services (the game VM) // lua/host_natives.cpp: the natives that need no world
|
||||
// StatsGlobalGetValue("death_count"): the profile's deaths (NG_DEATHS, default 0 = a fresh profile; the save is not read)
|
||||
int lua_death_count();
|
||||
void set_profile_death_count(int deaths); // the deaths of the game profile (its session stats, read in place); NG_DEATHS overrides
|
||||
|
||||
// Something a script asked the engine to do. Recorded instead of executed.
|
||||
struct HostEvent {
|
||||
enum class Kind { EntityLoad, EntityLoadCameraBound, EntityLoadEndGameItem, PixelScene } kind;
|
||||
std::string path; // entity xml, or materials_filename for PixelScene
|
||||
std::string path2; // colors_filename / visual file for PixelScene
|
||||
std::string background; // PixelScene background file
|
||||
double x = 0, y = 0;
|
||||
int id = 0; // the id the script got back
|
||||
std::vector<SpawnComp> comps; // EntityAddComponent calls on that id
|
||||
};
|
||||
|
||||
class LuaHost {
|
||||
public:
|
||||
LuaHost(const Wak& wak, GameRandom& rng);
|
||||
~LuaHost();
|
||||
LuaHost(const LuaHost&) = delete;
|
||||
LuaHost& operator=(const LuaHost&) = delete;
|
||||
|
||||
// Runs a script from the archive (dofile semantics). false + message on error.
|
||||
bool run_file(const std::string& path, std::string* err = nullptr);
|
||||
bool run_string(const std::string& code, const std::string& chunk_name, std::string* err = nullptr);
|
||||
|
||||
// Calls a global function with numeric arguments. false + message on error.
|
||||
bool call_global(const std::string& name, const std::vector<double>& args, std::string* err = nullptr);
|
||||
|
||||
// Registers (or overrides) a native global function.
|
||||
void register_native(const char* name, NativeFn fn);
|
||||
// Calls a global function with mixed arguments. false + message on error (or when the function is not defined).
|
||||
bool call_function(const std::string& name, const std::vector<LVal>& args, std::string* err = nullptr);
|
||||
bool has_function(const std::string& name, bool own = false); // a global function of that name exists (own: defined in the current environment itself, not inherited)
|
||||
// The environment scripts run in (the engine's Lua VMs, LuaComponent vm_type): "" = the globals; otherwise a table of its own whose missing names fall back to the
|
||||
// globals. run_file / call_function / has_function use it, a dofile'd chunk takes the environment of the one calling it, dofile_once counts per environment.
|
||||
std::string env;
|
||||
void drop_env(const std::string& key);
|
||||
// The same with nresults results, each read with lua_tonumber (nil and non-numbers give 0, as the engine reads them). On an error they are all 0.
|
||||
bool call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err = nullptr);
|
||||
// The game world behind the entity/component API (null: scripts only record events).
|
||||
IEntityWorld* world = nullptr;
|
||||
void* user = nullptr; // owner-specific data for natives (e.g. the gun runtime)
|
||||
|
||||
lua_State* state() { return L_; }
|
||||
GameRandom& rng() { return rng_; }
|
||||
const Wak& wak() const { return wak_; }
|
||||
|
||||
// RegisterSpawnFunction(color, name): ARGB colour -> Lua function name.
|
||||
const std::map<uint32_t, std::string>& spawn_functions() const { return spawn_functions_; }
|
||||
// What scripts asked for since the last clear_events().
|
||||
const std::vector<HostEvent>& events() const { return events_; }
|
||||
void clear_events() { events_.clear(); }
|
||||
// Documented engine functions that scripts called but we do not implement (name -> calls).
|
||||
const std::map<std::string, int>& unimplemented_calls() const { return missing_; }
|
||||
std::vector<std::string> stub_names(); // the engine functions that are still counting stubs (no implementation)
|
||||
const std::vector<std::string>& printed() const { return printed_; }
|
||||
|
||||
// When true (default) every spell/flag counts as unlocked, like a fully progressed profile.
|
||||
bool assume_all_unlocked = true;
|
||||
// Persistent flags (AddFlagPersistent): shared by every VM, kept in our own save folder (never the game's save); HasFlagPersistent is true for a flag set
|
||||
// here, otherwise assume_all_unlocked.
|
||||
static std::set<std::string>& persistent_flags() { static std::set<std::string> f; return f; }
|
||||
static std::string& persistent_file() { static std::string p; return p; }
|
||||
static void save_persistent_flags();
|
||||
static inline std::vector<std::string> profile_bones; // the files of the profile's save00/persistent/bones_new (dead players' wands, read in place): SpawnApparition
|
||||
static inline bool profile_flags = false; // the game profile's flags were read (save00/persistent/flags): HasFlagPersistent answers from them, not assume_all_unlocked
|
||||
// ModLuaFileAppend: file -> the files that run after it, for every VM
|
||||
static std::map<std::string, std::vector<std::string>>& mod_appends() { static std::map<std::string, std::vector<std::string>> m; return m; }
|
||||
static inline bool scripts_changed = false; // a mod changed a file: the script cache is emptied
|
||||
// ModSettingGet / ModSettingSet (current values) and the next values (applied at the next run start), mod magic numbers
|
||||
static std::map<std::string, std::string>& mod_settings() { static std::map<std::string, std::string> m; return m; }
|
||||
static std::map<std::string, std::string>& mod_settings_next() { static std::map<std::string, std::string> m; return m; }
|
||||
static std::vector<std::string>& active_mods() { static std::vector<std::string> m; return m; }
|
||||
static std::map<std::string, std::string>& magic_numbers() { static std::map<std::string, std::string> m; return m; }
|
||||
static std::string magic_value(const Wak& wak, const std::string& key); // data/magic_numbers.xml (and the mods' additions)
|
||||
static std::set<std::string>& unlocked_items() { static std::set<std::string> s; return s; } // UnlockItem
|
||||
static std::map<std::string, std::string>& session_numbers() { static std::map<std::string, std::string> m; return m; }
|
||||
static std::vector<std::string>& poly_table(bool rare) { static std::vector<std::string> c, r; return rare ? r : c; } // the polymorph random tables (00b00360)
|
||||
static void build_poly_table(const Wak& wak); // once: the .xml files of data/entities/animals/ (excluded and rare words as 00b00360)
|
||||
static std::map<std::string, std::vector<std::string>>& stashes() { static std::map<std::string, std::vector<std::string>> m; return m; }
|
||||
// CellFactory_GetAll*: every material with its kind (0 liquid, 1 sand, 2 gas, 3 fire, 4 solid), static flag and tags
|
||||
struct MaterialBrief { std::string name; int kind = 0; bool is_static = false, particle_fx = false; std::vector<std::string> tags; }; // (0078d8d0: is_static = solid_static_type for a solid, liquid_static otherwise; particle_fx = is_just_particle_fx)
|
||||
std::function<std::vector<MaterialBrief>()> material_list;
|
||||
struct BiomeImage { int w = 0, h = 0; std::vector<uint32_t> px; }; // BiomeMapSetSize / SetPixel (ARGB): the map a biome-map script makes
|
||||
BiomeImage biome_image;
|
||||
static std::function<bool(const std::string& file, int& w, int& h, std::vector<uint32_t>& argb)>& png_decoder() { static std::function<bool(const std::string&, int&, int&, std::vector<uint32_t>&)> f; return f; }
|
||||
// runs a biome-map script (BIOME_MAP pointing to a .lua file, BiomeMapLoad_KeepPlayer) in a VM of its own; false when it made no map
|
||||
static bool run_biome_map_script(const Wak& wak, uint32_t world_seed, const std::string& file, BiomeImage& out, std::string* err = nullptr);
|
||||
std::vector<std::string> reflected; // RegisterPerk / GameRegisterStatusEffect / RegisterStreamingEvent ids
|
||||
std::string current_mod; // the mod whose init.lua is running (ModTextFileWhoSetContent)
|
||||
std::function<bool(const std::string& file, int& w, int& h, std::vector<uint32_t>& argb)> decode_png; // ModImageMakeEditable reads the image
|
||||
std::function<void(const std::string& file, int w, int h, const std::vector<uint32_t>& argb)> on_image_edited; // ModImageSetPixel: the host's copy of the image
|
||||
static std::vector<std::string>& mod_material_files() { static std::vector<std::string> v; return v; }
|
||||
static std::vector<std::string>& mod_audio_files() { static std::vector<std::string> v; return v; }
|
||||
// World geometry reported to scripts (biome map size in chunks).
|
||||
int biome_map_w = 70, biome_map_h = 48;
|
||||
// BiomeMapGetVerticalPositionInsideBiome(x, y): 0 = top of the biome area, 1 = bottom. Unset: 0.
|
||||
std::function<double(double, double)> vertical_position;
|
||||
// Terrain query for RaytracePlatforms: returns true on hit and fills hit_x/hit_y.
|
||||
// Unset = no terrain known, nothing is hit.
|
||||
std::function<bool(double x1, double y1, double x2, double y2, double& hit_x, double& hit_y)> raytrace;
|
||||
// CellFactory_GetType / CellFactory_GetName: the material table (unset: the functions stay stubs)
|
||||
std::function<int(const std::string&)> material_type; // -1 when there is no such material
|
||||
std::function<std::string(int)> material_name;
|
||||
std::function<std::string(int)> material_ui_name; // CellFactory_GetUIName: the ui_name of the material ("$mat_..." key)
|
||||
// year, month, day, hour, minute, second; year 0 = use the system clock.
|
||||
int fixed_date[6] = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
// ---- used by native thunks (public so free functions in the .cpp can reach them) ----
|
||||
struct ActionRow {
|
||||
std::string id;
|
||||
int type = 0;
|
||||
std::map<int, double> prob; // level -> spawn probability
|
||||
};
|
||||
const std::vector<ActionRow>& actions();
|
||||
void add_event(HostEvent e) { events_.push_back(std::move(e)); }
|
||||
// World generation has no entity runtime: the components of a spawn (the file's, read from it, and those EntityAddComponent recorded) get handles here, so that
|
||||
// EntityGetComponent / ComponentGetValue / ComponentSetValue work on them (load_verlet_rope_with_two_joints sets the world positions of its joints). A write to a
|
||||
// recorded component changes the record; a write to one of the file's goes with the spawn as an "=Type" merge.
|
||||
std::vector<int> gen_components(int ent, const std::string& type, const std::string& tag);
|
||||
bool gen_get(int cid, const std::string& key, std::string& out);
|
||||
bool gen_set(int cid, const std::string& key, const std::string& value);
|
||||
int gen_handle(int ent, int index, const std::string& type) { const int id = next_entity_id++; gen_comps_[id] = {ent, index, type}; return id; }
|
||||
bool add_spawn_comp(int id, SpawnComp c) { for (size_t i = events_.size(); i-- > 0;) if (events_[i].id == id) { events_[i].comps.push_back(std::move(c)); return true; } return false; }
|
||||
// The English text of a translation key (data/translations/common.csv; with or without the leading $), "" when unknown; the last row of a key wins.
|
||||
const std::string& translate(const std::string& key);
|
||||
void note_missing(const std::string& n) { ++missing_[n]; }
|
||||
bool load_once(const std::string& path); // true if first time
|
||||
std::map<uint32_t, std::string> spawn_functions_;
|
||||
std::set<std::string> run_flags;
|
||||
std::map<std::string, std::string> globals;
|
||||
// The world state of GlobalsSetValue / GameAddFlagRun is one store for every Lua VM of the game (the chunk spawn scripts of a holy mountain take the next perks
|
||||
// of TEMPLE_NEXT_PERK_INDEX that the previous one left): once set (main.cpp: the game host's), every host reads and writes these, under the mutex (the chunk
|
||||
// spawns run on worker threads); unset, each host keeps its own (tools, tests)
|
||||
static inline std::map<std::string, std::string>* shared_globals = nullptr;
|
||||
static inline std::set<std::string>* shared_run_flags = nullptr;
|
||||
static inline std::mutex shared_mutex;
|
||||
std::map<std::string, std::string>& world_globals() { return shared_globals ? *shared_globals : globals; }
|
||||
std::set<std::string>& world_run_flags() { return shared_run_flags ? *shared_run_flags : run_flags; }
|
||||
// A trial run (the chunk scripts of an area set up ahead, area_setup.cpp): the writes stay in this host (globals / run_flags over the shared ones) and wrote_world
|
||||
// tells that the scripts change the world state; such a tile runs again for real when the player reaches it, as the engine runs a chunk's spawns when it loads
|
||||
bool buffer_world_writes = false, wrote_world = false;
|
||||
std::set<std::string> removed_run_flags;
|
||||
std::string loose_dir; // the game folder next to data.wak (the translations are loose files there)
|
||||
std::map<std::string, double> stats, biome_stats_base; // StatsGetValue / StatsBiomeGetValue (the host counts enemies_killed...; StatsResetBiome starts the biome counters over)
|
||||
int next_entity_id = 1000;
|
||||
struct GenComp { int ent = 0; int index = 0; std::string type; }; // index >= 0: the spawn's recorded component; -1 - k: the k-th component of the file
|
||||
std::map<int, GenComp> gen_comps_;
|
||||
std::map<std::string, std::vector<SpawnComp>> file_comps_; // an entity file's components (Base files included), by path
|
||||
const std::vector<SpawnComp>& file_components(const std::string& path);
|
||||
std::vector<std::string> printed_;
|
||||
|
||||
private:
|
||||
void register_api();
|
||||
lua_State* L_ = nullptr;
|
||||
const Wak& wak_;
|
||||
GameRandom& rng_;
|
||||
std::vector<HostEvent> events_;
|
||||
std::map<std::string, int> missing_;
|
||||
std::set<std::string> loaded_once_;
|
||||
std::vector<ActionRow> actions_;
|
||||
size_t actions_len_ = (size_t)-1;
|
||||
bool tried_actions_ = false;
|
||||
std::map<std::string, std::string> texts_;
|
||||
bool texts_loaded_ = false;
|
||||
};
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "lua/mod_loader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "core/xml.h"
|
||||
|
||||
namespace ng {
|
||||
|
||||
ModLoader::ModLoader(const Wak& wak, uint32_t world_seed) : wak_(wak) {
|
||||
rng_.set_world_seed(world_seed);
|
||||
host_ = std::make_unique<LuaHost>(wak, rng_);
|
||||
}
|
||||
|
||||
const std::vector<std::string>& ModLoader::discover(const std::filesystem::path& game_dir, const std::filesystem::path& mod_config) {
|
||||
mods_.clear();
|
||||
if (std::getenv("NG_NOMODS")) return mods_;
|
||||
std::error_code ec;
|
||||
auto present = [&](const std::string& id) { return std::filesystem::is_regular_file(game_dir / "mods" / id / "init.lua", ec); };
|
||||
if (const char* list = std::getenv("NG_MODS")) {
|
||||
std::stringstream ss(list);
|
||||
std::string id;
|
||||
while (std::getline(ss, id, ',')) if (!id.empty() && present(id)) mods_.push_back(id);
|
||||
} else { // <Mods><Mod enabled="1" name="id" workshop_item_id="0" .../></Mods>: the enabled local mods, in the listed order
|
||||
std::ifstream in(mod_config, std::ios::binary);
|
||||
if (in) {
|
||||
const std::string text((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
if (auto root = parse_xml(text))
|
||||
for (const auto& m : root->children) {
|
||||
if (m->name != "Mod" || m->attr_or("enabled", "0") != "1") continue;
|
||||
const std::string id = m->attr_or("name");
|
||||
if (!id.empty() && present(id)) mods_.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
LuaHost::active_mods() = mods_;
|
||||
return mods_;
|
||||
}
|
||||
|
||||
void ModLoader::run_in(const std::string& env, const std::string& file) {
|
||||
const std::string saved = host_->env;
|
||||
host_->env = env;
|
||||
std::string err;
|
||||
if (!host_->run_file(file, &err)) { errors_.push_back(file + ": " + err); std::fprintf(stderr, "mod init: %s: %s\n", file.c_str(), err.c_str()); }
|
||||
host_->env = saved;
|
||||
}
|
||||
|
||||
void ModLoader::run_init() {
|
||||
run_in("init:game", "data/scripts/init.lua");
|
||||
for (const std::string& id : mods_) {
|
||||
const std::string settings = "mods/" + id + "/settings.lua"; // the defaults of the mod's settings (ModSettingsUpdate in the init scope)
|
||||
if (wak_.exists(settings)) {
|
||||
run_in("settings:" + id, settings);
|
||||
const std::string saved = host_->env;
|
||||
host_->env = "settings:" + id;
|
||||
std::string err;
|
||||
host_->current_mod = id;
|
||||
if (!host_->call_function("ModSettingsUpdate", {LVal::number(0)}, &err) && err.find("not defined") == std::string::npos) std::fprintf(stderr, "mod settings %s: %s\n", id.c_str(), err.c_str());
|
||||
host_->env = saved;
|
||||
}
|
||||
for (auto& [k, v] : LuaHost::mod_settings_next()) LuaHost::mod_settings()[k] = v; // (a new run applies the next values)
|
||||
host_->current_mod = id;
|
||||
run_in("init:" + id, "mods/" + id + "/init.lua");
|
||||
}
|
||||
host_->current_mod.clear();
|
||||
call("OnModPreInit");
|
||||
call("OnModInit");
|
||||
call("OnModPostInit");
|
||||
}
|
||||
|
||||
void ModLoader::call(const char* fn, const std::vector<LVal>& args, bool game) {
|
||||
std::vector<std::string> envs;
|
||||
if (game) envs.push_back("init:game");
|
||||
for (const std::string& id : mods_) envs.push_back("init:" + id);
|
||||
const std::string saved = host_->env, saved_mod = host_->current_mod;
|
||||
for (const std::string& env : envs) {
|
||||
host_->env = env;
|
||||
host_->current_mod = env.substr(5);
|
||||
std::string err;
|
||||
if (host_->call_function(fn, args, &err) || err.find("not defined") != std::string::npos) continue;
|
||||
const std::string key = env + "|" + fn;
|
||||
if (std::find(reported_.begin(), reported_.end(), key) == reported_.end()) { reported_.push_back(key); std::fprintf(stderr, "%s %s: %s\n", env.c_str(), fn, err.c_str()); errors_.push_back(env + " " + fn + ": " + err); }
|
||||
}
|
||||
host_->env = saved;
|
||||
host_->current_mod = saved_mod;
|
||||
}
|
||||
|
||||
} // namespace ng
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
// The init VM of the engine: data/scripts/init.lua (the game's own: the weather, the game over on death) and the init.lua of every enabled mod,
|
||||
// each in a context of its own (an environment table of one Lua state, as the engine keeps one per mod). The host calls the callbacks of the
|
||||
// mod API at their moments: OnModPreInit, OnModInit, OnModPostInit before the materials and the world exist, OnMagicNumbersAndWorldSeedInitialized,
|
||||
// OnBiomeConfigLoaded, OnWorldInitialized, OnPlayerSpawned, OnWorldPreUpdate / OnWorldPostUpdate every frame, OnPlayerDied, OnPausedChanged.
|
||||
// The enabled mods are the local ones (<game>/mods/<id>) listed enabled in the game's mod_config.xml (read only), or NG_MODS=a,b; NG_NOMODS=1: none.
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/random.h"
|
||||
#include "lua/lua_host.h"
|
||||
|
||||
namespace ng {
|
||||
|
||||
class ModLoader {
|
||||
public:
|
||||
ModLoader(const Wak& wak, uint32_t world_seed);
|
||||
// the mods to load: the game folder (data.wak's folder) and the profile's mod_config.xml; returns their ids in load order
|
||||
const std::vector<std::string>& discover(const std::filesystem::path& game_dir, const std::filesystem::path& mod_config);
|
||||
// runs the init files (the game's, then the mods') and OnModPreInit / OnModInit / OnModPostInit; the mods' settings.lua give their defaults
|
||||
void run_init();
|
||||
// a callback in every context that defines it (game: also in the game's init.lua); errors go to stderr once per callback and context
|
||||
void call(const char* fn, const std::vector<LVal>& args = {}, bool game = true);
|
||||
LuaHost& host() { return *host_; }
|
||||
const std::vector<std::string>& mods() const { return mods_; }
|
||||
const std::vector<std::string>& errors() const { return errors_; }
|
||||
|
||||
private:
|
||||
void run_in(const std::string& env, const std::string& file);
|
||||
const Wak& wak_;
|
||||
GameRandom rng_;
|
||||
std::unique_ptr<LuaHost> host_;
|
||||
std::vector<std::string> mods_;
|
||||
std::vector<std::string> errors_;
|
||||
std::vector<std::string> reported_;
|
||||
};
|
||||
|
||||
} // namespace ng
|
||||
Reference in new issue
Block a user