Skip to content

feat(nwscript): add 14 missing actions for full K1CP compatibility#95

Open
gnutix wants to merge 13 commits intoKobaltBlu:masterfrom
gnutix:feat/k1cp-missing-nwscript-functions
Open

feat(nwscript): add 14 missing actions for full K1CP compatibility#95
gnutix wants to merge 13 commits intoKobaltBlu:masterfrom
gnutix:feat/k1cp-missing-nwscript-functions

Conversation

@gnutix
Copy link
Copy Markdown

@gnutix gnutix commented Apr 10, 2026

Hello there!

I'm a chronic kotOR nostalgic (and a web/PHP developer with almost 20 years of experience), and I've often wondered how I could contribute to amazing open source projects around this game, but they are mostly in programming languages I know nothing about, or require extensive modder/games internals knowledge. I only did some mapping/scripting in JK2/JKA, but that was a loooong time ago, so... anyway! :'D

Today I found your project, noticed it was active, and was curious to learn more about it. So I looked around the repo/history/PRs, ran the online playground, and noticed the character creation menu only allowed "the quick path" (no way to define skills, traits and such). Knowing this project was in JS, which felt less intimidating, I thought maybe that was my opportunity to contribute. So I fired up Claude to investigate these menus actions. It quickly got a bit complicated, so I set it aside. But then I got curious if the project was fully compatible with K1CP or not.

As KotOR.js already loads K1CP's modified files automatically when pointed at a patched game directory (Override files, modified .mod archives, patched dialog.tlk, recompiled .ncs scripts all load through the existing resource system), the only missing piece was to make sure all functions used in K1CP's files were implemented in KotOR.js.

Here's how Claude found these 14 missing functions, and then proceeded to implement them :

  1. Cloned K1CP and extracted all 824 .nss source files.
  2. Parsed every script to extract engine function calls (filtering out user-defined cp_*, GN_*, UT_* functions).
  3. Cross-referenced with all 772 engine function definitions in NWScriptDefK1.ts.

I thought this was probably a good candidate for a first PR on such a project ; small, isolated scope. So here it is!

I've re-read every commit attentively, and challenged Claude on every possible topic I could think of (code style convention, type conversion, function signature mismatches, etc), so the quality is "as good as I can get it" with the knowledge I have. This might not look like it, but this PR took me the whole afternoon. 😅

Important disclaimer: I've not tested any of these changes, as I wouldn't know how. It would require saves for a dozen specific game points, which I don't have. And even with those, most of these won't have a visible/lasting effect anyway, or wouldn't crash the game. So they seemed safe enough for me to at least open a PR about them. Below you'll find some patches for more changes that I wasn't so sure were safe.

Each function is a separate commit for easy cherry-picking, in case you like some but not all of them.

Implemented Functions

# Function Used by Implementation approach
1 GetObjectType 3 scripts Maps internal ModuleObjectType bitmask → NWModuleObjectType enum
2 SendMessageToPC 1 script Logs to console (single-player equivalent)
3 SetItemNonEquippable 1 script Sets existing ModuleItem.nonEquippable property
4 SetLockHeadFollowInDialog 6 scripts Toggles existing ModuleCreature.headTrackingEnabled (mirrors SetLockOrientationInDialog)
5 GiveItem 6 scripts Instant item transfer, reuses ActionGiveItem logic without action queue
6+7 GetInventoryDisturbType + GetInventoryDisturbItem 1 script Accessors reading from new properties on ModuleObject (see limitations)
8 GetModuleItemAcquired 1 script Accessor reading from new property on Module (see limitations)
9 SetAvailableNPCId 1 script Sets moduleObject on PartyManager.NPCS slot; also fixes arg signature (stub had empty args: [] but function takes [int, object])
10 ActionMoveAwayFromObject 5 scripts Calculates opposite-direction point, queues ActionMoveToPoint
11 ActionCastFakeSpellAtObject 7 scripts Queues ActionCastSpell with cheat mode (animations only, no spell effects)
12 EffectTrueSeeing 1 script New EffectTrueSeeing class + factory registration
13 SurrenderToEnemies 5 scripts 10m radius scan, clear actions/effects, set faction rep to neutral
14 SurrenderRetainBuffs 1 script Same as above but preserves self-applied effects

Known Limitations

Event wiring not yet connected for some actions

GetInventoryDisturbType, GetInventoryDisturbItem, and GetModuleItemAcquired have working accessor implementations, but the properties they read from are never populated by the current event system. This means they will return default values (0 / undefined) at runtime.

Why these are still worth including: The accessor plumbing is correct and follows the patterns of other event-based getters (e.g., GetLastOpenedBy, GetEnteringObject). The event wiring is a separate concern that touches high-traffic code paths (ModuleObject.addItem(), InventoryManager.addItem()), so I'd prefer to discuss the approach rather than include it silently.

Proposed patch: wire up OnInventoryDisturbed in ModuleObject.addItem()

The K1CP script k_pkor_therangen.nss (Korriban Therangen Obelisk) checks for INVENTORY_DISTURB_TYPE_ADDED when a grenade is placed into the obelisk. This requires firing the event from addItem():

--- a/src/module/ModuleObject.ts
+++ b/src/module/ModuleObject.ts
@@ -1717,6 +1717,16 @@
   addItem(item: ModuleItem){
     item.load();
 
+    // Fire OnInventoryDisturbed event (INVENTORY_DISTURB_TYPE_ADDED = 0)
+    this.lastInventoryDisturbType = 0;
+    this.lastInventoryDisturbItem = item;
+    if(BitWise.InstanceOfObject(this, ModuleObjectType.ModulePlaceable)){
+      const instance = (this as any).scripts?.[ModuleObjectScript.PlaceableOnInvDisturbed];
+      if(instance){
+        instance.run(this);
+      }
+    }
+
     const eItem = this.getItemByTag(item.getTag());

And set the properties during the existing retrieveInventory() flow:

--- a/src/module/ModulePlaceable.ts
+++ b/src/module/ModulePlaceable.ts
@@ -405,6 +405,8 @@
   retrieveInventory(){
     while(this.inventory.length){
       const item = this.inventory.pop();
+      this.lastInventoryDisturbType = 1; // INVENTORY_DISTURB_TYPE_REMOVED
+      this.lastInventoryDisturbItem = item;
       GameState.InventoryManager.addItem(item);
     }

Concern: addItem() is called from many places (NWScript actions, store purchases, save game loading, creature inventory init). The original engine likely only fires OnInventoryDisturbed for player-initiated UI actions, not programmatic transfers. This may need a guard or a silent parameter to avoid unintended script triggers during loading.

Proposed patch: wire up Mod_OnAcquirItem in InventoryManager

The K1CP script k_pdan_14b_itmaq.nss (Dantooine diary) expects the module-level Mod_OnAcquirItem event to fire when the player picks up an item:

--- a/src/managers/InventoryManager.ts
+++ b/src/managers/InventoryManager.ts
@@ -143,6 +143,14 @@
         InventoryManager.inventory.push(item);
+
+        // Fire module OnAcquireItem event
+        if(GameState.module){
+          GameState.module.lastItemAcquired = item;
+          const script = GameState.module.scripts?.[ModuleObjectScript.ModuleOnPlayerAcquireItem];
+          if(script){
+            script.run(GameState.module.area, 0);
+          }
+        }
+
         return item;

Concern: Same issue — InventoryManager.addItem() is called during save game deserialization, store purchases, and scripted item creation, not just player pickup. The event should probably only fire from specific call sites (retrieveInventory, ActionGiveItem, ActionTakeItem), not globally.

I'd appreciate guidance on the right approach to implement and test these event-wiring changes, I case you want me to include them.
Let me know if there's anything else I can do.

gnutix added 13 commits April 10, 2026 16:05
Returns the NWScript OBJECT_TYPE_* constant for a given object by
mapping from the internal ModuleObjectType bitmask to the
NWModuleObjectType enum values the scripts expect.

Used by 3 K1 Community Patch scripts (k_pman_steam01, k_ptar_spawnkand,
k_ptar_spwnkand2) to check if objects are creatures before applying
effects or destroying them.
Logs the message to the console. In the original engine this displayed
a server message to the player; in KotOR.js single-player context,
console logging is the appropriate equivalent.

Used by K1CP's cp_inc_debug.nss for debug output.
Sets the nonEquippable flag on a ModuleItem, which is already read from
and written to GFF templates (ModuleItem.ts lines 699-700, 870).

Used by K1CP's cp_inc_tat.nss to re-enable equipping of items.
When nValue is TRUE, head tracking is disabled (locked) so the
creature's head stays fixed during dialog instead of following the
speaker. Uses the existing headTrackingEnabled property on
ModuleCreature which controls the updateHeadTracking() behavior.

Mirrors the pattern of the existing SetLockOrientationInDialog (action
505) which locks body orientation in dialogs.

Used by 6 K1CP scripts for Leviathan, Star Forge, and Unknown World
dialog cutscenes.
Instant item transfer between objects, mirroring the logic from the
existing ActionGiveItem action class. Items given to party members are
routed through InventoryManager (shared party inventory); items given
to non-party objects use addItem directly.

Used by 6 K1CP scripts across Taris, Kashyyyk, Leviathan, Tatooine,
and Unknown World for inventory transfers during quest scripts.
…turbItem

Adds accessor implementations that read from new properties on
ModuleObject (lastInventoryDisturbType, lastInventoryDisturbItem).
These properties should be set by the inventory system when firing the
OnInventoryDisturbed event, which is defined but not yet fully wired.

Used by K1CP's k_pkor_therangen.nss (Korriban therma grenade trap).
Returns the last item acquired from the module's lastItemAcquired
property. This property should be set by the item acquisition system
before firing the OnItemAcquired module event.

Used by K1CP's k_pdan_14b_itmaq.nss (Dantooine diary item detection).
Also fixes the function signature: the stub had empty args but the
original engine expects (nNPC: int, oCreature: object). Sets the
moduleObject reference on the PartyManager NPC slot so the party
system uses the specified creature instance.

Used by K1CP's k_pdan_13_area.nss to assign Carth and Bastila's
in-world object IDs to their party slots on Dantooine.
Calculates a target point that is fMoveAwayRange meters from
oFleeFrom, along the direction from oFleeFrom through the caller,
then queues an ActionMoveToPoint to walk/run there. If the caller is
already at least fMoveAwayRange away, the action is a no-op.

No other open-source KotOR/NWN engine reimplementation (reone, xoreos)
has this function implemented, so behavior is derived from the
NWScript spec: "the distance we wish the action subject to put between
themselves and oFleeFrom."

Used by 5 K1CP scripts: Taris NPC movement (cp_tar04_gendjmp,
cp_tar04_rukiljp), generic AI flee behavior (k_inc_generic), Ebon
Hawk (k_pebo_ud), and Taris vulkar cow (k_ptar_vulcow_ud).
Queues an ActionCastSpell with cheat mode enabled so the conjure/cast
animations and visuals play without requiring the caster to actually
know the spell. The actual spell effects are not applied by this
action — K1CP scripts apply effects separately via ApplyEffectToObject.

Uses the same ActionCastSpell infrastructure as ActionCastSpellAtObject
(action 48), matching the parameters used by that implementation.

Used by 7 K1CP scripts for cutscene force power visuals on Endar
Spire, Korriban, Kashyyyk, Leviathan, and Unknown World.
Adds the EffectTrueSeeing effect class following the same pattern as
EffectInvisibility and other simple effect types. Registers it in
GameEffectFactory (both as a static member and in the EffectFromStruct
switch) and exports it from the effects index.

The effect grants true seeing, allowing the affected creature to see
through stealth/invisibility. The game effect type EffectTrueseeing
(0x48) was already defined in the enums.

Used by K1CP's cp_lev_awarefix.nss to fix an awareness issue on the
Leviathan.
Iterates all creatures within a 10-metre radius of the caller NPC,
clears their actions and effects, and sets their faction reputation
towards the caller to neutral (50). Skips if the caller is a PC, per
the original engine spec.

Uses the existing FactionManager.SetFactionReputation and the
creature.isHostile/clearAllActions/clearAllEffects methods.

Used by 5 K1CP scripts on Taris (Calo/Vulkar) and Kashyyyk
(Chuundar/Freyyr/Xor) to end combat for dialog transitions.
Same as SurrenderToEnemies (action 476) but does NOT clear effects on
affected creatures, preserving self-applied buffs. This distinction is
per the original engine spec.

Used by K1CP's k_psta_ud_bastil.nss for a Bastila dialog transition
on the Star Forge where she should retain her force powers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant