YALootBoxes/scripts/4_World/YALootBoxes_ActionOpen.c
smthspec ff51bde976 Fix open action (register custom action), stop restart-stacking, numeric log level, config docs
Action (the real "no action" cause):
- DayZ does not auto-discover custom actions; ActionConstructor holds a
  hardcoded list and AddAction() silently drops anything missing from it.
  Added a modded ActionConstructor that registers YALB_ActionOpenLootbox, so
  the open action finally attaches to the box.
- Switched the action target condition CCTObject -> CCTCursor (the look-at
  pattern vanilla uses for world-object interactions).

Restart stacking:
- Spawn boxes with CreateObjectEx + ECE_NOPERSISTENCY_WORLD so they are never
  saved to the world DB and don't survive restarts.
- Added CleanupOrphanedBoxes(): on init, scan each spawn location and delete
  any leftover YALootBoxes_Base before spawning (clears boxes persisted before
  this change).

Logging:
- DebugLogLevel changed from a string to a numeric field (0=off 1=ERROR
  2=WARN 3=INFO 4=DEBUG 5=VERBOSE), clamped at load. Removed the string parser.
  NOTE: existing main_config.json has a string value and must be regenerated.

Config docs:
- Each generated JSON now includes a "_comment" array describing every field
  and its range (ignored by the loader). README + configfiles.readme updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:31:20 +03:00

279 lines
No EOL
10 KiB
C

// ============================================================
// YALootBoxes_ActionOpen.c
// Custom UserAction: hold-to-open a locked lootbox.
// Lives in: scripts/4_World/YALootBoxes_ActionOpen.c
//
// Registration of this action onto YALootBoxes_Base happens by
// overriding SetActions()/AddAction() on the entity — see note
// at bottom of file.
// ============================================================
class YALB_OpenLootboxActionData : ActionData
{
// currently empty - base ActionData carries everything we need
};
// Continuous-action timing lives on the CALLBACK class (ActionContinuousBaseCB),
// not the action - CreateActionComponent() is defined there. The action points
// at this CB via m_CallbackClass. CAContinuousTime takes the channel time in
// SECONDS. Falls back to 5s on the client, where MainConfig is null (config is
// server-only); the server is authoritative on when the channel actually ends.
class YALB_OpenLootboxCB : ActionContinuousBaseCB
{
override void CreateActionComponent()
{
float secs = 5.0;
if (YALB_ConfigLoader.MainConfig && YALB_ConfigLoader.MainConfig.OpenChannelDurationSeconds > 0)
secs = YALB_ConfigLoader.MainConfig.OpenChannelDurationSeconds;
m_ActionData.m_ActionComponent = new CAContinuousTime(secs);
}
};
class YALB_ActionOpenLootbox : ActionContinuousBase
{
void YALB_ActionOpenLootbox()
{
m_CallbackClass = YALB_OpenLootboxCB;
m_CommandUID = DayZPlayerConstants.CMD_ACTIONMOD_OPENITEM; // "open item" channel anim
m_FullBody = false;
}
override void CreateConditionComponents()
{
m_ConditionItem = new CCINone(); // no item-in-hands restriction here
m_ConditionTarget = new CCTCursor(UAMaxDistances.DEFAULT); // look-at-object targeting
}
override string GetText()
{
return "Open lootbox";
}
override typename GetInputType()
{
return ContinuousInteractActionInput;
}
// Offer the action when the target is our lootbox, it's locked, and the
// player is holding *some* item. We deliberately do NOT check the specific
// key item here: ActionCondition runs client-side to display the prompt,
// but the box-type config (required items) lives in $profile and is loaded
// server-side only, so GetTypeDef() is null on clients. The exact key-item
// requirement is enforced authoritatively in OnFinishProgressServer; the
// proximity "Requires: X" message tells the player what to hold.
override bool ActionCondition(PlayerBase player, ActionTarget target, ItemBase item)
{
YALootBoxes_Base box = YALootBoxes_Base.Cast(target.GetObject());
if (!box) return false;
if (!box.IsLocked()) return false;
ItemBase handItem = player.GetItemInHands();
if (!handItem) return false;
return true;
}
// Fired server-side when the channel completes WITHOUT being cancelled.
override void OnFinishProgressServer(ActionData action_data)
{
super.OnFinishProgressServer(action_data);
YALootBoxes_Base box = YALootBoxes_Base.Cast(action_data.m_Target.GetObject());
PlayerBase player = action_data.m_Player;
if (!box || !player) return;
YALB_BoxTypeDef def = box.GetTypeDef();
if (!def) return;
ItemBase handItem = player.GetItemInHands();
// Authoritative key-item gate (ActionCondition can't do this on the
// client - see note there). Wrong/empty item: tell the player what's
// needed and bail without rolling.
if (!handItem || !def.IsValidKeyItem(handItem.GetType()))
{
YALB_NotifyPlayer(player, "Requires: " + def.GetRequiredItemsDisplayString());
return;
}
float roll = Math.RandomFloat01();
if (roll <= def.openChance)
{
// SUCCESS
box.UnlockAndFill();
if (handItem)
handItem.DecreaseHealth(def.itemDamageOnOpen, false);
YALB_NotifyPlayer(player, "The lock gives way.");
}
else
{
// FAILURE - apply exactly one weighted-random effect
YALB_ApplyRandomFailEffect(player, def, handItem);
YALB_NotifyPlayer(player, "The attempt failed.");
}
}
// NOTE: early bail-out (per requirements - fixed channel, instant roll
// at the end, interruption allowed) is handled by ActionContinuousBase
// itself: releasing the button cancels the action WITHOUT calling
// OnFinishProgressServer, so no roll/penalty/progress is saved. No
// override is needed here.
};
// ------------------------------------------------------------
// Failure-effect resolution
// ------------------------------------------------------------
void YALB_ApplyRandomFailEffect(PlayerBase player, YALB_BoxTypeDef def, ItemBase handItem)
{
if (!def.failEffects || def.failEffects.Count() == 0) return;
// Weighted pick: sum chances, roll within sum, walk the list.
float totalWeight = 0;
foreach (YALB_FailEffect e : def.failEffects)
totalWeight += e.chance;
if (totalWeight <= 0) return;
float roll = Math.RandomFloat(0, totalWeight);
float cumulative = 0;
YALB_FailEffect chosen = null;
foreach (YALB_FailEffect effect : def.failEffects)
{
cumulative += effect.chance;
if (roll <= cumulative)
{
chosen = effect;
break;
}
}
if (!chosen) return;
string playerName = "<unknown>";
if (player.GetIdentity()) playerName = player.GetIdentity().GetName();
Log_Verbose(string.Format("Player %1 triggered fail effect '%2' on box type '%3'.",
playerName, chosen.type, def.type));
switch (chosen.type)
{
case "EXPLOSIVE":
YALB_ApplyExplosiveDamage(player, chosen.damage, chosen.radius);
break;
case "HEALTH":
player.DecreaseHealth("", "Health", chosen.damage);
break;
case "BLEEDING":
{
// AttemptAddBleedingSource(component) needs a valid body-component
// bit; GetBleedingSourcesLocations() is the engine's set of those.
// Pick random ones (a duplicate just no-ops if already bleeding).
BleedingSourcesManagerServer bleedMgr = player.GetBleedingManagerServer();
if (bleedMgr)
{
set<int> bleedLocs = bleedMgr.GetBleedingSourcesLocations();
if (bleedLocs && bleedLocs.Count() > 0)
{
for (int bi = 0; bi < chosen.bleedSources; bi++)
{
int comp = bleedLocs.Get(Math.RandomInt(0, bleedLocs.Count()));
bleedMgr.AttemptAddBleedingSource(comp);
}
}
}
break;
}
case "SICKNESS":
YALB_ApplySickness(player, chosen.agent);
break;
case "BROKEN_BONE":
// Vanilla broken-leg API. The player modifier system drives
// the HUD icon / forced-prone reaction off this state.
player.SetBrokenLegs(eBrokenLegs.BROKEN_LEGS);
break;
default:
Log_Warn("Unknown fail effect type: " + chosen.type);
break;
}
}
void YALB_ApplyExplosiveDamage(PlayerBase player, float damage, float radius)
{
// Direct damage to the player rather than a real grenade explosion entity,
// since we don't want a separate live explosive object to manage/clean up.
vector pos = player.GetPosition();
player.DecreaseHealth("", "Health", damage);
// Optional: damage nearby players too, to sell the "explosion" effect
array<Man> nearbyPlayers = new array<Man>();
GetGame().GetWorld().GetPlayerList(nearbyPlayers);
foreach (Man m : nearbyPlayers)
{
PlayerBase other = PlayerBase.Cast(m);
if (!other || other == player) continue;
float dist = vector.Distance(other.GetPosition(), pos);
if (dist <= radius)
{
float falloff = 1.0 - (dist / radius);
other.DecreaseHealth("", "Health", damage * falloff);
}
}
}
void YALB_ApplySickness(PlayerBase player, string agentName)
{
// Maps to vanilla DayZ agent system (e.g. eAgents.SALMONELLA, eAgents.INFLUENZA)
int agentId = YALB_AgentNameToId(agentName);
if (agentId >= 0)
{
player.InsertAgent(agentId, 1000); // 1000 = full dose, matches vanilla scale
}
}
int YALB_AgentNameToId(string agentName)
{
if (agentName == "Salmonella") return eAgents.SALMONELLA;
if (agentName == "Influenza") return eAgents.INFLUENZA;
if (agentName == "Cholera") return eAgents.CHOLERA;
if (agentName == "Brain") return eAgents.BRAIN;
if (agentName == "Wound_Bacterial") return eAgents.WOUND_AGENT;
return -1;
}
void YALB_NotifyPlayer(PlayerBase player, string message)
{
if (!player || !player.GetIdentity()) return;
GetGame().RPCSingleParam(player, ERPCs.RPC_USER_ACTION_MESSAGE, new Param1<string>(message), true, player.GetIdentity());
}
// ------------------------------------------------------------
// ACTION REGISTRATION (REQUIRED)
// ------------------------------------------------------------
// DayZ does NOT auto-discover custom actions. ActionConstructor
// holds a hardcoded list of every action; AddAction() looks an
// action up there and silently drops it if missing. So a custom
// action MUST be added via a modded ActionConstructor, or it will
// never attach to anything (this is why the open prompt was
// missing even though the box and SetActions() were correct).
// ------------------------------------------------------------
modded class ActionConstructor
{
override void RegisterActions(TTypenameArray actions)
{
super.RegisterActions(actions);
actions.Insert(YALB_ActionOpenLootbox);
}
}
// ------------------------------------------------------------
// NOTE: the action is attached to the box via SetActions()/
// AddAction() in YALootBoxes_Lootbox.c. ContinuousInteractActionInput
// detects actions from the TARGET object you look at, so registering
// it on the box (not the held tool) is correct.
// ------------------------------------------------------------