overlord_event_system/async_handler/
handler.rs

1use crate::{
2    attributes, cases::try_finalize_item, event::OverlordEvent,
3    gacha::item_case::generate_item_from_template, game_config_helpers::GameConfigLookup,
4    script::ScriptRunner, state::OverlordState,
5};
6
7use configs::game_config::GameConfig;
8
9use analytics::constants::METRICS_TARGET;
10
11use essences::{
12    ad_usage::{AdPlacement, AdUsageData},
13    bundles::{BundleAbility, BundleElement, BundleRawStep},
14    character_state::CharacterState,
15    currency::{
16        CurrencyConsumer, CurrencySource, CurrencyUnit, check_can_decrease_currencies,
17        decrease_currencies, from_es_currencies, increase_currencies,
18    },
19    entity::EntityState,
20    fighting::ActiveFight,
21    items::Item,
22    mail::Mail,
23    offers::OfferTemplate,
24    quest::{QuestGroupType, QuestInstance},
25};
26
27use event_system::event::EventRhaiEnum;
28pub use event_system::{
29    event::{EventPluginized, EventStruct},
30    plugin::{cron::CronMark, delayed::DelayedMark},
31    script::types::ConditionalProgress,
32    system::{AsyncEventHandler, EventHandleResult},
33};
34use rand::TryRngCore;
35use rand::{
36    SeedableRng,
37    rngs::{OsRng, StdRng},
38};
39
40#[derive(Debug)]
41pub struct OverlordAsyncEventHandler {
42    pub(super) game_config: configs::SharedGameConfig,
43    pub(super) script_runner: ScriptRunner<OverlordEvent, OverlordState>,
44    #[allow(dead_code)]
45    pub(super) frontend: bool,
46    pub(super) start_fight_tick: u64,
47    pub(super) compute_fields_duration: opentelemetry::metrics::Histogram<f64>,
48    /// Tracks the source/consumer for currency change metrics logging.
49    pub(super) last_currency_source: Option<String>,
50}
51
52impl AsyncEventHandler<OverlordEvent, OverlordState> for OverlordAsyncEventHandler {
53    #[tracing::instrument(
54        skip_all,
55        fields(
56            character_id = %state.character_state.character.id,
57            current_power = %state.character_state.character.power,
58            event_type = %event,
59            sampling_label = tracing::field::Empty,
60        ),
61        target = METRICS_TARGET,
62    )]
63    fn handle_event(
64        &mut self,
65        event: &OverlordEvent,
66        state: OverlordState,
67        rand_gen: rand::rngs::StdRng,
68        current_tick: u64,
69    ) -> EventHandleResult<OverlordEvent, OverlordState> {
70        self.last_currency_source = match event {
71            OverlordEvent::CurrencyIncrease {
72                currency_source, ..
73            } => Some(format!("{currency_source:?}")),
74            OverlordEvent::CurrencyDecrease {
75                currency_consumer, ..
76            } => Some(format!("{currency_consumer:?}")),
77            other => Some(other.to_string()),
78        };
79        let game_config = self.game_config.get();
80        let mut events: Vec<EventPluginized<OverlordEvent, OverlordState>> = Vec::new();
81        if let Some(fight) = &state.active_fight {
82            for entity in &fight.entities {
83                for effect_id in &entity.effect_ids {
84                    let Ok(effect) = game_config.require_effect(*effect_id) else {
85                        tracing::error!("Couldn't find effect in config with id = {effect_id}");
86                        return EventHandleResult::fail(state);
87                    };
88                    if let Some(events_subscribe) = &effect.events_subscribe
89                        && events_subscribe.contains(&event.to_string())
90                    {
91                        events.push(EventPluginized::now(OverlordEvent::CastEffectFromEvent {
92                            entity_id: entity.id,
93                            effect_id: effect.id,
94                            caller_event: Box::new(event.clone()),
95                        }));
96                    }
97                }
98            }
99        }
100
101        let mut result = match &event {
102            // Items
103            OverlordEvent::OpenItemCase { batch_size } => {
104                self.handle_open_item_case(*batch_size, state)
105            }
106            OverlordEvent::AutoChestOpenItemCase { batch_size } => {
107                self.handle_auto_chest_open_item_case(*batch_size, state)
108            }
109            OverlordEvent::PlayerEquipItem { item_id } => {
110                self.handle_player_equip_item(*item_id, state)
111            }
112            OverlordEvent::SellItem { item_id } => self.handle_sell_item(*item_id, state),
113            OverlordEvent::ItemSold { .. } => self.handle_noop(state),
114            OverlordEvent::PlayerNewItems { items } => self.handle_player_new_items(items, state),
115            OverlordEvent::UpgradeItemCase {} => self.handle_noop(state),
116            OverlordEvent::ItemCaseUpgraded {} => self.handle_noop(state),
117            OverlordEvent::SpeedupUpgradeItemCase {} => self.handle_noop(state),
118            OverlordEvent::SkipUpgradeItemCase {} => self.handle_noop(state),
119            OverlordEvent::ClaimUpgradeItemCase {} => self.handle_noop(state),
120
121            OverlordEvent::EnableAutoSell {} => self.handle_enable_auto_sell(state),
122            OverlordEvent::DisableAutoSell {} => self.handle_disable_auto_sell(state),
123
124            OverlordEvent::SetGearOverrideEnabled { item_type, enabled } => {
125                self.handle_set_gear_override_enabled(*item_type, *enabled, state)
126            }
127
128            OverlordEvent::EnableCaseUpgradePopUp {} => {
129                self.handle_enable_case_upgrade_pop_up(state)
130            }
131            OverlordEvent::DisableCaseUpgradePopUp {} => {
132                self.handle_disable_case_upgrade_pop_up(state)
133            }
134
135            // Abilities
136            OverlordEvent::OpenAbilityCase { .. } => self.handle_noop(state),
137            OverlordEvent::SetAbilityGachaWishlist { .. } => self.handle_noop(state),
138            OverlordEvent::UpgradeAbilitySlot { .. } => self.handle_noop(state),
139            OverlordEvent::AbilityCaseOpened { .. } => self.handle_noop(state),
140            OverlordEvent::NewAbilities { .. } => self.handle_noop(state),
141            OverlordEvent::UpgradeAbilityCase {} => self.handle_noop(state),
142            OverlordEvent::FastEquipAbilities {} => self.handle_noop(state),
143            OverlordEvent::EquipAbility {
144                slot_id,
145                ability_id,
146            } => self.handle_equip_ability(*slot_id, *ability_id, current_tick, state),
147            OverlordEvent::UnequipAbility { slot_id } => {
148                self.handle_unequip_ability(*slot_id, state)
149            }
150            OverlordEvent::EquipAbilities { equipped_abilities } => {
151                self.handle_equip_abilities(equipped_abilities.clone(), current_tick, state)
152            }
153            OverlordEvent::UpgradeAbility { .. } => self.handle_noop(state),
154            OverlordEvent::UpgradeAllAbilities {} => self.handle_noop(state),
155            OverlordEvent::UpgradedAbilities { .. } => self.handle_noop(state),
156
157            // Fight management
158            OverlordEvent::StartGame {} => self.handle_noop(state),
159            OverlordEvent::PrepareFight { prepare_fight_type } => {
160                self.handle_prepare_fight(prepare_fight_type.clone(), rand_gen, state)
161            }
162            OverlordEvent::StartFight { fight_id } => {
163                self.handle_start_fight(event.clone(), *fight_id, rand_gen, current_tick, state)
164            }
165            OverlordEvent::EndFight {
166                fight_id,
167                is_win,
168                pvp_state,
169            } => self.handle_end_fight(*fight_id, *is_win, pvp_state, state),
170            OverlordEvent::StageCleared {} => self.handle_noop(state),
171            OverlordEvent::RaidDungeon { .. } => self.handle_noop(state),
172
173            // Moving
174            OverlordEvent::StartMove {
175                entity_id,
176                to,
177                duration_ticks,
178            } => self.handle_start_move(*entity_id, to.clone(), *duration_ticks, state),
179            OverlordEvent::EndMove { entity_id } => self.handle_end_move(*entity_id, state),
180
181            // Fighting
182            OverlordEvent::SpawnEntity {
183                id,
184                entity_template_id,
185                position,
186                entity_team,
187                has_big_hp_bar,
188                entity_attributes,
189            } => self.handle_spawn_entity(
190                *id,
191                *entity_template_id,
192                position.clone(),
193                entity_team.clone(),
194                *has_big_hp_bar,
195                entity_attributes.clone(),
196                current_tick,
197                state,
198            ),
199            OverlordEvent::FightProgress {} => self.handle_fight_progress(current_tick, state),
200            OverlordEvent::StartCastAbility {
201                by_entity_id,
202                ability_id,
203                ..
204            } => self.handle_start_cast_ability(
205                event.clone(),
206                *by_entity_id,
207                *ability_id,
208                rand_gen,
209                current_tick,
210                state,
211            ),
212            OverlordEvent::StartedCastAbility { .. } => self.handle_noop(state),
213            OverlordEvent::CastAbility {
214                by_entity_id,
215                to_entity_id,
216                ability_id,
217                ..
218            } => self.handle_cast_ability(
219                event.clone(),
220                *by_entity_id,
221                *to_entity_id,
222                *ability_id,
223                rand_gen,
224                current_tick,
225                state,
226            ),
227            OverlordEvent::StartCastProjectile {
228                by_entity_id,
229                to_entity_id,
230                projectile_id,
231                level,
232                delay: _,
233            } => self.handle_start_cast_projectile(
234                event.clone(),
235                *by_entity_id,
236                *to_entity_id,
237                *projectile_id,
238                *level,
239                current_tick,
240                state,
241            ),
242            OverlordEvent::StartedCastProjectile { .. } => self.handle_noop(state),
243            OverlordEvent::CastProjectile {
244                by_entity_id,
245                to_entity_id,
246                projectile_id,
247                level,
248                projectile_data,
249            } => self.handle_cast_projectile(
250                event.clone(),
251                *by_entity_id,
252                *to_entity_id,
253                *projectile_id,
254                *level,
255                projectile_data,
256                rand_gen,
257                current_tick,
258                state,
259            ),
260            OverlordEvent::Damage {
261                entity_id,
262                damage,
263                damage_data: _,
264            } => self.handle_damage(*entity_id, *damage, current_tick, rand_gen, state),
265            OverlordEvent::Heal { entity_id, heal } => self.handle_heal(*entity_id, *heal, state),
266            OverlordEvent::CounterAttack { .. } => self.handle_noop(state),
267            OverlordEvent::WaveCleared {} => self.handle_noop(state),
268            OverlordEvent::Multicast { .. } => self.handle_noop(state),
269            OverlordEvent::Evasion { .. } => self.handle_noop(state),
270            OverlordEvent::PlayerDeath {} => self.handle_player_death(state),
271            OverlordEvent::EntityDeath { entity_id, reward } => {
272                self.handle_entity_death(*entity_id, reward.clone(), rand_gen, state)
273            }
274            OverlordEvent::EntityIncrAttribute {
275                entity_id,
276                attribute,
277                delta,
278            } => self.handle_entity_incr_attribute(*entity_id, attribute, *delta, state),
279            OverlordEvent::EntityApplyEffect {
280                entity_id,
281                effect_id,
282            } => self.handle_entity_apply_effect(*entity_id, *effect_id, current_tick, state),
283            OverlordEvent::CastEffect {
284                entity_id,
285                effect_id,
286            } => {
287                self.handle_cast_effect(*entity_id, *effect_id, None, rand_gen, current_tick, state)
288            }
289            OverlordEvent::CastEffectFromEvent {
290                entity_id,
291                effect_id,
292                caller_event,
293            } => self.handle_cast_effect(
294                *entity_id,
295                *effect_id,
296                Some(caller_event.to_owned()),
297                rand_gen,
298                current_tick,
299                state,
300            ),
301            OverlordEvent::FightCustomEvent { .. } => self.handle_noop(state),
302            OverlordEvent::FightVisualEvent { .. } => self.handle_noop(state),
303            OverlordEvent::NewCharacterLevel { level } => {
304                self.handle_new_character_level(*level, state)
305            }
306
307            // Vassal links
308            OverlordEvent::ClaimSuzerainReward {} => self.handle_noop(state),
309            OverlordEvent::ClaimVassalReward { .. } => self.handle_noop(state),
310            OverlordEvent::NewSuzerain { new_suzerain } => {
311                self.handle_new_suzerain(new_suzerain.clone(), state)
312            }
313            OverlordEvent::RemoveVassal { vassal_id } => {
314                self.handle_remove_vassal(*vassal_id, state)
315            }
316
317            // Quests
318            OverlordEvent::ClaimQuest { quest_id } => {
319                self.handle_claim_quest(*quest_id, rand_gen, state)
320            }
321            OverlordEvent::NewQuests { quest_ids } => {
322                self.handle_new_quests(quest_ids.clone(), state)
323            }
324            OverlordEvent::UpdateActiveLoopTaskId { quest_id } => {
325                self.handle_update_active_loop_task_id(*quest_id, state)
326            }
327
328            OverlordEvent::ClaimQuestProgressionReward { quest_group_type } => {
329                self.handle_claim_quest_progression_reward(quest_group_type.to_owned(), state)
330            }
331
332            OverlordEvent::ResetRepeatingQuests { quest_ids } => {
333                self.handle_reset_repeating_quests(quest_ids.clone(), state)
334            }
335
336            // Vassal Tasks
337            OverlordEvent::GiveTask { .. } => self.handle_noop(state),
338            OverlordEvent::NewTask { new_task } => self.handle_new_task(new_task.clone(), state),
339            OverlordEvent::AcceptTask { task_id, is_good } => {
340                self.handle_accept_task(*task_id, *is_good, state)
341            }
342            OverlordEvent::TaskAccepted {
343                task_id,
344                started_good,
345                started_at,
346                finish_at,
347            } => self.handle_task_accepted(*task_id, *started_good, *started_at, *finish_at, state),
348            OverlordEvent::HitHands { task_id } => self.handle_hit_hands(*task_id, state),
349            OverlordEvent::HandsHitted { task_id } => self.handle_hands_hitted(*task_id, state),
350            OverlordEvent::ClaimTaskReward { task_id } => {
351                self.handle_claim_task_reward(*task_id, state)
352            }
353            OverlordEvent::TaskFinished { task_id } => self.handle_finish_task(*task_id, state),
354
355            // Resist Task
356            OverlordEvent::GiveResistTask { new_resist_task } => {
357                self.handle_give_resist_task(new_resist_task.clone(), state)
358            }
359            OverlordEvent::AcceptResistTask {} => self.handle_noop(state),
360            OverlordEvent::ResistTaskAccepted { new_resist_task } => {
361                self.handle_resist_task_accepted(new_resist_task.clone(), state)
362            }
363            OverlordEvent::CatchResistTask { task_id } => {
364                self.handle_catch_resist_task(*task_id, state)
365            }
366            OverlordEvent::ResistTaskCatched { task_id } => {
367                self.handle_resist_task_catched(*task_id, state)
368            }
369            OverlordEvent::ResistTaskFinished { task_id } => {
370                self.handle_resist_task_finished(*task_id, state)
371            }
372            OverlordEvent::ClaimResistTaskReward { task_id } => {
373                self.handle_claim_resist_task_reward(*task_id, state)
374            }
375
376            // Gifts
377            OverlordEvent::SendGift {
378                receiver_id,
379                config_gift_id,
380            } => self.handle_send_gift(*receiver_id, *config_gift_id, state),
381            OverlordEvent::NewGift { new_gift } => self.handle_new_gift(new_gift.clone(), state),
382            OverlordEvent::AcceptGift { gift_id } => self.handle_accept_gift(*gift_id, state),
383
384            // PVP
385            OverlordEvent::StartVassalPVPSync { .. } => self.handle_noop(state),
386            OverlordEvent::StartArenaPVPSync { .. } => self.handle_noop(state),
387            OverlordEvent::StartArenaRematchSync { .. } => self.handle_noop(state),
388            OverlordEvent::RefreshArenaMatchmaking {} => self.handle_noop(state),
389            OverlordEvent::BuyArenaTicket {} => self.handle_noop(state),
390
391            // Referral Rewards
392            OverlordEvent::ClaimReferralLvlUpReward { level } => {
393                self.handle_claim_referral_lvlup_reward(*level, state)
394            }
395            OverlordEvent::ClaimReferralDailyReward {} => {
396                self.handle_claim_referral_daily_reward(state)
397            }
398            OverlordEvent::PatronQuestCompleted { quest_id } => {
399                self.handle_patron_quest_completed(*quest_id, state)
400            }
401            OverlordEvent::HiddenQuestCompleted { quest_id } => {
402                self.handle_hidden_quest_completed(*quest_id, rand_gen, state)
403            }
404            OverlordEvent::QuestCompleted { .. } => self.handle_noop(state),
405            OverlordEvent::ReferralDailyRewardStatusUpdate {
406                referral_daily_reward_status,
407            } => self
408                .handle_referral_daily_reward_status_update(*referral_daily_reward_status, state),
409
410            // AutoChest
411            OverlordEvent::EnableAutoChest {} => self.handle_enable_auto_chest(state),
412            OverlordEvent::DisableAutoChest {} => self.handle_disable_auto_chest(state),
413
414            OverlordEvent::EnableAutoChestFilter { filter_id } => {
415                self.handle_enable_auto_chest_filter(*filter_id, state)
416            }
417            OverlordEvent::DisableAutoChestFilter {} => {
418                self.handle_disable_auto_chest_filter(state)
419            }
420
421            OverlordEvent::EnableAutoChestPowerCompare {} => {
422                self.handle_enable_auto_chest_power_compare(state)
423            }
424            OverlordEvent::DisableAutoChestPowerCompare {} => {
425                self.handle_disable_auto_chest_power_compare(state)
426            }
427
428            OverlordEvent::UpdateAutoChestBatchSize { batch_size } => {
429                self.handle_update_auto_chest_batch_size(*batch_size, state)
430            }
431
432            OverlordEvent::NewAutoChestFilter { filter } => {
433                self.handle_new_auto_chest_filter(filter.clone(), state)
434            }
435
436            OverlordEvent::UpdateAutoChestFilter { updated_filter } => {
437                self.handle_update_auto_chest_filter(updated_filter.clone(), state)
438            }
439
440            OverlordEvent::RemoveAutoChestFilter { filter_id } => {
441                self.handle_remove_auto_chest_filter(*filter_id, state)
442            }
443
444            // Technical
445            OverlordEvent::SetCustomValue { key, value } => {
446                self.handle_set_custom_value(key.clone(), *value, state)
447            }
448            OverlordEvent::SetConnectionStore { key, value } => {
449                self.handle_set_connection_store(key.clone(), *value, state)
450            }
451
452            // Ability Presets
453            OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
454            OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
455
456            // AfkReward
457            OverlordEvent::ClaimAfkReward {} => self.handle_noop(state),
458            OverlordEvent::AfkRewardClaimed {} => self.handle_noop(state),
459            OverlordEvent::AfkRewardsGatingUnlocked {} => {
460                self.handle_afk_rewards_gating_unlocked(state)
461            }
462            OverlordEvent::ClaimAfkInstantRewardGems {} => self.handle_noop(state),
463
464            // Bundles
465            OverlordEvent::ClaimBundleStepGeneric { .. } => self.handle_noop(state),
466            OverlordEvent::AddBundleGroup { bundle_ids, source } => {
467                self.handle_add_bundle_group(bundle_ids, *source, state)
468            }
469
470            // Classes
471            OverlordEvent::ChangeClass { .. } => self.handle_noop(state),
472
473            // User Account
474            OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
475            OverlordEvent::SetUsername { .. } => self.handle_noop(state),
476            OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
477
478            // Cheat
479            OverlordEvent::SetMaxHp {
480                entity_id,
481                new_max_hp,
482                new_hp,
483            } => self.handle_set_max_hp(*entity_id, *new_max_hp, *new_hp, state),
484
485            OverlordEvent::RunCheat { cheat } => {
486                self.handle_run_cheat(cheat, current_tick, rand_gen, state)
487            }
488
489            OverlordEvent::Error { .. } => self.handle_noop(state),
490            OverlordEvent::CustomRhai { .. } => self.handle_noop(state),
491
492            // Currencies — the event itself performs the state mutation
493            OverlordEvent::CurrencyIncrease { currencies, .. } => {
494                self.handle_currency_increase(currencies, state)
495            }
496            OverlordEvent::CurrencyDecrease { currencies, .. } => {
497                self.handle_currency_decrease(currencies, state)
498            }
499            // Skins
500            OverlordEvent::BuySkins { .. } => self.handle_noop(state),
501            OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
502
503            // Mails
504            OverlordEvent::ClaimMail { .. } => self.handle_noop(state),
505            OverlordEvent::ClaimAllMails {} => self.handle_noop(state),
506            OverlordEvent::ClaimAllQuests { .. } => self.handle_noop(state),
507            OverlordEvent::MakeRead { .. } => self.handle_noop(state),
508            OverlordEvent::MakeAllRead {} => self.handle_noop(state),
509            OverlordEvent::DeleteMail { .. } => self.handle_noop(state),
510            OverlordEvent::DeleteAllMails {} => self.handle_noop(state),
511            OverlordEvent::NewMail { new_mail } => self.handle_new_mail(new_mail.clone(), state),
512            // Offers
513            OverlordEvent::NewOffer { .. } => self.handle_noop(state),
514            OverlordEvent::BuyOffer { .. } => self.handle_noop(state),
515            OverlordEvent::ResetOffers { new_offers } => {
516                self.handle_reset_offers(new_offers, state)
517            }
518            OverlordEvent::OfferPurchaseCompleted { .. } => self.handle_noop(state),
519            OverlordEvent::OfferPurchaseFailed { .. } => self.handle_noop(state),
520            OverlordEvent::PurchasesBanned {} => self.handle_purchases_banned(state),
521
522            // Pets
523            OverlordEvent::EquipPet { slot_id, pet_id } => {
524                self.handle_equip_pet(*slot_id, *pet_id, current_tick, state)
525            }
526            OverlordEvent::UnequipPet { slot_id } => {
527                self.handle_unequip_pet(*slot_id, current_tick, state)
528            }
529            OverlordEvent::FastEquipPets {} => self.handle_noop(state),
530            OverlordEvent::EquipPets { equipped_pets } => {
531                self.handle_equip_pets(equipped_pets.clone(), current_tick, state)
532            }
533            OverlordEvent::UpgradePet { .. } => self.handle_noop(state),
534            OverlordEvent::UpgradeAllPets {} => self.handle_noop(state),
535            OverlordEvent::UpgradedPets { .. } => self.handle_noop(state),
536            OverlordEvent::UpgradePetSlot { .. } => self.handle_noop(state),
537
538            // Pet Gacha
539            OverlordEvent::OpenPetCase { .. } => self.handle_noop(state),
540            OverlordEvent::SetPetGachaWishlist { .. } => self.handle_noop(state),
541            OverlordEvent::PetCaseOpened { .. } => self.handle_noop(state),
542            OverlordEvent::NewPets { .. } => self.handle_noop(state),
543            OverlordEvent::UpgradePetCase {} => self.handle_noop(state),
544
545            // Tutorial
546            OverlordEvent::TutorialShown { .. } => self.handle_noop(state),
547            OverlordEvent::TutorialStepCompleted { step_number } => {
548                self.handle_tutorial_step_completed(*step_number, state)
549            }
550            OverlordEvent::ClientLifecycle { .. } => self.handle_noop(state),
551
552            // Party
553            OverlordEvent::AddCharacterToParty { .. } => self.handle_noop(state),
554            OverlordEvent::RemoveCharacterFromParty {} => self.handle_noop(state),
555            OverlordEvent::RefreshPartyPlayers {} => self.handle_noop(state),
556            OverlordEvent::RefreshPartyMemberState {} => self.handle_noop(state),
557
558            // Talent Tree
559            OverlordEvent::StartTalentResearch { .. } => self.handle_noop(state),
560            OverlordEvent::TalentResearchStarted { .. } => self.handle_noop(state),
561            OverlordEvent::SpeedupTalentResearch {} => self.handle_noop(state),
562            OverlordEvent::SkipTalentResearch {} => self.handle_noop(state),
563            OverlordEvent::ClaimTalentResearch {} => self.handle_claim_talent_research(state),
564
565            // Statue
566            OverlordEvent::StatueRoll { .. } => self.handle_noop(state),
567            OverlordEvent::StatueActivateSet { .. } => self.handle_noop(state),
568            OverlordEvent::StatueAddSet {} => self.handle_noop(state),
569            OverlordEvent::StatueRenameSet { .. } => self.handle_noop(state),
570            OverlordEvent::StatueLockSlot { .. } => self.handle_noop(state),
571            OverlordEvent::StatueRollNewSlot { .. } => self.handle_noop(state),
572            OverlordEvent::UserRating { .. } => self.handle_noop(state),
573            OverlordEvent::WatchAd { .. } => self.handle_noop(state),
574            OverlordEvent::ShowBird { .. } => self.handle_show_bird(state),
575            OverlordEvent::BirdShown {} => self.handle_bird_shown(state),
576            OverlordEvent::ResetAdUsage { placements } => {
577                self.handle_reset_ad_usage(placements, state)
578            }
579            OverlordEvent::ResetInstantRewardGemsPressCount {} => {
580                self.handle_reset_instant_reward_gems_press_count(state)
581            }
582        };
583
584        if result.success() {
585            let (state, events) = result.state_and_events_mut();
586            self.update_quests_progress(state, events, event);
587            self.try_give_new_offers(state, events, event);
588        }
589
590        result.events_mut().append(&mut events);
591        result
592    }
593
594    fn compute_fields(
595        &self,
596        state: &mut OverlordState,
597        prev_state: &OverlordState,
598    ) -> Vec<OverlordEvent> {
599        let _span = tracing::info_span!("compute_fields").entered();
600        let start = std::time::Instant::now();
601        let game_config = self.game_config.get();
602        let mut events = Vec::new();
603        self.log_currency_change_metrics(state, prev_state);
604
605        if state.character_state.character.character_experience
606            != prev_state.character_state.character.character_experience
607        {
608            events.append(&mut self.compute_character_level(state));
609        }
610
611        if state.character_state != prev_state.character_state {
612            match attributes::calculate_player_entity_stats_with_zeroes(
613                &EntityState::Character(&state.character_state),
614                &game_config,
615                &self.script_runner,
616            ) {
617                Ok(attributes) => {
618                    state.character_state.player_attributes = attributes.attributes.clone();
619                    state.character_state.player_attributes.remove_zeroes();
620
621                    if let Some(fight) = &mut state.active_fight
622                        && let Some(entity) = fight
623                            .entities
624                            .iter_mut()
625                            .find(|ent| ent.id == fight.player_id)
626                    {
627                        entity.max_hp = attributes.max_hp;
628                        // This rewrites old attributes with new ones, keeping custon rhai attributes
629                        entity
630                            .attributes
631                            .0
632                            .extend(attributes.attributes.0.iter().map(|(k, v)| (k.clone(), *v)));
633                        entity.attributes.remove_zeroes();
634                    }
635                }
636                Err(err) => {
637                    tracing::error!("Failed to fill player entity attributes: {:?}", err);
638                }
639            }
640
641            if let Ok(power) = self
642                .script_runner
643                .calculate_character_power(&state.character_state, &game_config)
644            {
645                state.character_state.character.power = power;
646            }
647        }
648
649        self.compute_fields_duration
650            .record(start.elapsed().as_secs_f64(), &[]);
651
652        events
653    }
654}
655
656impl OverlordAsyncEventHandler {
657    pub fn new(game_config: configs::SharedGameConfig, frontend: bool) -> Self {
658        let game_config_for_runner = game_config.get();
659        let script_runner = ScriptRunner::new(&game_config_for_runner);
660        let meter = opentelemetry::global::meter("compute_fields");
661        Self {
662            game_config,
663            script_runner,
664            frontend,
665            start_fight_tick: 0,
666            last_currency_source: None,
667            compute_fields_duration: meter
668                .f64_histogram("compute_fields_duration_seconds")
669                .with_boundaries(vec![
670                    0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
671                ])
672                .build(),
673        }
674    }
675
676    /// Sets the source label used by `log_currency_change_metrics` for the next
677    /// `compute_fields` diff. Use when state mutations happen outside the normal
678    /// `CurrencyIncrease` / `CurrencyDecrease` event flow (e.g., starter bundle
679    /// seeding on character creation).
680    pub fn set_last_currency_source(&mut self, source: Option<CurrencySource>) {
681        self.last_currency_source = source.map(|s| format!("{s:?}"));
682    }
683
684    fn handle_set_custom_value(
685        &self,
686        key: String,
687        value: i64,
688        mut state: OverlordState,
689    ) -> EventHandleResult<OverlordEvent, OverlordState> {
690        state
691            .character_state
692            .character
693            .custom_values
694            .insert(&key, value);
695        EventHandleResult::ok(state)
696    }
697
698    fn handle_set_connection_store(
699        &self,
700        key: String,
701        value: i64,
702        mut state: OverlordState,
703    ) -> EventHandleResult<OverlordEvent, OverlordState> {
704        state.connection_store.insert(key, value);
705        EventHandleResult::ok(state)
706    }
707
708    fn handle_new_character_level(
709        &self,
710        level: i64,
711        mut state: OverlordState,
712    ) -> EventHandleResult<OverlordEvent, OverlordState> {
713        state.character_state.character.character_level = level;
714
715        EventHandleResult::ok(state)
716    }
717
718    fn handle_purchases_banned(
719        &self,
720        mut state: OverlordState,
721    ) -> EventHandleResult<OverlordEvent, OverlordState> {
722        state.character_state.character.purchases_banned = true;
723        EventHandleResult::ok(state)
724    }
725
726    fn handle_new_mail(
727        &self,
728        new_mail: Mail,
729        mut state: OverlordState,
730    ) -> EventHandleResult<OverlordEvent, OverlordState> {
731        if !state.incoming_mails.contains(&new_mail) {
732            state.incoming_mails.push(new_mail);
733        }
734
735        EventHandleResult::ok(state)
736    }
737
738    fn handle_noop(&self, state: OverlordState) -> EventHandleResult<OverlordEvent, OverlordState> {
739        EventHandleResult::ok(state)
740    }
741
742    fn handle_tutorial_step_completed(
743        &self,
744        step_number: i16,
745        mut state: OverlordState,
746    ) -> EventHandleResult<OverlordEvent, OverlordState> {
747        if !state
748            .character_state
749            .character
750            .completed_tutorials
751            .contains(&step_number)
752        {
753            state
754                .character_state
755                .character
756                .completed_tutorials
757                .push(step_number);
758        } else {
759            tracing::error!("Provided step {step_number} is already in state");
760        }
761        EventHandleResult::ok(state)
762    }
763}
764
765impl OverlordAsyncEventHandler {
766    fn log_currency_change_metrics(&self, state: &OverlordState, prev_state: &OverlordState) {
767        let character_id = state.character_state.character.id;
768        let current_currencies = &state.character_state.currencies;
769        let prev_currencies = &prev_state.character_state.currencies;
770        let source = self.last_currency_source.as_deref().unwrap_or("unknown");
771
772        for current_currency in current_currencies {
773            let amount_before = prev_currencies
774                .iter()
775                .find(|unit| unit.currency_id == current_currency.currency_id)
776                .map_or(0, |unit| unit.amount);
777            let delta = current_currency.amount - amount_before;
778            let amount_after = current_currency.amount;
779            if delta > 0 {
780                tracing::info!(
781                    target: METRICS_TARGET,
782                    character_id = %character_id,
783                    event_type = "increase_currency",
784                    currency_id = %current_currency.currency_id,
785                    amount = delta,
786                    amount_before,
787                    amount_after,
788                    source = %source,
789                    "Add currency",
790                );
791            } else if delta < 0 {
792                tracing::info!(
793                    target: METRICS_TARGET,
794                    character_id = %character_id,
795                    event_type = "decrease_currency",
796                    currency_id = %current_currency.currency_id,
797                    amount = -delta,
798                    amount_before,
799                    amount_after,
800                    source = %source,
801                    "Decrease currency",
802                );
803            }
804        }
805
806        for prev_currency in prev_currencies {
807            if current_currencies
808                .iter()
809                .any(|unit| unit.currency_id == prev_currency.currency_id)
810            {
811                continue;
812            }
813
814            // Currency disappeared from state: emit a decrease whose
815            // post-balance is 0 (entry removed). Pre-balance is the value the
816            // prev state still carried.
817            let delta = -prev_currency.amount;
818            if delta > 0 {
819                tracing::info!(
820                    target: METRICS_TARGET,
821                    character_id = %character_id,
822                    event_type = "increase_currency",
823                    currency_id = %prev_currency.currency_id,
824                    amount = delta,
825                    amount_before = prev_currency.amount,
826                    amount_after = prev_currency.amount + delta,
827                    source = %source,
828                    "Add currency",
829                );
830            } else if delta < 0 {
831                tracing::info!(
832                    target: METRICS_TARGET,
833                    character_id = %character_id,
834                    event_type = "decrease_currency",
835                    currency_id = %prev_currency.currency_id,
836                    amount = -delta,
837                    amount_before = prev_currency.amount,
838                    amount_after = 0_i64,
839                    source = %source,
840                    "Decrease currency",
841                );
842            }
843        }
844    }
845
846    pub fn compute_character_level(&self, state: &mut OverlordState) -> Vec<OverlordEvent> {
847        let game_config = self.game_config.get();
848        let current_level = game_config
849            .require_character_level(state.character_state.character.character_level)
850            .unwrap_or_else(|err| panic!("{err:?}"));
851
852        let new_level = game_config
853            .character_levels
854            .iter()
855            .fold(current_level, |mut acc, x| {
856                if x.level > acc.level
857                    && state.character_state.character.character_experience >= x.required_experience
858                {
859                    acc = x;
860                }
861                acc
862            });
863
864        if new_level.level != current_level.level {
865            vec![OverlordEvent::NewCharacterLevel {
866                level: new_level.level,
867            }]
868        } else {
869            Vec::new()
870        }
871    }
872
873    fn try_give_new_offers(
874        &self,
875        state: &mut OverlordState,
876        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
877        trigger_event: &OverlordEvent,
878    ) {
879        for offer in &self.game_config.get().offers_templates {
880            if !offer.enabled {
881                continue;
882            }
883
884            if offer.limit_of_buys.is_some_and(|limit| {
885                state
886                    .offers_info
887                    .offer_buy_counts
888                    .get(&offer.id)
889                    .copied()
890                    .unwrap_or(0)
891                    >= limit as u32
892            }) {
893                continue;
894            }
895
896            if state
897                .offers_info
898                .active_offers
899                .iter()
900                .any(|x| x.template_id == offer.id)
901            {
902                continue;
903            }
904
905            if !offer.events_subscribe.contains(&trigger_event.to_string()) {
906                continue;
907            }
908
909            let should_give = match self.should_give_new_offer(
910                &state.character_state,
911                offer,
912                &offer.trigger_script,
913                trigger_event.clone(),
914            ) {
915                Ok(progress) => progress,
916                Err(e) => {
917                    tracing::error!(
918                        "Failed determining for offer id: {}\n Error: {e:?}",
919                        offer.id
920                    );
921                    continue;
922                }
923            };
924
925            if should_give {
926                events.push(EventPluginized::now(OverlordEvent::NewOffer {
927                    offer_template_id: offer.id,
928                }));
929            }
930        }
931    }
932
933    fn should_give_new_offer(
934        &self,
935        character_state: &CharacterState,
936        offer: &OfferTemplate,
937        script: &str,
938        trigger_event: OverlordEvent,
939    ) -> anyhow::Result<bool> {
940        self.script_runner.run_expression::<bool>(
941            |mut scope_setter| {
942                trigger_event.add_event_to_scope(&mut scope_setter, "Event");
943                scope_setter.set_const("CharacterState", character_state.clone());
944                scope_setter.set_const("Offer", offer.clone());
945                scope_setter
946            },
947            script,
948        )
949    }
950
951    fn update_quests_progress(
952        &self,
953        state: &mut OverlordState,
954        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
955        trigger_event: &OverlordEvent,
956    ) {
957        let game_config = self.game_config.get();
958        let all_active_quests = state.quest_groups.get_not_claimed_quests_mut();
959        let active_fight = &state.active_fight;
960
961        for active_quest in all_active_quests {
962            let Some(quest_template) = game_config.quest(active_quest.id) else {
963                continue;
964            };
965
966            if state.patron.is_none()
967                && (quest_template.quest_group_type == QuestGroupType::PatronLifetime
968                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
969            {
970                continue;
971            }
972
973            if !quest_template
974                .events_subscribe
975                .contains(&trigger_event.to_string())
976            {
977                continue;
978            }
979
980            if quest_template.quest_group_type == QuestGroupType::LoopTask
981                && !quest_template.progress_if_inactive
982                && let Some(active_loop_task_id) =
983                    state.character_state.character.active_loop_task_id
984                && active_loop_task_id != quest_template.id
985            {
986                continue;
987            }
988
989            let was_completed = active_quest.is_completed(quest_template.progress_target);
990
991            if !active_quest.is_completed(quest_template.progress_target) {
992                let progress = match self.get_quest_progress(
993                    &state.character_state,
994                    active_fight,
995                    active_quest,
996                    &quest_template.progress_script,
997                    trigger_event.clone(),
998                ) {
999                    Ok(progress) => progress,
1000                    Err(e) => {
1001                        tracing::error!(
1002                            "Failed updating quest progress for quest id: {}\n Error: {e:?}",
1003                            active_quest.id
1004                        );
1005                        continue;
1006                    }
1007                };
1008                active_quest.current = progress;
1009            }
1010
1011            if !was_completed {
1012                if (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1013                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1014                    && active_quest.is_completed(quest_template.progress_target)
1015                {
1016                    events.push(EventPluginized::now(OverlordEvent::PatronQuestCompleted {
1017                        quest_id: active_quest.id,
1018                    }));
1019                }
1020
1021                if (quest_template.quest_group_type != QuestGroupType::Hidden)
1022                    && active_quest.is_completed(quest_template.progress_target)
1023                {
1024                    events.push(EventPluginized::now(OverlordEvent::QuestCompleted {
1025                        quest_id: active_quest.id,
1026                    }));
1027                }
1028            }
1029
1030            if (quest_template.quest_group_type == QuestGroupType::Hidden)
1031                && active_quest.is_completed(quest_template.progress_target)
1032            {
1033                events.push(EventPluginized::now(OverlordEvent::HiddenQuestCompleted {
1034                    quest_id: active_quest.id,
1035                }));
1036            }
1037        }
1038    }
1039
1040    pub fn get_quest_progress(
1041        &self,
1042        character_state: &CharacterState,
1043        active_fight: &Option<ActiveFight>,
1044        quest: &QuestInstance,
1045        script: &str,
1046        trigger_event: OverlordEvent,
1047    ) -> anyhow::Result<i64> {
1048        self.script_runner.run_expression::<i64>(
1049            |mut scope_setter| {
1050                trigger_event.add_event_to_scope(&mut scope_setter, "Event");
1051                scope_setter.set_const("CharacterState", character_state.clone());
1052                if let Some(fight) = active_fight.as_ref() {
1053                    scope_setter.set_const("ActiveFight", fight.clone());
1054                }
1055                scope_setter.set_const("Quest", quest.clone());
1056                scope_setter
1057            },
1058            script,
1059        )
1060    }
1061
1062    fn handle_currency_increase(
1063        &self,
1064        currencies: &[CurrencyUnit],
1065        mut state: OverlordState,
1066    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1067        increase_currencies(&mut state.character_state.currencies, currencies);
1068        EventHandleResult::ok(state)
1069    }
1070
1071    fn handle_currency_decrease(
1072        &self,
1073        currencies: &[CurrencyUnit],
1074        mut state: OverlordState,
1075    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1076        let non_zero: Vec<_> = currencies
1077            .iter()
1078            .filter(|c| c.amount > 0)
1079            .cloned()
1080            .collect();
1081        if let Err(e) = decrease_currencies(&mut state.character_state.currencies, &non_zero) {
1082            tracing::error!("CurrencyDecrease failed: {e}");
1083            return EventHandleResult::fail(state);
1084        }
1085        EventHandleResult::ok(state)
1086    }
1087
1088    /// Creates a `CurrencyIncrease` event. The actual state mutation happens when the
1089    /// event is processed by the handler — callers don't need to mutate state manually.
1090    pub fn currency_increase(
1091        currencies: &[CurrencyUnit],
1092        currency_source: CurrencySource,
1093    ) -> EventPluginized<OverlordEvent, OverlordState> {
1094        EventPluginized::now(OverlordEvent::CurrencyIncrease {
1095            currencies: currencies.to_owned(),
1096            currency_source,
1097        })
1098    }
1099
1100    /// Creates a `CurrencyDecrease` event after validating sufficient balance.
1101    /// Returns `None` if the player doesn't have enough currency.
1102    /// The actual state mutation happens when the event is processed by the handler.
1103    pub fn currency_decrease(
1104        state: &OverlordState,
1105        currencies: &[CurrencyUnit],
1106        currency_consumer: CurrencyConsumer,
1107    ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
1108        if !check_can_decrease_currencies(&state.character_state.currencies, currencies) {
1109            tracing::error!(
1110                "currency_decrease({currency_consumer:?}): not enough currency, \
1111                 required={currencies:?}, available={:?}",
1112                state.character_state.currencies
1113            );
1114            return None;
1115        }
1116        Some(EventPluginized::now(OverlordEvent::CurrencyDecrease {
1117            currencies: currencies.to_owned(),
1118            currency_consumer,
1119        }))
1120    }
1121
1122    pub fn process_currency(
1123        &self,
1124        raw_item: &BundleRawStep,
1125        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1126    ) -> BundleElement {
1127        let currency_unit = from_es_currencies(
1128            &script_runner.run_currencies_calculate(|scope| scope, &raw_item.script),
1129        );
1130
1131        BundleElement::Currencies(currency_unit)
1132    }
1133
1134    pub fn process_ability(
1135        &self,
1136        raw_item: &BundleRawStep,
1137        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1138        config: &GameConfig,
1139    ) -> BundleElement {
1140        let ability_shards =
1141            script_runner.run_ability_shards_calculate(|scope| scope, &raw_item.script);
1142
1143        let abilities = ability_shards
1144            .iter()
1145            .filter_map(|ability_shard| {
1146                let Some(template) = config.ability_template(ability_shard.ability_id).cloned()
1147                else {
1148                    tracing::error!(
1149                        "Failed to get ability with ability_id={}",
1150                        ability_shard.ability_id
1151                    );
1152                    return None;
1153                };
1154
1155                Some(BundleAbility {
1156                    template,
1157                    shards_amount: ability_shard.shards_amount,
1158                })
1159            })
1160            .collect();
1161
1162        BundleElement::Abilities(abilities)
1163    }
1164
1165    pub fn process_item(
1166        &self,
1167        raw_item: &BundleRawStep,
1168        character_level: i64,
1169        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1170        config: &GameConfig,
1171    ) -> anyhow::Result<BundleElement> {
1172        let mut rng = StdRng::seed_from_u64(OsRng.try_next_u64()?);
1173
1174        let item_ids = script_runner.run_item_ids_script(|scope| scope, &raw_item.script);
1175
1176        let items: Vec<Item> = item_ids
1177            .iter()
1178            .filter_map(|&item_id| {
1179                let Some(template) = config.item_template(item_id) else {
1180                    tracing::error!("Failed to get ability with item_id={}", item_id);
1181                    return None;
1182                };
1183
1184                let Some(rarity) = config.item_rarity(template.rarity_id).cloned() else {
1185                    tracing::error!("Failed to get item rarity with id={}", template.rarity_id);
1186                    return None;
1187                };
1188
1189                Some(generate_item_from_template(
1190                    template,
1191                    rarity,
1192                    character_level,
1193                    config,
1194                    &mut rng,
1195                ))
1196            })
1197            .collect();
1198
1199        let finalized_items = items
1200            .into_iter()
1201            .filter_map(
1202                |mut item| match try_finalize_item(&mut item, config, script_runner) {
1203                    Ok(()) => Some(item),
1204                    Err(e) => {
1205                        tracing::error!("Failed to finalize item: {}", e);
1206                        None
1207                    }
1208                },
1209            )
1210            .collect();
1211
1212        Ok(BundleElement::Items(finalized_items))
1213    }
1214
1215    fn handle_reset_ad_usage(
1216        &mut self,
1217        placements: &[AdPlacement],
1218        mut state: OverlordState,
1219    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1220        let now = ::time::utc_now();
1221        for placement in placements {
1222            state.character_state.ad_usage.insert(
1223                *placement,
1224                AdUsageData {
1225                    daily_count: 0,
1226                    last_reset_at: now,
1227                },
1228            );
1229        }
1230
1231        EventHandleResult::ok(state)
1232    }
1233
1234    fn handle_reset_instant_reward_gems_press_count(
1235        &mut self,
1236        mut state: OverlordState,
1237    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1238        state
1239            .character_state
1240            .character
1241            .instant_reward_gems_press_count = 0;
1242        EventHandleResult::ok(state)
1243    }
1244
1245    fn handle_show_bird(
1246        &mut self,
1247        mut state: OverlordState,
1248    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1249        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1250        let cooldown_until = ::time::utc_now()
1251            + chrono::TimeDelta::seconds(bird_config.post_show_cooldown_sec as i64);
1252        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1253        EventHandleResult::ok(state)
1254    }
1255
1256    fn handle_bird_shown(
1257        &mut self,
1258        mut state: OverlordState,
1259    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1260        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1261        let cooldown_until =
1262            ::time::utc_now() + chrono::TimeDelta::seconds(bird_config.cooldown_sec as i64);
1263        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1264        EventHandleResult::ok(state)
1265    }
1266}