This commit is contained in:
Gaël committed 2026-10-19 20:59:15 +02:00
commit 87bff164f1
416 files changed
+279512

No files matched your search

+406
View File
@@ -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",
+97
View File
@@ -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
+514
View File
@@ -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
+456
View File
@@ -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;
}
}
+34
View File
@@ -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);
}
+24
View File
@@ -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;
}
+48
View File
@@ -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));}
+35
View File
@@ -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
+25
View File
@@ -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";
}
+17
View File
@@ -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;
}
}
+373
View File
@@ -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
+791
View File
@@ -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
+194
View File
@@ -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
+91
View File
@@ -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
+41
View File
@@ -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