Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/smoke #48

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Binary file not shown.
Binary file not shown.
1,525 changes: 1,525 additions & 0 deletions archive/source/raw/base/cyberscript/entity/smoke.ent.json

Large diffs are not rendered by default.

1,085,123 changes: 1,085,123 additions & 0 deletions archive/source/raw/base/cyberscript/workspot/cyberscript_workspot_base.workspot.json

Large diffs are not rendered by default.

163,496 changes: 163,496 additions & 0 deletions archive/source/raw/base/cyberscript/workspot/smoke.workspot.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pages/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ Welcome to Addicted notebook !
- [2023-03-17](./travelog/2023-03-17.md)
- [2023-03-31](./travelog/2023-03-31.md)
- [2023-04-07](./travelog/2023-04-07.md)
- [2023-04-08](./travelog/2023-04-08.md)
35 changes: 35 additions & 0 deletions pages/travelog/2023-04-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# understanding entity and workspot

For a while I've tried to play animation on V to add some interesting gameplay mechanic for Addicted.
But I never quite understood them, until today.

A couple of weeks ago I had a talk with @Eli and @donk7413, the authors of [Cyberscript](https://www.nexusmods.com/cyberpunk2077/mods/6475) and the [Smoke Everywhere](https://www.nexusmods.com/cyberpunk2077/mods/7768) mods.

When asked about how they managed to do it and which animation name I should use, they mentioned having merged all `.workspot` files in a gigantic file for convenience (namely [Cyberscript Core Animation Archive](https://www.nexusmods.com/cyberpunk2077/mods/7691)), so that `Cyberscript` can play any animation in-game on V or any NPC.

At first it didn't ring a bell, but then @psiberx mentioned about modders "abusing" `.workspot` by spawning an invisible `.ent` to have the target being animated.

I gave a couple of tries with latest Codeware pre-release (which introduces [DynamicEntitySystem](https://github.com/psiberx/cp2077-codeware/wiki#spawning-entities) allowing to spawn `.ent` at runtime, among others) a couple of days ago unsuccessfully.

Finally today I sat, examined `.ent` and `.workspot` in `WolvenKit` from both `Cyberscript Core Animation Archive` and `Smoke Everywhere` and start seeing a pattern that I had seen in other files previously, but didn't paid attention at that time.

![Analyzing Cyberscript Core Animation Archive and Smoke Everywhere](pictures/CET+RED-workspot-analysis.png)

## clearing misconceptions

Actually the ones I personally had before:

- `.ent` can be anything, not just a "living entity".
it can be a cigarette, the act of smoking.. it can really be "whatever".
- it contains `components` with e.g. `workspotMapper`, `entSlot`, etc
- it also contains obviously a `workWorkspotResourceComponent` with points out to the `.workspot`.
- `.workspot` gets triggered throughout `.ent`: _you don't spawn a workspot and play it on a target_, you spawn an entity which references the workspot and plays the workspot through the entity (which in turn play the animation(s), and you can "jump" from one animation to another in the same workspot).

I managed to reproduce `Smoke Everywhere` in `.reds` piece by piece.

Next step will be to progressively clear up `components` one-by-one to check which one(s) are actually required or not.

## credits

All credits to @Eli and @donk7413 on [REDmodding Discord](https://discord.gg/redmodding).
Also @psiberx for always providing hints that lead me back on tracks.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pages/travelog/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ A little travel blog in modding land.
- [2023-03-17](./2023-03-17.md): AI powered dialogs research
- [2023-03-31](./2023-03-31.md): custom sounds
- [2023-04-07](./2023-04-07.md): how to translate this mod ?
- [2023-04-08](./2023-04-08.md): understanding entity and workspot
167 changes: 167 additions & 0 deletions scripts/Addicted/Debug.reds
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,170 @@ public func Checkup() -> Void {
public func DebugSound(sound: String) -> Void {
GameObject.PlaySound(this, StringToName(sound));
}

public class SmokeCallback extends DelayCallback {
public let player: wref<PlayerPuppet>;
public let step: Int32;
public let entityID: EntityID;
public func Call() -> Void {
if this.step == 1 {
GameInstance
.GetAudioSystem(this.player.GetGame())
.Play(n"q101_sc_06c_johnny_flicks_cigarette", this.player.GetEntityID(), n"Addicted:Smoke");

GameObjectEffectHelper
.StartEffectEvent(this.player, n"cigarette_smoke_exhaust");

let callback = new SmokeCallback();
callback.player = this.player;
callback.step = this.step + 1;
callback.entityID = this.entityID;
GameInstance
.GetDelaySystem(this.player.GetGame())
.DelayCallback(callback, 3.0);
}
if this.step == 2 {
GameInstance
.GetWorkspotSystem(this.player.GetGame())
.SendJumpToAnimEnt(this.player, n"stand_car_lean180__rh_cigarette__01__drop_ash__01", false);

let callback = new SmokeCallback();
callback.player = this.player;
callback.step = this.step + 1;
callback.entityID = this.entityID;
GameInstance
.GetDelaySystem(this.player.GetGame())
.DelayCallback(callback, 1.6);
}
if this.step == 3 {
GameInstance
.GetWorkspotSystem(this.player.GetGame())
.SendJumpToAnimEnt(this.player, n"sit_barstool_bar_lean0__2h_on_bar__01__smoke_ash__01", false);

let callback = new SmokeCallback();
callback.player = this.player;
callback.step = this.step + 1;
callback.entityID = this.entityID;
GameInstance
.GetDelaySystem(this.player.GetGame())
.DelayCallback(callback, 2.0);

}
if this.step == 4 {
GameInstance
.GetWorkspotSystem(this.player.GetGame())
.SendJumpToAnimEnt(this.player, n"sit_barstool_bar_lean0__2h_on_bar__01__smoke_idle__01", false);

let callback = new SmokeCallback();
callback.player = this.player;
callback.step = this.step + 1;
callback.entityID = this.entityID;
GameInstance
.GetDelaySystem(this.player.GetGame())
.DelayCallback(callback, 6.0);

}
if this.step == 5 {
GameInstance
.GetWorkspotSystem(this.player.GetGame())
.SendJumpToAnimEnt(this.player, n"sit_barstool_bar_lean0__2h_on_bar__01__smoke_ash__01", false);

GameInstance
.GetAudioSystem(this.player.GetGame())
.Play(n"cmn_generic_work_extinguish_cigarette", this.player.GetEntityID(), n"Addicted:Smoke:End");

let callback = new SmokeCallback();
callback.player = this.player;
callback.step = this.step + 1;
callback.entityID = this.entityID;
GameInstance
.GetDelaySystem(this.player.GetGame())
.DelayCallback(callback, 1.0);
}
if this.step == 6 {
GameObjectEffectHelper
.BreakEffectLoopEvent(this.player, n"cigarette_smoke_exhaust");

GameInstance.GetTransactionSystem(this.player.GetGame())
.RemoveItemFromSlot(this.player, t"AttachmentSlots.WeaponLeft");

GameInstance
.GetDynamicEntitySystem()
.DeleteEntity(this.entityID);

GameInstance
.GetWorkspotSystem(this.player.GetGame())
.SendSlowExitSignal(this.player, n"sit_barstool_bar_lean0__2h_on_bar__01__smoke_ash__01");
}
}
}

@addField(PlayerPuppet)
public let entitySystem: ref<DynamicEntitySystem>;

@addMethod(PlayerPuppet)
private cb func OnEntityUpdate(event: ref<DynamicEntityEvent>) {
LogChannel(n"DEBUG", s"Entity \(event.GetEventType()) \(EntityID.GetHash(event.GetEntityID())) \(event.GetClassName())");
if Equals(event.GetEntityTag(), n"Addicted") && Equals(EnumInt(event.GetEventType()), EnumInt(DynamicEntityEventType.Spawned)) {
E(s"found event with tag Addicted and entityID \(ToString(event.GetEntityID()))");
let device = this.entitySystem.GetEntity(event.GetEntityID()) as GameObject;
// let device = GameInstance.FindEntityByID(this.GetGame(), event.GetEntityID());
E(s"device: \(device.GetClassName())");
let workspotSystem: ref<WorkspotGameSystem> = GameInstance.GetWorkspotSystem(this.GetGame());
workspotSystem.PlayInDevice(device, this);
// workspotSystem.SendJumpToTagCommandEnt(this, n"Addicted", true, event.GetEntityID());
// workspotSystem.SendJumpToTagCommandEnt(this, n"Animated5005", true, event.GetEntityID());
// workspotSystem.SendJumpToAnimEnt(this, n"Animated5005", true);
// workspotSystem.SendJumpToAnimEnt(this, n"Addicted", true);
workspotSystem.SendJumpToAnimEnt(this, n"stand_car_lean180__rh_cigarette__01__smoke__01", true); // these AnimEnt can be found in "base\\cyberscript\\workspot\\smoke.workspot"

GameInstance.GetTransactionSystem(this.GetGame())
.GiveItem(this, ItemID.FromTDBID(t"Items.crowd_cigarette_i_stick"), 1);
GameInstance.GetTransactionSystem(this.GetGame())
.GiveItem(this, ItemID.FromTDBID(t"Items.apparel_lighter_a"), 1);
GameInstance.GetTransactionSystem(this.GetGame())
.AddItemToSlot(this, t"AttachmentSlots.WeaponLeft", ItemID.FromTDBID(t"Items.crowd_cigarette_i_stick"));
GameInstance.GetTransactionSystem(this.GetGame())
.AddItemToSlot(this, t"AttachmentSlots.WeaponRight", ItemID.FromTDBID(t"Items.apparel_lighter_a"));
let left = new AIEquipCommand();
left.slotId = t"AttachmentSlots.WeaponLeft";
left.itemId = t"Items.crowd_cigarette_i_stick";
let right = new AIEquipCommand();
right.slotId = t"AttachmentSlots.WeaponRight";
right.itemId = t"Items.apparel_lighter_a";
let controller = this.GetAIControllerComponent();
controller.SendCommand(left);
controller.SendCommand(right);

let callback = new SmokeCallback();
callback.player = this;
callback.step = 1;
callback.entityID = event.GetEntityID();
GameInstance
.GetDelaySystem(this.GetGame())
.DelayCallback(callback, 3.0);
}
}

// use like: Game.GetPlayer():Smoke();
@addMethod(PlayerPuppet)
public func Smoke() -> Void {
this.entitySystem = GameInstance.GetDynamicEntitySystem();
this.entitySystem.RegisterListener(n"Addicted", this, n"OnEntityUpdate");
let deviceSpec = new DynamicEntitySpec();
deviceSpec.templatePath = r"base\\cyberscript\\entity\\smoke.ent"; // this is responsible for linking to "base\\cyberscript\\workspot\\smoke.workspot"
deviceSpec.position = this.GetWorldPosition();
deviceSpec.orientation = EulerAngles.ToQuat(Vector4.ToRotation(this.GetWorldPosition()));
deviceSpec.persistState = false;
deviceSpec.persistSpawn = false;
deviceSpec.tags = [n"Addicted"];
this.entitySystem.CreateEntity(deviceSpec);
// let workspotSystem: ref<WorkspotGameSystem> = GameInstance.GetWorkspotSystem(this.GetGame());
// workspotSystem.SendJumpToTagCommandEnt(this, n"Addicted", true);
// workspotSystem.SendJumpToAnimEnt(this, n"Animated5005", true);
// workspotSystem.SendJumpToAnimEnt(this, n"int_recreation_001__cigarette_i_stick", true);
// workspotSystem.SendJumpToAnimEnt(this, n"base\\items\\interactive\\recreation\\int_recreation_001__cigarette_i_stick.ent", true);
// workspotSystem.SendJumpToAnimEnt(this, n"crowd_cigarette", true);
// workspotSystem.SendJumpToAnimEnt(this, n"Items.cigarette_i_stick", true);
// workspotSystem.SendJumpToAnimEnt(this, n"cigarette_i_stick", true);
}
16 changes: 16 additions & 0 deletions scripts/Addicted/System.reds
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Addicted.Helpers.*
import Addicted.Manager.*
import Addicted.Crossover.AlterNeuroBlockerStatusEffects
import Addicted.Crossover.AlterBlackLaceStatusEffects
import Addicted.Component.*

public class UpdateWithdrawalSymptomsCallback extends DelayCallback {
public let system: wref<AddictedSystem>;
Expand Down Expand Up @@ -41,6 +42,7 @@ public class AddictedSystem extends ScriptableSystem {

private let updateSymtomsID: DelayID;

private let callbackSystem: wref<CallbackSystem>;

private final func OnPlayerAttach(request: ref<PlayerAttachRequest>) -> Void {
let player: ref<PlayerPuppet> = GetPlayer(this.GetGameInstance());
Expand Down Expand Up @@ -77,6 +79,9 @@ public class AddictedSystem extends ScriptableSystem {
this.healerManager = new HealerManager();
this.healerManager.Initialize(this);

this.callbackSystem = GameInstance.GetCallbackSystem();
this.callbackSystem.RegisterCallback(n"Entity/Assemble", this, n"OnEntityAssemble");

// ModSettings.RegisterListenerToModifications(this);
}

Expand Down Expand Up @@ -114,6 +119,17 @@ public class AddictedSystem extends ScriptableSystem {
this.onoManager.Register(this.player);
}

private cb func OnEntityAssemble(event: ref<EntityLifecycleEvent>) {
let entity = event.GetEntity();
let template = entity.GetTemplatePath();
if template == r"base\\characters\\entities\\player\\player_ma_fpp.ent" || template == r"base\\characters\\entities\\player\\player_wa_fpp.ent" {
let puppet = entity as PlayerPuppet;
puppet.AddComponent(new TobaccoComponent());
puppet.AddTag(n"Addicted");
this.callbackSystem.UnregisterCallback(n"Entity/Assemble", this);
}
}

public func RefreshConfig() -> Void {
E(s"refresh config");
this.config = new AddictedConfig();
Expand Down
50 changes: 50 additions & 0 deletions scripts/Addicted/components/TobaccoComponent.reds
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Addicted.Component

import Addicted.*
import Addicted.Utils.E

// look into: idle
// - not in combat
// - not in stealth
// - not in vehicle
// - not on a mission
// - not puchasing (e.g. from seller)
// - not in a game menu
// - not chased by police
// - not in dialog
// - not on the phone
// - not controlling device
// - not underwater
// - not in braindance
// - not in workspot
// - not hacking
// - not hit by car
// - scanner usage ?
// forced consumption if Johnny is possessing ?
// ---- player
// protected cb func OnAction(action: ListenerAction, consumer: ListenerActionConsumer) -> Bool;
// public final static func GetCurrentLocomotionState(player: wref<PlayerPuppet>) -> gamePSMLocomotionStates;
// protected cb func OnStoppedBeingTrackedAsHostile(evt: ref<StoppedBeingTrackedAsHostile>) -> Bool;

public class TobaccoComponent extends ScriptableComponent {
private let owner: wref<PlayerPuppet>;
private let inCombat: Int32;
private let handle: ref<CallbackHandle>;
public final func RegisterListener() -> Void {
let blackboard: ref<IBlackboard> = this.owner.GetPlayerStateMachineBlackboard();
this.handle = blackboard.RegisterListenerInt(GetAllBlackboardDefs().PlayerStateMachine.Combat, this, n"OnCombatStateChanged");
}
public final func UnregisterListener() -> Void {
let blackboard: ref<IBlackboard> = this.owner.GetPlayerStateMachineBlackboard();
blackboard.UnregisterListenerInt(GetAllBlackboardDefs().PlayerStateMachine.Combat, this.handle);
}
protected cb func OnCombatStateChanged(newState: Int32) -> Bool {
let wasInCombat: Bool = this.inCombat == EnumInt(gamePSMCombat.InCombat);
let notAnymore: Bool = newState == EnumInt(gamePSMCombat.Default) || newState == EnumInt(gamePSMCombat.OutOfCombat);
let health: Float = GameInstance.GetStatPoolsSystem(this.owner.GetGame()).GetStatPoolValue(Cast<StatsObjectID>(this.owner.GetEntityID()), gamedataStatPoolType.Health);
let lowHealthThreshold: Float = PlayerPuppet.GetLowHealthThreshold();
if wasInCombat && notAnymore && (health <= lowHealthThreshold) {
// trigger compulsion
}
}
}