VoxCaster: every on-demand Darktide voice line on the com wheel or a keybind
- 98 commands across com wheel, item, enemy, ability and blitz groups - shared command table drives wheel dropdowns, keybinds and localization - tools/check_upstream.py validates triggers against the game source dump - weekly GitHub Action opens an issue when triggers change upstream
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
name: Check Darktide source for VO changes
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 9 * * 1" # every Monday 09:00 UTC
|
||||||
|
workflow_dispatch: # "Run workflow" button in the Actions tab
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Compare mod commands against game source
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
python3 tools/check_upstream.py --json report.json | tee report.txt
|
||||||
|
echo "changed=$?" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Open / update issue if something changed
|
||||||
|
if: steps.check.outputs.changed == '1'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TITLE="VO triggers changed in Darktide source"
|
||||||
|
BODY="$(printf '%s\n\n```\n%s\n```\n\nRun `python3 tools/check_upstream.py` locally, add the new triggers to `VoxCaster_commands.lua` + `_localization.lua`, and re-run until it says *Up to date*.' "Automated check on $(date -u +%F)." "$(cat report.txt)")"
|
||||||
|
EXISTING=$(gh issue list --state open --search "$TITLE in:title" --json number --jq '.[0].number')
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
gh issue comment "$EXISTING" --body "$BODY"
|
||||||
|
else
|
||||||
|
gh issue create --title "$TITLE" --body "$BODY" --label "upstream"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
report.json
|
||||||
|
report.txt
|
||||||
|
__pycache__/
|
||||||
|
*.zip
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# VoxCaster
|
||||||
|
|
||||||
|
A [Darktide](https://www.playdarktide.com/) mod (requires the
|
||||||
|
[Darktide Mod Framework](https://www.nexusmods.com/warhammer40kdarktide/mods/19))
|
||||||
|
that lets you put **any** of your character's on-demand voice lines on the
|
||||||
|
communication wheel or on a keybind.
|
||||||
|
|
||||||
|
98 lines, in five groups:
|
||||||
|
|
||||||
|
| Group | What | Examples |
|
||||||
|
|---|---|---|
|
||||||
|
| Com wheel | The vanilla wheel lines + the two the wheel never shows | Yes, No, Thanks, *My Pleasure*, *Take This* |
|
||||||
|
| Item | Every item-tag callout | Ammo, Medipack, Stimms, Grimoire, Vacuum Capsule |
|
||||||
|
| Enemy | Every enemy-tag callout | Scab Gunner, Trapper, Crusher, Daemonhost (sleeping/awake) |
|
||||||
|
| Ability | Class ability lines | **Hive Scum - Missile Launcher**, Voice of Command, Bull Rush, Chorus |
|
||||||
|
| Blitz | Grenade / throw lines | Krak, Big Rock, Grenade Box, Tox, Flash |
|
||||||
|
|
||||||
|
Com wheel, item and enemy lines work on every character. **Ability and blitz
|
||||||
|
lines only play on the class/personality that owns them** (the game's dialogue
|
||||||
|
rules check your voice profile), and some require enemies nearby.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Copy the `VoxCaster/` folder into `Warhammer 40,000 DARKTIDE/mods/` and add
|
||||||
|
`VoxCaster` to `mod_load_order.txt`.
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
Mod Options → VoxCaster:
|
||||||
|
|
||||||
|
- Eight dropdowns, one per wheel slot. Anything you pick replaces the vanilla
|
||||||
|
entry in that slot.
|
||||||
|
- Five keybind groups. Bind any line to any key.
|
||||||
|
|
||||||
|
## Keeping it up to date
|
||||||
|
|
||||||
|
Fatshark adds classes and lines over time. This repo tracks the
|
||||||
|
[decompiled game source](https://github.com/Aussiemon/Darktide-Source-Code) so
|
||||||
|
new lines can be added the moment they show up there.
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 tools/check_upstream.py
|
||||||
|
```
|
||||||
|
|
||||||
|
verifies every trigger in the mod still exists in the game and lists any new
|
||||||
|
ones the mod doesn't expose yet. A GitHub Action runs it every Monday and opens
|
||||||
|
an issue when something changes. To add a line:
|
||||||
|
|
||||||
|
1. Add one entry to `VoxCaster_commands.lua` (`com`, `item`, `enemy`,
|
||||||
|
`ability` or `blitz`) with the next free id.
|
||||||
|
2. Add the matching `option_<key>` string to `VoxCaster_localization.lua`.
|
||||||
|
3. Re-run the checker until it says `Up to date.`
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Each command is a `(concept, trigger)` pair fed to the same `Vo.*` call the
|
||||||
|
game itself uses:
|
||||||
|
|
||||||
|
| Group | Call |
|
||||||
|
|---|---|
|
||||||
|
| Com wheel / item / enemy | `Vo.on_demand_vo_event(unit, concept, trigger)` |
|
||||||
|
| Ability | `Vo.play_combat_ability_event(unit, vo_tag)` |
|
||||||
|
| Blitz | `Vo.throwing_item_event(unit, projectile_template)` |
|
||||||
|
|
||||||
|
Enemy callouts pass the breed name as the tag with no target unit, which the
|
||||||
|
game accepts. `Vo.on_demand_vo_event` is hooked so ability/blitz entries on the
|
||||||
|
wheel are routed to the right call.
|
||||||
|
|
||||||
|
Originally based on the "For the Vacuum Capsule" mod.
|
||||||
|
|
||||||
|
## Rename
|
||||||
|
|
||||||
|
`tools/rename.sh NewName` renames the folder, files and every reference.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
return {
|
||||||
|
run = function()
|
||||||
|
fassert(rawget(_G, "new_mod"), "`VoxCaster` encountered an error loading the Darktide Mod Framework.")
|
||||||
|
|
||||||
|
new_mod("VoxCaster", {
|
||||||
|
mod_script = "VoxCaster/scripts/mods/VoxCaster/VoxCaster",
|
||||||
|
mod_data = "VoxCaster/scripts/mods/VoxCaster/VoxCaster_data",
|
||||||
|
mod_localization = "VoxCaster/scripts/mods/VoxCaster/VoxCaster_localization",
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
packages = {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
local mod = get_mod("VoxCaster")
|
||||||
|
|
||||||
|
local Vo = require("scripts/utilities/vo")
|
||||||
|
|
||||||
|
local commands = mod.commands
|
||||||
|
or mod:io_dofile("VoxCaster/scripts/mods/VoxCaster/VoxCaster_commands")
|
||||||
|
local commands_by_id = mod.commands_by_id
|
||||||
|
|
||||||
|
local HudElementSmartTagging_instance
|
||||||
|
|
||||||
|
-- Wheel slot indices used by HudElementSmartTagging:_populate_wheel
|
||||||
|
local WHEEL_SLOTS = {
|
||||||
|
{ setting = "plugin_wheel_bottom", index = 1 },
|
||||||
|
{ setting = "plugin_wheel_bottom_right", index = 2 },
|
||||||
|
{ setting = "plugin_wheel_right", index = 3 },
|
||||||
|
{ setting = "plugin_wheel_top_right", index = 4 },
|
||||||
|
{ setting = "plugin_wheel_top", index = 5 },
|
||||||
|
{ setting = "plugin_wheel_top_left", index = 6 },
|
||||||
|
{ setting = "plugin_wheel_left", index = 7 },
|
||||||
|
{ setting = "plugin_wheel_bottom_left", index = 8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Localization for wheel labels.
|
||||||
|
-- The wheel looks display names up through LocalizationManager, so we answer
|
||||||
|
-- our own keys directly instead of stuffing the cache on every call.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local wheel_loc = {}
|
||||||
|
for _, cmd in ipairs(commands) do
|
||||||
|
wheel_loc["loc_ftvc_" .. cmd.key] = cmd.text
|
||||||
|
end
|
||||||
|
|
||||||
|
mod:hook(LocalizationManager, "localize", function(func, self, key, no_cache, context)
|
||||||
|
local custom = wheel_loc[key]
|
||||||
|
if custom then
|
||||||
|
return custom
|
||||||
|
end
|
||||||
|
return func(self, key, no_cache, context)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Build the option table the smart-tag wheel expects for a command.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local function wheel_option(cmd)
|
||||||
|
return {
|
||||||
|
icon = cmd.icon,
|
||||||
|
display_name = "loc_ftvc_" .. cmd.key,
|
||||||
|
voice_event_data = {
|
||||||
|
voice_tag_concept = cmd.concept,
|
||||||
|
voice_tag_id = cmd.trigger,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local base_wheel_options
|
||||||
|
|
||||||
|
mod:hook("HudElementSmartTagging", "_populate_wheel", function(func, self, options)
|
||||||
|
-- First call comes from the game's init with the vanilla wheel. Keep a copy so we
|
||||||
|
-- can rebuild from a clean slate when the user changes a dropdown mid-session.
|
||||||
|
if not base_wheel_options then
|
||||||
|
base_wheel_options = table.clone(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, slot in ipairs(WHEEL_SLOTS) do
|
||||||
|
local id = mod:get(slot.setting) or 0
|
||||||
|
local cmd = id > 0 and commands_by_id[id]
|
||||||
|
if cmd then
|
||||||
|
options[slot.index] = wheel_option(cmd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
func(self, options)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- VO dispatch. Ability and blitz lines don't go through the on-demand concepts,
|
||||||
|
-- so route them to the same Vo calls the game itself uses for them.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
local function fire_vo(player_unit, concept, trigger)
|
||||||
|
if concept == "combat_ability" then
|
||||||
|
Vo.play_combat_ability_event(player_unit, trigger)
|
||||||
|
elseif concept == "throwing_item" then
|
||||||
|
Vo.throwing_item_event(player_unit, trigger)
|
||||||
|
else
|
||||||
|
-- For on_demand_vo_tag_enemy, target_unit is nil, so the game falls back to
|
||||||
|
-- event_data.enemy_tag = trigger (the breed name). That's exactly what we want.
|
||||||
|
Vo.on_demand_vo_event(player_unit, concept, trigger)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The com wheel calls Vo.on_demand_vo_event with whatever we put in voice_event_data,
|
||||||
|
-- so intercept our two non-on-demand concepts there too.
|
||||||
|
mod:hook(Vo, "on_demand_vo_event", function(func, unit, concept, trigger_id, target_unit)
|
||||||
|
if concept == "combat_ability" or concept == "throwing_item" then
|
||||||
|
return fire_vo(unit, concept, trigger_id)
|
||||||
|
end
|
||||||
|
return func(unit, concept, trigger_id, target_unit)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local function play_command(cmd)
|
||||||
|
if not HudElementSmartTagging_instance then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local parent = HudElementSmartTagging_instance._parent
|
||||||
|
local player_unit = parent and parent:player_unit()
|
||||||
|
|
||||||
|
if player_unit then
|
||||||
|
fire_vo(player_unit, cmd.concept, cmd.trigger)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register keybind_<id> for every command. These exist from load so the
|
||||||
|
-- keybind widgets always resolve; they become live once the HUD element is up.
|
||||||
|
for _, cmd in ipairs(commands) do
|
||||||
|
mod["keybind_" .. cmd.id] = function()
|
||||||
|
play_command(cmd)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mod:hook_safe("HudElementSmartTagging", "update", function(self)
|
||||||
|
if not HudElementSmartTagging_instance then
|
||||||
|
HudElementSmartTagging_instance = self
|
||||||
|
end
|
||||||
|
|
||||||
|
if mod.setting_dirty then
|
||||||
|
if base_wheel_options then
|
||||||
|
self:_populate_wheel(table.clone(base_wheel_options))
|
||||||
|
end
|
||||||
|
mod.setting_dirty = false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
mod.on_setting_changed = function(setting_id)
|
||||||
|
if string.find(setting_id, "^plugin_wheel_") then
|
||||||
|
mod.setting_dirty = true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
-- Shared command list. Loaded by both the main script and the _data file.
|
||||||
|
-- IDs 1-27 are the original mod's IDs and must not change (they are what
|
||||||
|
-- gets stored in the user's dropdown settings). New entries are appended.
|
||||||
|
local mod = get_mod("VoxCaster")
|
||||||
|
|
||||||
|
local VOQueryConstants = require("scripts/settings/dialogue/vo_query_constants")
|
||||||
|
local concepts = VOQueryConstants.concepts
|
||||||
|
local trigger_ids = VOQueryConstants.trigger_ids
|
||||||
|
|
||||||
|
local ICON_WHEEL = "content/ui/materials/hud/communication_wheel/icons/"
|
||||||
|
local ICON_ITEM = "content/ui/materials/hud/interactions/icons/"
|
||||||
|
local ICON_POCKET = "content/ui/materials/icons/pocketables/hud/"
|
||||||
|
|
||||||
|
-- Small helpers so each entry is one line.
|
||||||
|
-- group: "wheel" | "item" | "enemy" (used for dropdown label prefix)
|
||||||
|
local function com(id, key, text, trigger, icon)
|
||||||
|
return { id = id, key = key, text = text, group = "wheel", icon = icon or (ICON_WHEEL .. "attention"),
|
||||||
|
concept = concepts.on_demand_com_wheel, trigger = trigger }
|
||||||
|
end
|
||||||
|
local function item(id, key, text, trigger, icon)
|
||||||
|
return { id = id, key = key, text = text, group = "item", icon = icon or (ICON_ITEM .. "pocketable_default"),
|
||||||
|
concept = concepts.on_demand_vo_tag_item, trigger = trigger }
|
||||||
|
end
|
||||||
|
-- Enemy tag lines: the game's Vo.on_demand_vo_event() sets event_data.enemy_tag = trigger_id
|
||||||
|
-- when no target unit is given, so passing the breed name is enough to fire the callout.
|
||||||
|
local function enemy(id, key, text, breed_name)
|
||||||
|
return { id = id, key = key, text = text, group = "enemy", icon = ICON_WHEEL .. "enemy",
|
||||||
|
concept = concepts.on_demand_vo_tag_enemy, trigger = breed_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Class ability lines. Fired by the game via Vo.play_combat_ability_event(unit, vo_tag)
|
||||||
|
-- (concept "combat_ability", ability_name = vo_tag). The launcher line is the wield
|
||||||
|
-- vo_tag on the missile launcher weapon template.
|
||||||
|
local function ability(id, key, text, vo_tag, icon)
|
||||||
|
return { id = id, key = key, text = text, group = "ability", icon = icon or (ICON_WHEEL .. "attention"),
|
||||||
|
concept = "combat_ability", trigger = vo_tag }
|
||||||
|
end
|
||||||
|
-- Blitz/throw lines. Fired by Vo.throwing_item_event(unit, projectile_template_name)
|
||||||
|
-- (concept "throwing_item", item = template name).
|
||||||
|
local function blitz(id, key, text, projectile_name)
|
||||||
|
return { id = id, key = key, text = text, group = "blitz", icon = ICON_ITEM .. "pocketable_default",
|
||||||
|
concept = "throwing_item", trigger = projectile_name }
|
||||||
|
end
|
||||||
|
|
||||||
|
local commands = {
|
||||||
|
-- ===== Original IDs (unchanged) =====
|
||||||
|
com(1, "for_the_emperor", "For The Emperor!", trigger_ids.com_wheel_vo_for_the_emperor, "content/ui/materials/icons/system/escape/achievements"),
|
||||||
|
item(2, "vacuum_capsule", "Vacuum Capsule", trigger_ids.smart_tag_vo_pickup_container, ICON_ITEM .. "training_grounds"),
|
||||||
|
com(3, "yes", "Yes", trigger_ids.com_wheel_vo_yes, ICON_POCKET .. "scripture"),
|
||||||
|
com(4, "no", "No", trigger_ids.com_wheel_vo_no, "content/ui/materials/icons/system/settings/category_interface"),
|
||||||
|
item(5, "cryonic_rod", "Cryonic Rod", trigger_ids.smart_tag_vo_pickup_control_rod, ICON_ITEM .. "training_grounds"),
|
||||||
|
item(6, "power_cell", "Power Cell", trigger_ids.smart_tag_vo_pickup_battery, ICON_ITEM .. "training_grounds"),
|
||||||
|
item(7, "scripture", "Scripture", trigger_ids.smart_tag_vo_pickup_side_mission_tome, ICON_POCKET .. "scripture"),
|
||||||
|
item(8, "grimoire", "Grimoire", trigger_ids.smart_tag_vo_pickup_side_mission_grimoire, ICON_POCKET .. "grimoire"),
|
||||||
|
item(9, "relic", "Relic", trigger_ids.smart_tag_vo_pickup_side_mission_consumable, ICON_POCKET .. "grimoire"),
|
||||||
|
item(10, "health_booster", "Health Booster", trigger_ids.smart_tag_vo_pickup_health_booster or "pup_stimm_health", ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
com(11, "mine", "Mine", trigger_ids.com_wheel_vo_need_that, ICON_ITEM .. "pocketable_default"),
|
||||||
|
item(12, "ammo", "Ammo", trigger_ids.smart_tag_vo_pickup_ammo, ICON_ITEM .. "pocketable_ammo"),
|
||||||
|
item(13, "grenade", "Grenade", trigger_ids.smart_tag_vo_small_grenade),
|
||||||
|
item(14, "plasteel", "Plasteel", trigger_ids.smart_tag_vo_pickup_forge_metal),
|
||||||
|
item(15, "diamantine", "Diamantine", trigger_ids.smart_tag_vo_pickup_platinum),
|
||||||
|
com(16, "following", "Following", trigger_ids.com_wheel_vo_follow_you, ICON_POCKET .. "scripture"),
|
||||||
|
item(17, "medicae_station", "Medicae Station", trigger_ids.smart_tag_vo_station_health, ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
item(18, "uncharged_medicae", "Uncharged Medicae", trigger_ids.smart_tag_vo_station_health_without_battery, ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
item(19, "medipack", "Medipack", trigger_ids.smart_tag_vo_pickup_medical_crate, ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
-- NOTE: the constant for this one points at a generic_mission_vo tag; the on-demand rule wants this literal.
|
||||||
|
item(20, "medipack_down", "Medipack Down", "pup_deployed_medical_crate", ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
item(21, "ammo_crate", "Ammo Crate", trigger_ids.smart_tag_vo_pickup_deployed_ammo_crate, ICON_ITEM .. "pocketable_ammo"),
|
||||||
|
com(22, "need_ammo", "Need Ammo", trigger_ids.com_wheel_vo_need_ammo, ICON_WHEEL .. "ammo"),
|
||||||
|
com(23, "need_healing", "Need Healing", trigger_ids.com_wheel_vo_need_health, ICON_WHEEL .. "health"),
|
||||||
|
com(24, "look_there", "Look There", trigger_ids.com_wheel_vo_over_here, ICON_WHEEL .. "attention"),
|
||||||
|
com(25, "go_there", "Go There", trigger_ids.com_wheel_vo_lets_go_this_way, ICON_WHEEL .. "location"),
|
||||||
|
com(26, "enemy", "Enemy", trigger_ids.com_wheel_vo_enemy_over_here, ICON_WHEEL .. "enemy"),
|
||||||
|
com(27, "thanks", "Thanks", trigger_ids.com_wheel_vo_thank_you, ICON_WHEEL .. "thanks"),
|
||||||
|
|
||||||
|
-- ===== New: remaining com wheel lines =====
|
||||||
|
com(28, "my_pleasure", "My Pleasure", trigger_ids.com_wheel_vo_my_pleasure, ICON_WHEEL .. "thanks"),
|
||||||
|
com(29, "take_this", "Take This", trigger_ids.com_wheel_vo_take_this, ICON_ITEM .. "pocketable_default"),
|
||||||
|
|
||||||
|
-- ===== New: stimms =====
|
||||||
|
item(30, "stimm_health", "Stimm (Health)", trigger_ids.smart_tag_vo_pickup_stimm_health, ICON_ITEM .. "pocketable_medkit"),
|
||||||
|
item(31, "stimm_concentration", "Stimm (Concentration)", trigger_ids.smart_tag_vo_pickup_stimm_concentration),
|
||||||
|
item(32, "stimm_power", "Stimm (Power)", trigger_ids.smart_tag_vo_pickup_stimm_power),
|
||||||
|
item(33, "stimm_speed", "Stimm (Speed)", trigger_ids.smart_tag_vo_pickup_stimm_speed),
|
||||||
|
|
||||||
|
-- ===== New: enemy tag callouts (breed names from dialogues/on_demand_vo) =====
|
||||||
|
-- Scabs (Moebian 6th)
|
||||||
|
enemy(34, "scab_gunner", "Scab Gunner", "renegade_gunner"),
|
||||||
|
enemy(35, "scab_plasma", "Scab Plasma Gunner", "renegade_plasma_gunner"),
|
||||||
|
enemy(36, "scab_rager", "Scab Rager", "renegade_berzerker"),
|
||||||
|
enemy(37, "scab_mauler", "Scab Mauler", "renegade_executor"),
|
||||||
|
enemy(38, "scab_bomber", "Scab Bomber", "renegade_grenadier"),
|
||||||
|
enemy(39, "scab_flamer", "Scab Flamer", "renegade_flamer"),
|
||||||
|
enemy(40, "scab_shotgunner", "Scab Shotgunner", "renegade_shocktrooper"),
|
||||||
|
enemy(41, "scab_trapper", "Scab Trapper", "renegade_netgunner"),
|
||||||
|
enemy(42, "scab_sniper", "Scab Sniper", "renegade_sniper"),
|
||||||
|
enemy(43, "scab_captain", "Captain", "renegade_captain"),
|
||||||
|
-- Dregs (Admonition)
|
||||||
|
enemy(44, "dreg_gunner", "Dreg Gunner", "cultist_gunner"),
|
||||||
|
enemy(45, "dreg_rager", "Dreg Rager", "cultist_berzerker"),
|
||||||
|
enemy(46, "dreg_tox_bomber", "Dreg Tox Bomber", "cultist_grenadier"),
|
||||||
|
enemy(47, "dreg_tox_flamer", "Dreg Tox Flamer", "cultist_flamer"),
|
||||||
|
enemy(48, "dreg_shotgunner", "Dreg Shotgunner", "cultist_shocktrooper"),
|
||||||
|
enemy(49, "mutant", "Mutant", "cultist_mutant"),
|
||||||
|
-- Chaos / ogryns / monsters
|
||||||
|
enemy(50, "pox_hound", "Pox Hound", "chaos_hound"),
|
||||||
|
enemy(51, "pox_burster", "Pox Burster", "chaos_poxwalker_bomber"),
|
||||||
|
enemy(52, "bulwark", "Bulwark", "chaos_ogryn_bulwark"),
|
||||||
|
enemy(53, "crusher", "Crusher", "chaos_ogryn_executor"),
|
||||||
|
enemy(54, "reaper", "Reaper", "chaos_ogryn_gunner"),
|
||||||
|
enemy(55, "plague_ogryn", "Plague Ogryn", "chaos_plague_ogryn"),
|
||||||
|
enemy(56, "chaos_spawn", "Chaos Spawn", "chaos_spawn"),
|
||||||
|
enemy(57, "daemonhost", "Daemonhost (sleeping)", "chaos_daemonhost"),
|
||||||
|
enemy(58, "daemonhost_awake", "Daemonhost (awake)", "aggroed"),
|
||||||
|
enemy(59, "trapper_fled", "Trapper Fled", "seen_netgunner_flee"),
|
||||||
|
enemy(60, "houndmaster", "Houndmaster", "chaos_ogryn_houndmaster"),
|
||||||
|
|
||||||
|
-- ===== New: class ability lines =====
|
||||||
|
ability(61, "hs_missile_launcher", "Hive Scum - Missile Launcher", "broker_missile_launcher", ICON_WHEEL .. "enemy"),
|
||||||
|
ability(62, "hs_focus", "Hive Scum - Focus", "ability_focus"),
|
||||||
|
ability(63, "hs_rage", "Hive Scum - Rage", "ability_rage"),
|
||||||
|
ability(64, "hs_stimm_field", "Hive Scum - Stimm Field", "ability_stimm"),
|
||||||
|
ability(65, "vet_voice_of_command", "Veteran - Voice of Command", "ability_ranger"),
|
||||||
|
ability(66, "zealot_dash", "Zealot - Chastise", "ability_maniac"),
|
||||||
|
ability(67, "zealot_stealth", "Zealot - Shroudfield", "ability_pious_stabber"),
|
||||||
|
ability(68, "zealot_relic", "Zealot - Chorus", "ability_litany"),
|
||||||
|
ability(69, "psyker_venting", "Psyker - Venting", "ability_venting"),
|
||||||
|
ability(70, "psyker_shards", "Psyker - Assail", "ability_gunslinger"),
|
||||||
|
ability(71, "psyker_stance", "Psyker - Scrier's Gaze", "ability_buff_stance"),
|
||||||
|
ability(72, "psyker_biomancer_low", "Psyker - Biomancer (low)", "ability_biomancer_low"),
|
||||||
|
ability(73, "psyker_biomancer_high", "Psyker - Biomancer (high)", "ability_biomancer_high"),
|
||||||
|
ability(74, "ogryn_charge", "Ogryn - Bull Rush", "ability_bonebreaker"),
|
||||||
|
ability(75, "ogryn_taunt", "Ogryn - Loyal Protector", "ability_bullgryn"),
|
||||||
|
ability(76, "ogryn_gunlugger", "Ogryn - Point-Blank Barrage", "ability_gun_lugger"),
|
||||||
|
ability(77, "arb_charge", "Arbitrator - Charge", "ability_charge_a"),
|
||||||
|
ability(78, "arb_shout", "Arbitrator - Shout", "ability_howl_a"),
|
||||||
|
ability(79, "arb_stance", "Arbitrator - Stance", "ability_stance_a"),
|
||||||
|
|
||||||
|
-- ===== New: blitz / throw lines =====
|
||||||
|
blitz(80, "blitz_frag", "Frag Grenade", "frag_grenade"),
|
||||||
|
blitz(81, "blitz_krak", "Krak Grenade", "krak_grenade"),
|
||||||
|
blitz(82, "blitz_smoke", "Smoke Grenade", "smoke_grenade"),
|
||||||
|
blitz(83, "blitz_shock", "Shock Grenade", "shock_grenade"),
|
||||||
|
blitz(84, "blitz_fire", "Fire Grenade", "fire_grenade"),
|
||||||
|
blitz(85, "blitz_tox", "Tox Grenade", "broker_tox_grenade"),
|
||||||
|
blitz(86, "blitz_flash", "Flash Grenade", "quick_flash_grenade"),
|
||||||
|
blitz(87, "blitz_rock", "Big Rock", "ogryn_grenade_friend_rock"),
|
||||||
|
blitz(88, "blitz_box", "Grenade Box", "ogryn_grenade_box"),
|
||||||
|
blitz(98, "blitz_box_cluster", "Grenade Box (cluster)", "ogryn_grenade_box_cluster"),
|
||||||
|
blitz(89, "blitz_ogryn_frag", "Ogryn Frag Bomb", "ogryn_grenade_frag"),
|
||||||
|
blitz(90, "blitz_arb", "Arbites Grenade", "adamant_grenade"),
|
||||||
|
blitz(91, "blitz_shock_mine", "Shock Mine", "shock_mine"),
|
||||||
|
|
||||||
|
-- class_rework database ability lines
|
||||||
|
ability(92, "vet_squad_leader", "Veteran - Squad Leader", "ability_squad_leader"),
|
||||||
|
ability(93, "vet_shock_trooper", "Veteran - Shock Trooper", "ability_shock_trooper"),
|
||||||
|
ability(94, "banisher", "Banisher", "ability_banisher"),
|
||||||
|
ability(95, "banisher_impact", "Banisher (impact)", "ability_banisher_impact"),
|
||||||
|
ability(96, "protectorate_start", "Protectorate (start)", "ability_protectorate_start"),
|
||||||
|
ability(97, "protectorate_stop", "Protectorate (stop)", "ability_protectorate_stop"),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Fast lookup by id (dropdown values) and a stable ordered list for the UI.
|
||||||
|
local by_id = {}
|
||||||
|
for _, cmd in ipairs(commands) do
|
||||||
|
by_id[cmd.id] = cmd
|
||||||
|
end
|
||||||
|
|
||||||
|
mod.commands = commands
|
||||||
|
mod.commands_by_id = by_id
|
||||||
|
|
||||||
|
return commands
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
local mod = get_mod("VoxCaster")
|
||||||
|
|
||||||
|
local commands = mod.commands
|
||||||
|
or mod:io_dofile("VoxCaster/scripts/mods/VoxCaster/VoxCaster_commands")
|
||||||
|
|
||||||
|
-- Dropdown order: com wheel lines, then item callouts, then enemy callouts.
|
||||||
|
-- Within a group, alphabetical by display text.
|
||||||
|
local GROUP_ORDER = { wheel = 1, item = 2, enemy = 3, ability = 4, blitz = 5 }
|
||||||
|
|
||||||
|
local function sorted_commands()
|
||||||
|
local list = {}
|
||||||
|
for _, cmd in ipairs(commands) do
|
||||||
|
list[#list + 1] = cmd
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b)
|
||||||
|
local ga, gb = GROUP_ORDER[a.group], GROUP_ORDER[b.group]
|
||||||
|
if ga ~= gb then
|
||||||
|
return ga < gb
|
||||||
|
end
|
||||||
|
return a.text < b.text
|
||||||
|
end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_options()
|
||||||
|
local options = {
|
||||||
|
{ text = "option_nil", value = 0 },
|
||||||
|
}
|
||||||
|
for _, cmd in ipairs(sorted_commands()) do
|
||||||
|
options[#options + 1] = { text = "option_" .. cmd.key, value = cmd.id }
|
||||||
|
end
|
||||||
|
return options
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_keybind_widgets(group)
|
||||||
|
local widgets = {}
|
||||||
|
for _, cmd in ipairs(sorted_commands()) do
|
||||||
|
if cmd.group == group then
|
||||||
|
widgets[#widgets + 1] = {
|
||||||
|
setting_id = "option_" .. cmd.key, -- same IDs as the original mod, keeps existing binds
|
||||||
|
type = "keybind",
|
||||||
|
keybind_trigger = "pressed",
|
||||||
|
keybind_type = "function_call",
|
||||||
|
function_name = "keybind_" .. cmd.id,
|
||||||
|
default_value = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return widgets
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wheel_dropdown(setting_id)
|
||||||
|
return {
|
||||||
|
setting_id = setting_id,
|
||||||
|
type = "dropdown",
|
||||||
|
default_value = 0,
|
||||||
|
options = get_options(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
name = mod:localize("mod_title"),
|
||||||
|
description = mod:localize("mod_description"),
|
||||||
|
is_togglable = true,
|
||||||
|
options = {
|
||||||
|
widgets = {
|
||||||
|
wheel_dropdown("plugin_wheel_top_left"),
|
||||||
|
wheel_dropdown("plugin_wheel_top"),
|
||||||
|
wheel_dropdown("plugin_wheel_top_right"),
|
||||||
|
wheel_dropdown("plugin_wheel_left"),
|
||||||
|
wheel_dropdown("plugin_wheel_right"),
|
||||||
|
wheel_dropdown("plugin_wheel_bottom_left"),
|
||||||
|
wheel_dropdown("plugin_wheel_bottom"),
|
||||||
|
wheel_dropdown("plugin_wheel_bottom_right"),
|
||||||
|
{
|
||||||
|
setting_id = "plugin_keybinds_wheel",
|
||||||
|
type = "group",
|
||||||
|
sub_widgets = get_keybind_widgets("wheel"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_id = "plugin_keybinds_item",
|
||||||
|
type = "group",
|
||||||
|
sub_widgets = get_keybind_widgets("item"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_id = "plugin_keybinds_enemy",
|
||||||
|
type = "group",
|
||||||
|
sub_widgets = get_keybind_widgets("enemy"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_id = "plugin_keybinds_ability",
|
||||||
|
type = "group",
|
||||||
|
sub_widgets = get_keybind_widgets("ability"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
setting_id = "plugin_keybinds_blitz",
|
||||||
|
type = "group",
|
||||||
|
sub_widgets = get_keybind_widgets("blitz"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
return {
|
||||||
|
mod_title = { en = "VoxCaster" },
|
||||||
|
mod_description = { en = "Put any voice line (com wheel, item/enemy callouts, class abilities, blitz throws) on the com wheel or a keybind." },
|
||||||
|
|
||||||
|
plugin_wheel_top = { en = "Top" },
|
||||||
|
plugin_wheel_top_right = { en = "Top Right" },
|
||||||
|
plugin_wheel_right = { en = "Right" },
|
||||||
|
plugin_wheel_bottom_right = { en = "Bottom Right" },
|
||||||
|
plugin_wheel_bottom = { en = "Bottom" },
|
||||||
|
plugin_wheel_bottom_left = { en = "Bottom Left" },
|
||||||
|
plugin_wheel_left = { en = "Left" },
|
||||||
|
plugin_wheel_top_left = { en = "Top Left" },
|
||||||
|
|
||||||
|
plugin_keybinds_wheel = { en = "Keybinds: Com Wheel" },
|
||||||
|
plugin_keybinds_item = { en = "Keybinds: Item Callouts" },
|
||||||
|
plugin_keybinds_enemy = { en = "Keybinds: Enemy Callouts" },
|
||||||
|
plugin_keybinds_ability = { en = "Keybinds: Class Abilities" },
|
||||||
|
plugin_keybinds_blitz = { en = "Keybinds: Blitz / Throws" },
|
||||||
|
|
||||||
|
option_nil = { en = "None" },
|
||||||
|
option_for_the_emperor = { en = "For The Emperor!" },
|
||||||
|
option_vacuum_capsule = { en = "[Item] Vacuum Capsule" },
|
||||||
|
option_yes = { en = "Yes" },
|
||||||
|
option_no = { en = "No" },
|
||||||
|
option_cryonic_rod = { en = "[Item] Cryonic Rod" },
|
||||||
|
option_power_cell = { en = "[Item] Power Cell" },
|
||||||
|
option_scripture = { en = "[Item] Scripture" },
|
||||||
|
option_grimoire = { en = "[Item] Grimoire" },
|
||||||
|
option_relic = { en = "[Item] Relic" },
|
||||||
|
option_health_booster = { en = "[Item] Health Booster" },
|
||||||
|
option_mine = { en = "Mine" },
|
||||||
|
option_ammo = { en = "[Item] Ammo" },
|
||||||
|
option_grenade = { en = "[Item] Grenade" },
|
||||||
|
option_plasteel = { en = "[Item] Plasteel" },
|
||||||
|
option_diamantine = { en = "[Item] Diamantine" },
|
||||||
|
option_following = { en = "Following" },
|
||||||
|
option_medicae_station = { en = "[Item] Medicae Station" },
|
||||||
|
option_uncharged_medicae = { en = "[Item] Uncharged Medicae" },
|
||||||
|
option_medipack = { en = "[Item] Medipack" },
|
||||||
|
option_medipack_down = { en = "[Item] Medipack Down" },
|
||||||
|
option_ammo_crate = { en = "[Item] Ammo Crate" },
|
||||||
|
option_need_ammo = { en = "Need Ammo" },
|
||||||
|
option_need_healing = { en = "Need Healing" },
|
||||||
|
option_look_there = { en = "Look There" },
|
||||||
|
option_go_there = { en = "Go There" },
|
||||||
|
option_enemy = { en = "Enemy" },
|
||||||
|
option_thanks = { en = "Thanks" },
|
||||||
|
option_my_pleasure = { en = "My Pleasure" },
|
||||||
|
option_take_this = { en = "Take This" },
|
||||||
|
option_stimm_health = { en = "[Item] Stimm (Health)" },
|
||||||
|
option_stimm_concentration = { en = "[Item] Stimm (Concentration)" },
|
||||||
|
option_stimm_power = { en = "[Item] Stimm (Power)" },
|
||||||
|
option_stimm_speed = { en = "[Item] Stimm (Speed)" },
|
||||||
|
option_scab_gunner = { en = "[Enemy] Scab Gunner" },
|
||||||
|
option_scab_plasma = { en = "[Enemy] Scab Plasma Gunner" },
|
||||||
|
option_scab_rager = { en = "[Enemy] Scab Rager" },
|
||||||
|
option_scab_mauler = { en = "[Enemy] Scab Mauler" },
|
||||||
|
option_scab_bomber = { en = "[Enemy] Scab Bomber" },
|
||||||
|
option_scab_flamer = { en = "[Enemy] Scab Flamer" },
|
||||||
|
option_scab_shotgunner = { en = "[Enemy] Scab Shotgunner" },
|
||||||
|
option_scab_trapper = { en = "[Enemy] Scab Trapper" },
|
||||||
|
option_scab_sniper = { en = "[Enemy] Scab Sniper" },
|
||||||
|
option_scab_captain = { en = "[Enemy] Captain" },
|
||||||
|
option_dreg_gunner = { en = "[Enemy] Dreg Gunner" },
|
||||||
|
option_dreg_rager = { en = "[Enemy] Dreg Rager" },
|
||||||
|
option_dreg_tox_bomber = { en = "[Enemy] Dreg Tox Bomber" },
|
||||||
|
option_dreg_tox_flamer = { en = "[Enemy] Dreg Tox Flamer" },
|
||||||
|
option_dreg_shotgunner = { en = "[Enemy] Dreg Shotgunner" },
|
||||||
|
option_mutant = { en = "[Enemy] Mutant" },
|
||||||
|
option_pox_hound = { en = "[Enemy] Pox Hound" },
|
||||||
|
option_pox_burster = { en = "[Enemy] Pox Burster" },
|
||||||
|
option_bulwark = { en = "[Enemy] Bulwark" },
|
||||||
|
option_crusher = { en = "[Enemy] Crusher" },
|
||||||
|
option_reaper = { en = "[Enemy] Reaper" },
|
||||||
|
option_plague_ogryn = { en = "[Enemy] Plague Ogryn" },
|
||||||
|
option_chaos_spawn = { en = "[Enemy] Chaos Spawn" },
|
||||||
|
option_daemonhost = { en = "[Enemy] Daemonhost (sleeping)" },
|
||||||
|
option_daemonhost_awake = { en = "[Enemy] Daemonhost (awake)" },
|
||||||
|
option_trapper_fled = { en = "[Enemy] Trapper Fled" },
|
||||||
|
option_houndmaster = { en = "[Enemy] Houndmaster" },
|
||||||
|
option_hs_missile_launcher = { en = "[Ability] Hive Scum - Missile Launcher" },
|
||||||
|
option_hs_focus = { en = "[Ability] Hive Scum - Focus" },
|
||||||
|
option_hs_rage = { en = "[Ability] Hive Scum - Rage" },
|
||||||
|
option_hs_stimm_field = { en = "[Ability] Hive Scum - Stimm Field" },
|
||||||
|
option_vet_voice_of_command = { en = "[Ability] Veteran - Voice of Command" },
|
||||||
|
option_zealot_dash = { en = "[Ability] Zealot - Chastise" },
|
||||||
|
option_zealot_stealth = { en = "[Ability] Zealot - Shroudfield" },
|
||||||
|
option_zealot_relic = { en = "[Ability] Zealot - Chorus" },
|
||||||
|
option_psyker_venting = { en = "[Ability] Psyker - Venting" },
|
||||||
|
option_psyker_shards = { en = "[Ability] Psyker - Assail" },
|
||||||
|
option_psyker_stance = { en = "[Ability] Psyker - Scrier's Gaze" },
|
||||||
|
option_psyker_biomancer_low = { en = "[Ability] Psyker - Biomancer (low)" },
|
||||||
|
option_psyker_biomancer_high = { en = "[Ability] Psyker - Biomancer (high)" },
|
||||||
|
option_ogryn_charge = { en = "[Ability] Ogryn - Bull Rush" },
|
||||||
|
option_ogryn_taunt = { en = "[Ability] Ogryn - Loyal Protector" },
|
||||||
|
option_ogryn_gunlugger = { en = "[Ability] Ogryn - Point-Blank Barrage" },
|
||||||
|
option_arb_charge = { en = "[Ability] Arbitrator - Charge" },
|
||||||
|
option_arb_shout = { en = "[Ability] Arbitrator - Shout" },
|
||||||
|
option_arb_stance = { en = "[Ability] Arbitrator - Stance" },
|
||||||
|
option_blitz_frag = { en = "[Blitz] Frag Grenade" },
|
||||||
|
option_blitz_krak = { en = "[Blitz] Krak Grenade" },
|
||||||
|
option_blitz_smoke = { en = "[Blitz] Smoke Grenade" },
|
||||||
|
option_blitz_shock = { en = "[Blitz] Shock Grenade" },
|
||||||
|
option_blitz_fire = { en = "[Blitz] Fire Grenade" },
|
||||||
|
option_blitz_tox = { en = "[Blitz] Tox Grenade" },
|
||||||
|
option_blitz_flash = { en = "[Blitz] Flash Grenade" },
|
||||||
|
option_blitz_rock = { en = "[Blitz] Big Rock" },
|
||||||
|
option_blitz_box = { en = "[Blitz] Grenade Box" },
|
||||||
|
option_blitz_box_cluster = { en = "[Blitz] Grenade Box (cluster)" },
|
||||||
|
option_blitz_ogryn_frag = { en = "[Blitz] Ogryn Frag Bomb" },
|
||||||
|
option_blitz_arb = { en = "[Blitz] Arbites Grenade" },
|
||||||
|
option_blitz_shock_mine = { en = "[Blitz] Shock Mine" },
|
||||||
|
option_vet_squad_leader = { en = "[Ability] Veteran - Squad Leader" },
|
||||||
|
option_vet_shock_trooper = { en = "[Ability] Veteran - Shock Trooper" },
|
||||||
|
option_banisher = { en = "[Ability] Banisher" },
|
||||||
|
option_banisher_impact = { en = "[Ability] Banisher (impact)" },
|
||||||
|
option_protectorate_start = { en = "[Ability] Protectorate (start)" },
|
||||||
|
option_protectorate_stop = { en = "[Ability] Protectorate (stop)" },
|
||||||
|
}
|
||||||
Executable
+201
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
check_upstream.py - keep VoxCaster in sync with Darktide's VO system.
|
||||||
|
|
||||||
|
Pulls the decompiled game source (Aussiemon/Darktide-Source-Code) and:
|
||||||
|
1. Verifies every trigger in VoxCaster_commands.lua still exists in the game.
|
||||||
|
2. Lists any on-demand / ability / blitz triggers the game has that the mod
|
||||||
|
doesn't expose yet.
|
||||||
|
|
||||||
|
Exit code 1 if anything is missing or new, so CI can flag it.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/check_upstream.py # report only
|
||||||
|
python3 tools/check_upstream.py --json out.json
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RAW = "https://raw.githubusercontent.com/Aussiemon/Darktide-Source-Code/master/"
|
||||||
|
API_TREE = "https://api.github.com/repos/Aussiemon/Darktide-Source-Code/git/trees/master?recursive=1"
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Used when the GitHub tree API is rate-limited. Keep this in sync with new classes.
|
||||||
|
FALLBACK_TEMPLATE_FILES = [
|
||||||
|
"scripts/settings/equipment/weapon_templates/missile_launchers/missile_launcher.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/broker_focus.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/broker_punk_rage.lua",
|
||||||
|
"scripts/settings/equipment/weapon_templates/combat_abilities/broker_stimm_field.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/adamant_charge.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/adamant_shout.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/adamant_stance.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/ogryn_charge.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/ogryn_gunlugger_stance.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/ogryn_taunt_shout.lua",
|
||||||
|
"scripts/settings/equipment/weapon_templates/grenades/psyker_chain_lightning.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/psyker_overcharge_stance.lua",
|
||||||
|
"scripts/settings/equipment/weapon_templates/grenades/psyker_smite.lua",
|
||||||
|
"scripts/settings/equipment/weapon_templates/grenades/psyker_throwing_knives.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/zealot_dash.lua",
|
||||||
|
"scripts/settings/ability/ability_templates/zealot_invisibility.lua",
|
||||||
|
"scripts/settings/equipment/weapon_templates/combat_abilities/zealot_relic.lua",
|
||||||
|
]
|
||||||
|
MOD_NAME = next(p.name for p in ROOT.iterdir() if p.is_dir() and (p / f"{p.name}.mod").exists())
|
||||||
|
COMMANDS_FILE = ROOT / MOD_NAME / "scripts" / "mods" / MOD_NAME / f"{MOD_NAME}_commands.lua"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, token=None):
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "voxcaster-sync"})
|
||||||
|
if token and "api.github.com" in url:
|
||||||
|
req.add_header("Authorization", f"Bearer {token}")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
return r.read().decode("utf-8-sig")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rules(lua):
|
||||||
|
return re.findall(r"define_rule\(\{(.*?)\n\t\}\)", lua, re.S)
|
||||||
|
|
||||||
|
|
||||||
|
def rule_values(rules, concept_wanted, field):
|
||||||
|
"""All values a query_context field takes for rules with the given concept."""
|
||||||
|
out = set()
|
||||||
|
for r in rules:
|
||||||
|
c = re.search(r'"concept",\s*OP\.EQ,\s*"([^"]+)"', r)
|
||||||
|
if not c or c.group(1) != concept_wanted:
|
||||||
|
continue
|
||||||
|
for v in re.findall(rf'"query_context",\s*"{field}",\s*OP\.EQ,\s*"([^"]+)"', r):
|
||||||
|
out.add(v)
|
||||||
|
for block in re.findall(rf'"query_context",\s*"{field}",\s*OP\.SET_INCLUDES,\s*args = \{{(.*?)\}}', r, re.S):
|
||||||
|
out.update(re.findall(r'"([^"]+)"', block))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def game_triggers(token=None):
|
||||||
|
"""Return {concept: set(trigger)} as the game currently knows them."""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# 1. On-demand rules (com wheel / item / enemy)
|
||||||
|
od = parse_rules(fetch(RAW + "dialogues/generated/on_demand_vo.lua"))
|
||||||
|
result["on_demand_com_wheel"] = rule_values(od, "on_demand_com_wheel", "trigger_id")
|
||||||
|
result["on_demand_vo_tag_item"] = rule_values(od, "on_demand_vo_tag_item", "item_tag")
|
||||||
|
result["on_demand_vo_tag_enemy"] = rule_values(od, "on_demand_vo_tag_enemy", "enemy_tag")
|
||||||
|
|
||||||
|
# 2. Ability + throw rules in the shared gameplay databases
|
||||||
|
ability, throw = set(), set()
|
||||||
|
for db in ("gameplay_vo", "class_rework"):
|
||||||
|
rules = parse_rules(fetch(RAW + f"dialogues/generated/{db}.lua"))
|
||||||
|
ability |= rule_values(rules, "combat_ability", "ability_name")
|
||||||
|
throw |= rule_values(rules, "throwing_item", "item")
|
||||||
|
|
||||||
|
# 3. vo_tags actually wired into ability / weapon templates (the reliable source
|
||||||
|
# for new class abilities - rules for them may live in per-class databases).
|
||||||
|
vo_tags = set()
|
||||||
|
paths = list(FALLBACK_TEMPLATE_FILES)
|
||||||
|
try:
|
||||||
|
tree = json.loads(fetch(API_TREE, token))["tree"]
|
||||||
|
paths = [
|
||||||
|
p["path"] for p in tree
|
||||||
|
if p["path"].endswith(".lua") and (
|
||||||
|
"ability_templates" in p["path"]
|
||||||
|
or "weapon_templates/combat_abilities" in p["path"]
|
||||||
|
or "weapon_templates/missile" in p["path"]
|
||||||
|
or "weapon_templates/grenades" in p["path"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
except Exception as e: # rate limit etc. - fall back to the known file list
|
||||||
|
print(f"[warn] tree API unavailable ({e}); using fallback template list", file=sys.stderr)
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
vo_tags |= set(re.findall(r'vo_tag\w* = "([^"]+)"', fetch(RAW + path)))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[warn] could not fetch {path}: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
# 4. Player projectile templates (valid throwing_item names)
|
||||||
|
ppt = fetch(RAW + "scripts/settings/projectile/player_projectile_templates.lua")
|
||||||
|
projectiles = set(re.findall(r"^projectile_templates\.(\w+) = \{", ppt, re.M))
|
||||||
|
|
||||||
|
# "valid" = every name the game would accept; "new" is only reported for names
|
||||||
|
# that have an actual dialogue rule or a wired-up vo_tag, to avoid noise from
|
||||||
|
# projectiles (staff balls, airstrikes...) that have no voice line.
|
||||||
|
result["combat_ability"] = ability | vo_tags
|
||||||
|
result["throwing_item"] = throw | projectiles
|
||||||
|
result["_new_candidates"] = {"combat_ability": ability | vo_tags, "throwing_item": throw}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def mod_commands():
|
||||||
|
"""Parse the mod's command list: returns list of (key, concept, trigger)."""
|
||||||
|
src = COMMANDS_FILE.read_text()
|
||||||
|
consts = {}
|
||||||
|
vq = fetch(RAW + "scripts/settings/dialogue/vo_query_constants.lua")
|
||||||
|
for k, v in re.findall(r'^\s*(\w+) = "([^"]+)"', vq, re.M):
|
||||||
|
consts[k] = v
|
||||||
|
|
||||||
|
kind_to_concept = {
|
||||||
|
"com": "on_demand_com_wheel",
|
||||||
|
"item": "on_demand_vo_tag_item",
|
||||||
|
"enemy": "on_demand_vo_tag_enemy",
|
||||||
|
"ability": "combat_ability",
|
||||||
|
"blitz": "throwing_item",
|
||||||
|
}
|
||||||
|
cmds = []
|
||||||
|
for kind, _id, key, _text, trig in re.findall(
|
||||||
|
r'^\s*(com|item|enemy|ability|blitz)\((\d+),\s*"([^"]+)",\s*"([^"]+)",\s*([^,)]+)', src, re.M
|
||||||
|
):
|
||||||
|
trig = trig.strip()
|
||||||
|
m = re.match(r'trigger_ids\.(\w+)(?:\s*or\s*"([^"]+)")?', trig)
|
||||||
|
if m:
|
||||||
|
value = consts.get(m.group(1), m.group(2))
|
||||||
|
else:
|
||||||
|
value = trig.strip('"')
|
||||||
|
cmds.append((key, kind_to_concept[kind], value))
|
||||||
|
return cmds
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--json", help="write machine-readable report here")
|
||||||
|
args = ap.parse_args()
|
||||||
|
token = os.environ.get("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
game = game_triggers(token)
|
||||||
|
cmds = mod_commands()
|
||||||
|
|
||||||
|
missing = [(k, c, t) for k, c, t in cmds if t is None or t not in game.get(c, set())]
|
||||||
|
game = {c: ts for c, ts in game.items()} # keep _new_candidates for the "new" pass
|
||||||
|
have = {(c, t) for _, c, t in cmds}
|
||||||
|
candidates = {c: ts for c, ts in game.items() if not c.startswith("_")}
|
||||||
|
candidates.update(game["_new_candidates"])
|
||||||
|
new = {c: sorted(t for t in ts if (c, t) not in have) for c, ts in candidates.items()}
|
||||||
|
new = {c: ts for c, ts in new.items() if ts}
|
||||||
|
|
||||||
|
print(f"Checked {len(cmds)} commands against the game source.\n")
|
||||||
|
if missing:
|
||||||
|
print("MISSING - these triggers no longer exist in the game (lines will be silent):")
|
||||||
|
for k, c, t in missing:
|
||||||
|
print(f" - {k}: {c} / {t}")
|
||||||
|
print()
|
||||||
|
if new:
|
||||||
|
print("NEW - triggers in the game that the mod does not expose yet:")
|
||||||
|
for c, ts in new.items():
|
||||||
|
print(f" [{c}]")
|
||||||
|
for t in ts:
|
||||||
|
print(f" - {t}")
|
||||||
|
print()
|
||||||
|
if not missing and not new:
|
||||||
|
print("Up to date.")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
Path(args.json).write_text(json.dumps({"missing": missing, "new": new}, indent=2))
|
||||||
|
|
||||||
|
return 1 if (missing or new) else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rename the mod. Usage: tools/rename.sh NewName
|
||||||
|
set -euo pipefail
|
||||||
|
NEW="${1:?usage: tools/rename.sh NewName}"
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
OLD=$(ls -d */ | sed 's#/##' | while read d; do [ -f "$d/$d.mod" ] && echo "$d"; done | head -1)
|
||||||
|
[ -n "$OLD" ] || { echo "could not find current mod folder"; exit 1; }
|
||||||
|
echo "Renaming $OLD -> $NEW"
|
||||||
|
git mv "$OLD/scripts/mods/$OLD" "$OLD/scripts/mods/$NEW" 2>/dev/null || mv "$OLD/scripts/mods/$OLD" "$OLD/scripts/mods/$NEW"
|
||||||
|
for f in "$OLD/scripts/mods/$NEW/$OLD"*.lua "$OLD/$OLD.mod"; do
|
||||||
|
n="${f//$OLD/$NEW}"; n="${n/#$NEW\/scripts/$OLD/scripts}"; n="${n/#$NEW\/$NEW.mod/$OLD/$NEW.mod}"
|
||||||
|
git mv "$f" "$n" 2>/dev/null || mv "$f" "$n"
|
||||||
|
done
|
||||||
|
git mv "$OLD" "$NEW" 2>/dev/null || mv "$OLD" "$NEW"
|
||||||
|
grep -rl "$OLD" "$NEW" README.md tools .github | xargs sed -i "s/$OLD/$NEW/g"
|
||||||
|
echo "Done. Review with: git status"
|
||||||
Reference in New Issue
Block a user