Skip to content

Commit 0576665

Browse files
committed
port alt use vendors
1 parent 66dba9a commit 0576665

File tree

10 files changed

+707
-620
lines changed

10 files changed

+707
-620
lines changed

.legacy/AltUseVendors.zip

-19.2 KB
Binary file not shown.

AltUseVendors/__init__.py

Lines changed: 0 additions & 594 deletions
This file was deleted.

AltUseVendors/Readme.md renamed to alt_use_vendors/Readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## Alt Use Vendors v3
4+
- Upgraded to v3 sdk.
5+
36
## Alt Use Vendors v2.3
47
Thanks to @Siggless
58

alt_use_vendors/__init__.py

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
from typing import Any
2+
3+
import unrealsdk
4+
from mods_base import Game, ObjectFlags, build_mod, hook
5+
from unrealsdk import logging
6+
from unrealsdk.hooks import Block, Type
7+
from unrealsdk.unreal import BoundFunction, UObject, WrappedStruct
8+
9+
from alt_use_vendors.ak_events import AKE_INTERACT_BY_VENDOR_NAME, find_and_play_akevent
10+
11+
from .enums import (
12+
EChangeStatus,
13+
ECurrencyType,
14+
ENetRole,
15+
EShopType,
16+
ESkillEventType,
17+
ETransactionStatus,
18+
EUsabilityType,
19+
)
20+
from .shop_info import GRENADE_RESOURCE_NAME, SHOP_INFO_MAP
21+
22+
type Actor = UObject
23+
type AkEvent = UObject
24+
type AmmoResourcePool = UObject
25+
type InteractionIconDefinition = UObject
26+
type WillowInventory = UObject
27+
type WillowPlayerController = UObject
28+
type WillowVendingMachine = UObject
29+
30+
31+
# ==================================================================================================
32+
33+
icon_map: dict[EShopType, InteractionIconDefinition] = {}
34+
35+
36+
def create_icons() -> None:
37+
"""
38+
Creates the icon objects we're using.
39+
40+
If an object of the same name already exists, uses that instead.
41+
"""
42+
if icon_map:
43+
return
44+
45+
base_icon = unrealsdk.find_object(
46+
"InteractionIconDefinition",
47+
"GD_InteractionIcons.Default.Icon_DefaultUse",
48+
)
49+
50+
for shop_type, info in SHOP_INFO_MAP.items():
51+
try:
52+
icon = unrealsdk.find_object(
53+
"InteractionIconDefinition",
54+
f"GD_InteractionIcons.Default.{info.icon_name}",
55+
)
56+
except ValueError:
57+
icon = unrealsdk.construct_object(
58+
cls=base_icon.Class,
59+
outer=base_icon.Outer,
60+
name=info.icon_name,
61+
flags=ObjectFlags.KEEP_ALIVE,
62+
template_obj=base_icon,
63+
)
64+
65+
icon.Icon = info.icon
66+
icon.Action = "UseSecondary"
67+
icon.Text = info.icon_text
68+
69+
icon_map[shop_type] = icon
70+
71+
72+
# Called when any interactive object is created. Use it to enable alt use and add the icons.
73+
@hook("WillowGame.WillowInteractiveObject:InitializeFromDefinition")
74+
def initialize_from_definition( # noqa: D103
75+
obj: UObject,
76+
args: WrappedStruct,
77+
_ret: Any,
78+
_func: BoundFunction,
79+
) -> None:
80+
if obj.Class.Name != "WillowVendingMachine":
81+
return
82+
83+
if obj.ShopType in SHOP_INFO_MAP:
84+
args.Definition.HUDIconDefSecondary = icon_map[obj.ShopType]
85+
obj.SetUsability(True, EUsabilityType.UT_Secondary)
86+
87+
88+
def trigger_money_is_power(pc: WillowPlayerController) -> None:
89+
"""
90+
If the Game is TPS, triggers the removal of the Doppelganger's Money is Power stacks.
91+
92+
Args:
93+
pc: The player controller that has spent money.
94+
"""
95+
if Game.get_current() is not Game.TPS:
96+
return
97+
pc.GetSkillManager().NotifySkillEvent(ESkillEventType.SEVT_OnPaidCashForUse, pc, pc, None, None)
98+
99+
100+
# This is called whenever someone uses an interactive object.
101+
# At this point, the secondary use cost is not necessarily accurate - the player who used it might
102+
# not be the last one who updated the cost. Hooking in at this point lets us easily overwrite it.
103+
@hook("WillowGame.WillowPlayerController:PerformedSecondaryUseAction")
104+
def performed_secondary_use_action( # noqa: D103
105+
obj: UObject,
106+
_args: WrappedStruct,
107+
_ret: Any,
108+
_func: BoundFunction,
109+
) -> tuple[type[Block], bool] | None:
110+
if obj.Role < ENetRole.ROLE_Authority:
111+
return None
112+
if obj.CurrentUsableObject is None:
113+
return None
114+
if obj.CurrentInteractionIcon[1].IconDef is None:
115+
return None
116+
117+
vendor = obj.CurrentUsableObject
118+
if vendor.Class.Name != "WillowVendingMachine":
119+
return None
120+
if vendor.ShopType not in SHOP_INFO_MAP:
121+
return None
122+
123+
obj.UsableObjectUpdateTime = 0.0
124+
125+
info = SHOP_INFO_MAP[vendor.ShopType]
126+
127+
cost = info.cost_function(obj, vendor)
128+
wallet = obj.PlayerReplicationInfo.GetCurrencyOnHand(ECurrencyType.CURRENCY_Credits)
129+
if cost == 0 or wallet < cost:
130+
obj.NotifyUnableToAffordUsableObject(EUsabilityType.UT_Secondary)
131+
return Block, False
132+
133+
if info.requires_manual_payment:
134+
obj.PlayerReplicationInfo.AddCurrencyOnHand(ECurrencyType.CURRENCY_Credits, -cost)
135+
obj.SetPendingTransactionStatus(ETransactionStatus.TS_TransactionComplete)
136+
trigger_money_is_power(obj)
137+
138+
info.purchase_function(obj, vendor)
139+
140+
vendor_name = vendor.InteractiveObjectDefinition.Name
141+
interact_event = AKE_INTERACT_BY_VENDOR_NAME.get(vendor_name)
142+
if interact_event is None:
143+
logging.warning(f"[Alt Use Vendors] Couldn't find interact voice line for {vendor_name}")
144+
else:
145+
find_and_play_akevent(vendor, interact_event)
146+
147+
update_vendor_costs(obj, vendor.ShopType)
148+
return Block, True
149+
150+
151+
# ==================================================================================================
152+
153+
# This map keeps track of which vendors each player is near to
154+
# Historically, we've run into some issues with hitches when updating costs
155+
# The current approach is to only do an update when it changes (i.e. on shooting or taking damage),
156+
# and only to the vendors they can actually see - which is where this comes in
157+
player_vendor_map: dict[WillowPlayerController, set[WillowVendingMachine]] = {}
158+
159+
160+
# Called when a player moves near any interactive object - use it to add to the map
161+
@hook("WillowGame.WillowInteractiveObject:Touch")
162+
def interactive_obj_touch( # noqa: D103
163+
obj: UObject,
164+
args: WrappedStruct,
165+
_ret: Any,
166+
_func: BoundFunction,
167+
) -> None:
168+
if obj.Class.Name != "WillowVendingMachine":
169+
return
170+
if (pawn := args.Other).Class.Name != "WillowPlayerPawn":
171+
return
172+
173+
pc = pawn.Controller
174+
if pc not in player_vendor_map:
175+
player_vendor_map[pc] = set()
176+
player_vendor_map[pc].add(obj)
177+
178+
update_vendor_costs(pc, obj.ShopType)
179+
180+
181+
# Called when a player moves away from an interactive object - use it to remove from the map
182+
@hook("WillowGame.WillowInteractiveObject:UnTouch")
183+
def interactive_obj_untouch( # noqa: D103
184+
obj: UObject,
185+
args: WrappedStruct,
186+
_ret: Any,
187+
_func: BoundFunction,
188+
) -> None:
189+
if obj.Class.Name != "WillowVendingMachine":
190+
return
191+
if (other := args.Other).Class.Name != "WillowPlayerPawn":
192+
return
193+
194+
pc = other.Controller
195+
if pc not in player_vendor_map:
196+
return
197+
198+
player_vendor_map[pc].discard(obj)
199+
200+
if not player_vendor_map[pc]:
201+
del player_vendor_map[pc]
202+
203+
204+
# Called on starting to load into a new level - use it to clear the map
205+
@hook("WillowGame.WillowPlayerController:WillowShowLoadingMovie")
206+
def show_loading_movie(*_: Any) -> None: # noqa: D103
207+
player_vendor_map.clear()
208+
209+
210+
def update_vendor_costs(pc: WillowPlayerController, shop_type: EShopType) -> None: # type: ignore
211+
"""
212+
Updates the alt use cost for vendors given associated with the given player and type.
213+
214+
Args:
215+
pc: The player controller to look for associated vendors of.
216+
shop_type: The type of ship to filter to.
217+
"""
218+
if (nearby_vendors := player_vendor_map.get(pc)) is None:
219+
return
220+
221+
info = SHOP_INFO_MAP[shop_type]
222+
223+
for vendor in nearby_vendors:
224+
if vendor.ShopType != shop_type:
225+
continue
226+
227+
vendor.Behavior_ChangeUsabilityCost(
228+
EChangeStatus.CHANGE_Enable,
229+
ECurrencyType.CURRENCY_Credits,
230+
info.cost_function(pc, vendor),
231+
EUsabilityType.UT_Secondary,
232+
)
233+
234+
235+
# ==================================================================================================
236+
237+
238+
# Called to fire a gun, *after* ammo is removed - use to update ammo costs
239+
@hook("WillowGame.WillowWeapon:InstantFire")
240+
def weapon_instant_fire( # noqa: D103
241+
obj: UObject,
242+
_args: WrappedStruct,
243+
_ret: Any,
244+
_func: BoundFunction,
245+
) -> None:
246+
if (pawn := obj.Owner).Class.Name != "WillowPlayerPawn":
247+
return
248+
update_vendor_costs(pawn.Controller, EShopType.SType_Items)
249+
250+
251+
# Called to remove the grenade ammo after throwing one - use to update ammo costs
252+
@hook("WillowGame.WillowPlayerController:ConsumeProjectileResource", Type.POST)
253+
def consume_projectile_resource( # noqa: D103
254+
obj: UObject,
255+
args: WrappedStruct,
256+
_ret: Any,
257+
_func: BoundFunction,
258+
) -> None:
259+
if (proj_resource := args.ProjectileDefinition.Resource) is None:
260+
return
261+
if proj_resource.Name != GRENADE_RESOURCE_NAME:
262+
return
263+
264+
update_vendor_costs(obj, EShopType.SType_Items)
265+
266+
267+
# Called whenever a player takes damage - use it to update health costs
268+
@hook("WillowGame.WillowPlayerPawn:TakeDamage", Type.POST)
269+
def player_pawn_take_damage( # noqa: D103
270+
obj: UObject,
271+
_args: WrappedStruct,
272+
_ret: Any,
273+
_func: BoundFunction,
274+
) -> None:
275+
update_vendor_costs(obj.Controller, EShopType.SType_Health)
276+
277+
278+
# Negative costs don't display for the weapon/trash vendor, so no need to have a change hook for it
279+
280+
# ==================================================================================================
281+
282+
283+
def on_enable() -> None: # noqa: D103
284+
create_icons()
285+
286+
287+
mod = build_mod()

alt_use_vendors/ak_events.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import unrealsdk
2+
from mods_base import Game, ObjectFlags
3+
from unrealsdk.unreal import UObject
4+
5+
type Actor = UObject
6+
type AkEvent = UObject
7+
8+
# Sound AkEvents for playing
9+
AKE_BUY: str = "Ake_UI.UI_Vending.Ak_Play_UI_Vending_Buy"
10+
AKE_SELL: str = "Ake_UI.UI_Vending.Ak_Play_UI_Vending_Sell"
11+
12+
AKE_INTERACT_BY_VENDOR_NAME: dict[str, str] = {
13+
Game.BL2: {
14+
"InteractiveObj_VendingMachine_GrenadesAndAmmo": (
15+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Purchase"
16+
),
17+
"InteractiveObj_VendingMachine_HealthItems": (
18+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Zed_Store_Welcome"
19+
),
20+
"VendingMachine_Weapons_Definition": (
21+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Bye"
22+
),
23+
"IO_Aster_VendingMachine_GrenadesAndAmmo": (
24+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Marcus_Store_Purchase"
25+
),
26+
"IO_Aster_VendingMachine_HealthItems": (
27+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Zed_Store_Purchase"
28+
),
29+
"VendingMachine_Aster_Weapons_Definition": (
30+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Marcus_Store_Bye"
31+
),
32+
"VendingMachine_TorgueToken": "Ake_Iris_VO.Ak_Play_Iris_TorgueVendingMachine_Purchase",
33+
},
34+
Game.TPS: {
35+
"InteractiveObj_VendingMachine_GrenadesAndAmmo": (
36+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Purchase"
37+
),
38+
"InteractiveObj_VendingMachine_HealthItems": (
39+
"Ake_Cork_VOCT_Contextuals.Cork_VOCT_NurseNina.Ak_Play_VOCT_Cork_NurseNina_Store_Purchase"
40+
),
41+
"VendingMachine_Weapons_Definition": (
42+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Bye"
43+
),
44+
"InteractiveObj_VendingMachine_GrenadesAndAmmo_Marigold": (
45+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Purchase"
46+
),
47+
"InteractiveObj_VendingMachine_HealthItems_Marigold": (
48+
"Ake_Cork_VOCT_Contextuals.Cork_VOCT_NurseNina.Ak_Play_VOCT_Cork_NurseNina_Store_Purchase"
49+
),
50+
"VendingMachine_Weapons_Definition_Marigold": (
51+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Bye"
52+
),
53+
"InteractiveObj_VendingMachine_GrenadesAndAmmo_Marigold_BL1": (
54+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Purchase"
55+
),
56+
"InteractiveObj_VendingMachine_HealthItems_Marigold_BL1": (
57+
"Ake_Marigold_VOCT_Contextuals.Dlc_Marigold_VOCT_Zed.Ak_Play_Dlc_Marigold_VOCT_Zed_Vending_Welcome"
58+
),
59+
"VendingMachine_Weapons_Definition_Marigold_BL1": (
60+
"Ake_VOCT_Contextual.Ak_Play_VOCT_Marcus_Vending_Munition_Bye"
61+
),
62+
},
63+
Game.AoDK: {
64+
"IO_Aster_VendingMachine_GrenadesAndAmmo": (
65+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Marcus_Store_Purchase"
66+
),
67+
"IO_Aster_VendingMachine_HealthItems": (
68+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Zed_Store_Purchase"
69+
),
70+
"VendingMachine_Aster_Weapons_Definition": (
71+
"Ake_Aster_VO.VOCT.Ak_Play_VOCT_Aster_Marcus_Store_Bye"
72+
),
73+
},
74+
}[Game.get_current()]
75+
76+
77+
# AkEvents are not loaded by default, and we don't want to do an "expensive" find object call, or a
78+
# truely expensive load package, so we'll just cache them (and keep alive) as we use them
79+
akevent_cache: dict[str, AkEvent] = {}
80+
81+
82+
def find_and_play_akevent(actor: Actor, event_name: str) -> None:
83+
"""
84+
Attempts to find and play an AkEvent.
85+
86+
Silently drops it on failure.
87+
88+
Args:
89+
actor: The actor to play at.
90+
event_name: The object name of the event to play.
91+
"""
92+
93+
event = akevent_cache.get(event_name)
94+
if event is None:
95+
try:
96+
event = unrealsdk.find_object("AkEvent", event_name)
97+
except ValueError:
98+
return
99+
event.ObjectFlags |= ObjectFlags.KEEP_ALIVE
100+
akevent_cache[event_name] = event
101+
102+
actor.PlayAkEvent(event)

0 commit comments

Comments
 (0)