Compare commits

..

58 Commits

Author SHA1 Message Date
Chris Miles 1ce51ca3b0 [Release] 22.58.0 (#4532) 2024-11-05 22:02:32 -06:00
hg 25ef3d2cdb [Code] Update perlbind to 1.1.0 (#4529)
- Adds a perl::ref alias for perl::reference

- Optimizes array return pushes by accessing SV* values directly instead
  of using operator[] scalar_proxy

- Allows functions with a perl::hash parameter to accept hashes with any
  scalar key type instead of requiring explicit string keys

  e.g., foo(123 => 1) will now work on functions accepting a perl::hash
2024-11-05 20:14:29 -06:00
hg 95249889a6 [Code] Add mysql prepared statement support (#4530)
This adds support for using prepared statements for MySQL queries. It is
intended for use in a database quest API but it can be used in source
with some caveats:

 - It uses exceptions for error handling instead of returning a fake
   result that needs checked. Usage must be wrapped in try/catch.

 - DBcore has a connection mutex which indicates the connection might be
   shared with other threads. This mutex is locked for certain stmt
   operations in an attempt to make it safe to use with multi threaded
   connections.

 - Prepared statements should only be used on the main thread since the
   internal logging is not synchronized.

 - Unlike the current query API which retrieves all results as strings,
   results are stored in buffers that represent the db field type.
   Getter functions are available to retrieve values as desired types.
2024-11-05 20:12:17 -06:00
mmcgarvey 428cccfa50 [Feature] Focus Skill Attack Spells (#4528)
* Add Rule Spells:AllowFocusOnSkillDamageSpells

* Currently, focus mods defaults to 0 when processing spell effect 193.
* The default value for this rule is false.
* When false, the rule will retain the current default behavior.
* When true, the aforementioned focus effects will allow focus effects (185, 459, and 482) to modify spell effect 193.

* Removed undesirable whitespace

* Update spell_effects.cpp

---------

Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com>
2024-10-31 08:13:16 -04:00
Alex King 41dd8a5754 [Quest API] Add Spawn Circle/Grid Methods to Perl/Lua (#4524)
* [Quest API] Add Spawn Circle/Grid Methods to Perl/Lua

* Update lua_general.cpp

* Update questmgr.cpp

* Update questmgr.cpp
2024-10-23 23:40:25 -04:00
Alex King d02d766563 [Bug Fix] Fix cross_zone_set_entity_variable_by_char_id in Lua (#4526) 2024-10-23 22:47:02 -04:00
Alex King dfd2729b28 [Bug Fix] Add Missing Lua Registers (#4525) 2024-10-23 22:37:21 -04:00
Chris Miles b92eafd21b [Release] 22.57.1 (#4523) 2024-10-22 00:02:14 -05:00
nytmyr d6d5d992cb [Bots] Fix pet buffs from saving duplicates every save (#4520)
* [Bots] Fix pet buffs from saving duplicates every save

Previously we were not checking the pet index properly when clearing buffs in the database before saving which resulted in no prior data being deleted.

This corrects the logic for the save and also will clean up any buffs for pets that don't exist in the table.

* Changes

* Update world_boot.cpp

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-10-21 23:57:42 -05:00
Alex King d524cb6a5a [Bots] Enable Bot Commands Only if Rule Enabled (#4519) 2024-10-21 23:49:36 -05:00
Alex e6469878ce [Loginserver] Automatifc Opcode File Creation (#4521)
* Loginserver will auto create the opcodes file if it doesn't exist on load.

* Use path manager in login opcodes.

---------

Co-authored-by: KimLS <KimLS@peqtgc.com>
2024-10-21 23:48:43 -05:00
Chris Miles 9583099ace [Release] 22.57.0 (#4517) 2024-10-20 16:17:15 -05:00
nytmyr cf3483b402 [Bots] Fix timers loading on spawn and zone (#4516)
Timers were not properly checking their expiration time on spawn and load and could cause invalid timers to load if the server was restarted resulting in improper lockouts.
2024-10-20 10:44:30 -04:00
carolus21rex 311af7bbe9 [Cleanup] Fixed a typo in Zoning.cpp (#4515)
* Fixed a typo in Zoning.cpp changed reguest to request.

* Update zoning.cpp

---------

Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com>
2024-10-19 21:59:10 -04:00
Alex King be42b73f5c [Rules] Add Rule to disable PVP Regions (#4513) 2024-10-17 01:48:19 -05:00
Mitch Freeman f76c798910 [BugFix] Fix a display error regarding a few trader/buyer query errors (#4514) 2024-10-17 01:43:24 -05:00
Alex ae198ae043 [Crash] Fixes a crash when the faction_list db table is empty. (#4511)
Co-authored-by: KimLS <KimLS@peqtgc.com>
2024-10-13 20:50:28 -05:00
Alex King 520943ebf1 [Logs] Add NPC Trades to Player Events (#4505)
* [Logs] Add NPC Trades to Player Events

* Update player_event_discord_formatter.cpp

* Push

* Fix money and add NPC info

* [Logs] Add NPC Trades to Player Events

* Update player_event_discord_formatter.cpp

* Push

* Minor logic fix

* Push

* Update perl_client.cpp

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-10-13 17:26:10 -05:00
Fryguy 9ac306fe67 [Bug] FindBestZ selecting false zone floor as bestz - Results in roambox failures (#4504)
Added underworld checks per the EQMac project
2024-10-13 15:53:09 -05:00
Alex King 7a1d69d0d4 [Bug Fix] Fix Spells:DefaultAOEMaxTargets Default Value (#4508) 2024-10-12 14:32:40 -04:00
Alex King c873fe5a22 [Bug Fix] Fix Mercenary Encounter Crash (#4509) 2024-10-11 23:00:09 -04:00
Fryguy e06b0c4b0c [Bug Fix] Master of Disguise should apply to illusions casted by others. (#4506)
Many era comments outline how Master of Disguise would apply to Project Illusion spells on you:

https://thesafehouse.org/forums/forum/everquest-wing/main-lounge/14249-new-aa-master-of-disguise/page4

https://thesafehouse.org/forums/forum/everquest-wing/training-studios/18143-master-of-disguise-broken

```
Im not a big fan of wolf form, but having a 1200 min NDT is pretty nice  I also agree its great to shrink on a raid once and not have to worry about it. 7 aa is a little steep imho, but with a name change and some frog potions, I may reapply to my guild as the servers only froggy rogue /cackle.
```

```
share form of the great wolf gave a 1500min timer.
```
2024-10-11 13:39:36 -04:00
catapultam-habeo ed2130f649 [Bug Fix] Correctly limit max targets of PBAOE (#4507)
* fix pbaoe max targets incorrectly set

* fix scratch copy
2024-10-11 13:15:19 -04:00
Alex King 448a33a60c [Quest API] Add Scripting Support to Mercenaries (#4500)
* [Quest API] Add Scripting Support to Mercenaries

* Cleanup

* Cleanup

* Update lua_merc.h

* Update mob.cpp

* XYZH

* Final

* Update attack.cpp

* Update attack.cpp

* Simplify event invocation

* Inline example

* Nullptr init example

* EVENT_TIMER simplify add EventPlayerNpcBotMerc

* EVENT_TIMER_START

* Remove has_start_event

* EVENT_TIMER_START with settimerMS

* EVENT_POPUP_RESPONSE

* Consolidation

* Update attack.cpp

* Push

* Update quest_parser_collection.h

* Comments

* Cleanup per comments

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-10-10 21:29:29 -04:00
Fryguy 8f86cb353e [Bug Fix] Spells - Self Only (Yellow) cast when non group member is targeted (#4503)
* [Bug Fix] Spells - Self Only (Yellow) cast when non group member is targeted

When using a Yellow gem invis spell, it should cast on yourself regardless of the targetted entity.

* Update spells.cpp

---------

Co-authored-by: Alex King <89047260+Kinglykrab@users.noreply.github.com>
2024-10-10 21:27:49 -04:00
Alex 178129443f [Loginserver] Login Fatal Error Spamming (#4476)
Co-authored-by: KimLS <KimLS@peqtgc.com>
2024-10-09 02:15:49 -05:00
Alex King a7c3b41afc [Quest API] Add Buff Fade Methods to Perl/Lua (#4501)
* [Quest API] Add Buff Fade Methods to Perl/Lua

* BuffFadeSongs()
2024-10-09 02:12:33 -05:00
Alex King a5a568d548 [Bug Fix] Fix character_exp_modifiers Default Values (#4502) 2024-10-09 02:11:57 -05:00
Alex King e3198edb86 [Quest API] Add EVENT_READ_ITEM to Perl/Lua (#4497)
* [Quest API] Add EVENT_READ_ITEM to Perl/Lua

* Add item_id export

* Add item export.

* Update client.cpp
2024-10-08 18:25:14 -04:00
Alex King 8568cf7d49 [Bug Fix] Fix NPC::CanTalk() Crash (#4499)
* [Bug FIx] Fix NPC::CanTalk() Crash

* Update npc.cpp

* Update mob.cpp

* Update npc.cpp
2024-10-07 00:17:49 -05:00
Alex King 1fb7a860a1 [Bug Fix] Fix #set motd Crash (#4495) 2024-10-05 07:58:22 -05:00
nytmyr 7eaee2649e [Bots] Add "silent" option to ^spawn and mute raid spawn (#4494)
When zoning or forming a raid, bots would spam their spawn message. They will now be muted.

Adds an optional argument "silent" to the ^spawn command. This will bypass ^oo spawnmessage settings and not send a spawn message. Example: ^spawn Warbot silent
2024-10-04 20:20:52 -04:00
Alex King a17f467b98 [Quest API] Add NPC List Filter Methods to Perl/Lua (#4493)
* [Quest API] Add GetNPCsByNPCIDs to Perl/Lua

* Push

* Update entity.cpp

* Separate methods.
2024-10-03 20:28:57 -04:00
Alex King 3359839a9b [Bug Fix] Fix Targeted AOE Max Targets Rule (#4488) 2024-10-02 20:25:35 -05:00
Alex 7e51e629f9 [Loginserver] Larion loginserver support (#4492)
* Add larion version and opcode path

* WIP: getting server to work

* Identify server_id

* Add missing opcode, add opcodes file

---------

Co-authored-by: KimLS <KimLS@peqtgc.com>
2024-10-02 20:20:13 -05:00
Alex King dc6c28a52d [Cleanup] Remove Extra Skill in EQ::skills::GetExtraDamageSkills() (#4486) 2024-10-02 20:07:19 -05:00
Alex King 78aee0780a [Bug Fix] Fix Group ID 0 in Group::SaveGroupLeaderAA() (#4487) 2024-10-02 20:06:56 -05:00
Chris Miles bcd943a964 [Code Cleanup] Optimization Code Cleanup (#4489)
* Initial push

* More

* More

* Further simplify

* More cleanup

* More consolidation

* Fix

* Update

* Update npc.cpp

---------

Co-authored-by: Kinglykrab <kinglykrab@gmail.com>
2024-09-30 18:34:42 -04:00
nytmyr 56608e84bd [Bots] Add attack flag when told to attack (#4490)
This adds a flag to mobs that are told to attack by their owner to prevent unintended attacks.

Previously, if you were to send your bots to attack a target and then switch targets: before casters land their spell or if melee (especially anyone with pets)  hasn't engaged before the target switch, they could switch to your new target and attack.

This adds a flag upon attack and bots will only attack flagged targets.
2024-09-29 17:59:26 -04:00
regneq 8d23e710ce [Bug Fix] fixed a bug where it would use npc value instead of faction value in the database. (#4491) 2024-09-29 17:42:43 -04:00
Morzain 4d11077b21 [Bug Fix] Add character_instance_safereturns to tables_to_zero_id (#4485)
Co-authored-by: morzain <morzain@users.noreply.github.com>
2024-09-26 18:14:32 -04:00
Alex King 5c0bdfdc4c [Bug Fix] Fix issue with Client::SaveDisciplines() not specifying character ID (#4481) 2024-09-23 23:00:52 -05:00
Alex King 6130e10831 [Release] 22.56.2 (#4480) 2024-09-19 21:59:53 -05:00
Alex King c3e1c531d2 [Bug Fix] Fix Issue with Database::ReserveName (#4477) 2024-09-19 21:15:14 -05:00
Alex King b52719a535 [Quest API] Add GrantAllAAPoints() Overload To Perl/Lua (#4474) 2024-09-19 21:09:24 -05:00
Alex King 1af252466f [Bug Fix] Fix Untrained Disciplines in Client::SaveDisciplines() (#4472)
* [Bug Fix] Fix Untrained Disciplines in Client::SaveDisciplines()

* [Bug Fix] Fix Infinite Loop in Adventure::Finished() (#4473)

Fix infinite loop condition when bot encountered

* [Bug Fix] Fix Untrained Disciplines in Client::SaveDisciplines()

* Change to release

---------

Co-authored-by: oddx2k <103136558+oddx2k@users.noreply.github.com>
2024-09-19 21:09:09 -05:00
catapultam-habeo 699d22fc28 [Bug Fix] Fix 'Teleport Doors' from being blocked by GM flag (#4475)
* gm flag blocks teleport doors with keys from working instead of allowing them to work

* correct coniditional logic
2024-09-18 17:18:07 -04:00
Mitch Freeman 5d1fe68906 [Bug Fix] Parcel purchase of bazaar items with unlimited charges (#4479)
Fix for unlimited charges in bazaar
2024-09-18 09:36:00 -04:00
oddx2k 52dcf35425 [Bug Fix] Fix Infinite Loop in Adventure::Finished() (#4473)
Fix infinite loop condition when bot encountered
2024-09-13 13:20:55 -04:00
Alex King a7550fbd9e [Release] 22.56.0 (#4471)
* [Release] 22.55.2

### Code

* Add IsCloseToBanker method ([#4462](https://github.com/EQEmu/Server/pull/4462)) @Akkadius 2024-08-27

### Feature

* Add Rule to Limit Task Update Messages ([#4459](https://github.com/EQEmu/Server/pull/4459)) @Kinglykrab 2024-08-28
* Allow NPCs to cast Sacrifice ([#4470](https://github.com/EQEmu/Server/pull/4470)) @fuzzlecutter 2024-09-12
* Lazy Load Bank Contents ([#4453](https://github.com/EQEmu/Server/pull/4453)) @catapultam-habeo 2024-08-27

### Fixes

* Add RULE_STRING to RuleManager::ResetRules ([#4467](https://github.com/EQEmu/Server/pull/4467)) @Kinglykrab 2024-09-07
* Fix Bard Effect in Migration 9237 ([#4468](https://github.com/EQEmu/Server/pull/4468)) @Kinglykrab 2024-09-09
* ModernAAScalingEnabled() Calculation Error ([#4469](https://github.com/EQEmu/Server/pull/4469)) @carolus21rex 2024-09-11

### Performance

* Move Discipline Loading to Client::CompleteConnect() ([#4466](https://github.com/EQEmu/Server/pull/4466)) @Kinglykrab 2024-09-09

### Rules

* Add a Bandolier Swap Delay Rule ([#4465](https://github.com/EQEmu/Server/pull/4465)) @Kinglykrab 2024-09-08

* 22.56.0
2024-09-12 20:39:48 -05:00
fuzzlecutter cc0171dfe1 [Feature] Allow NPCs to cast Sacrifice (#4470)
* [Feature] Teach npcs how to cast sacrifice

* [Feature] Teach npcs how to cast sacrifice

- Remove the hardcoded limit preventing npcs from casting sacrifice. The
  npc will receive as loot an emerald essence as expected.

* Update client.cpp
* Update client_packet.cpp
* Update spell_effects.cpp

* rename Client::SacrificeCaster to Client::sacrifice_caster_id
2024-09-12 15:42:44 -04:00
carolus21rex 913c5da70f [Bug Fix] ModernAAScalingEnabled() Calculation Error (#4469)
Current version only looks at your unspent AAs, meaning if you have 2000 spent AAs and 1 unspent AA, your scaling will be based on the 1 unspent AA instead of the 2001 total AA.

Here's the original log which is custom code found in the ModernAAScalingEnabled function:

[Wed Sep 11 14:10:19 2024] [AA] [ScaleAAXPBasedOnCurrentAATotal] AA Experience Calculation: add_aaxp = 660796, Base Bonus = 256.000000, Half-Life = 64.000000, Minimum Bonus = 1.000000, Earned AA = 1, Calculated Bonus = 253.242371

Custom code looks like this:

uint64 totalWithExpMod = add_aaxp;
	if (RuleB(AA, EnableLogrithmicClasslessAABonus)) {
		float base_bonus = RuleR(AA, InitialLogrithmicClasslessAABonus);
		float half_life = RuleR(AA, HalfLifeLogrithmicClasslessAABonus);
		float min_bon = RuleR(AA, MinimumLogrithmicClasslessAABonus);
		float bonus_expon = earnedAA / half_life;

		float bonus = base_bonus * std::pow(0.5, bonus_expon);
		Log(Logs::General,
			Logs::AA,
			"AA Experience Calculation: add_aaxp = %d, Base Bonus = %f, Half-Life = %f, Minimum Bonus = %f, Earned AA = %d, Calculated Bonus = %f",
			add_aaxp, base_bonus, half_life, min_bon, earnedAA, bonus);

		if (bonus < min_bon) bonus = min_bon;

		totalWithExpMod = (uint64)(totalWithExpMod * bonus);
	}

After the fix, the log becomes:

[Wed Sep 11 14:10:19 2024] [AA] [ScaleAAXPBasedOnCurrentAATotal] AA Experience Calculation: add_aaxp = 660796, Base Bonus = 256.000000, Half-Life = 64.000000, Minimum Bonus = 1.000000, Earned AA = 1, Calculated Bonus = 253.242371

Which is much closer to the expected behavior
2024-09-11 17:06:48 -04:00
Alex King 40fecbfaf5 [Performance] Move Discipline Loading to Client::CompleteConnect() (#4466)
* [Performance] Move Character Discipline Loading

* Push

* Final
2024-09-09 18:20:12 -05:00
Alex King b1646381b0 [Bug Fix] Fix Bard Effect in Migration 9237 (#4468) 2024-09-09 18:02:58 -05:00
Alex King bb1578796b [Rule] Add a Bandolier Swap Delay Rule (#4465)
* [Rule] Add a Bandolier Swap Delay Rule

* Push

* Update exp.cpp
2024-09-07 22:05:44 -05:00
Alex King 0e5a38f072 [Bug Fix] Add RULE_STRING to RuleManager::ResetRules (#4467)
* [Bug Fix] Add RULE_STRING to RuleManager::ResetRules

* Update rulesys.cpp
2024-09-07 18:29:46 -05:00
Alex King 39876ab858 [Feature] Add Rule to Limit Task Update Messages (#4459)
* [Feature] Add Rule to Limit Task Update Messages

* Update task_client_state.cpp

* Update task_client_state.cpp

* Change rule
2024-08-27 21:49:07 -04:00
catapultam-habeo ff16a76481 [Feature] Lazy Load Bank Contents (#4453)
* initial work porting this to upstream

* more

* track complete connect

* it sucks to suck

* Few optimizations

* Move sent_inventory init

* Move var

* Adjustments

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2024-08-27 13:21:55 -05:00
110 changed files with 4722 additions and 1805 deletions
+139
View File
@@ -1,3 +1,142 @@
## [22.58.0] 11/5/2024
### Code
* Add mysql prepared statement support ([#4530](https://github.com/EQEmu/Server/pull/4530)) @hgtw 2024-11-06
* Update perlbind to 1.1.0 ([#4529](https://github.com/EQEmu/Server/pull/4529)) @hgtw 2024-11-06
### Feature
* Focus Skill Attack Spells ([#4528](https://github.com/EQEmu/Server/pull/4528)) @mmcgarvey 2024-10-31
### Fixes
* Add Missing Lua Registers ([#4525](https://github.com/EQEmu/Server/pull/4525)) @Kinglykrab 2024-10-24
* Fix cross_zone_set_entity_variable_by_char_id in Lua ([#4526](https://github.com/EQEmu/Server/pull/4526)) @Kinglykrab 2024-10-24
### Loginserver
* Automatifc Opcode File Creation ([#4521](https://github.com/EQEmu/Server/pull/4521)) @KimLS 2024-10-22
### Quest API
* Add Spawn Circle/Grid Methods to Perl/Lua ([#4524](https://github.com/EQEmu/Server/pull/4524)) @Kinglykrab 2024-10-24
## [22.57.1] 10/22/2024
### Bots
* Enable Bot Commands Only if Rule Enabled ([#4519](https://github.com/EQEmu/Server/pull/4519)) @Kinglykrab 2024-10-22
* Fix pet buffs from saving duplicates every save ([#4520](https://github.com/EQEmu/Server/pull/4520)) @nytmyr 2024-10-22
### Loginserver
* Automatic Opcode File Creation ([#4521](https://github.com/EQEmu/Server/pull/4521)) @KimLS 2024-10-22
## [22.57.0] 10/20/2024
### Bots
* Add "silent" option to ^spawn and mute raid spawn ([#4494](https://github.com/EQEmu/Server/pull/4494)) @nytmyr 2024-10-05
* Add attack flag when told to attack ([#4490](https://github.com/EQEmu/Server/pull/4490)) @nytmyr 2024-09-29
* Fix timers loading on spawn and zone ([#4516](https://github.com/EQEmu/Server/pull/4516)) @nytmyr 2024-10-20
### Code
* Fixed a typo in Zoning.cpp ([#4515](https://github.com/EQEmu/Server/pull/4515)) @carolus21rex 2024-10-20
* Optimization Code Cleanup ([#4489](https://github.com/EQEmu/Server/pull/4489)) @Akkadius 2024-09-30
* Remove Extra Skill in EQ::skills::GetExtraDamageSkills() ([#4486](https://github.com/EQEmu/Server/pull/4486)) @Kinglykrab 2024-10-03
### Crash
* Fixes a crash when the faction_list db table is empty. ([#4511](https://github.com/EQEmu/Server/pull/4511)) @KimLS 2024-10-14
### Fixes
* Add character_instance_safereturns to tables_to_zero_id ([#4485](https://github.com/EQEmu/Server/pull/4485)) @Morzain 2024-09-26
* Correctly limit max targets of PBAOE ([#4507](https://github.com/EQEmu/Server/pull/4507)) @catapultam-habeo 2024-10-11
* FindBestZ selecting false zone floor as bestz - Results in roambox failures ([#4504](https://github.com/EQEmu/Server/pull/4504)) @fryguy503 2024-10-13
* Fix #set motd Crash ([#4495](https://github.com/EQEmu/Server/pull/4495)) @Kinglykrab 2024-10-05
* Fix `character_exp_modifiers` Default Values ([#4502](https://github.com/EQEmu/Server/pull/4502)) @Kinglykrab 2024-10-09
* Fix a display error regarding a few trader/buyer query errors ([#4514](https://github.com/EQEmu/Server/pull/4514)) @neckkola 2024-10-17
* Fix Group ID 0 in Group::SaveGroupLeaderAA() ([#4487](https://github.com/EQEmu/Server/pull/4487)) @Kinglykrab 2024-10-03
* Fix Mercenary Encounter Crash ([#4509](https://github.com/EQEmu/Server/pull/4509)) @Kinglykrab 2024-10-12
* Fix NPC::CanTalk() Crash ([#4499](https://github.com/EQEmu/Server/pull/4499)) @Kinglykrab 2024-10-07
* Fix Spells:DefaultAOEMaxTargets Default Value ([#4508](https://github.com/EQEmu/Server/pull/4508)) @Kinglykrab 2024-10-12
* Fix Targeted AOE Max Targets Rule ([#4488](https://github.com/EQEmu/Server/pull/4488)) @Kinglykrab 2024-10-03
* fixed a bug where it would use npc value instead of faction value in the database. ([#4491](https://github.com/EQEmu/Server/pull/4491)) @regneq 2024-09-29
* Master of Disguise should apply to illusions casted by others. ([#4506](https://github.com/EQEmu/Server/pull/4506)) @fryguy503 2024-10-11
* Spells - Self Only (Yellow) cast when non group member is targeted ([#4503](https://github.com/EQEmu/Server/pull/4503)) @fryguy503 2024-10-11
### Loginserver
* Larion loginserver support ([#4492](https://github.com/EQEmu/Server/pull/4492)) @KimLS 2024-10-03
* Login Fatal Error Spamming ([#4476](https://github.com/EQEmu/Server/pull/4476)) @KimLS 2024-10-09
### Logs
* Add NPC Trades to Player Events ([#4505](https://github.com/EQEmu/Server/pull/4505)) @Kinglykrab 2024-10-13
### Quest API
* Add Buff Fade Methods to Perl/Lua ([#4501](https://github.com/EQEmu/Server/pull/4501)) @Kinglykrab 2024-10-09
* Add EVENT_READ_ITEM to Perl/Lua ([#4497](https://github.com/EQEmu/Server/pull/4497)) @Kinglykrab 2024-10-08
* Add NPC List Filter Methods to Perl/Lua ([#4493](https://github.com/EQEmu/Server/pull/4493)) @Kinglykrab 2024-10-04
* Add Scripting Support to Mercenaries ([#4500](https://github.com/EQEmu/Server/pull/4500)) @Kinglykrab 2024-10-11
### Rules
* Add Rule to disable PVP Regions ([#4513](https://github.com/EQEmu/Server/pull/4513)) @Kinglykrab 2024-10-17
## [22.56.3] 9/23/2024
### Fixes
* Fix issue with Client::SaveDisciplines() not specifying character ID ([#4481](https://github.com/EQEmu/Server/pull/4477)) @Kinglykrab 2024-09-23
## [22.56.2] 9/20/2024
### Fixes
* Fix Issue with Database::ReserveName ([#4477](https://github.com/EQEmu/Server/pull/4477)) @Kinglykrab 2024-09-20
### Quest API
* Add GrantAllAAPoints() Overload To Perl/Lua ([#4474](https://github.com/EQEmu/Server/pull/4474)) @Kinglykrab 2024-09-20
## [22.56.1] 9/20/2024
### Fixes
* Fix Untrained Disciplines in Client::SaveDisciplines() ([#4472](https://github.com/EQEmu/Server/pull/4472)) @Kinglykrab 2024-09-13
* Fix Infinite Loop in Adventure::Finished() ([#4473](https://github.com/EQEmu/Server/pull/4473)) @oddx2k 2024-09-13
## [22.56.0] 9/12/2024
### Code
* Add IsCloseToBanker method ([#4462](https://github.com/EQEmu/Server/pull/4462)) @Akkadius 2024-08-27
### Feature
* Add Rule to Limit Task Update Messages ([#4459](https://github.com/EQEmu/Server/pull/4459)) @Kinglykrab 2024-08-28
* Allow NPCs to cast Sacrifice ([#4470](https://github.com/EQEmu/Server/pull/4470)) @fuzzlecutter 2024-09-12
* Lazy Load Bank Contents ([#4453](https://github.com/EQEmu/Server/pull/4453)) @catapultam-habeo 2024-08-27
### Fixes
* Add RULE_STRING to RuleManager::ResetRules ([#4467](https://github.com/EQEmu/Server/pull/4467)) @Kinglykrab 2024-09-07
* Fix Bard Effect in Migration 9237 ([#4468](https://github.com/EQEmu/Server/pull/4468)) @Kinglykrab 2024-09-09
* ModernAAScalingEnabled() Calculation Error ([#4469](https://github.com/EQEmu/Server/pull/4469)) @carolus21rex 2024-09-11
### Performance
* Move Discipline Loading to Client::CompleteConnect() ([#4466](https://github.com/EQEmu/Server/pull/4466)) @Kinglykrab 2024-09-09
### Rules
* Add a Bandolier Swap Delay Rule ([#4465](https://github.com/EQEmu/Server/pull/4465)) @Kinglykrab 2024-09-08
## [22.55.1] 8/26/2024
### Code
+2
View File
@@ -62,6 +62,7 @@ SET(common_sources
mutex.cpp
mysql_request_result.cpp
mysql_request_row.cpp
mysql_stmt.cpp
opcode_map.cpp
opcodemgr.cpp
packet_dump.cpp
@@ -586,6 +587,7 @@ SET(common_headers
mutex.h
mysql_request_result.h
mysql_request_row.h
mysql_stmt.h
op_codes.h
opcode_dispatch.h
opcodemgr.h
+24 -9
View File
@@ -285,16 +285,31 @@ bool Database::SetAccountStatus(const std::string& account_name, int16 status)
bool Database::ReserveName(uint32 account_id, const std::string& name)
{
const auto& l = CharacterDataRepository::GetWhere(
*this,
fmt::format(
"`name` = '{}'",
Strings::Escape(name)
)
const std::string& where_filter = fmt::format(
"`name` = '{}'",
Strings::Escape(name)
);
if (!l.empty()) {
LogInfo("Account: [{}] tried to request name: [{}], but it is already taken", account_id, name);
if (RuleB(Bots, Enabled)) {
const auto& b = BotDataRepository::GetWhere(*this, where_filter);
if (!b.empty()) {
LogInfo("Account [{}] requested name [{}] but name is already taken by a bot", account_id, name);
return false;
}
}
const auto& c = CharacterDataRepository::GetWhere(*this, where_filter);
if (!c.empty()) {
LogInfo("Account [{}] requested name [{}] but name is already taken by a character", account_id, name);
return false;
}
const auto& n = NpcTypesRepository::GetWhere(*this, where_filter);
if (!n.empty()) {
LogInfo("Account [{}] requested name [{}] but name is already taken by an NPC", account_id, name);
return false;
}
@@ -1845,7 +1860,7 @@ bool Database::CopyCharacter(
const int64 new_character_id = (CharacterDataRepository::GetMaxId(*this) + 1);
std::vector<std::string> tables_to_zero_id = { "keyring", "data_buckets" };
std::vector<std::string> tables_to_zero_id = { "keyring", "data_buckets", "character_instance_safereturns" };
TransactionBegin();
+13 -1
View File
@@ -4947,7 +4947,7 @@ UPDATE `aa_ability` SET `auto_grant_enabled` = 1 WHERE `grant_only` = 0 AND `cha
.version = 9237,
.description = "2023_10_15_import_13th_floor.sql",
.check = "SHOW COLUMNS FROM `items` LIKE 'bardeffect';",
.condition = "contains",
.condition = "missing",
.match = "mediumint",
.sql = R"(
ALTER TABLE `items`
@@ -5746,6 +5746,18 @@ ALTER TABLE `inventory`
ADD COLUMN `guid` BIGINT UNSIGNED NULL DEFAULT '0' AFTER `ornament_hero_model`;
ALTER TABLE `inventory_snapshots`
ADD COLUMN `guid` BIGINT UNSIGNED NULL DEFAULT '0' AFTER `ornament_hero_model`;
)"
},
ManifestEntry{
.version = 9284,
.description = "2024_10_08_character_exp_modifiers_default.sql",
.check = "SHOW CREATE TABLE `character_exp_modifiers`",
.condition = "contains",
.match = "`exp_modifier` float NOT NULL,",
.sql = R"(
ALTER TABLE `character_exp_modifiers`
MODIFY COLUMN `aa_modifier` float NOT NULL DEFAULT 1.0 AFTER `instance_version`,
MODIFY COLUMN `exp_modifier` float NOT NULL DEFAULT 1.0 AFTER `aa_modifier`;
)"
}
// -- template; copy/paste this when you need to create a new entry
+6
View File
@@ -7,6 +7,7 @@
#include "timer.h"
#include "dbcore.h"
#include "mysql_stmt.h"
#include <fstream>
#include <iostream>
@@ -436,3 +437,8 @@ MySQLRequestResult DBcore::QueryDatabaseMulti(const std::string &query)
return r;
}
mysql::PreparedStmt DBcore::Prepare(std::string query)
{
return mysql::PreparedStmt(*mysql, std::move(query), m_mutex);
}
+7
View File
@@ -17,6 +17,8 @@
#define CR_SERVER_GONE_ERROR 2006
#define CR_SERVER_LOST 2013
namespace mysql { class PreparedStmt; }
class DBcore {
public:
enum eStatus {
@@ -48,6 +50,11 @@ public:
}
void SetMutex(Mutex *mutex);
// only safe on connections shared with other threads if results buffered
// unsafe to use off main thread due to internal server logging
// throws std::runtime_error on failure
mysql::PreparedStmt Prepare(std::string query);
protected:
bool Open(
const char *iHost,
+6
View File
@@ -756,4 +756,10 @@ namespace PCNPCOnlyFlagType {
constexpr int NPC = 2;
}
namespace BookType {
constexpr uint8 Scroll = 0;
constexpr uint8 Book = 1;
constexpr uint8 ItemInfo = 2;
}
#endif /*COMMON_EMU_CONSTANTS_H*/
+1
View File
@@ -534,6 +534,7 @@ N(OP_Stamina),
N(OP_Stun),
N(OP_Surname),
N(OP_SwapSpell),
N(OP_SystemFingerprint),
N(OP_TargetBuffs),
N(OP_TargetCommand),
N(OP_TargetHoTT),
+1
View File
@@ -171,6 +171,7 @@ void EQEmuConfig::parse_config()
PluginDir = _root["server"]["directories"].get("plugins", "plugins/").asString();
LuaModuleDir = _root["server"]["directories"].get("lua_modules", "lua_modules/").asString();
PatchDir = _root["server"]["directories"].get("patches", "./").asString();
OpcodeDir = _root["server"]["directories"].get("opcodes", "./").asString();
SharedMemDir = _root["server"]["directories"].get("shared_memory", "shared/").asString();
LogDir = _root["server"]["directories"].get("logs", "logs/").asString();
+1
View File
@@ -95,6 +95,7 @@ class EQEmuConfig
std::string PluginDir;
std::string LuaModuleDir;
std::string PatchDir;
std::string OpcodeDir;
std::string SharedMemDir;
std::string LogDir;
@@ -789,50 +789,36 @@ std::string PlayerEventDiscordFormatter::FormatNPCHandinEvent(
);
}
std::string npc_info = fmt::format(
"{} ({})\n",
e.npc_name,
e.npc_id
);
npc_info += fmt::format(
"Is Quest Handin: {}",
e.is_quest_handin ? "Yes" : "No"
);
std::vector<DiscordField> f = {};
BuildDiscordField(&f, "NPC", npc_info);
if (!handin_items_info.empty()) {
BuildDiscordField(
&f,
"Handin Items",
fmt::format(
"{}",
handin_items_info
)
);
BuildDiscordField(&f, "Handin Items", handin_items_info);
}
if (!handin_money_info.empty()) {
BuildDiscordField(
&f,
"Handin Money",
fmt::format(
"{}",
handin_money_info
)
);
BuildDiscordField(&f, "Handin Money", handin_money_info);
}
if (!return_items_info.empty()) {
BuildDiscordField(
&f,
"Return Items",
fmt::format(
"{}",
return_items_info
)
);
BuildDiscordField(&f, "Return Items", return_items_info);
}
if (!return_money_info.empty()) {
BuildDiscordField(
&f,
"Return Money",
fmt::format(
"{}",
return_money_info
)
);
BuildDiscordField(&f, "Return Money", return_money_info);
}
std::vector<DiscordEmbed> embeds = {};
+47 -47
View File
@@ -654,53 +654,53 @@ const int32_t RETENTION_DAYS_DEFAULT = 7;
void PlayerEventLogs::SetSettingsDefaults()
{
m_settings[PlayerEvent::GM_COMMAND].event_enabled = 1;
m_settings[PlayerEvent::ZONING].event_enabled = 1;
m_settings[PlayerEvent::AA_GAIN].event_enabled = 1;
m_settings[PlayerEvent::AA_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::FORAGE_SUCCESS].event_enabled = 0;
m_settings[PlayerEvent::FORAGE_FAILURE].event_enabled = 0;
m_settings[PlayerEvent::FISH_SUCCESS].event_enabled = 0;
m_settings[PlayerEvent::FISH_FAILURE].event_enabled = 0;
m_settings[PlayerEvent::ITEM_DESTROY].event_enabled = 1;
m_settings[PlayerEvent::WENT_ONLINE].event_enabled = 0;
m_settings[PlayerEvent::WENT_OFFLINE].event_enabled = 0;
m_settings[PlayerEvent::LEVEL_GAIN].event_enabled = 1;
m_settings[PlayerEvent::LEVEL_LOSS].event_enabled = 1;
m_settings[PlayerEvent::LOOT_ITEM].event_enabled = 1;
m_settings[PlayerEvent::MERCHANT_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::MERCHANT_SELL].event_enabled = 1;
m_settings[PlayerEvent::GROUP_JOIN].event_enabled = 0;
m_settings[PlayerEvent::GROUP_LEAVE].event_enabled = 0;
m_settings[PlayerEvent::RAID_JOIN].event_enabled = 0;
m_settings[PlayerEvent::RAID_LEAVE].event_enabled = 0;
m_settings[PlayerEvent::GROUNDSPAWN_PICKUP].event_enabled = 1;
m_settings[PlayerEvent::NPC_HANDIN].event_enabled = 1;
m_settings[PlayerEvent::SKILL_UP].event_enabled = 0;
m_settings[PlayerEvent::TASK_ACCEPT].event_enabled = 1;
m_settings[PlayerEvent::TASK_UPDATE].event_enabled = 1;
m_settings[PlayerEvent::TASK_COMPLETE].event_enabled = 1;
m_settings[PlayerEvent::TRADE].event_enabled = 1;
m_settings[PlayerEvent::GIVE_ITEM].event_enabled = 1;
m_settings[PlayerEvent::SAY].event_enabled = 0;
m_settings[PlayerEvent::REZ_ACCEPTED].event_enabled = 1;
m_settings[PlayerEvent::DEATH].event_enabled = 1;
m_settings[PlayerEvent::COMBINE_FAILURE].event_enabled = 1;
m_settings[PlayerEvent::COMBINE_SUCCESS].event_enabled = 1;
m_settings[PlayerEvent::DROPPED_ITEM].event_enabled = 1;
m_settings[PlayerEvent::SPLIT_MONEY].event_enabled = 1;
m_settings[PlayerEvent::DZ_JOIN].event_enabled = 1;
m_settings[PlayerEvent::DZ_LEAVE].event_enabled = 1;
m_settings[PlayerEvent::TRADER_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::TRADER_SELL].event_enabled = 1;
m_settings[PlayerEvent::BANDOLIER_CREATE].event_enabled = 0;
m_settings[PlayerEvent::BANDOLIER_SWAP].event_enabled = 0;
m_settings[PlayerEvent::DISCOVER_ITEM].event_enabled = 1;
m_settings[PlayerEvent::POSSIBLE_HACK].event_enabled = 1;
m_settings[PlayerEvent::KILLED_NPC].event_enabled = 0;
m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1;
m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1;
m_settings[PlayerEvent::ITEM_CREATION].event_enabled = 1;
m_settings[PlayerEvent::GM_COMMAND].event_enabled = 1;
m_settings[PlayerEvent::ZONING].event_enabled = 1;
m_settings[PlayerEvent::AA_GAIN].event_enabled = 1;
m_settings[PlayerEvent::AA_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::FORAGE_SUCCESS].event_enabled = 0;
m_settings[PlayerEvent::FORAGE_FAILURE].event_enabled = 0;
m_settings[PlayerEvent::FISH_SUCCESS].event_enabled = 0;
m_settings[PlayerEvent::FISH_FAILURE].event_enabled = 0;
m_settings[PlayerEvent::ITEM_DESTROY].event_enabled = 1;
m_settings[PlayerEvent::WENT_ONLINE].event_enabled = 0;
m_settings[PlayerEvent::WENT_OFFLINE].event_enabled = 0;
m_settings[PlayerEvent::LEVEL_GAIN].event_enabled = 1;
m_settings[PlayerEvent::LEVEL_LOSS].event_enabled = 1;
m_settings[PlayerEvent::LOOT_ITEM].event_enabled = 1;
m_settings[PlayerEvent::MERCHANT_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::MERCHANT_SELL].event_enabled = 1;
m_settings[PlayerEvent::GROUP_JOIN].event_enabled = 0;
m_settings[PlayerEvent::GROUP_LEAVE].event_enabled = 0;
m_settings[PlayerEvent::RAID_JOIN].event_enabled = 0;
m_settings[PlayerEvent::RAID_LEAVE].event_enabled = 0;
m_settings[PlayerEvent::GROUNDSPAWN_PICKUP].event_enabled = 1;
m_settings[PlayerEvent::NPC_HANDIN].event_enabled = 1;
m_settings[PlayerEvent::SKILL_UP].event_enabled = 0;
m_settings[PlayerEvent::TASK_ACCEPT].event_enabled = 1;
m_settings[PlayerEvent::TASK_UPDATE].event_enabled = 1;
m_settings[PlayerEvent::TASK_COMPLETE].event_enabled = 1;
m_settings[PlayerEvent::TRADE].event_enabled = 1;
m_settings[PlayerEvent::GIVE_ITEM].event_enabled = 1;
m_settings[PlayerEvent::SAY].event_enabled = 0;
m_settings[PlayerEvent::REZ_ACCEPTED].event_enabled = 1;
m_settings[PlayerEvent::DEATH].event_enabled = 1;
m_settings[PlayerEvent::COMBINE_FAILURE].event_enabled = 1;
m_settings[PlayerEvent::COMBINE_SUCCESS].event_enabled = 1;
m_settings[PlayerEvent::DROPPED_ITEM].event_enabled = 1;
m_settings[PlayerEvent::SPLIT_MONEY].event_enabled = 1;
m_settings[PlayerEvent::DZ_JOIN].event_enabled = 1;
m_settings[PlayerEvent::DZ_LEAVE].event_enabled = 1;
m_settings[PlayerEvent::TRADER_PURCHASE].event_enabled = 1;
m_settings[PlayerEvent::TRADER_SELL].event_enabled = 1;
m_settings[PlayerEvent::BANDOLIER_CREATE].event_enabled = 0;
m_settings[PlayerEvent::BANDOLIER_SWAP].event_enabled = 0;
m_settings[PlayerEvent::DISCOVER_ITEM].event_enabled = 1;
m_settings[PlayerEvent::POSSIBLE_HACK].event_enabled = 1;
m_settings[PlayerEvent::KILLED_NPC].event_enabled = 0;
m_settings[PlayerEvent::KILLED_NAMED_NPC].event_enabled = 1;
m_settings[PlayerEvent::KILLED_RAID_NPC].event_enabled = 1;
m_settings[PlayerEvent::ITEM_CREATION].event_enabled = 1;
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_ITEM].event_enabled = 1;
m_settings[PlayerEvent::GUILD_TRIBUTE_DONATE_PLAT].event_enabled = 1;
m_settings[PlayerEvent::PARCEL_SEND].event_enabled = 1;
+11 -5
View File
@@ -860,10 +860,12 @@ namespace PlayerEvent {
class HandinEntry {
public:
uint32 item_id;
std::string item_name;
uint16 charges;
bool attuned;
uint32 item_id;
std::string item_name;
std::vector<uint32> augment_ids;
std::vector<std::string> augment_names;
uint16 charges;
bool attuned;
// cereal
template<class Archive>
@@ -872,6 +874,8 @@ namespace PlayerEvent {
ar(
CEREAL_NVP(item_id),
CEREAL_NVP(item_name),
CEREAL_NVP(augment_ids),
CEREAL_NVP(augment_names),
CEREAL_NVP(charges),
CEREAL_NVP(attuned)
);
@@ -905,6 +909,7 @@ namespace PlayerEvent {
HandinMoney handin_money;
std::vector<HandinEntry> return_items;
HandinMoney return_money;
bool is_quest_handin;
// cereal
template<class Archive>
@@ -916,7 +921,8 @@ namespace PlayerEvent {
CEREAL_NVP(handin_items),
CEREAL_NVP(handin_money),
CEREAL_NVP(return_items),
CEREAL_NVP(return_money)
CEREAL_NVP(return_money),
CEREAL_NVP(is_quest_handin)
);
}
};
+586
View File
@@ -0,0 +1,586 @@
#include "mysql_stmt.h"
#include "eqemu_logsys.h"
#include "mutex.h"
#include "timer.h"
#include <charconv>
namespace mysql
{
void PreparedStmt::StmtDeleter::operator()(MYSQL_STMT* stmt) noexcept
{
// The connection must be locked when closing the stmt to avoid mysql errors
// in case another thread tries to use it during the close. If the mutex is
// changed to one that throws then exceptions need to be caught here.
LockMutex lock(mutex);
mysql_stmt_close(stmt);
}
PreparedStmt::PreparedStmt(MYSQL& mysql, std::string query, Mutex* mutex, StmtOptions opts)
: m_stmt(mysql_stmt_init(&mysql), { mutex }), m_query(std::move(query)), m_mutex(mutex), m_options(opts)
{
LockMutex lock(m_mutex);
if (mysql_stmt_prepare(m_stmt.get(), m_query.c_str(), static_cast<unsigned long>(m_query.size())) != 0)
{
ThrowError(fmt::format("Prepare error: {}", GetStmtError()));
}
m_params.resize(mysql_stmt_param_count(m_stmt.get()));
m_inputs.resize(m_params.size());
}
void PreparedStmt::ThrowError(const std::string& error)
{
LogMySQLError("{}", error);
throw std::runtime_error(error);
}
std::string PreparedStmt::GetStmtError()
{
auto err = mysql_stmt_errno(m_stmt.get());
auto str = mysql_stmt_error(m_stmt.get());
return fmt::format("({}) [{}] for query [{}]", err, str, m_query);
}
template <typename T>
void PreparedStmt::BindInput(size_t index, T value)
{
if (index >= m_inputs.size())
{
ThrowError(fmt::format("Cannot bind input, index {} out of range", index));
}
impl::Bind& arg = m_inputs[index];
arg.is_null = std::is_same_v<T, std::nullptr_t>;
MYSQL_BIND& bind = m_params[index];
bind.is_unsigned = std::is_unsigned_v<T>;
bind.is_null = &arg.is_null;
bind.length = &arg.length;
auto old_type = bind.buffer_type;
if constexpr (std::is_arithmetic_v<T>)
{
if (arg.buffer.size() < sizeof(T))
{
arg.buffer.resize(std::max(sizeof(T), sizeof(int64_t)));
bind.buffer = arg.buffer.data();
m_need_bind = true;
}
memcpy(arg.buffer.data(), &value, sizeof(T));
}
if constexpr (std::is_same_v<T, int8_t> || std::is_same_v<T, uint8_t> || std::is_same_v<T, bool>)
{
bind.buffer_type = MYSQL_TYPE_TINY;
}
else if constexpr (std::is_same_v<T, int16_t> || std::is_same_v<T, uint16_t>)
{
bind.buffer_type = MYSQL_TYPE_SHORT;
}
else if constexpr (std::is_same_v<T, int32_t> || std::is_same_v<T, uint32_t>)
{
bind.buffer_type = MYSQL_TYPE_LONG;
}
else if constexpr (std::is_same_v<T, int64_t> || std::is_same_v<T, uint64_t>)
{
bind.buffer_type = MYSQL_TYPE_LONGLONG;
}
else if constexpr (std::is_same_v<T, float>)
{
bind.buffer_type = MYSQL_TYPE_FLOAT;
}
else if constexpr (std::is_same_v<T, double>)
{
bind.buffer_type = MYSQL_TYPE_DOUBLE;
}
else if constexpr (std::is_same_v<T, std::string_view>)
{
bind.buffer_type = MYSQL_TYPE_STRING;
if (arg.buffer.empty() || arg.buffer.size() < value.size())
{
arg.buffer.resize(static_cast<size_t>((value.size() + 1) * 1.5));
bind.buffer = arg.buffer.data();
bind.buffer_length = static_cast<unsigned long>(arg.buffer.size());
m_need_bind = true;
}
std::copy(value.begin(), value.end(), arg.buffer.begin());
arg.length = static_cast<unsigned long>(value.size());
}
else if constexpr (!std::is_same_v<T, std::nullptr_t>)
{
static_assert(false_v<T>, "Cannot bind unsupported type");
}
if (old_type != bind.buffer_type)
{
m_need_bind = true;
}
}
void PreparedStmt::BindInput(size_t index, const char* str)
{
BindInput(index, std::string_view(str));
}
void PreparedStmt::BindInput(size_t index, const std::string& str)
{
BindInput(index, std::string_view(str));
}
StmtResult PreparedStmt::Execute()
{
CheckArgs(0);
return DoExecute();
}
StmtResult PreparedStmt::Execute(const std::vector<param_t>& args)
{
CheckArgs(args.size());
for (size_t i = 0; i < args.size(); ++i)
{
std::visit([&](const auto& arg) { BindInput(i, arg); }, args[i]);
}
return DoExecute();
}
template <typename T>
StmtResult PreparedStmt::Execute(const std::vector<T>& args)
{
CheckArgs(args.size());
for (size_t i = 0; i < args.size(); ++i)
{
BindInput(i, args[i]);
}
return DoExecute();
}
void PreparedStmt::CheckArgs(size_t argc)
{
if (argc != m_params.size())
{
ThrowError(fmt::format("Bad arg count (got {}, expected {}) for [{}]", argc, m_params.size(), m_query));
}
}
StmtResult PreparedStmt::DoExecute()
{
BenchTimer timer;
LockMutex lock(m_mutex);
if (m_need_bind && mysql_stmt_bind_param(m_stmt.get(), m_params.data()) != 0)
{
ThrowError(fmt::format("Bind param error: {}", GetStmtError()));
}
m_need_bind = false;
if (mysql_stmt_execute(m_stmt.get()) != 0)
{
ThrowError(fmt::format("Execute error: {}", GetStmtError()));
}
my_bool attr = m_options.use_max_length;
mysql_stmt_attr_set(m_stmt.get(), STMT_ATTR_UPDATE_MAX_LENGTH, &attr);
if (m_options.buffer_results && mysql_stmt_store_result(m_stmt.get()) != 0)
{
ThrowError(fmt::format("Store result error: {}", GetStmtError()));
}
// Result buffers are bound on first execute and re-used if needed
if (m_results.empty())
{
BindResults();
}
StmtResult res(m_stmt.get(), m_results.size());
if (m_results.empty())
{
LogMySQLQuery("{} -- ({} row(s) affected) ({:.6f}s)", m_query, res.RowsAffected(), timer.elapsed());
}
else
{
LogMySQLQuery("{} -- ({} row(s) returned) ({:.6f}s)", m_query, res.RowCount(), timer.elapsed());
}
return res;
}
void PreparedStmt::BindResults()
{
MYSQL_RES* res = mysql_stmt_result_metadata(m_stmt.get());
if (!res)
{
return; // did not produce a result set
}
MYSQL_FIELD* fields = mysql_fetch_fields(res);
m_columns.resize(mysql_num_fields(res));
m_results.resize(m_columns.size());
for (int i = 0; i < static_cast<int>(m_columns.size()); ++i)
{
impl::BindColumn& col = m_columns[i].m_col;
MYSQL_BIND& bind = m_results[i];
col.index = i;
col.name = fields[i].name;
col.buffer_type = fields[i].type;
col.is_unsigned = (fields[i].flags & UNSIGNED_FLAG) != 0;
col.buffer.resize(GetResultBufferSize(fields[i]));
bind.buffer_type = col.buffer_type;
bind.buffer = col.buffer.data();
bind.buffer_length = static_cast<unsigned long>(col.buffer.size());
bind.is_unsigned = col.is_unsigned;
bind.is_null = &col.is_null;
bind.length = &col.length;
bind.error = &col.error;
}
mysql_free_result(res);
if (!m_results.empty() && mysql_stmt_bind_result(m_stmt.get(), m_results.data()) != 0)
{
ThrowError(fmt::format("Bind result error: {}", GetStmtError()));
}
}
int PreparedStmt::GetResultBufferSize(const MYSQL_FIELD& field) const
{
switch (field.type)
{
case MYSQL_TYPE_TINY:
return sizeof(int8_t);
case MYSQL_TYPE_SHORT:
return sizeof(int16_t);
case MYSQL_TYPE_INT24:
case MYSQL_TYPE_LONG:
return sizeof(int32_t);
case MYSQL_TYPE_LONGLONG:
return sizeof(int64_t);
case MYSQL_TYPE_FLOAT:
return sizeof(float);
case MYSQL_TYPE_DOUBLE:
return sizeof(double);
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
return sizeof(MYSQL_TIME);
default: // if max_length is unavailable for strings buffers are resized on fetch
return field.max_length + 1; // ensure valid buffer created
}
}
StmtRow PreparedStmt::Fetch()
{
StmtRow row;
if (!m_columns.empty())
{
int rc = mysql_stmt_fetch(m_stmt.get());
if (rc == 1)
{
ThrowError(fmt::format("Fetch error: {}", GetStmtError()));
}
if (rc != MYSQL_NO_DATA)
{
if (rc == MYSQL_DATA_TRUNCATED)
{
FetchTruncated();
}
row = StmtRow(m_columns);
}
}
return row;
}
void PreparedStmt::FetchTruncated()
{
for (int i = 0; i < static_cast<int>(m_columns.size()); ++i)
{
impl::BindColumn& col = m_columns[i].m_col;
if (col.error)
{
MYSQL_BIND& bind = m_results[i];
col.buffer.resize(static_cast<size_t>(col.length * 1.5));
bind.buffer = col.buffer.data();
bind.buffer_length = static_cast<unsigned long>(col.buffer.size());
mysql_stmt_fetch_column(m_stmt.get(), &bind, i, 0);
}
}
if (mysql_stmt_bind_result(m_stmt.get(), m_results.data()) != 0)
{
ThrowError(fmt::format("Fetch rebind result error: {}", GetStmtError()));
}
}
// ---------------------------------------------------------------------------
StmtResult::StmtResult(MYSQL_STMT* stmt, size_t columns)
{
m_num_cols = static_cast<int>(columns);
m_num_rows = mysql_stmt_num_rows(stmt); // requires buffered results
m_affected = mysql_stmt_affected_rows(stmt);
m_insert_id = mysql_stmt_insert_id(stmt);
}
// ---------------------------------------------------------------------------
const StmtColumn* StmtRow::GetColumn(size_t index) const
{
return index < m_columns.size() ? &m_columns[index] : nullptr;
}
const StmtColumn* StmtRow::GetColumn(std::string_view name) const
{
auto it = std::ranges::find_if(m_columns,
[name](const StmtColumn& col) { return col.Name() == name; });
return it != m_columns.end() ? &(*it) : nullptr;
}
std::optional<std::string> StmtRow::operator[](size_t index) const
{
return GetStr(index);
}
std::optional<std::string> StmtRow::operator[](std::string_view name) const
{
return GetStr(name);
}
std::optional<std::string> StmtRow::GetStr(size_t index) const
{
const StmtColumn* col = GetColumn(index);
return col ? col->GetStr() : std::nullopt;
}
std::optional<std::string> StmtRow::GetStr(std::string_view name) const
{
const StmtColumn* col = GetColumn(name);
return col ? col->GetStr() : std::nullopt;
}
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> StmtRow::Get(size_t index) const
{
const StmtColumn* col = GetColumn(index);
return col ? col->Get<T>() : std::nullopt;
}
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> StmtRow::Get(std::string_view name) const
{
const StmtColumn* col = GetColumn(name);
return col ? col->Get<T>() : std::nullopt;
}
// ---------------------------------------------------------------------------
static time_t MakeTime(const MYSQL_TIME& mt)
{
// buffer mt given in mysql session time zone (assumes local)
std::tm tm{};
tm.tm_year = mt.year - 1900;
tm.tm_mon = mt.month - 1;
tm.tm_mday = mt.day;
tm.tm_hour = mt.hour;
tm.tm_min = mt.minute;
tm.tm_sec = mt.second;
tm.tm_isdst = -1;
return std::mktime(&tm);
}
static int MakeSeconds(const MYSQL_TIME& mt)
{
return (mt.neg ? -1 : 1) * static_cast<int>(mt.hour * 3600 + mt.minute * 60 + mt.second);
}
static uint64_t MakeBits(std::span<const uint8_t> data)
{
// byte stream for bits is in big endian
uint64_t bits = 0;
for (size_t i = 0; i < data.size() && i < sizeof(uint64_t); ++i)
{
bits |= static_cast<uint64_t>(data[data.size() - i - 1] & 0xff) << (i * 8);
}
return bits;
}
template <typename T>
static T FromString(std::string_view sv)
{
if constexpr (std::is_same_v<T, bool>)
{
// return false for empty (zero-length) strings
return !sv.empty();
}
else
{
// non numbers return a zero initialized T (could return nullopt instead)
T value = {};
std::from_chars(sv.data(), sv.data() + sv.size(), value);
return value;
}
}
static std::string FormatTime(enum_field_types type, const MYSQL_TIME& mt)
{
switch (type)
{
case MYSQL_TYPE_TIME: // hhh:mm:ss '-838:59:59' to '838:59:59'
return fmt::format("{}{:02d}:{:02d}:{:02d}", mt.neg ? "-" : "", mt.hour, mt.minute, mt.second);
case MYSQL_TYPE_DATE: // YYYY-MM-DD '1000-01-01' to '9999-12-31'
return fmt::format("{}-{:02d}-{:02d}", mt.year, mt.month, mt.day);
case MYSQL_TYPE_DATETIME: // YYYY-MM-DD hh:mm:ss '1000-01-01 00:00:00' to '9999-12-31 23:59:59'
case MYSQL_TYPE_TIMESTAMP: // YYYY-MM-DD hh:mm:ss '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC
return fmt::format("{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", mt.year, mt.month, mt.day, mt.hour, mt.minute, mt.second);
default:
return std::string();
}
}
std::optional<std::string_view> StmtColumn::GetStrView() const
{
if (m_col.is_null)
{
return std::nullopt;
}
switch (m_col.buffer_type)
{
case MYSQL_TYPE_NEWDECIMAL:
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_VAR_STRING:
case MYSQL_TYPE_STRING:
return std::make_optional<std::string_view>(reinterpret_cast<const char*>(m_col.buffer.data()), m_col.length);
default:
return std::nullopt;
}
}
std::optional<std::string> StmtColumn::GetStr() const
{
if (m_col.is_null)
{
return std::nullopt;
}
switch (m_col.buffer_type)
{
case MYSQL_TYPE_TINY:
return m_col.is_unsigned ? fmt::format_int(BitCast<uint8_t>()).c_str() : fmt::format_int(BitCast<int8_t>()).c_str();
case MYSQL_TYPE_SHORT:
return m_col.is_unsigned ? fmt::format_int(BitCast<uint16_t>()).c_str() : fmt::format_int(BitCast<int16_t>()).c_str();
case MYSQL_TYPE_INT24:
case MYSQL_TYPE_LONG:
return m_col.is_unsigned ? fmt::format_int(BitCast<uint32_t>()).c_str() : fmt::format_int(BitCast<int32_t>()).c_str();
case MYSQL_TYPE_LONGLONG:
return m_col.is_unsigned ? fmt::format_int(BitCast<uint64_t>()).c_str() : fmt::format_int(BitCast<int64_t>()).c_str();
case MYSQL_TYPE_FLOAT:
return fmt::format("{}", BitCast<float>());
case MYSQL_TYPE_DOUBLE:
return fmt::format("{}", BitCast<double>());
case MYSQL_TYPE_TIME:
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP:
return FormatTime(m_col.buffer_type, BitCast<MYSQL_TIME>());
case MYSQL_TYPE_BIT:
return fmt::format_int(*Get<uint64_t>()).c_str();
case MYSQL_TYPE_NEWDECIMAL:
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_VAR_STRING:
case MYSQL_TYPE_STRING:
return std::make_optional<std::string>(reinterpret_cast<const char*>(m_col.buffer.data()), m_col.length);
default:
return std::nullopt;
}
}
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> StmtColumn::Get() const
{
if (m_col.is_null)
{
return std::nullopt;
}
switch (m_col.buffer_type)
{
case MYSQL_TYPE_TINY:
return m_col.is_unsigned ? static_cast<T>(BitCast<uint8_t>()) : static_cast<T>(BitCast<int8_t>());
case MYSQL_TYPE_SHORT:
return m_col.is_unsigned ? static_cast<T>(BitCast<uint16_t>()) : static_cast<T>(BitCast<int16_t>());
case MYSQL_TYPE_INT24:
case MYSQL_TYPE_LONG:
return m_col.is_unsigned ? static_cast<T>(BitCast<uint32_t>()) : static_cast<T>(BitCast<int32_t>());
case MYSQL_TYPE_LONGLONG:
return m_col.is_unsigned ? static_cast<T>(BitCast<uint64_t>()) : static_cast<T>(BitCast<int64_t>());
case MYSQL_TYPE_FLOAT:
return static_cast<T>(BitCast<float>());
case MYSQL_TYPE_DOUBLE:
return static_cast<T>(BitCast<double>());
case MYSQL_TYPE_TIME: // return as total seconds
return static_cast<T>(MakeSeconds(BitCast<MYSQL_TIME>()));
case MYSQL_TYPE_DATE:
case MYSQL_TYPE_DATETIME:
case MYSQL_TYPE_TIMESTAMP: // return as epoch timestamp
return static_cast<T>(MakeTime(BitCast<MYSQL_TIME>()));
case MYSQL_TYPE_BIT:
return static_cast<T>(MakeBits({ m_col.buffer.data(), m_col.length }));
case MYSQL_TYPE_NEWDECIMAL:
case MYSQL_TYPE_TINY_BLOB:
case MYSQL_TYPE_MEDIUM_BLOB:
case MYSQL_TYPE_LONG_BLOB:
case MYSQL_TYPE_BLOB:
case MYSQL_TYPE_VAR_STRING:
case MYSQL_TYPE_STRING:
return FromString<T>({ reinterpret_cast<const char*>(m_col.buffer.data()), m_col.length });
default:
return std::nullopt;
}
}
// ---------------------------------------------------------------------------
// explicit template instantiations for supported types
template void PreparedStmt::BindInput(size_t, std::string_view);
template void PreparedStmt::BindInput(size_t, std::nullptr_t);
template StmtResult PreparedStmt::Execute(const std::vector<std::string_view>&);
template StmtResult PreparedStmt::Execute(const std::vector<std::string>&);
template StmtResult PreparedStmt::Execute(const std::vector<const char*>&);
#define INSTANTIATE(T) \
template void PreparedStmt::BindInput(size_t, T); \
template StmtResult PreparedStmt::Execute(const std::vector<T>&); \
template std::optional<T> StmtRow::Get(size_t) const; \
template std::optional<T> StmtRow::Get(std::string_view) const; \
template std::optional<T> StmtColumn::Get() const;
INSTANTIATE(bool);
INSTANTIATE(int8_t);
INSTANTIATE(uint8_t);
INSTANTIATE(int16_t);
INSTANTIATE(uint16_t);
INSTANTIATE(int32_t);
INSTANTIATE(uint32_t);
INSTANTIATE(int64_t);
INSTANTIATE(uint64_t);
INSTANTIATE(float);
INSTANTIATE(double);
} // namespace mysql
+221
View File
@@ -0,0 +1,221 @@
#pragma once
#include "mysql.h"
#include <cassert>
#include <cstring>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
class Mutex;
namespace mysql
{
// support MySQL 8.0.1+ API which removed the my_bool type
#if !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 80001
using my_bool = bool;
#endif
template <typename>
inline constexpr bool false_v = false;
namespace impl
{
struct Bind
{
std::vector<uint8_t> buffer;
unsigned long length = 0;
my_bool is_null = false;
my_bool error = false;
};
struct BindColumn : Bind
{
int index = 0;
std::string name;
bool is_unsigned = false;
enum_field_types buffer_type = {};
};
} // namespace impl
// ---------------------------------------------------------------------------
struct StmtOptions
{
// Enable buffering (storing) entire result set after executing a statement
bool buffer_results = true;
// Enable MySQL to update max_length of fields in execute result set (requires buffering)
bool use_max_length = true;
};
// ---------------------------------------------------------------------------
// Holds ownership of bound column value buffer
class StmtColumn
{
public:
int Index() const { return m_col.index; }
bool IsNull() const { return m_col.is_null; }
bool IsUnsigned() const { return m_col.is_unsigned; }
enum_field_types Type() const { return m_col.buffer_type; }
const std::string& Name() const { return m_col.name; }
// Get view of column value buffer
std::span<const uint8_t> GetBuf() const { return { m_col.buffer.data(), m_col.length }; }
// Get view of column string value. Returns nullopt if value is NULL or not a string
std::optional<std::string_view> GetStrView() const;
// Get column value as string. Returns nullopt if value is NULL or field type unsupported
std::optional<std::string> GetStr() const;
// Get column value as numeric T. Returns nullopt if value NULL or field type unsupported
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> Get() const;
private:
// uses memcpy for type punning buffer data to avoid UB with strict aliasing
template <typename T>
T BitCast() const
{
T val;
assert(sizeof(T) == m_col.length);
memcpy(&val, m_col.buffer.data(), sizeof(T));
return val;
}
friend class PreparedStmt; // access to allocate and bind buffers
friend class StmtResult; // access to resize truncated buffers
impl::BindColumn m_col;
};
// ---------------------------------------------------------------------------
// Provides a non-owning view of PreparedStmt column value buffers
// Evaluates false if it does not contain a valid row
class StmtRow
{
public:
StmtRow() = default;
StmtRow(std::span<const StmtColumn> columns) : m_columns(columns) {};
explicit operator bool() const noexcept { return !m_columns.empty(); }
int ColumnCount() const { return static_cast<int>(m_columns.size()); }
const StmtColumn* GetColumn(size_t index) const;
const StmtColumn* GetColumn(std::string_view name) const;
// Get specified column value as string
// Returns nullopt if column invalid, value is NULL, or field type unsupported
std::optional<std::string> operator[](size_t index) const;
std::optional<std::string> operator[](std::string_view name) const;
std::optional<std::string> GetStr(size_t index) const;
std::optional<std::string> GetStr(std::string_view name) const;
// Get specified column value as numeric T
// Returns nullopt if column invalid, value is NULL, or field type unsupported
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> Get(size_t index) const;
template <typename T> requires std::is_arithmetic_v<T>
std::optional<T> Get(std::string_view name) const;
auto begin() const { return m_columns.begin(); }
auto end() const { return m_columns.end(); }
private:
std::span<const StmtColumn> m_columns;
};
// ---------------------------------------------------------------------------
// Result meta data for an executed prepared statement
class StmtResult
{
public:
StmtResult() = default;
StmtResult(MYSQL_STMT* stmt, size_t columns);
int ColumnCount() const { return m_num_cols; }
uint64_t RowCount() const { return m_num_rows; }
uint64_t RowsAffected() const { return m_affected; }
uint64_t LastInsertID() const { return m_insert_id; }
private:
int m_num_cols = 0;
uint64_t m_num_rows = 0;
uint64_t m_affected = 0;
uint64_t m_insert_id = 0;
};
// ---------------------------------------------------------------------------
class PreparedStmt
{
public:
// Supported argument types for execute
using param_t = std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t,
int64_t, uint64_t, float, double, bool, std::string_view, std::nullptr_t>;
PreparedStmt() = delete;
PreparedStmt(MYSQL& mysql, std::string query, Mutex* mutex, StmtOptions opts = {});
const std::string& GetQuery() const { return m_query; }
StmtOptions GetOptions() const { return m_options; }
void SetOptions(StmtOptions options) { m_options = options; }
void FreeResult() { mysql_stmt_free_result(m_stmt.get()); }
// Execute the prepared statement with specified arguments
// Throws exception on error
template <typename T>
StmtResult Execute(const std::vector<T>& args);
StmtResult Execute(const std::vector<param_t>& args);
StmtResult Execute();
// Fetch the next row into column buffers (overwrites previous row values)
// Return value evaluates false if no more rows to fetch
// Throws exception on error
StmtRow Fetch();
private:
void CheckArgs(size_t argc);
StmtResult DoExecute();
void BindResults();
void FetchTruncated();
int GetResultBufferSize(const MYSQL_FIELD& field) const;
void ThrowError(const std::string& error);
std::string GetStmtError();
// bind an input value to a query parameter by index
template <typename T>
void BindInput(size_t index, T value);
void BindInput(size_t index, const char* str);
void BindInput(size_t index, const std::string& str);
struct StmtDeleter
{
Mutex* mutex = nullptr;
void operator()(MYSQL_STMT* stmt) noexcept;
};
private:
std::unique_ptr<MYSQL_STMT, StmtDeleter> m_stmt;
std::vector<MYSQL_BIND> m_params; // input binds
std::vector<MYSQL_BIND> m_results; // result binds
std::vector<impl::Bind> m_inputs; // execute buffers (addresses bound)
std::vector<StmtColumn> m_columns; // fetch buffers (addresses bound)
std::string m_query;
StmtOptions m_options = {};
bool m_need_bind = true;
Mutex* m_mutex = nullptr; // connection mutex
};
} // namespace mysql
+11
View File
@@ -74,6 +74,11 @@ void PathManager::LoadPaths()
m_patch_path = fs::relative(fs::path{m_server_path + "/" + c->PatchDir}).string();
}
// patches
if (File::Exists(fs::path{ m_server_path + "/" + c->OpcodeDir }.string())) {
m_opcode_path = fs::relative(fs::path{ m_server_path + "/" + c->OpcodeDir }).string();
}
// shared_memory_path
if (File::Exists(fs::path{m_server_path + "/" + c->SharedMemDir}.string())) {
m_shared_memory_path = fs::relative(fs::path{ m_server_path + "/" + c->SharedMemDir }).string();
@@ -89,6 +94,7 @@ void PathManager::LoadPaths()
LogInfo("lua_modules path [{}]", m_lua_modules_path);
LogInfo("maps path [{}]", m_maps_path);
LogInfo("patches path [{}]", m_patch_path);
LogInfo("opcode path [{}]", m_opcode_path);
LogInfo("plugins path [{}]", m_plugins_path);
LogInfo("quests path [{}]", m_quests_path);
LogInfo("shared_memory path [{}]", m_shared_memory_path);
@@ -129,6 +135,11 @@ const std::string &PathManager::GetPatchPath() const
return m_patch_path;
}
const std::string &PathManager::GetOpcodePath() const
{
return m_opcode_path;
}
const std::string &PathManager::GetLuaModulesPath() const
{
return m_lua_modules_path;
+2
View File
@@ -13,6 +13,7 @@ public:
[[nodiscard]] const std::string &GetLuaModulesPath() const;
[[nodiscard]] const std::string &GetMapsPath() const;
[[nodiscard]] const std::string &GetPatchPath() const;
[[nodiscard]] const std::string &GetOpcodePath() const;
[[nodiscard]] const std::string &GetPluginsPath() const;
[[nodiscard]] const std::string &GetQuestsPath() const;
[[nodiscard]] const std::string &GetServerPath() const;
@@ -24,6 +25,7 @@ private:
std::string m_lua_modules_path;
std::string m_maps_path;
std::string m_patch_path;
std::string m_opcode_path;
std::string m_plugins_path;
std::string m_quests_path;
std::string m_server_path;
@@ -236,6 +236,10 @@ public:
)
);
if (buyers.empty()) {
return all_entries;
}
std::vector<std::string> char_ids{};
for (auto const &bl : buyers) {
char_ids.push_back((std::to_string(bl.char_id)));
+4
View File
@@ -120,6 +120,10 @@ public:
}
DeleteWhere(db, fmt::format("`char_id` = '{}';", char_id));
if (buy_line_ids.empty()) {
return false;
}
BaseBuyerBuyLinesRepository::DeleteWhere(
db,
fmt::format("`id` IN({})", Strings::Implode(", ", buy_line_ids))
+4
View File
@@ -217,6 +217,10 @@ public:
delete_ids.push_back(std::to_string(e.id));
}
if (delete_ids.empty()) {
return 0;
}
return DeleteWhere(db, fmt::format("`id` IN({})", Strings::Implode(",", delete_ids)));
}
};
+2
View File
@@ -189,6 +189,8 @@ void RuleManager::ResetRules(bool reload) {
m_RuleRealValues[ Real__##rule_name ] = default_value;
#define RULE_BOOL(category_name, rule_name, default_value, notes) \
m_RuleBoolValues[ Bool__##rule_name ] = default_value;
#define RULE_STRING(category_name, rule_name, default_value, notes) \
m_RuleStringValues[ String__##rule_name ] = default_value;
#include "ruletypes.h"
// restore these rules to their pre-reset values
+8 -1
View File
@@ -229,6 +229,7 @@ RULE_BOOL(Character, GroupInvitesRequireTarget, false, "Enable to require player
RULE_BOOL(Character, PlayerTradingLoreFeedback, true, "If enabled, during a player to player trade, if lore items exist, it will output which items.")
RULE_INT(Character, MendAlwaysSucceedValue, 199, "Value at which mend will always succeed its skill check. Default: 199")
RULE_BOOL(Character, SneakAlwaysSucceedOver100, false, "When sneak skill is over 100, always succeed sneak/hide. Default: false")
RULE_INT(Character, BandolierSwapDelay, 0, "Bandolier swap delay in milliseconds, default is 0")
RULE_CATEGORY_END()
RULE_CATEGORY(Mercs)
@@ -337,6 +338,7 @@ RULE_STRING(World, IPExemptionZones, "", "Comma-delimited list of zones to exclu
RULE_STRING(World, MOTD, "", "Server MOTD sent on login, change from empty to have this be used instead of variables table 'motd' value")
RULE_STRING(World, Rules, "", "Server Rules, change from empty to have this be used instead of variables table 'rules' value, lines are pipe (|) separated, example: A|B|C")
RULE_BOOL(World, EnableAutoLogin, false, "Enables or disables auto login of characters, allowing people to log characters in directly from loginserver to ingame")
RULE_BOOL(World, EnablePVPRegions, true, "Enables or disables PVP Regions automatically setting your PVP flag")
RULE_CATEGORY_END()
RULE_CATEGORY(Zone)
@@ -474,7 +476,6 @@ RULE_BOOL(Spells, OldRainTargets, false, "Use old incorrectly implemented maximu
RULE_REAL(Spells, CallOfTheHeroAggroClearDist, 250.0, "Distance at which CoTH will wipe aggro. To disable and always enable aggro wipe on any distance of CoTH, set to 0.")
RULE_BOOL(Spells, NPCSpellPush, false, "Enable spell push on NPCs")
RULE_BOOL(Spells, July242002PetResists, true, "Enable Pets using PCs resist change from July 24 2002")
RULE_INT(Spells, AOEMaxTargets, 0, "Max number of targets a Targeted AOE spell can cast on. Set to 0 for no limit.")
RULE_BOOL(Spells, CazicTouchTargetsPetOwner, true, "If True, causes Cazic Touch to swap targets from pet to pet owner if a pet is tanking.")
RULE_BOOL(Spells, PreventFactionWarOnCharmBreak, false, "Enable spell interupts and dot removal on charm break to prevent faction wars.")
RULE_BOOL(Spells, AllowDoubleInvis, true, "Allows you to cast invisibility spells on a player that is already invisible, live like behavior.")
@@ -514,6 +515,10 @@ RULE_BOOL(Spells, UseClassicSpellFocus, false, "Enabling will tell the server to
RULE_BOOL(Spells, ManaTapsOnAnyClass, false, "Enabling this will allow you to cast mana taps on any class, this will bypass ManaTapsRequireNPCMana rule.")
RULE_INT(Spells, HealAmountMessageFilterThreshold, 100, "Lifetaps below this threshold will not have a message sent to the client (Heal will still process) 0 to Disable.")
RULE_BOOL(Spells, SnareOverridesSpeedBonuses, false, "Enabling will allow snares to override any speed bonuses the entity may have. Default: False")
RULE_INT(Spells, TargetedAOEMaxTargets, 4, "Max number of targets a Targeted AOE spell can cast on. Set to 0 for no limit.")
RULE_INT(Spells, PointBlankAOEMaxTargets, 0, "Max number of targets a Point-Blank AOE spell can cast on. Set to 0 for no limit.")
RULE_INT(Spells, DefaultAOEMaxTargets, 0, "Max number of targets that an AOE spell which does not meet other descriptions can cast on. Set to 0 for no limit.")
RULE_BOOL(Spells, AllowFocusOnSkillDamageSpells, false, "Allow focus effects 185, 459, and 482 to enhance SkillAttack spell effect 193")
RULE_CATEGORY_END()
RULE_CATEGORY(Combat)
@@ -715,6 +720,7 @@ RULE_BOOL(TaskSystem, ExpRewardsIgnoreLevelBasedEXPMods, false, "Rewarding Level
RULE_INT(TaskSystem, SharedTasksWorldProcessRate, 6000, "Timer interval (milliseconds) that shared tasks are processed in world")
RULE_INT(TaskSystem, SharedTasksTerminateTimerMS, 120000, "Delay (milliseconds) until a shared task is terminated if requirements are no longer met after member removal (default: 2 minutes)")
RULE_BOOL(TaskSystem, UpdateOneElementPerTask, true, "If true (live-like) task updates only increment the first matching activity. If false all matching elements will be incremented.")
RULE_INT(TaskSystem, MaxUpdateMessages, 50, "Maximum update messages for non-GiveCash activity types in IncrementDoneCount")
RULE_CATEGORY_END()
RULE_CATEGORY(Range)
@@ -909,6 +915,7 @@ RULE_BOOL(Inventory, AllowAnyWeaponTransformation, false, "Weapons can use any w
RULE_BOOL(Inventory, TransformSummonedBags, false, "Transforms summoned bags into disenchanted ones instead of deleting")
RULE_BOOL(Inventory, AllowMultipleOfSameAugment, false, "Allows multiple of the same augment to be placed in an item via #augmentitem or MQ2, set to true to allow")
RULE_INT(Inventory, AlternateAugmentationSealer, 53, "Allows RoF+ clients to augment items from a special container type")
RULE_BOOL(Inventory, LazyLoadBank, true, "Don't load bank during zoning, only when in proximinity to a banker. May increase zone speed and stability")
RULE_CATEGORY_END()
RULE_CATEGORY(Client)
+17 -18
View File
@@ -47,6 +47,7 @@
#include "repositories/character_corpses_repository.h"
#include "repositories/skill_caps_repository.h"
#include "repositories/inventory_repository.h"
#include "repositories/books_repository.h"
namespace ItemField
{
@@ -1391,30 +1392,28 @@ const EQ::ItemData* SharedDatabase::IterateItems(uint32* id) const
return nullptr;
}
std::string SharedDatabase::GetBook(const char *txtfile, int16 *language)
Book_Struct SharedDatabase::GetBook(const std::string& text_file)
{
char txtfile2[20];
std::string txtout;
strcpy(txtfile2, txtfile);
const auto& l = BooksRepository::GetWhere(
*this,
fmt::format(
"`name` = '{}'",
Strings::Escape(text_file)
)
);
const std::string query = StringFormat("SELECT txtfile, language FROM books WHERE name = '%s'", txtfile2);
auto results = QueryDatabase(query);
if (!results.Success()) {
txtout.assign(" ",1);
return txtout;
Book_Struct b;
if (l.empty()) {
return b;
}
if (results.RowCount() == 0) {
LogError("No book to send, ({})", txtfile);
txtout.assign(" ",1);
return txtout;
}
const auto& e = l.front();
auto& row = results.begin();
txtout.assign(row[0],strlen(row[0]));
*language = static_cast<int16>(Strings::ToInt(row[1]));
b.language = e.language;
b.text = e.txtfile;
return txtout;
return b;
}
// Create appropriate EQ::ItemInstance class
+8 -3
View File
@@ -41,8 +41,7 @@ struct NPCFactionList;
struct FactionAssociations;
namespace EQ
{
namespace EQ {
struct ItemData;
class ItemInstance;
@@ -50,6 +49,12 @@ namespace EQ
class MemoryMappedFile;
}
struct Book_Struct
{
uint8 language;
std::string text;
};
/*
This object is inherited by world and zone's DB object,
and is mainly here to facilitate shared memory, and other
@@ -114,7 +119,7 @@ public:
int admin
);
std::string GetBook(const char *txtfile, int16 *language);
Book_Struct GetBook(const std::string& text_file);
/**
* items
-1
View File
@@ -249,7 +249,6 @@ const std::vector<EQ::skills::SkillType>& EQ::skills::GetExtraDamageSkills()
EQ::skills::SkillFlyingKick,
EQ::skills::SkillKick,
EQ::skills::SkillRoundKick,
EQ::skills::SkillRoundKick,
EQ::skills::SkillTigerClaw,
EQ::skills::SkillFrenzy
};
+2 -2
View File
@@ -25,7 +25,7 @@
// Build variables
// these get injected during the build pipeline
#define CURRENT_VERSION "22.55.1-dev" // always append -dev to the current version for custom-builds
#define CURRENT_VERSION "22.58.0-dev" // always append -dev to the current version for custom-builds
#define LOGIN_VERSION "0.8.0"
#define COMPILE_DATE __DATE__
#define COMPILE_TIME __TIME__
@@ -42,7 +42,7 @@
* Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt
*/
#define CURRENT_BINARY_DATABASE_VERSION 9283
#define CURRENT_BINARY_DATABASE_VERSION 9284
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9045
#endif
+2
View File
@@ -251,4 +251,6 @@ private:
scalar m_value;
};
using ref = reference;
} // namespace perlbind
+4 -3
View File
@@ -28,8 +28,8 @@ struct pusher
++m_pushed;
}
void push(const std::string& value) { mPUSHp(value.c_str(), value.size()); ++m_pushed; }
void push(scalar value) { mPUSHs(value.release()); ++m_pushed; };
void push(reference value) { mPUSHs(value.release()); ++m_pushed; };
void push(scalar value) { mPUSHs(value.release()); ++m_pushed; }
void push(reference value) { mPUSHs(value.release()); ++m_pushed; }
void push(array value)
{
@@ -38,7 +38,8 @@ struct pusher
for (int i = 0; i < count; ++i)
{
// mortalizes one reference to array element to avoid copying
PUSHs(sv_2mortal(SvREFCNT_inc(value[i].sv())));
SV** sv = av_fetch(static_cast<AV*>(value), i, true);
mPUSHs(SvREFCNT_inc(*sv));
}
m_pushed += count;
}
+1 -1
View File
@@ -242,7 +242,7 @@ struct read_as<hash>
static bool check(PerlInterpreter* my_perl, int i, int ax, int items)
{
int remaining = items - i;
return remaining > 0 && remaining % 2 == 0 && SvTYPE(ST(i)) == SVt_PV;
return remaining > 0 && remaining % 2 == 0 && SvTYPE(ST(i)) < SVt_PVAV;
}
static hash get(PerlInterpreter* my_perl, int i, int ax, int items)
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
constexpr int perlbind_version_major = 1;
constexpr int perlbind_version_minor = 0;
constexpr int perlbind_version_minor = 1;
constexpr int perlbind_version_patch = 0;
constexpr int perlbind_version()
+7
View File
@@ -138,6 +138,13 @@ public:
*/
unsigned int GetPlaySequence() const { return m_play_sequence_id; }
/**
* Gets the client version
*
* @return
*/
LSClientVersion GetClientVersion() const { return m_client_version; }
/**
* Gets the connection for this client
*
+119 -12
View File
@@ -7,6 +7,79 @@ extern bool run_server;
#include "../common/eqemu_logsys.h"
#include "../common/misc.h"
#include "../common/path_manager.h"
#include "../common/file.h"
void CheckTitaniumOpcodeFile(const std::string &path) {
if (File::Exists(path)) {
return;
}
auto f = fopen(path.c_str(), "w");
if (f) {
fprintf(f, "#EQEmu Public Login Server OPCodes\n");
fprintf(f, "OP_SessionReady=0x0001\n");
fprintf(f, "OP_Login=0x0002\n");
fprintf(f, "OP_ServerListRequest=0x0004\n");
fprintf(f, "OP_PlayEverquestRequest=0x000d\n");
fprintf(f, "OP_PlayEverquestResponse=0x0021\n");
fprintf(f, "OP_ChatMessage=0x0016\n");
fprintf(f, "OP_LoginAccepted=0x0017\n");
fprintf(f, "OP_ServerListResponse=0x0018\n");
fprintf(f, "OP_Poll=0x0029\n");
fprintf(f, "OP_EnterChat=0x000f\n");
fprintf(f, "OP_PollResponse=0x0011\n");
fclose(f);
}
}
void CheckSoDOpcodeFile(const std::string& path) {
if (File::Exists(path)) {
return;
}
auto f = fopen(path.c_str(), "w");
if (f) {
fprintf(f, "#EQEmu Public Login Server OPCodes\n");
fprintf(f, "OP_SessionReady=0x0001\n");
fprintf(f, "OP_Login=0x0002\n");
fprintf(f, "OP_ServerListRequest=0x0004\n");
fprintf(f, "OP_PlayEverquestRequest=0x000d\n");
fprintf(f, "OP_PlayEverquestResponse=0x0022\n");
fprintf(f, "OP_ChatMessage=0x0017\n");
fprintf(f, "OP_LoginAccepted=0x0018\n");
fprintf(f, "OP_ServerListResponse=0x0019\n");
fprintf(f, "OP_Poll=0x0029\n");
fprintf(f, "OP_LoginExpansionPacketData=0x0031\n");
fprintf(f, "OP_EnterChat=0x000f\n");
fprintf(f, "OP_PollResponse=0x0011\n");
fclose(f);
}
}
void CheckLarionOpcodeFile(const std::string& path) {
if (File::Exists(path)) {
return;
}
auto f = fopen(path.c_str(), "w");
if (f) {
fprintf(f, "#EQEmu Public Login Server OPCodes\n");
fprintf(f, "OP_SessionReady=0x0001\n");
fprintf(f, "OP_Login=0x0002\n");
fprintf(f, "OP_ServerListRequest=0x0004\n");
fprintf(f, "OP_PlayEverquestRequest=0x000d\n");
fprintf(f, "OP_PlayEverquestResponse=0x0022\n");
fprintf(f, "OP_ChatMessage=0x0017\n");
fprintf(f, "OP_LoginAccepted=0x0018\n");
fprintf(f, "OP_ServerListResponse=0x0019\n");
fprintf(f, "OP_Poll=0x0029\n");
fprintf(f, "OP_EnterChat=0x000f\n");
fprintf(f, "OP_PollResponse=0x0011\n");
fprintf(f, "OP_SystemFingerprint=0x0016\n");
fprintf(f, "OP_ExpansionList=0x0030\n");
fclose(f);
}
}
ClientManager::ClientManager()
{
@@ -19,14 +92,12 @@ ClientManager::ClientManager()
std::string opcodes_path = fmt::format(
"{}/{}",
path.GetServerPath(),
server.config.GetVariableString(
"client_configuration",
"titanium_opcodes",
"login_opcodes.conf"
)
path.GetOpcodePath(),
"login_opcodes.conf"
);
CheckTitaniumOpcodeFile(opcodes_path);
if (!titanium_ops->LoadOpcodes(opcodes_path.c_str())) {
LogError(
"ClientManager fatal error: couldn't load opcodes for Titanium file [{0}]",
@@ -58,14 +129,12 @@ ClientManager::ClientManager()
opcodes_path = fmt::format(
"{}/{}",
path.GetServerPath(),
server.config.GetVariableString(
"client_configuration",
"sod_opcodes",
"login_opcodes.conf"
)
path.GetOpcodePath(),
"login_opcodes_sod.conf"
);
CheckSoDOpcodeFile(opcodes_path);
if (!sod_ops->LoadOpcodes(opcodes_path.c_str())) {
LogError(
"ClientManager fatal error: couldn't load opcodes for SoD file {0}",
@@ -88,6 +157,44 @@ ClientManager::ClientManager()
clients.push_back(c);
}
);
int larion_port = server.config.GetVariableInt("client_configuration", "larion_port", 15900);
EQStreamManagerInterfaceOptions larion_opts(larion_port, false, false);
larion_stream = new EQ::Net::EQStreamManager(larion_opts);
larion_ops = new RegularOpcodeManager;
opcodes_path = fmt::format(
"{}/{}",
path.GetOpcodePath(),
"login_opcodes_larion.conf"
);
CheckLarionOpcodeFile(opcodes_path);
if (!larion_ops->LoadOpcodes(opcodes_path.c_str())) {
LogError(
"ClientManager fatal error: couldn't load opcodes for Larion file [{0}]",
server.config.GetVariableString("client_configuration", "larion_opcodes", "login_opcodes.conf")
);
run_server = false;
}
larion_stream->OnNewConnection(
[this](std::shared_ptr<EQ::Net::EQStream> stream) {
LogInfo(
"New Larion client connection from [{0}:{1}]",
long2ip(stream->GetRemoteIP()),
stream->GetRemotePort()
);
stream->SetOpcodeManager(&larion_ops);
Client* c = new Client(stream, cv_larion);
clients.push_back(c);
}
);
}
ClientManager::~ClientManager()
+2
View File
@@ -55,6 +55,8 @@ private:
EQ::Net::EQStreamManager *titanium_stream;
OpcodeManager *sod_ops;
EQ::Net::EQStreamManager *sod_stream;
OpcodeManager *larion_ops;
EQ::Net::EQStreamManager* larion_stream;
};
#endif
+2 -1
View File
@@ -83,7 +83,8 @@ struct PlayEverquestResponse_Struct {
enum LSClientVersion {
cv_titanium,
cv_sod
cv_sod,
cv_larion
};
enum LSClientStatus {
@@ -0,0 +1,14 @@
#EQEmu Public Login Server OPCodes
OP_SessionReady=0x0001
OP_Login=0x0002
OP_ServerListRequest=0x0004
OP_PlayEverquestRequest=0x000d
OP_PlayEverquestResponse=0x0022
OP_ChatMessage=0x0017
OP_LoginAccepted=0x0018
OP_ServerListResponse=0x0019
OP_Poll=0x0029
OP_EnterChat=0x000f
OP_PollResponse=0x0011
OP_SystemFingerprint=0x0016
OP_ExpansionList=0x0030
+1 -1
View File
@@ -137,7 +137,7 @@ std::unique_ptr<EQApplicationPacket> ServerManager::CreateServerListPacket(Clien
use_local_ip ? "Local" : "Remote"
);
world_server->SerializeForClientServerList(buf, use_local_ip);
world_server->SerializeForClientServerList(buf, use_local_ip, client->GetClientVersion());
}
return std::make_unique<EQApplicationPacket>(OP_ServerListResponse, buf);
+24 -12
View File
@@ -977,7 +977,7 @@ bool WorldServer::ValidateWorldServerAdminLogin(
return false;
}
void WorldServer::SerializeForClientServerList(SerializeBuffer &out, bool use_local_ip) const
void WorldServer::SerializeForClientServerList(SerializeBuffer& out, bool use_local_ip, LSClientVersion version) const
{
// see LoginClientServerData_Struct
if (use_local_ip) {
@@ -987,19 +987,31 @@ void WorldServer::SerializeForClientServerList(SerializeBuffer &out, bool use_lo
out.WriteString(GetRemoteIP());
}
switch (GetServerListID()) {
case LS::ServerType::Legends:
out.WriteInt32(LS::ServerTypeFlags::Legends);
break;
case LS::ServerType::Preferred:
out.WriteInt32(LS::ServerTypeFlags::Preferred);
break;
default:
out.WriteInt32(LS::ServerTypeFlags::Standard);
break;
if (version == cv_larion) {
out.WriteUInt32(9000);
}
switch (GetServerListID()) {
case LS::ServerType::Legends:
out.WriteInt32(LS::ServerTypeFlags::Legends);
break;
case LS::ServerType::Preferred:
out.WriteInt32(LS::ServerTypeFlags::Preferred);
break;
default:
out.WriteInt32(LS::ServerTypeFlags::Standard);
break;
}
if (version == cv_larion) {
auto server_id = GetServerId();
//if this is 0, the client will not show the server in the list
out.WriteUInt32(1);
out.WriteUInt32(server_id);
}
else {
out.WriteUInt32(GetServerId());
}
out.WriteUInt32(GetServerId());
out.WriteString(GetServerLongName());
out.WriteString("us"); // country code
out.WriteString("en"); // language code
+2 -1
View File
@@ -7,6 +7,7 @@
#include "../common/packet_dump.h"
#include "database.h"
#include "../common/event/timer.h"
#include "login_types.h"
#include <string>
#include <memory>
@@ -149,7 +150,7 @@ public:
bool HandleNewLoginserverRegisteredOnly(Database::DbWorldRegistration &world_registration);
bool HandleNewLoginserverInfoUnregisteredAllowed(Database::DbWorldRegistration &world_registration);
void SerializeForClientServerList(class SerializeBuffer& out, bool use_local_ip) const;
void SerializeForClientServerList(class SerializeBuffer& out, bool use_local_ip, LSClientVersion version) const;
private:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eqemu-server",
"version": "22.55.1",
"version": "22.58.0",
"repository": {
"type": "git",
"url": "https://github.com/EQEmu/Server.git"
+1
View File
@@ -287,6 +287,7 @@ void Adventure::Finished(AdventureWinStatus ws)
auto character_id = database.GetCharacterID(*iter);
if (character_id == 0) {
++iter;
continue;
}
+26 -8
View File
@@ -315,6 +315,13 @@ void LoginServer::ProcessLSFatalError(uint16_t opcode, EQ::Net::Packet &p)
error,
reason
);
if (m_legacy_client) {
m_legacy_client.release();
}
else if (m_client) {
m_client.release();
}
}
void LoginServer::ProcessSystemwideMessage(uint16_t opcode, EQ::Net::Packet &p)
@@ -598,6 +605,11 @@ bool LoginServer::Connect()
void LoginServer::SendInfo()
{
if (m_client == nullptr && m_legacy_client == nullptr) {
LogDebug("No client to send info to loginserver");
return;
}
const WorldConfig *Config = WorldConfig::get();
auto pack = new ServerPacket;
@@ -643,6 +655,11 @@ void LoginServer::SendInfo()
void LoginServer::SendStatus()
{
if (m_client == nullptr && m_legacy_client == nullptr) {
LogDebug("No client to send status to loginserver");
return;
}
auto pack = new ServerPacket;
pack->opcode = ServerOP_LSStatus;
pack->size = sizeof(ServerLSStatus_Struct);
@@ -671,20 +688,21 @@ void LoginServer::SendStatus()
*/
void LoginServer::SendPacket(ServerPacket *pack)
{
if (m_is_legacy) {
if (m_legacy_client) {
m_legacy_client->SendPacket(pack);
}
if (m_legacy_client) {
m_legacy_client->SendPacket(pack);
}
else {
if (m_client) {
m_client->SendPacket(pack);
}
else if (m_client) {
m_client->SendPacket(pack);
}
}
void LoginServer::SendAccountUpdate(ServerPacket *pack)
{
if (m_client == nullptr && m_legacy_client == nullptr) {
LogDebug("No client to send account update to loginserver");
return;
}
auto *ls_account_update = (ServerLSAccountUpdate_Struct *) pack->pBuffer;
if (CanUpdate()) {
LogInfo(
+7
View File
@@ -294,6 +294,13 @@ bool WorldBoot::DatabaseLoadRoutines(int argc, char **argv)
database.ClearBuyerDetails();
LogInfo("Clearing buyer table details");
if (RuleB(Bots, Enabled)) {
LogInfo("Clearing [bot_pet_buffs] table of stale entries");
database.QueryDatabase(
"DELETE FROM bot_pet_buffs WHERE NOT EXISTS (SELECT * FROM bot_pets WHERE bot_pets.pets_index = bot_pet_buffs.pets_index)"
);
}
if (!content_db.LoadItems(hotfix_name)) {
LogError("Error: Could not load item data. But ignoring");
}
+3
View File
@@ -65,6 +65,7 @@ SET(zone_sources
lua_inventory.cpp
lua_item.cpp
lua_iteminst.cpp
lua_merc.cpp
lua_mob.cpp
lua_mod.cpp
lua_npc.cpp
@@ -115,6 +116,7 @@ SET(zone_sources
perl_groups.cpp
perl_hateentry.cpp
perl_inventory.cpp
perl_merc.cpp
perl_mob.cpp
perl_npc.cpp
perl_object.cpp
@@ -224,6 +226,7 @@ SET(zone_headers
lua_inventory.h
lua_item.h
lua_iteminst.h
lua_merc.h
lua_mob.h
lua_mod.h
lua_npc.h
+5 -1
View File
@@ -2185,7 +2185,7 @@ void Client::AutoGrantAAPoints() {
SendAlternateAdvancementStats();
}
void Client::GrantAllAAPoints(uint8 unlock_level)
void Client::GrantAllAAPoints(uint8 unlock_level, bool skip_grant_only)
{
//iterate through every AA
for (auto& aa : zone->aa_abilities) {
@@ -2195,6 +2195,10 @@ void Client::GrantAllAAPoints(uint8 unlock_level)
continue;
}
if (ability->grant_only && skip_grant_only) {
continue;
}
const uint8 level = unlock_level ? unlock_level : GetLevel();
AA::Rank* rank = ability->first;
+98 -167
View File
@@ -1550,17 +1550,18 @@ void Mob::DoAttack(Mob *other, DamageHitInfo &hit, ExtraAttackOptions *opts, boo
hit.damage_done = 0;
}
if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_USE_SKILL)) {
const auto& export_string = fmt::format(
parse->EventBotMerc(
EVENT_USE_SKILL,
this,
nullptr,
[&]() {
return fmt::format(
"{} {}",
hit.skill,
GetSkill(hit.skill)
);
parse->EventBot(EVENT_USE_SKILL, CastToBot(), nullptr, export_string, 0);
}
}
);
}
}
@@ -1739,7 +1740,7 @@ bool Mob::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool
(HasOwner() && GetOwner()->IsClient() && other->IsClient())
)
) {
for (auto const& [id, mob] : entity_list.GetCloseMobList(other)) {
for (auto const& [id, mob] : other->GetCloseMobList()) {
if (!mob) {
continue;
}
@@ -1922,11 +1923,9 @@ bool Client::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::Skil
}
if (killer_mob) {
if (killer_mob->IsNPC()) {
if (parse->HasQuestSub(killer_mob->GetNPCTypeID(), EVENT_SLAY)) {
parse->EventNPC(EVENT_SLAY, killer_mob->CastToNPC(), this, "", 0);
}
parse->EventBotMercNPC(EVENT_SLAY, killer_mob, this);
if (killer_mob->IsNPC()) {
killed_by = KilledByTypes::Killed_NPC;
auto emote_id = killer_mob->GetEmoteID();
@@ -1934,12 +1933,6 @@ bool Client::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::Skil
killer_mob->CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::KilledPC, emoteid, this);
}
killer_mob->TrySpellOnKill(killed_level, spell);
} else if (killer_mob->IsBot()) {
if (parse->BotHasQuestSub(EVENT_SLAY)) {
parse->EventBot(EVENT_SLAY, killer_mob->CastToBot(), this, "", 0);
}
killer_mob->TrySpellOnKill(killed_level, spell);
}
@@ -2319,8 +2312,7 @@ bool NPC::Attack(Mob* other, int Hand, bool bRiposte, bool IsStrikethrough, bool
//Guard Assist Code
if (RuleB(Character, PVPEnableGuardFactionAssist)) {
if (IsClient() && other->IsClient() || (HasOwner() && GetOwner()->IsClient() && other->IsClient())) {
auto& mob_list = entity_list.GetCloseMobList(other);
for (auto& e : mob_list) {
for (auto& e : other->GetCloseMobList()) {
auto mob = e.second;
if (!mob) {
continue;
@@ -2459,14 +2451,10 @@ void NPC::Damage(Mob* other, int64 damage, uint16 spell_id, EQ::skills::SkillTyp
spell_id = SPELL_UNKNOWN;
//handle EVENT_ATTACK. Resets after we have not been attacked for 12 seconds
if (attacked_timer.Check())
{
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_ATTACK)) {
LogCombat("Triggering EVENT_ATTACK due to attack by [{}]", other ? other->GetName() : "nullptr");
parse->EventNPC(EVENT_ATTACK, this, other, "", 0);
}
if (attacked_timer.Check()) {
parse->EventMercNPC(EVENT_ATTACK, this, other);
}
attacked_timer.Start(CombatEventTimer_expire);
if (!IsEngaged())
@@ -2507,41 +2495,22 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy
Mob* owner_or_self = killer_mob ? killer_mob->GetOwnerOrSelf() : nullptr;
if (IsNPC()) {
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_DEATH)) {
const auto& export_string = fmt::format(
"{} {} {} {}",
killer_mob ? killer_mob->GetID() : 0,
damage,
spell,
static_cast<int>(attack_skill)
);
auto exports = [&]() {
return fmt::format(
"{} {} {} {}",
killer_mob ? killer_mob->GetID() : 0,
damage,
spell,
static_cast<int>(attack_skill)
);
};
if (parse->EventNPC(EVENT_DEATH, this, owner_or_self, export_string, 0) != 0) {
if (GetHP() < 0) {
SetHP(0);
}
return false;
}
if (parse->EventBotMercNPC(EVENT_DEATH, this, owner_or_self, exports) != 0) {
if (GetHP() < 0) {
SetHP(0);
}
} else if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_DEATH)) {
const auto& export_string = fmt::format(
"{} {} {} {}",
killer_mob ? killer_mob->GetID() : 0,
damage,
spell,
static_cast<int>(attack_skill)
);
if (parse->EventBot(EVENT_DEATH, CastToBot(), owner_or_self, export_string, 0) != 0) {
if (GetHP() < 0) {
SetHP(0);
}
return false;
}
}
return false;
}
if (killer_mob && killer_mob->IsOfClientBot() && IsValidSpell(spell) && damage > 0) {
@@ -2973,8 +2942,7 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy
entity_list.UnMarkNPC(GetID());
entity_list.RemoveNPC(GetID());
// entity_list.RemoveMobFromCloseLists(this);
close_mobs.clear();
m_close_mobs.clear();
SetID(0);
ApplyIllusionToCorpse(illusion_spell_id, corpse);
@@ -3079,10 +3047,8 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy
}
}
if (killer_mob && killer_mob->IsBot()) {
if (parse->BotHasQuestSub(EVENT_NPC_SLAY)) {
parse->EventBot(EVENT_NPC_SLAY, killer_mob->CastToBot(), this, "", 0);
}
if (killer_mob) {
parse->EventBotMerc(EVENT_NPC_SLAY, killer_mob, this);
killer_mob->TrySpellOnKill(killed_level, spell);
}
@@ -3110,24 +3076,29 @@ bool NPC::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillTy
}
}
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_DEATH_COMPLETE)) {
const auto& export_string = fmt::format(
"{} {} {} {} {} {} {} {} {}",
killer_mob ? killer_mob->GetID() : 0,
damage,
spell,
static_cast<int>(attack_skill),
entity_id,
m_combat_record.GetStartTime(),
m_combat_record.GetEndTime(),
m_combat_record.GetDamageReceived(),
m_combat_record.GetHealingReceived()
);
std::vector<std::any> args = { corpse };
std::vector<std::any> args = { corpse };
parse->EventNPC(EVENT_DEATH_COMPLETE, this, owner_or_self, export_string, 0, &args);
}
parse->EventMercNPC(
EVENT_DEATH_COMPLETE,
this,
owner_or_self,
[&]() {
return fmt::format(
"{} {} {} {} {} {} {} {} {}",
killer_mob ? killer_mob->GetID() : 0,
damage,
spell,
static_cast<int>(attack_skill),
entity_id,
m_combat_record.GetStartTime(),
m_combat_record.GetEndTime(),
m_combat_record.GetDamageReceived(),
m_combat_record.GetHealingReceived()
);
},
0,
&args
);
// Zone controller process EVENT_DEATH_ZONE (Death events)
if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_DEATH_ZONE)) {
@@ -4287,100 +4258,60 @@ void Mob::CommonDamage(Mob* attacker, int64 &damage, const uint16 spell_id, cons
//final damage has been determined.
int old_hp_ratio = (int)GetHPRatio();
const auto has_bot_given_event = parse->BotHasQuestSub(EVENT_DAMAGE_GIVEN);
const auto has_bot_taken_event = parse->BotHasQuestSub(EVENT_DAMAGE_TAKEN);
const auto has_npc_given_event = (
(
IsNPC() &&
parse->HasQuestSub(CastToNPC()->GetNPCTypeID(), EVENT_DAMAGE_GIVEN)
) ||
(
attacker &&
attacker->IsNPC() &&
parse->HasQuestSub(attacker->CastToNPC()->GetNPCTypeID(), EVENT_DAMAGE_GIVEN)
)
);
const auto has_npc_taken_event = (
(
IsNPC() &&
parse->HasQuestSub(CastToNPC()->GetNPCTypeID(), EVENT_DAMAGE_TAKEN)
) ||
(
attacker &&
attacker->IsNPC() &&
parse->HasQuestSub(attacker->CastToNPC()->GetNPCTypeID(), EVENT_DAMAGE_TAKEN)
)
);
const auto has_player_given_event = parse->PlayerHasQuestSub(EVENT_DAMAGE_GIVEN);
const auto has_player_taken_event = parse->PlayerHasQuestSub(EVENT_DAMAGE_TAKEN);
const auto has_given_event = (
has_bot_given_event ||
has_npc_given_event ||
has_player_given_event
);
const auto has_taken_event = (
has_bot_taken_event ||
has_npc_taken_event ||
has_player_taken_event
);
std::vector<std::any> args;
int64 damage_override = 0;
if (has_given_event && attacker) {
const auto export_string = fmt::format(
"{} {} {} {} {} {} {} {} {}",
GetID(),
damage,
spell_id,
static_cast<int>(skill_used),
FromDamageShield ? 1 : 0,
avoidable ? 1 : 0,
buffslot,
iBuffTic ? 1 : 0,
static_cast<int>(special)
);
if (attacker) {
args = { this };
if (attacker->IsBot() && has_bot_given_event) {
parse->EventBot(EVENT_DAMAGE_GIVEN, attacker->CastToBot(), this, export_string, 0);
} else if (attacker->IsClient() && has_player_given_event) {
args.push_back(this);
parse->EventPlayer(EVENT_DAMAGE_GIVEN, attacker->CastToClient(), export_string, 0, &args);
} else if (attacker->IsNPC() && has_npc_given_event) {
parse->EventNPC(EVENT_DAMAGE_GIVEN, attacker->CastToNPC(), this, export_string, 0);
}
parse->EventMob(
EVENT_DAMAGE_GIVEN,
attacker,
this,
[&]() {
return fmt::format(
"{} {} {} {} {} {} {} {} {}",
GetID(),
damage,
spell_id,
static_cast<int>(skill_used),
FromDamageShield ? 1 : 0,
avoidable ? 1 : 0,
buffslot,
iBuffTic ? 1 : 0,
static_cast<int>(special)
);
},
0,
&args
);
}
if (has_taken_event) {
const auto export_string = fmt::format(
"{} {} {} {} {} {} {} {} {}",
attacker ? attacker->GetID() : 0,
damage,
spell_id,
static_cast<int>(skill_used),
FromDamageShield ? 1 : 0,
avoidable ? 1 : 0,
buffslot,
iBuffTic ? 1 : 0,
static_cast<int>(special)
);
args = { attacker };
if (IsBot() && has_bot_taken_event) {
damage_override = parse->EventBot(EVENT_DAMAGE_TAKEN, CastToBot(), attacker ? attacker : nullptr, export_string, 0);
} else if (IsClient() && has_player_taken_event) {
args.push_back(attacker ? attacker : nullptr);
damage_override = parse->EventPlayer(EVENT_DAMAGE_TAKEN, CastToClient(), export_string, 0, &args);
} else if (IsNPC() && has_npc_taken_event) {
damage_override = parse->EventNPC(EVENT_DAMAGE_TAKEN, CastToNPC(), attacker ? attacker : nullptr, export_string, 0);
}
}
damage_override = parse->EventMob(
EVENT_DAMAGE_TAKEN,
this,
attacker,
[&]() {
return fmt::format(
"{} {} {} {} {} {} {} {} {}",
attacker ? attacker->GetID() : 0,
damage,
spell_id,
static_cast<int>(skill_used),
FromDamageShield ? 1 : 0,
avoidable ? 1 : 0,
buffslot,
iBuffTic ? 1 : 0,
static_cast<int>(special)
);
},
0,
&args
);
if (damage_override > 0) {
damage = damage_override;
+7 -12
View File
@@ -72,7 +72,7 @@ Mob *Aura::GetOwner()
// not 100% sure how this one should work and PVP affects ...
void Aura::ProcessOnAllFriendlies(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this, distance);
auto &mob_list = GetCloseMobList(distance);
std::set<int> delayed_remove;
bool is_buff = IsBuffSpell(spell_id); // non-buff spells don't cast on enter
@@ -127,7 +127,7 @@ void Aura::ProcessOnAllFriendlies(Mob *owner)
void Aura::ProcessOnAllGroupMembers(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this, distance);
auto &mob_list = GetCloseMobList(distance);
std::set<int> delayed_remove;
bool is_buff = IsBuffSpell(spell_id); // non-buff spells don't cast on enter
@@ -369,7 +369,7 @@ void Aura::ProcessOnAllGroupMembers(Mob *owner)
void Aura::ProcessOnGroupMembersPets(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this,distance);
auto &mob_list = GetCloseMobList(distance);
std::set<int> delayed_remove;
bool is_buff = IsBuffSpell(spell_id); // non-buff spells don't cast on enter
// This type can either live on the pet (level 55/70 MAG aura) or on the pet owner (level 85 MAG aura)
@@ -576,7 +576,7 @@ void Aura::ProcessOnGroupMembersPets(Mob *owner)
void Aura::ProcessTotem(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this, distance);
auto &mob_list = GetCloseMobList(distance);
std::set<int> delayed_remove;
bool is_buff = IsBuffSpell(spell_id); // non-buff spells don't cast on enter
@@ -634,9 +634,7 @@ void Aura::ProcessTotem(Mob *owner)
void Aura::ProcessEnterTrap(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this, distance);
for (auto &e : mob_list) {
for (auto &e : GetCloseMobList(distance)) {
auto mob = e.second;
if (!mob) {
continue;
@@ -656,9 +654,7 @@ void Aura::ProcessEnterTrap(Mob *owner)
void Aura::ProcessExitTrap(Mob *owner)
{
auto &mob_list = entity_list.GetCloseMobList(this, distance);
for (auto &e : mob_list) {
for (auto &e : GetCloseMobList(distance)) {
auto mob = e.second;
if (!mob) {
continue;
@@ -689,8 +685,7 @@ void Aura::ProcessExitTrap(Mob *owner)
// and hard to reason about
void Aura::ProcessSpawns()
{
const auto &clients = entity_list.GetCloseMobList(this, distance);
for (auto &e : clients) {
for (auto &e: GetCloseMobList(distance)) {
if (!e.second) {
continue;
}
+2 -12
View File
@@ -1578,17 +1578,7 @@ bool Bot::Process()
return false;
}
if (mob_close_scan_timer.Check()) {
LogAIScanCloseDetail(
"is_moving [{}] bot [{}] timer [{}]",
moving ? "true" : "false",
GetCleanName(),
mob_close_scan_timer.GetDuration()
);
entity_list.ScanCloseMobs(close_mobs, this, IsMoving());
}
ScanCloseMobProcess();
SpellProcess();
if (tic_timer.Check()) {
@@ -2978,7 +2968,7 @@ void Bot::SetOwnerTarget(Client* bot_owner) {
if (NOT_HOLDING && NOT_PASSIVE) {
auto attack_target = bot_owner->GetTarget();
if (attack_target) {
if (attack_target && HasBotAttackFlag(attack_target)) {
InterruptSpell();
WipeHateList();
+5
View File
@@ -35,6 +35,11 @@ void bot_command_attack(Client *c, const Seperator *sep)
return;
}
if (!c->HasBotAttackFlag(target_mob)) {
target_mob->SetBotAttackFlag(c->CharacterID());
target_mob->bot_attack_flag_timer.Start(10000);
}
size_t attacker_count = 0;
Bot *first_attacker = nullptr;
sbl.remove(nullptr);
+11 -3
View File
@@ -857,7 +857,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
c->Message(
Chat::White,
fmt::format(
"Usage: {} [bot_name]",
"Usage: {} [bot_name] [optional: silent]",
sep->arg[0]
).c_str()
);
@@ -1045,9 +1045,17 @@ void bot_command_spawn(Client *c, const Seperator *sep)
message_index = VALIDATECLASSID(my_bot->GetClass());
}
if (c->GetBotOption(Client::booSpawnMessageSay)) {
std::string silent_confirm = sep->arg[2];
bool silentTell = false;
if (!silent_confirm.compare("silent")) {
silentTell = true;
}
if (!silentTell && c->GetBotOption(Client::booSpawnMessageSay)) {
Bot::BotGroupSay(my_bot, bot_spawn_message[message_index].c_str());
} else if (c->GetBotOption(Client::booSpawnMessageTell)) {
}
else if (!silentTell && c->GetBotOption(Client::booSpawnMessageTell)) {
my_bot->OwnerMessage(bot_spawn_message[message_index]);
}
}
+2 -2
View File
@@ -828,7 +828,7 @@ bool BotDatabase::LoadTimers(Bot* b)
BotTimer_Struct t{ };
for (const auto& e : l) {
if (t.timer_value < (Timer::GetCurrentTime() + t.recast_time)) {
if (e.timer_value < (Timer::GetCurrentTime() + e.recast_time)) {
t.timer_id = e.timer_id;
t.timer_value = e.timer_value;
t.recast_time = e.recast_time;
@@ -1451,7 +1451,7 @@ bool BotDatabase::DeletePetBuffs(const uint32 bot_id)
return true;
}
BotPetBuffsRepository::DeleteOne(database, saved_pet_index);
BotPetBuffsRepository::DeleteWhere(database, fmt::format("pets_index = {}", saved_pet_index));
return true;
}
+3 -1
View File
@@ -312,10 +312,12 @@ void Client::SpawnRaidBotsOnConnect(Raid* raid) {
for (const auto& m: r_members) {
if (strlen(m.member_name) != 0) {
for (const auto& b: bots_list) {
for (const auto& b : bots_list) {
if (strcmp(m.member_name, b.bot_name) == 0) {
std::string buffer = "^spawn ";
buffer.append(m.member_name);
std::string silent = " silent";
buffer.append(silent);
bot_command_real_dispatch(this, buffer.c_str());
auto bot = entity_list.GetBotByBotName(m.member_name);
+520 -151
View File
@@ -145,47 +145,48 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
0, // in_heroic_strikethrough
false // in_keeps_sold_items
),
hpupdate_timer(2000),
camp_timer(29000),
process_timer(100),
consume_food_timer(CONSUMPTION_TIMER),
zoneinpacket_timer(1000),
linkdead_timer(RuleI(Zone,ClientLinkdeadMS)),
dead_timer(2000),
global_channel_timer(1000),
fishing_timer(8000),
endupkeep_timer(1000),
autosave_timer(RuleI(Character, AutosaveIntervalS) * 1000),
client_scan_npc_aggro_timer(RuleI(Aggro, ClientAggroCheckIdleInterval)),
client_zone_wide_full_position_update_timer(5 * 60 * 1000),
tribute_timer(Tribute_duration),
proximity_timer(ClientProximity_interval),
TaskPeriodic_Timer(RuleI(TaskSystem, PeriodicCheckTimer) * 1000),
charm_update_timer(6000),
rest_timer(1),
pick_lock_timer(1000),
charm_class_attacks_timer(3000),
charm_cast_timer(3500),
qglobal_purge_timer(30000),
TrackingTimer(2000),
RespawnFromHoverTimer(0),
merc_timer(RuleI(Mercs, UpkeepIntervalMS)),
ItemQuestTimer(500),
anon_toggle_timer(250),
afk_toggle_timer(250),
helm_toggle_timer(250),
aggro_meter_timer(AGGRO_METER_UPDATE_MS),
m_Proximity(FLT_MAX, FLT_MAX, FLT_MAX), //arbitrary large number
m_ZoneSummonLocation(-2.0f,-2.0f,-2.0f,-2.0f),
m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f),
m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f),
last_region_type(RegionTypeUnsupported),
m_dirtyautohaters(false),
mob_close_scan_timer(6000),
position_update_timer(10000),
consent_throttle_timer(2000),
tmSitting(0),
parcel_timer(RuleI(Parcel, ParcelDeliveryDelay))
hpupdate_timer(2000),
camp_timer(29000),
process_timer(100),
consume_food_timer(CONSUMPTION_TIMER),
zoneinpacket_timer(1000),
linkdead_timer(RuleI(Zone, ClientLinkdeadMS)),
dead_timer(2000),
global_channel_timer(1000),
fishing_timer(8000),
endupkeep_timer(1000),
autosave_timer(RuleI(Character, AutosaveIntervalS) * 1000),
m_client_npc_aggro_scan_timer(RuleI(Aggro, ClientAggroCheckIdleInterval)),
m_client_zone_wide_full_position_update_timer(5 * 60 * 1000),
tribute_timer(Tribute_duration),
proximity_timer(ClientProximity_interval),
TaskPeriodic_Timer(RuleI(TaskSystem, PeriodicCheckTimer) * 1000),
charm_update_timer(6000),
rest_timer(1),
pick_lock_timer(1000),
charm_class_attacks_timer(3000),
charm_cast_timer(3500),
qglobal_purge_timer(30000),
TrackingTimer(2000),
RespawnFromHoverTimer(0),
merc_timer(RuleI(Mercs, UpkeepIntervalMS)),
ItemQuestTimer(500),
anon_toggle_timer(250),
afk_toggle_timer(250),
helm_toggle_timer(250),
aggro_meter_timer(AGGRO_METER_UPDATE_MS),
m_Proximity(FLT_MAX, FLT_MAX, FLT_MAX), //arbitrary large number
m_ZoneSummonLocation(-2.0f, -2.0f, -2.0f, -2.0f),
m_AutoAttackPosition(0.0f, 0.0f, 0.0f, 0.0f),
m_AutoAttackTargetLocation(0.0f, 0.0f, 0.0f),
last_region_type(RegionTypeUnsupported),
m_dirtyautohaters(false),
m_position_update_timer(10000),
consent_throttle_timer(2000),
tmSitting(0),
parcel_timer(RuleI(Parcel, ParcelDeliveryDelay)),
lazy_load_bank_check_timer(1000),
bandolier_throttle_timer(0)
{
for (auto client_filter = FilterNone; client_filter < _FilterCount; client_filter = eqFilterType(client_filter + 1)) {
SetFilter(client_filter, FilterShow);
@@ -284,6 +285,7 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
memset(&m_epp, 0, sizeof(m_epp));
PendingTranslocate = false;
PendingSacrifice = false;
sacrifice_caster_id = 0;
controlling_boat_id = 0;
controlled_mob_id = 0;
qGlobals = nullptr;
@@ -391,7 +393,6 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
SetBotPrecombat(false);
AI_Init();
}
Client::~Client() {
@@ -444,7 +445,7 @@ Client::~Client() {
m_tradeskill_object = nullptr;
}
close_mobs.clear();
m_close_mobs.clear();
if(IsDueling() && GetDuelTarget() != 0) {
Entity* entity = entity_list.GetID(GetDuelTarget());
@@ -1242,45 +1243,28 @@ void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_s
entity_list.ProcessProximitySay(message, this, language);
}
Mob* t = GetTarget();
if (
GetTarget() &&
GetTarget()->IsNPC() &&
!IsInvisible(GetTarget())
t &&
!IsInvisible(t) &&
DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say)
) {
auto* t = GetTarget()->CastToNPC();
if (!t->IsEngaged()) {
CheckLDoNHail(t);
CheckEmoteHail(t, message);
const bool is_engaged = t->IsEngaged();
if (DistanceNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say)) {
if (parse->HasQuestSub(t->GetNPCTypeID(), EVENT_SAY)) {
parse->EventNPC(EVENT_SAY, t, this, message, language);
}
if (RuleB(TaskSystem, EnableTaskSystem)) {
if (UpdateTasksOnSpeakWith(t)) {
t->DoQuestPause(this);
}
}
}
if (is_engaged) {
parse->EventBotMercNPC(EVENT_AGGRO_SAY, t, this, [&]() { return message; }, language);
} else {
if (parse->HasQuestSub(t->GetNPCTypeID(), EVENT_AGGRO_SAY)) {
if (DistanceSquaredNoZ(m_Position, t->GetPosition()) <= RuleI(Range, Say)) {
parse->EventNPC(EVENT_AGGRO_SAY, t, this, message, language);
}
}
parse->EventBotMercNPC(EVENT_SAY, t, this, [&]() { return message; }, language);
}
}
else if (GetTarget() && GetTarget()->IsBot() && !IsInvisible(GetTarget())) {
if (DistanceNoZ(m_Position, GetTarget()->GetPosition()) <= RuleI(Range, Say)) {
if (GetTarget()->IsEngaged()) {
if (parse->BotHasQuestSub(EVENT_AGGRO_SAY)) {
parse->EventBot(EVENT_AGGRO_SAY, GetTarget()->CastToBot(), this, message, language);
}
} else {
if (parse->BotHasQuestSub(EVENT_SAY)) {
parse->EventBot(EVENT_SAY, GetTarget()->CastToBot(), this, message, language);
if (t->IsNPC() && !is_engaged) {
CheckLDoNHail(t->CastToNPC());
CheckEmoteHail(t->CastToNPC(), message);
if (RuleB(TaskSystem, EnableTaskSystem)) {
if (UpdateTasksOnSpeakWith(t->CastToNPC())) {
t->CastToNPC()->DoQuestPause(this);
}
}
}
@@ -2298,52 +2282,67 @@ void Client::SetGM(bool toggle) {
UpdateWho();
}
void Client::ReadBook(BookRequest_Struct *book) {
int16 book_language=0;
char *txtfile = book->txtfile;
void Client::ReadBook(BookRequest_Struct* book)
{
const std::string& text_file = book->txtfile;
if(txtfile[0] == '0' && txtfile[1] == '\0') {
//invalid book... coming up on non-book items.
if (text_file.empty()) {
return;
}
std::string booktxt2 = content_db.GetBook(txtfile, &book_language);
int length = booktxt2.length();
auto b = content_db.GetBook(text_file);
if (booktxt2[0] != '\0') {
#if EQDEBUG >= 6
LogInfo("Client::ReadBook() textfile:[{}] Text:[{}]", txtfile, booktxt2.c_str());
#endif
auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct));
if (!b.text.empty()) {
auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct));
auto inst = const_cast<EQ::ItemInstance*>(m_inv[book->invslot]);
BookText_Struct *out = (BookText_Struct *) outapp->pBuffer;
out->window = book->window;
out->type = book->type;
out->invslot = book->invslot;
out->target_id = book->target_id;
out->can_cast = 0; // todo: implement
out->can_scribe = false;
auto t = (BookText_Struct*) outapp->pBuffer;
if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END)
{
const EQ::ItemInstance* inst = m_inv[book->invslot];
if (inst && inst->GetItem())
{
auto recipe = TradeskillRecipeRepository::GetWhere(content_db,
fmt::format("learned_by_item_id = {} LIMIT 1", inst->GetItem()->ID));
out->type = inst->GetItem()->Book;
out->can_scribe = !recipe.empty();
t->window = book->window;
t->type = book->type;
t->invslot = book->invslot;
t->target_id = book->target_id;
t->can_cast = 0; // todo: implement
t->can_scribe = false;
if (ClientVersion() >= EQ::versions::ClientVersion::SoF && book->invslot <= EQ::invbag::GENERAL_BAGS_END) {
if (inst && inst->GetItem()) {
auto recipe = TradeskillRecipeRepository::GetWhere(
content_db,
fmt::format(
"learned_by_item_id = {} LIMIT 1",
inst->GetItem()->ID
)
);
t->type = inst->GetItem()->Book;
t->can_scribe = !recipe.empty();
}
}
memcpy(out->booktext, booktxt2.c_str(), length);
memcpy(t->booktext, b.text.c_str(), b.text.size());
if (EQ::ValueWithin(book_language, Language::CommonTongue, Language::Unknown27)) {
if (m_pp.languages[book_language] < Language::MaxValue) {
GarbleMessage(out->booktext, (Language::MaxValue - m_pp.languages[book_language]));
if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) {
if (m_pp.languages[b.language] < Language::MaxValue) {
GarbleMessage(t->booktext, (Language::MaxValue - m_pp.languages[b.language]));
}
}
// Send only books and scrolls to this event
if (parse->PlayerHasQuestSub(EVENT_READ_ITEM) && t->type != BookType::ItemInfo) {
std::vector<std::any> args = {
b.text,
t->can_cast,
t->can_scribe,
t->invslot,
t->target_id,
t->type,
inst
};
parse->EventPlayer(EVENT_READ_ITEM, this, book->txtfile, inst ? inst->GetID() : 0, &args);
}
QueuePacket(outapp);
safe_delete(outapp);
}
@@ -3970,7 +3969,7 @@ void Client::SetEndurance(int32 newEnd)
CheckManaEndUpdate();
}
void Client::SacrificeConfirm(Client *caster)
void Client::SacrificeConfirm(Mob *caster)
{
auto outapp = new EQApplicationPacket(OP_Sacrifice, sizeof(Sacrifice_Struct));
Sacrifice_Struct *ss = (Sacrifice_Struct *)outapp->pBuffer;
@@ -3997,14 +3996,14 @@ void Client::SacrificeConfirm(Client *caster)
ss->Confirm = 0;
QueuePacket(outapp);
safe_delete(outapp);
// We store the Caster's name, because when the packet comes back, it only has the victim's entityID in it,
// We store the Caster's id, because when the packet comes back, it only has the victim's entityID in it,
// not the caster.
SacrificeCaster += caster->GetName();
sacrifice_caster_id = caster->GetID();
PendingSacrifice = true;
}
//Essentially a special case death function
void Client::Sacrifice(Client *caster)
void Client::Sacrifice(Mob *caster)
{
if (GetLevel() >= RuleI(Spells, SacrificeMinLevel) && GetLevel() <= RuleI(Spells, SacrificeMaxLevel)) {
int exploss = (int)(GetLevel() * (GetLevel() / 18.0) * 12000);
@@ -4052,8 +4051,11 @@ void Client::Sacrifice(Client *caster)
}
Save();
GoToDeath();
if (caster) // I guess it's possible?
caster->SummonItem(RuleI(Spells, SacrificeItemID));
if (caster && caster->IsClient()) {
caster->CastToClient()->SummonItem(RuleI(Spells, SacrificeItemID));
} else if (caster && caster->IsNPC()) {
caster->CastToNPC()->AddItem(RuleI(Spells, SacrificeItemID), 1, false);
}
}
} else {
caster->MessageString(Chat::Red, SAC_TOO_LOW); // This being is not a worthy sacrifice.
@@ -7654,10 +7656,10 @@ void Client::SetFactionLevel(
content_db.GetFactionData(&faction_modifiers, class_id, race_id, deity_id, e.faction_id);
if (is_quest) {
if (e.npc_value > 0) {
e.npc_value = -std::abs(e.npc_value);
} else if (e.npc_value < 0) {
e.npc_value = std::abs(e.npc_value);
if (e.value > 0) {
e.value = -std::abs(e.value);
} else if (e.value < 0) {
e.value = std::abs(e.value);
}
}
@@ -8675,14 +8677,16 @@ int Client::GetAccountAge() {
void Client::CheckRegionTypeChanges()
{
if (!zone->HasWaterMap())
if (!zone->HasWaterMap()) {
return;
}
auto new_region = zone->watermap->ReturnRegionType(glm::vec3(m_Position));
// still same region, do nothing
if (last_region_type == new_region)
if (last_region_type == new_region) {
return;
}
// If we got out of water clear any water aggro for water only npcs
if (last_region_type == RegionTypeWater) {
@@ -8693,13 +8697,15 @@ void Client::CheckRegionTypeChanges()
last_region_type = new_region;
// PVP is the only state we need to keep track of, so we can just return now for PVP servers
if (RuleI(World, PVPSettings) > 0)
if (RuleI(World, PVPSettings) > 0) {
return;
}
if (last_region_type == RegionTypePVP)
if (last_region_type == RegionTypePVP && RuleB(World, EnablePVPRegions)) {
temp_pvp = true;
else if (temp_pvp)
} else if (temp_pvp) {
temp_pvp = false;
}
}
void Client::ProcessAggroMeter()
@@ -9248,19 +9254,6 @@ bool Client::GotoPlayerRaid(const std::string& player_name)
return true;
}
glm::vec4 &Client::GetLastPositionBeforeBulkUpdate()
{
return last_position_before_bulk_update;
}
/**
* @param in_last_position_before_bulk_update
*/
void Client::SetLastPositionBeforeBulkUpdate(glm::vec4 in_last_position_before_bulk_update)
{
Client::last_position_before_bulk_update = in_last_position_before_bulk_update;
}
void Client::SendToGuildHall()
{
std::string zone_short_name = "guildhall";
@@ -10925,23 +10918,24 @@ void Client::SetIPExemption(int exemption_amount)
void Client::ReadBookByName(std::string book_name, uint8 book_type)
{
int16 book_language = 0;
std::string book_text = content_db.GetBook(book_name.c_str(), &book_language);
int length = book_text.length();
auto b = content_db.GetBook(book_name);
if (book_text[0] != '\0') {
LogDebug("Client::ReadBookByName() Book Name: [{}] Text: [{}]", book_name, book_text.c_str());
auto outapp = new EQApplicationPacket(OP_ReadBook, length + sizeof(BookText_Struct));
BookText_Struct *out = (BookText_Struct *) outapp->pBuffer;
out->window = 0xFF;
out->type = book_type;
out->invslot = 0;
if (!b.text.empty()) {
LogDebug("Book Name: [{}] Text: [{}]", book_name, b.text);
memcpy(out->booktext, book_text.c_str(), length);
auto outapp = new EQApplicationPacket(OP_ReadBook, b.text.size() + sizeof(BookText_Struct));
if (EQ::ValueWithin(book_language, Language::CommonTongue, Language::Unknown27)) {
if (m_pp.languages[book_language] < Language::MaxValue) {
GarbleMessage(out->booktext, (Language::MaxValue - m_pp.languages[book_language]));
auto o = (BookText_Struct *) outapp->pBuffer;
o->window = std::numeric_limits<uint8>::max();
o->type = book_type;
o->invslot = 0;
memcpy(o->booktext, b.text.c_str(), b.text.size());
if (EQ::ValueWithin(b.language, Language::CommonTongue, Language::Unknown27)) {
if (m_pp.languages[b.language] < Language::MaxValue) {
GarbleMessage(o->booktext, (Language::MaxValue - m_pp.languages[b.language]));
}
}
@@ -11092,7 +11086,9 @@ void Client::SaveDisciplines()
{
std::vector<CharacterDisciplinesRepository::CharacterDisciplines> v;
for (int slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) {
std::vector<std::string> delete_slots;
for (uint16 slot_id = 0; slot_id < MAX_PP_DISCIPLINES; slot_id++) {
if (IsValidSpell(m_pp.disciplines.values[slot_id])) {
auto e = CharacterDisciplinesRepository::NewEntity();
@@ -11101,9 +11097,22 @@ void Client::SaveDisciplines()
e.disc_id = m_pp.disciplines.values[slot_id];
v.emplace_back(e);
} else {
delete_slots.emplace_back(std::to_string(slot_id));
}
}
if (!delete_slots.empty()) {
CharacterDisciplinesRepository::DeleteWhere(
database,
fmt::format(
"`id` = {} AND `slot_id` IN ({})",
CharacterID(),
Strings::Join(delete_slots, ", ")
)
);
}
if (!v.empty()) {
CharacterDisciplinesRepository::ReplaceMany(database, v);
}
@@ -12311,6 +12320,248 @@ void Client::PlayerTradeEventLog(Trade *t, Trade *t2)
RecordPlayerEventLogWithClient(trader2, PlayerEvent::TRADE, e);
}
void Client::NPCHandinEventLog(Trade* t, NPC* n)
{
Client* c = t->GetOwner()->CastToClient();
std::vector<PlayerEvent::HandinEntry> hi = {};
std::vector<PlayerEvent::HandinEntry> ri = {};
PlayerEvent::HandinMoney hm{};
PlayerEvent::HandinMoney rm{};
if (
c->EntityVariableExists("HANDIN_ITEMS") &&
c->EntityVariableExists("HANDIN_MONEY") &&
c->EntityVariableExists("RETURN_ITEMS") &&
c->EntityVariableExists("RETURN_MONEY")
) {
const std::string& handin_items = c->GetEntityVariable("HANDIN_ITEMS");
const std::string& return_items = c->GetEntityVariable("RETURN_ITEMS");
const std::string& handin_money = c->GetEntityVariable("HANDIN_MONEY");
const std::string& return_money = c->GetEntityVariable("RETURN_MONEY");
// Handin Items
if (!handin_items.empty()) {
if (Strings::Contains(handin_items, ",")) {
const auto handin_data = Strings::Split(handin_items, ",");
for (const auto& h : handin_data) {
const auto item_data = Strings::Split(h, "|");
if (
item_data.size() == 3 &&
Strings::IsNumber(item_data[0]) &&
Strings::IsNumber(item_data[1]) &&
Strings::IsNumber(item_data[2])
) {
const uint32 item_id = Strings::ToUnsignedInt(item_data[0]);
if (item_id != 0) {
const auto* item = database.GetItem(item_id);
if (item) {
hi.emplace_back(
PlayerEvent::HandinEntry{
.item_id = item_id,
.item_name = item->Name,
.charges = static_cast<uint16>(Strings::ToUnsignedInt(item_data[1])),
.attuned = Strings::ToInt(item_data[2]) ? true : false
}
);
}
}
}
}
} else if (Strings::Contains(handin_items, "|")) {
const auto item_data = Strings::Split(handin_items, "|");
if (
item_data.size() == 3 &&
Strings::IsNumber(item_data[0]) &&
Strings::IsNumber(item_data[1]) &&
Strings::IsNumber(item_data[2])
) {
const uint32 item_id = Strings::ToUnsignedInt(item_data[0]);
const auto* item = database.GetItem(item_id);
if (item) {
hi.emplace_back(
PlayerEvent::HandinEntry{
.item_id = item_id,
.item_name = item->Name,
.charges = static_cast<uint16>(Strings::ToUnsignedInt(item_data[1])),
.attuned = Strings::ToInt(item_data[2]) ? true : false
}
);
}
}
}
}
// Handin Money
if (!handin_money.empty()) {
const auto hms = Strings::Split(handin_money, "|");
hm.copper = Strings::ToUnsignedInt(hms[0]);
hm.silver = Strings::ToUnsignedInt(hms[1]);
hm.gold = Strings::ToUnsignedInt(hms[2]);
hm.platinum = Strings::ToUnsignedInt(hms[3]);
}
// Return Items
if (!return_items.empty()) {
if (Strings::Contains(return_items, ",")) {
const auto return_data = Strings::Split(return_items, ",");
for (const auto& r : return_data) {
const auto item_data = Strings::Split(r, "|");
if (
item_data.size() == 3 &&
Strings::IsNumber(item_data[0]) &&
Strings::IsNumber(item_data[1]) &&
Strings::IsNumber(item_data[2])
) {
const uint32 item_id = Strings::ToUnsignedInt(item_data[0]);
const auto* item = database.GetItem(item_id);
if (item) {
ri.emplace_back(
PlayerEvent::HandinEntry{
.item_id = item_id,
.item_name = item->Name,
.charges = static_cast<uint16>(Strings::ToUnsignedInt(item_data[1])),
.attuned = Strings::ToInt(item_data[2]) ? true : false
}
);
}
}
}
} else if (Strings::Contains(return_items, "|")) {
const auto item_data = Strings::Split(return_items, "|");
if (
item_data.size() == 3 &&
Strings::IsNumber(item_data[0]) &&
Strings::IsNumber(item_data[1]) &&
Strings::IsNumber(item_data[2])
) {
const uint32 item_id = Strings::ToUnsignedInt(item_data[0]);
const auto* item = database.GetItem(item_id);
if (item) {
ri.emplace_back(
PlayerEvent::HandinEntry{
.item_id = item_id,
.item_name = item->Name,
.charges = static_cast<uint16>(Strings::ToUnsignedInt(item_data[1])),
.attuned = Strings::ToInt(item_data[2]) ? true : false
}
);
}
}
}
}
// Return Money
if (!return_money.empty()) {
const auto rms = Strings::Split(return_money, "|");
rm.copper = static_cast<uint32>(Strings::ToUnsignedInt(rms[0]));
rm.silver = static_cast<uint32>(Strings::ToUnsignedInt(rms[1]));
rm.gold = static_cast<uint32>(Strings::ToUnsignedInt(rms[2]));
rm.platinum = static_cast<uint32>(Strings::ToUnsignedInt(rms[3]));
}
c->DeleteEntityVariable("HANDIN_ITEMS");
c->DeleteEntityVariable("HANDIN_MONEY");
c->DeleteEntityVariable("RETURN_ITEMS");
c->DeleteEntityVariable("RETURN_MONEY");
const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0;
const bool event_has_data_to_record = (
!hi.empty() || handed_in_money
);
if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) {
auto e = PlayerEvent::HandinEvent{
.npc_id = n->GetNPCTypeID(),
.npc_name = n->GetCleanName(),
.handin_items = hi,
.handin_money = hm,
.return_items = ri,
.return_money = rm,
.is_quest_handin = true
};
RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e);
}
return;
}
uint8 item_count = 0;
hm.platinum = t->pp;
hm.gold = t->gp;
hm.silver = t->sp;
hm.copper = t->cp;
for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) {
if (c->GetInv().GetItem(i)) {
item_count++;
}
}
hi.reserve(item_count);
if (item_count > 0) {
for (uint16 i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; i++) {
const EQ::ItemInstance* inst = c->GetInv().GetItem(i);
if (inst) {
hi.emplace_back(
PlayerEvent::HandinEntry{
.item_id = inst->GetItem()->ID,
.item_name = inst->GetItem()->Name,
.charges = static_cast<uint16>(inst->GetCharges()),
.attuned = inst->IsAttuned()
}
);
if (inst->IsClassBag()) {
for (uint8 j = EQ::invbag::SLOT_BEGIN; j <= EQ::invbag::SLOT_END; j++) {
inst = c->GetInv().GetItem(i, j);
if (inst) {
hi.emplace_back(
PlayerEvent::HandinEntry{
.item_id = inst->GetItem()->ID,
.item_name = inst->GetItem()->Name,
.charges = static_cast<uint16>(inst->GetCharges()),
.attuned = inst->IsAttuned()
}
);
}
}
}
}
}
}
const bool handed_in_money = hm.platinum > 0 || hm.gold > 0 || hm.silver > 0 || hm.copper > 0;
ri = hi;
rm = hm;
const bool event_has_data_to_record = !hi.empty() || handed_in_money;
if (player_event_logs.IsEventEnabled(PlayerEvent::NPC_HANDIN) && event_has_data_to_record) {
auto e = PlayerEvent::HandinEvent{
.npc_id = n->GetNPCTypeID(),
.npc_name = n->GetCleanName(),
.handin_items = hi,
.handin_money = hm,
.return_items = ri,
.return_money = rm,
.is_quest_handin = false
};
RecordPlayerEventLogWithClient(c, PlayerEvent::NPC_HANDIN, e);
}
}
void Client::ShowSpells(Client* c, ShowSpellType show_spell_type)
{
std::string spell_string;
@@ -12706,3 +12957,121 @@ bool Client::TakeMoneyFromPPWithOverFlow(uint64 copper, bool update_client)
RecalcWeight();
return true;
}
void Client::SendTopLevelInventory()
{
EQ::ItemInstance* inst = nullptr;
static const int16 slots[][2] = {
{ EQ::invslot::POSSESSIONS_BEGIN, EQ::invslot::POSSESSIONS_END },
{ EQ::invbag::GENERAL_BAGS_BEGIN, EQ::invbag::GENERAL_BAGS_END },
{ EQ::invbag::CURSOR_BAG_BEGIN, EQ::invbag::CURSOR_BAG_END }
};
const auto& inv = GetInv();
const size_t slot_index_count = sizeof(slots) / sizeof(slots[0]);
for (int slot_index = 0; slot_index < slot_index_count; ++slot_index) {
for (int slot_id = slots[slot_index][0]; slot_id <= slots[slot_index][1]; ++slot_id) {
inst = inv.GetItem(slot_id);
if (inst) {
SendItemPacket(slot_id, inst, ItemPacketType::ItemPacketTrade);
}
}
}
}
// On a normal basis we limit mob movement updates based on distance
// This ensures we send a periodic full zone update to a client that has started moving after 5 or so minutes
//
// For very large zones we will also force a full update based on distance
//
// We ignore a small distance around us so that we don't interrupt already pathing deltas as those npcs will appear
// to full stop when they are actually still pathing
void Client::CheckSendBulkClientPositionUpdate()
{
float distance_moved = DistanceNoZ(m_last_position_before_bulk_update, GetPosition());
bool moved_far_enough_before_bulk_update = distance_moved >= zone->GetNpcPositionUpdateDistance();
bool is_ready_to_update = (
m_client_zone_wide_full_position_update_timer.Check() || moved_far_enough_before_bulk_update
);
if (IsMoving() && is_ready_to_update) {
LogDebug("[[{}]] Client Zone Wide Position Update NPCs", GetCleanName());
auto &mob_movement_manager = MobMovementManager::Get();
auto &mob_list = entity_list.GetMobList();
for (auto &it : mob_list) {
Mob *entity = it.second;
if (!entity->IsNPC()) {
continue;
}
int animation_speed = 0;
if (entity->IsMoving()) {
if (entity->IsRunning()) {
animation_speed = (entity->IsFeared() ? entity->GetFearSpeed() : entity->GetRunspeed());
}
else {
animation_speed = entity->GetWalkspeed();
}
}
mob_movement_manager.SendCommandToClients(entity, 0.0, 0.0, 0.0, 0.0, animation_speed, ClientRangeAny, this);
}
m_last_position_before_bulk_update = GetPosition();
}
}
const uint16 scan_npc_aggro_timer_idle = RuleI(Aggro, ClientAggroCheckIdleInterval);
const uint16 scan_npc_aggro_timer_moving = RuleI(Aggro, ClientAggroCheckMovingInterval);
void Client::CheckClientToNpcAggroTimer()
{
LogAggroDetail(
"ClientUpdate [{}] {}moving, scan timer [{}]",
GetCleanName(),
IsMoving() ? "" : "NOT ",
m_client_npc_aggro_scan_timer.GetRemainingTime()
);
if (IsMoving()) {
if (m_client_npc_aggro_scan_timer.GetRemainingTime() > scan_npc_aggro_timer_moving) {
LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName());
m_client_npc_aggro_scan_timer.Disable();
m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_moving);
m_client_npc_aggro_scan_timer.Trigger();
}
}
else if (m_client_npc_aggro_scan_timer.GetDuration() == scan_npc_aggro_timer_moving) {
LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName());
m_client_npc_aggro_scan_timer.Disable();
m_client_npc_aggro_scan_timer.Start(scan_npc_aggro_timer_idle);
}
}
void Client::ClientToNpcAggroProcess()
{
if (zone->CanDoCombat() && !GetFeigned() && m_client_npc_aggro_scan_timer.Check()) {
int npc_scan_count = 0;
for (auto &close_mob: GetCloseMobList()) {
Mob *mob = close_mob.second;
if (!mob) {
continue;
}
if (mob->IsClient()) {
continue;
}
if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) {
mob->AddToHateList(this, 25);
}
npc_scan_count++;
}
LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count);
}
}
+24 -13
View File
@@ -196,6 +196,7 @@ struct RespawnOption
float heading;
};
// do not ask what all these mean because I have no idea!
// named from the client's CEverQuest::GetInnateDesc, they're missing some
enum eInnateSkill {
@@ -762,8 +763,8 @@ public:
void GetRaidAAs(RaidLeadershipAA_Struct *into) const;
void ClearGroupAAs();
void UpdateGroupAAs(int32 points, uint32 type);
void SacrificeConfirm(Client* caster);
void Sacrifice(Client* caster);
void SacrificeConfirm(Mob* caster);
void Sacrifice(Mob* caster);
void GoToDeath();
inline const int32 GetInstanceID() const { return zone->GetInstanceID(); }
void SetZoning(bool in) { bZoning = in; }
@@ -1025,7 +1026,7 @@ public:
int GetSpentAA() { return m_pp.aapoints_spent; }
uint32 GetRequiredAAExperience();
void AutoGrantAAPoints();
void GrantAllAAPoints(uint8 unlock_level = 0);
void GrantAllAAPoints(uint8 unlock_level = 0, bool skip_grant_only = false);
bool HasAlreadyPurchasedRank(AA::Rank* rank);
void ListPurchasedAAs(Client *to, std::string search_criteria = std::string());
@@ -1245,7 +1246,7 @@ public:
bool PendingTranslocate;
time_t TranslocateTime;
bool PendingSacrifice;
std::string SacrificeCaster;
uint16 sacrifice_caster_id;
PendingTranslocate_Struct PendingTranslocateData;
void SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID);
@@ -1794,9 +1795,6 @@ public:
uint32 trapid; //ID of trap player has triggered. This is cleared when the player leaves the trap's radius, or it despawns.
void SetLastPositionBeforeBulkUpdate(glm::vec4 in_last_position_before_bulk_update);
glm::vec4 &GetLastPositionBeforeBulkUpdate();
Raid *p_raid_instance;
void ShowDevToolsMenu();
@@ -2012,6 +2010,8 @@ private:
void ZonePC(uint32 zoneID, uint32 instance_id, float x, float y, float z, float heading, uint8 ignorerestrictions, ZoneMode zm);
void ProcessMovePC(uint32 zoneID, uint32 instance_id, float x, float y, float z, float heading, uint8 ignorerestrictions = 0, ZoneMode zm = ZoneSolicited);
void SendTopLevelInventory();
glm::vec4 m_ZoneSummonLocation;
uint16 zonesummon_id;
uint8 zonesummon_ignorerestrictions;
@@ -2031,8 +2031,6 @@ private:
Timer fishing_timer;
Timer endupkeep_timer;
Timer autosave_timer;
Timer client_scan_npc_aggro_timer;
Timer client_zone_wide_full_position_update_timer;
Timer tribute_timer;
Timer proximity_timer;
@@ -2049,16 +2047,29 @@ private:
Timer afk_toggle_timer;
Timer helm_toggle_timer;
Timer aggro_meter_timer;
Timer mob_close_scan_timer;
Timer position_update_timer; /* Timer used when client hasn't updated within a 10 second window */
Timer consent_throttle_timer;
Timer dynamiczone_removal_timer;
Timer task_request_timer;
Timer pick_lock_timer;
Timer parcel_timer; //Used to limit the number of parcels to one every 30 seconds (default). Changable via rule.
Timer lazy_load_bank_check_timer;
Timer bandolier_throttle_timer;
bool m_lazy_load_bank = false;
int m_lazy_load_sent_bank_slots = 0;
glm::vec3 m_Proximity;
glm::vec4 last_position_before_bulk_update;
// client aggro
Timer m_client_npc_aggro_scan_timer;
void CheckClientToNpcAggroTimer();
void ClientToNpcAggroProcess();
// bulk position updates
glm::vec4 m_last_position_before_bulk_update;
Timer m_client_zone_wide_full_position_update_timer;
Timer m_position_update_timer;
void CheckSendBulkClientPositionUpdate();
void BulkSendInventoryItems();
@@ -2175,7 +2186,6 @@ private:
bool m_has_quest_compass = false;
std::vector<uint32_t> m_dynamic_zone_ids;
public:
enum BotOwnerOption : size_t {
booDeathMarquee,
@@ -2217,6 +2227,7 @@ private:
bool CanTradeFVNoDropItem();
void SendMobPositions();
void PlayerTradeEventLog(Trade *t, Trade *t2);
void NPCHandinEventLog(Trade* t, NPC* n);
// full and partial mail key cache
std::string m_mail_key_full;
+42 -134
View File
@@ -794,7 +794,7 @@ void Client::CompleteConnect()
// sent to a succor point
SendMobPositions();
SetLastPositionBeforeBulkUpdate(GetPosition());
m_last_position_before_bulk_update = GetPosition();
/* This sub event is for if a player logs in for the first time since entering world. */
if (firstlogon == 1) {
@@ -934,12 +934,13 @@ void Client::CompleteConnect()
}
database.LoadAuras(this); // this ends up spawning them so probably safer to load this later (here)
database.LoadCharacterDisciplines(this);
entity_list.RefreshClientXTargets(this);
worldserver.RequestTellQueue(GetName());
entity_list.ScanCloseMobs(close_mobs, this, true);
entity_list.ScanCloseMobs(this);
if (GetGM() && IsDevToolsEnabled()) {
ShowDevToolsMenu();
@@ -1318,7 +1319,6 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
database.LoadCharacterInspectMessage(cid, &m_inspect_message); /* Load Character Inspect Message */
database.LoadCharacterSpellBook(cid, &m_pp); /* Load Character Spell Book */
database.LoadCharacterMemmedSpells(cid, &m_pp); /* Load Character Memorized Spells */
database.LoadCharacterDisciplines(cid, &m_pp); /* Load Character Disciplines */
database.LoadCharacterLanguages(cid, &m_pp); /* Load Character Languages */
database.LoadCharacterLeadershipAbilities(cid, &m_pp); /* Load Character Leadership AA's */
database.LoadCharacterTribute(this); /* Load CharacterTribute */
@@ -3642,29 +3642,39 @@ void Client::Handle_OP_AutoFire(const EQApplicationPacket *app)
void Client::Handle_OP_Bandolier(const EQApplicationPacket *app)
{
// Although there are three different structs for OP_Bandolier, they are all the same size.
//
if (app->size != sizeof(BandolierCreate_Struct)) {
LogDebug("Size mismatch in OP_Bandolier expected [{}] got [{}]", sizeof(BandolierCreate_Struct), app->size);
DumpPacket(app);
return;
}
BandolierCreate_Struct *bs = (BandolierCreate_Struct*)app->pBuffer;
auto bs = (BandolierCreate_Struct*) app->pBuffer;
switch (bs->Action)
{
case bandolierCreate:
CreateBandolier(app);
break;
case bandolierRemove:
RemoveBandolier(app);
break;
case bandolierSet:
SetBandolier(app);
break;
default:
LogDebug("Unknown Bandolier action [{}]", bs->Action);
break;
switch (bs->Action) {
case bandolierCreate:
CreateBandolier(app);
break;
case bandolierRemove:
RemoveBandolier(app);
break;
case bandolierSet:
if (bandolier_throttle_timer.GetDuration() && !bandolier_throttle_timer.Check()) {
Message(
Chat::White,
fmt::format(
"You may only modify your bandolier once every {}.",
Strings::ToLower(Strings::MillisecondsToTime(RuleI(Character, BandolierSwapDelay)))
).c_str()
);
SendTopLevelInventory();
break;
}
SetBandolier(app);
break;
default:
LogDebug("Unknown Bandolier action [{}]", bs->Action);
break;
}
}
@@ -5002,103 +5012,9 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) {
SetMoving(!(cy == m_Position.y && cx == m_Position.x));
/**
* Client aggro scanning
*/
const uint16 client_scan_npc_aggro_timer_idle = RuleI(Aggro, ClientAggroCheckIdleInterval);
const uint16 client_scan_npc_aggro_timer_moving = RuleI(Aggro, ClientAggroCheckMovingInterval);
LogAggroDetail(
"ClientUpdate [{}] {}moving, scan timer [{}]",
GetCleanName(),
IsMoving() ? "" : "NOT ",
client_scan_npc_aggro_timer.GetRemainingTime()
);
if (IsMoving()) {
if (client_scan_npc_aggro_timer.GetRemainingTime() > client_scan_npc_aggro_timer_moving) {
LogAggroDetail("Client [{}] Restarting with moving timer", GetCleanName());
client_scan_npc_aggro_timer.Disable();
client_scan_npc_aggro_timer.Start(client_scan_npc_aggro_timer_moving);
client_scan_npc_aggro_timer.Trigger();
}
}
else if (client_scan_npc_aggro_timer.GetDuration() == client_scan_npc_aggro_timer_moving) {
LogAggroDetail("Client [{}] Restarting with idle timer", GetCleanName());
client_scan_npc_aggro_timer.Disable();
client_scan_npc_aggro_timer.Start(client_scan_npc_aggro_timer_idle);
}
/**
* Client mob close list cache scan timer
*/
const uint16 client_mob_close_scan_timer_moving = 6000;
const uint16 client_mob_close_scan_timer_idle = 60000;
LogAIScanCloseDetail(
"Client [{}] {}moving, scan timer [{}]",
GetCleanName(),
IsMoving() ? "" : "NOT ",
mob_close_scan_timer.GetRemainingTime()
);
if (IsMoving()) {
if (mob_close_scan_timer.GetRemainingTime() > client_mob_close_scan_timer_moving) {
LogAIScanCloseDetail("Client [{}] Restarting with moving timer", GetCleanName());
mob_close_scan_timer.Disable();
mob_close_scan_timer.Start(client_mob_close_scan_timer_moving);
mob_close_scan_timer.Trigger();
}
}
else if (mob_close_scan_timer.GetDuration() == client_mob_close_scan_timer_moving) {
LogAIScanCloseDetail("Client [{}] Restarting with idle timer", GetCleanName());
mob_close_scan_timer.Disable();
mob_close_scan_timer.Start(client_mob_close_scan_timer_idle);
}
/**
* On a normal basis we limit mob movement updates based on distance
* This ensures we send a periodic full zone update to a client that has started moving after 5 or so minutes
*
* For very large zones we will also force a full update based on distance
*
* We ignore a small distance around us so that we don't interrupt already pathing deltas as those npcs will appear
* to full stop when they are actually still pathing
*/
float distance_moved = DistanceNoZ(GetLastPositionBeforeBulkUpdate(), GetPosition());
bool moved_far_enough_before_bulk_update = distance_moved >= zone->GetNpcPositionUpdateDistance();
bool is_ready_to_update = (
client_zone_wide_full_position_update_timer.Check() || moved_far_enough_before_bulk_update
);
if (IsMoving() && is_ready_to_update) {
LogDebug("[[{}]] Client Zone Wide Position Update NPCs", GetCleanName());
auto &mob_movement_manager = MobMovementManager::Get();
auto &mob_list = entity_list.GetMobList();
for (auto &it : mob_list) {
Mob *entity = it.second;
if (!entity->IsNPC()) {
continue;
}
int animation_speed = 0;
if (entity->IsMoving()) {
if (entity->IsRunning()) {
animation_speed = (entity->IsFeared() ? entity->GetFearSpeed() : entity->GetRunspeed());
}
else {
animation_speed = entity->GetWalkspeed();
}
}
mob_movement_manager.SendCommandToClients(entity, 0.0, 0.0, 0.0, 0.0, animation_speed, ClientRangeAny, this);
}
SetLastPositionBeforeBulkUpdate(GetPosition());
}
CheckClientToNpcAggroTimer();
CheckScanCloseMobsMovingTimer();
CheckSendBulkClientPositionUpdate();
int32 new_animation = ppu->animation;
@@ -12068,15 +11984,7 @@ void Client::Handle_OP_PopupResponse(const EQApplicationPacket *app)
auto t = GetTarget();
if (t) {
if (t->IsNPC()) {
if (parse->HasQuestSub(t->GetNPCTypeID(), EVENT_POPUP_RESPONSE)) {
parse->EventNPC(EVENT_POPUP_RESPONSE, t->CastToNPC(), this, std::to_string(popup_response->popupid), 0);
}
} else if (t->IsBot()) {
if (parse->BotHasQuestSub(EVENT_POPUP_RESPONSE)) {
parse->EventBot(EVENT_POPUP_RESPONSE, t->CastToBot(), this, std::to_string(popup_response->popupid), 0);
}
}
parse->EventBotMercNPC(EVENT_POPUP_RESPONSE, t, this, [&]() { return std::to_string(popup_response->popupid); });
}
}
@@ -13130,14 +13038,14 @@ void Client::Handle_OP_ReadBook(const EQApplicationPacket *app)
LogError("Wrong size: OP_ReadBook, size=[{}], expected [{}]", app->size, sizeof(BookRequest_Struct));
return;
}
BookRequest_Struct* book = (BookRequest_Struct*)app->pBuffer;
ReadBook(book);
if (ClientVersion() >= EQ::versions::ClientVersion::SoF)
{
EQApplicationPacket EndOfBook(OP_FinishWindow, 0);
QueuePacket(&EndOfBook);
auto b = (BookRequest_Struct*) app->pBuffer;
ReadBook(b);
if (ClientVersion() >= EQ::versions::ClientVersion::SoF) {
EQApplicationPacket end_of_book(OP_FinishWindow, 0);
QueuePacket(&end_of_book);
}
return;
}
void Client::Handle_OP_RecipeAutoCombine(const EQApplicationPacket *app)
@@ -13671,11 +13579,11 @@ void Client::Handle_OP_Sacrifice(const EQApplicationPacket *app)
}
if (ss->Confirm) {
Client *Caster = entity_list.GetClientByName(SacrificeCaster.c_str());
Mob *Caster = entity_list.GetMob(sacrifice_caster_id);
if (Caster) Sacrifice(Caster);
}
PendingSacrifice = false;
SacrificeCaster.clear();
sacrifice_caster_id = 0;
}
void Client::Handle_OP_SafeFallSuccess(const EQApplicationPacket *app) // bit of a misnomer, sent whenever safe fall is used (success of fail)
+60 -56
View File
@@ -121,7 +121,7 @@ bool Client::Process() {
}
/* I haven't naturally updated my position in 10 seconds, updating manually */
if (!IsMoving() && position_update_timer.Check()) {
if (!IsMoving() && m_position_update_timer.Check()) {
SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0);
}
@@ -281,12 +281,37 @@ bool Client::Process() {
}
}
/**
* Scan close range mobs
* Used in aggro checks
*/
if (mob_close_scan_timer.Check()) {
entity_list.ScanCloseMobs(close_mobs, this, IsMoving());
ScanCloseMobProcess();
if (RuleB(Inventory, LazyLoadBank)) {
// poll once a second to see if we are close to a banker and we haven't loaded the bank yet
if (!m_lazy_load_bank && lazy_load_bank_check_timer.Check()) {
if (m_lazy_load_sent_bank_slots <= EQ::invslot::SHARED_BANK_END && IsCloseToBanker()) {
m_lazy_load_bank = true;
lazy_load_bank_check_timer.Disable();
}
}
if (m_lazy_load_bank && m_lazy_load_sent_bank_slots <= EQ::invslot::SHARED_BANK_END) {
const EQ::ItemInstance *inst = nullptr;
// Jump the gaps
if (m_lazy_load_sent_bank_slots < EQ::invslot::BANK_BEGIN) {
m_lazy_load_sent_bank_slots = EQ::invslot::BANK_BEGIN;
}
else if (m_lazy_load_sent_bank_slots > EQ::invslot::BANK_END &&
m_lazy_load_sent_bank_slots < EQ::invslot::SHARED_BANK_BEGIN) {
m_lazy_load_sent_bank_slots = EQ::invslot::SHARED_BANK_BEGIN;
}
else {
m_lazy_load_sent_bank_slots++;
}
inst = m_inv[m_lazy_load_sent_bank_slots];
if (inst) {
SendItemPacket(m_lazy_load_sent_bank_slots, inst, ItemPacketType::ItemPacketTrade);
}
}
}
bool may_use_attacks = false;
@@ -577,30 +602,7 @@ bool Client::Process() {
}
}
//At this point, we are still connected, everything important has taken
//place, now check to see if anybody wants to aggro us.
// only if client is not feigned
if (zone->CanDoCombat() && ret && !GetFeigned() && client_scan_npc_aggro_timer.Check()) {
int npc_scan_count = 0;
for (auto & close_mob : close_mobs) {
Mob *mob = close_mob.second;
if (!mob) {
continue;
}
if (mob->IsClient()) {
continue;
}
if (mob->CheckWillAggro(this) && !mob->CheckAggro(this)) {
mob->AddToHateList(this, 25);
}
npc_scan_count++;
}
LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count);
}
ClientToNpcAggroProcess();
if (client_state != CLIENT_LINKDEAD && (client_state == CLIENT_ERROR || client_state == DISCONNECTED || client_state == CLIENT_KICKED || !eqs->CheckState(ESTABLISHED)))
{
@@ -780,39 +782,41 @@ void Client::BulkSendInventoryItems()
last_pos = ob.tellp();
}
// Bank items
for (int16 slot_id = EQ::invslot::BANK_BEGIN; slot_id <= EQ::invslot::BANK_END; slot_id++) {
const EQ::ItemInstance* inst = m_inv[slot_id];
if (!inst)
continue;
if (!RuleB(Inventory, LazyLoadBank)) {
// Bank items
for (int16 slot_id = EQ::invslot::BANK_BEGIN; slot_id <= EQ::invslot::BANK_END; slot_id++) {
const EQ::ItemInstance* inst = m_inv[slot_id];
if (!inst)
continue;
inst->Serialize(ob, slot_id);
inst->Serialize(ob, slot_id);
if (ob.tellp() == last_pos)
LogInventory("Serialization failed on item slot [{}] during BulkSendInventoryItems. Item skipped", slot_id);
if (ob.tellp() == last_pos)
LogInventory("Serialization failed on item slot [{}] during BulkSendInventoryItems. Item skipped", slot_id);
last_pos = ob.tellp();
}
last_pos = ob.tellp();
}
// SharedBank items
for (int16 slot_id = EQ::invslot::SHARED_BANK_BEGIN; slot_id <= EQ::invslot::SHARED_BANK_END; slot_id++) {
const EQ::ItemInstance* inst = m_inv[slot_id];
if (!inst)
continue;
// SharedBank items
for (int16 slot_id = EQ::invslot::SHARED_BANK_BEGIN; slot_id <= EQ::invslot::SHARED_BANK_END; slot_id++) {
const EQ::ItemInstance* inst = m_inv[slot_id];
if (!inst)
continue;
inst->Serialize(ob, slot_id);
inst->Serialize(ob, slot_id);
if (ob.tellp() == last_pos)
LogInventory("Serialization failed on item slot [{}] during BulkSendInventoryItems. Item skipped", slot_id);
if (ob.tellp() == last_pos)
LogInventory("Serialization failed on item slot [{}] during BulkSendInventoryItems. Item skipped", slot_id);
last_pos = ob.tellp();
}
last_pos = ob.tellp();
}
}
auto outapp = new EQApplicationPacket(OP_CharInventory);
outapp->size = ob.size();
outapp->pBuffer = ob.detach();
QueuePacket(outapp);
safe_delete(outapp);
auto outapp = new EQApplicationPacket(OP_CharInventory);
outapp->size = ob.size();
outapp->pBuffer = ob.detach();
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::BulkSendMerchantInventory(int merchant_id, int npcid) {
+2 -2
View File
@@ -40,7 +40,7 @@ extern FastMath g_Math;
void CatchSignal(int sig_num);
int command_count; // how many commands we have
int command_count; // how many commands we have
// this is the pointer to the dispatch function, updated once
// init has been performed to point at the real function
@@ -96,7 +96,7 @@ int command_init(void)
command_add("augmentitem", "Force augments an item. Must have the augment item window open.", AccountStatus::GMImpossible, command_augmentitem) ||
command_add("ban", "[Character Name] [Reason] - Ban by character name", AccountStatus::GMLeadAdmin, command_ban) ||
command_add("bugs", "[Close|Delete|Review|Search|View] - Handles player bug reports", AccountStatus::QuestTroupe, command_bugs) ||
command_add("bot", "Type \"#bot help\" or \"^help\" to the see the list of available commands for bots.", AccountStatus::Player, command_bot) ||
(RuleB(Bots, Enabled) && command_add("bot", "Type \"#bot help\" or \"^help\" to the see the list of available commands for bots.", AccountStatus::Player, command_bot)) ||
command_add("camerashake", "[Duration (Milliseconds)] [Intensity (1-10)] - Shakes the camera on everyone's screen globally.", AccountStatus::QuestTroupe, command_camerashake) ||
command_add("castspell", "[Spell ID] [Instant (0 = False, 1 = True, Default is 1 if Unused)] - Cast a spell", AccountStatus::Guide, command_castspell) ||
command_add("chat", "[Channel ID] [Message] - Send a channel message to all zones", AccountStatus::GMMgmt, command_chat) ||
+2 -2
View File
@@ -542,8 +542,8 @@ void Doors::HandleClick(Client *sender, uint8 trigger)
if (EQ::ValueWithin(m_open_type, 57, 58) && HasDestinationZone()) {
bool has_key_required = (required_key_item && required_key_item == player_key);
if (sender->GetGM() && has_key_required) {
has_key_required = false;
if (sender->GetGM() && !has_key_required) {
has_key_required = true;
sender->Message(Chat::White, "Your GM flag allows you to open this door without a key.");
}
+12 -6
View File
@@ -1036,7 +1036,7 @@ void EntityList::AETaunt(Client* taunter, float range, int bonus_hate)
float range_squared = range * range;
for (auto& it: entity_list.GetCloseMobList(taunter, range)) {
for (auto& it: taunter->GetCloseMobList(range)) {
Mob *them = it.second;
if (!them) {
continue;
@@ -1096,7 +1096,7 @@ void EntityList::AESpell(
max_targets = nullptr;
}
int max_targets_allowed = RuleI(Range, AOEMaxTargets); // unlimited
int max_targets_allowed = RuleI(Spells, DefaultAOEMaxTargets);;
if (max_targets) { // rains pass this in since they need to preserve the count through waves
max_targets_allowed = *max_targets;
} else if (spells[spell_id].aoe_max_targets) {
@@ -1108,7 +1108,13 @@ void EntityList::AESpell(
!IsEffectInSpell(spell_id, SE_Lull) &&
!IsEffectInSpell(spell_id, SE_Mez)
) {
max_targets_allowed = 4;
max_targets_allowed = RuleI(Spells, TargetedAOEMaxTargets);
} else if (
IsPBAENukeSpell(spell_id) &&
IsDetrimentalSpell &&
!is_npc
) {
max_targets_allowed = RuleI(Spells, PointBlankAOEMaxTargets);
}
int target_hit_counter = 0;
@@ -1120,7 +1126,7 @@ void EntityList::AESpell(
distance
);
for (auto& it: entity_list.GetCloseMobList(caster_mob, distance)) {
for (auto& it: caster_mob->GetCloseMobList(distance)) {
current_mob = it.second;
if (!current_mob) {
continue;
@@ -1256,7 +1262,7 @@ void EntityList::MassGroupBuff(
float distance_squared = distance * distance;
bool is_detrimental_spell = IsDetrimentalSpell(spell_id);
for (auto& it: entity_list.GetCloseMobList(caster, distance)) {
for (auto& it: caster->GetCloseMobList(distance)) {
current_mob = it.second;
if (!current_mob) {
continue;
@@ -1306,7 +1312,7 @@ void EntityList::AEAttack(
float distance_squared = distance * distance;
int current_hits = 0;
for (auto& it: entity_list.GetCloseMobList(attacker, distance)) {
for (auto& it: attacker->GetCloseMobList(distance)) {
current_mob = it.second;
if (!current_mob) {
continue;
+228 -58
View File
@@ -57,6 +57,7 @@ void perl_register_expedition();
void perl_register_expedition_lock_messages();
void perl_register_bot();
void perl_register_buff();
void perl_register_merc();
#endif // EMBPERL_XS_CLASSES
#endif // EMBPERL_XS
@@ -203,6 +204,7 @@ const char* QuestEventSubroutines[_LargestEventID] = {
"EVENT_ENTITY_VARIABLE_UPDATE",
"EVENT_AA_LOSS",
"EVENT_SPELL_BLOCKED",
"EVENT_READ_ITEM",
// Add new events before these or Lua crashes
"EVENT_SPELL_EFFECT_BOT",
@@ -216,6 +218,8 @@ PerlembParser::PerlembParser() : perl(nullptr)
global_player_quest_status_ = questUnloaded;
bot_quest_status_ = questUnloaded;
global_bot_quest_status_ = questUnloaded;
merc_quest_status_ = questUnloaded;
global_merc_quest_status_ = questUnloaded;
}
PerlembParser::~PerlembParser()
@@ -257,6 +261,8 @@ void PerlembParser::ReloadQuests()
global_player_quest_status_ = questUnloaded;
bot_quest_status_ = questUnloaded;
global_bot_quest_status_ = questUnloaded;
merc_quest_status_ = questUnloaded;
global_merc_quest_status_ = questUnloaded;
item_quest_status_.clear();
spell_quest_status_.clear();
@@ -284,6 +290,8 @@ int PerlembParser::EventCommon(
bool is_global_npc_quest = false;
bool is_bot_quest = false;
bool is_global_bot_quest = false;
bool is_merc_quest = false;
bool is_global_merc_quest = false;
bool is_item_quest = false;
bool is_spell_quest = false;
@@ -294,6 +302,8 @@ int PerlembParser::EventCommon(
is_global_player_quest,
is_bot_quest,
is_global_bot_quest,
is_merc_quest,
is_global_merc_quest,
is_global_npc_quest,
is_item_quest,
is_spell_quest,
@@ -309,6 +319,8 @@ int PerlembParser::EventCommon(
is_global_player_quest,
is_bot_quest,
is_global_bot_quest,
is_merc_quest,
is_global_merc_quest,
is_global_npc_quest,
is_item_quest,
is_spell_quest,
@@ -338,6 +350,8 @@ int PerlembParser::EventCommon(
is_global_player_quest,
is_bot_quest,
is_global_bot_quest,
is_merc_quest,
is_global_merc_quest,
is_global_npc_quest,
is_item_quest,
is_spell_quest,
@@ -355,6 +369,8 @@ int PerlembParser::EventCommon(
is_global_player_quest,
is_bot_quest,
is_global_bot_quest,
is_merc_quest,
is_global_merc_quest,
is_global_npc_quest,
is_item_quest,
is_spell_quest,
@@ -381,7 +397,7 @@ int PerlembParser::EventCommon(
if (is_player_quest || is_global_player_quest) {
return SendCommands(package_name.c_str(), QuestEventSubroutines[event_id], 0, mob, mob, nullptr, nullptr);
} else if (is_bot_quest || is_global_bot_quest) {
} else if (is_bot_quest || is_global_bot_quest || is_merc_quest || is_global_merc_quest) {
return SendCommands(package_name.c_str(), QuestEventSubroutines[event_id], 0, npc_mob, mob, nullptr, nullptr);
} else if (is_item_quest) {
return SendCommands(package_name.c_str(), QuestEventSubroutines[event_id], 0, mob, mob, inst, nullptr);
@@ -1008,41 +1024,22 @@ int PerlembParser::SendCommands(
#ifdef EMBPERL_XS_CLASSES
dTHX;
{
std::string cl = fmt::format("${}::client", prefix);
std::string np = fmt::format("${}::npc", prefix);
std::string qi = fmt::format("${}::questitem", prefix);
std::string sp = fmt::format("${}::spell", prefix);
std::string enl = fmt::format("${}::entity_list", prefix);
std::string bot = fmt::format("${}::bot", prefix);
const std::vector<std::string>& suffixes = {
"bot",
"client",
"entity_list",
"merc",
"npc",
"questitem",
"spell"
};
if (clear_vars_.find(cl) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", cl);
perl->eval(e.c_str());
}
if (clear_vars_.find(np) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", np);
perl->eval(e.c_str());
}
if (clear_vars_.find(qi) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", qi);
perl->eval(e.c_str());
}
if (clear_vars_.find(sp) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", sp);
perl->eval(e.c_str());
}
if (clear_vars_.find(enl) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", enl);
perl->eval(e.c_str());
}
if (clear_vars_.find(bot) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", bot);
perl->eval(e.c_str());
for (const auto& suffix : suffixes) {
const std::string& key = fmt::format("${}::{}", prefix, suffix);
if (clear_vars_.find(suffix) != clear_vars_.end()) {
auto e = fmt::format("{} = undef;", key);
perl->eval(e.c_str());
}
}
}
@@ -1059,19 +1056,21 @@ int PerlembParser::SendCommands(
sv_setsv(client, _empty_sv);
}
//only export NPC if it's a npc quest
if (!other->IsClient() && other->IsNPC()) {
NPC* n = quest_manager.GetNPC();
buf = fmt::format("{}::npc", prefix);
SV* npc = get_sv(buf.c_str(), true);
sv_setref_pv(npc, "NPC", n);
}
if (!other->IsClient() && other->IsBot()) {
if (other->IsBot()) {
Bot* b = quest_manager.GetBot();
buf = fmt::format("{}::bot", prefix);
SV* bot = get_sv(buf.c_str(), true);
sv_setref_pv(bot, "Bot", b);
} else if (other->IsMerc()) {
Merc* m = quest_manager.GetMerc();
buf = fmt::format("{}::merc", prefix);
SV* merc = get_sv(buf.c_str(), true);
sv_setref_pv(merc, "Merc", m);
} else if (other->IsNPC()) {
NPC* n = quest_manager.GetNPC();
buf = fmt::format("{}::npc", prefix);
SV* npc = get_sv(buf.c_str(), true);
sv_setref_pv(npc, "NPC", n);
}
//only export QuestItem if it's an inst quest
@@ -1097,23 +1096,25 @@ int PerlembParser::SendCommands(
#endif
//now call the requested sub
ret_value = perl->dosub(std::string(prefix).append("::").append(event_id).c_str());
const std::string& sub_key = fmt::format("{}::{}", prefix, event_id);
ret_value = perl->dosub(sub_key.c_str());
#ifdef EMBPERL_XS_CLASSES
{
std::string cl = fmt::format("${}::client", prefix);
std::string np = fmt::format("${}::npc", prefix);
std::string qi = fmt::format("${}::questitem", prefix);
std::string sp = fmt::format("${}::spell", prefix);
std::string enl = fmt::format("${}::entity_list", prefix);
std::string bot = fmt::format("${}::bot", prefix);
const std::vector<std::string>& suffixes = {
"bot",
"client",
"entity_list",
"merc",
"npc",
"questitem",
"spell"
};
clear_vars_[cl] = 1;
clear_vars_[np] = 1;
clear_vars_[qi] = 1;
clear_vars_[sp] = 1;
clear_vars_[enl] = 1;
clear_vars_[bot] = 1;
for (const auto& suffix : suffixes) {
const std::string& key = fmt::format("${}::{}", prefix, suffix);
clear_vars_[key] = 1;
}
}
#endif
@@ -1183,6 +1184,7 @@ void PerlembParser::MapFunctions()
perl_register_expedition_lock_messages();
perl_register_bot();
perl_register_buff();
perl_register_merc();
#endif // EMBPERL_XS_CLASSES
}
@@ -1191,6 +1193,8 @@ void PerlembParser::GetQuestTypes(
bool& is_global_player_quest,
bool& is_bot_quest,
bool& is_global_bot_quest,
bool& is_merc_quest,
bool& is_global_merc_quest,
bool& is_global_npc_quest,
bool& is_item_quest,
bool& is_spell_quest,
@@ -1218,10 +1222,14 @@ void PerlembParser::GetQuestTypes(
if (is_global) {
if (npc_mob->IsBot()) {
is_global_bot_quest = true;
} else if (npc_mob->IsMerc()) {
is_global_merc_quest = true;
}
} else {
if (npc_mob->IsBot()) {
is_bot_quest = true;
} else if (npc_mob->IsMerc()) {
is_merc_quest = true;
}
}
} else {
@@ -1250,6 +1258,8 @@ void PerlembParser::GetQuestPackageName(
bool& is_global_player_quest,
bool& is_bot_quest,
bool& is_global_bot_quest,
bool& is_merc_quest,
bool& is_global_merc_quest,
bool& is_global_npc_quest,
bool& is_item_quest,
bool& is_spell_quest,
@@ -1267,6 +1277,8 @@ void PerlembParser::GetQuestPackageName(
!is_global_player_quest &&
!is_bot_quest &&
!is_global_bot_quest &&
!is_merc_quest &&
!is_global_merc_quest &&
!is_item_quest &&
!is_spell_quest
) {
@@ -1290,6 +1302,10 @@ void PerlembParser::GetQuestPackageName(
package_name = "qst_bot";
} else if (is_global_bot_quest) {
package_name = "qst_global_bot";
} else if (is_merc_quest) {
package_name = "qst_merc";
} else if (is_global_merc_quest) {
package_name = "qst_global_merc";
} else {
package_name = fmt::format("qst_spell_{}", object_id);
}
@@ -1315,6 +1331,8 @@ void PerlembParser::ExportQGlobals(
bool is_global_player_quest,
bool is_bot_quest,
bool is_global_bot_quest,
bool is_merc_quest,
bool is_global_merc_quest,
bool is_global_npc_quest,
bool is_item_quest,
bool is_spell_quest,
@@ -1330,6 +1348,8 @@ void PerlembParser::ExportQGlobals(
!is_global_player_quest &&
!is_bot_quest &&
!is_global_bot_quest &&
!is_merc_quest &&
!is_global_merc_quest &&
!is_item_quest &&
!is_spell_quest
) {
@@ -1465,6 +1485,8 @@ void PerlembParser::ExportMobVariables(
bool is_global_player_quest,
bool is_bot_quest,
bool is_global_bot_quest,
bool is_merc_quest,
bool is_global_merc_quest,
bool is_global_npc_quest,
bool is_item_quest,
bool is_spell_quest,
@@ -1490,6 +1512,8 @@ void PerlembParser::ExportMobVariables(
!is_global_player_quest &&
!is_bot_quest &&
!is_global_bot_quest &&
!is_merc_quest &&
!is_global_merc_quest &&
!is_item_quest
) {
if (mob && mob->IsClient() && npc_mob && npc_mob->IsNPC()) {
@@ -1520,6 +1544,8 @@ void PerlembParser::ExportMobVariables(
!is_global_player_quest &&
!is_bot_quest &&
!is_global_bot_quest &&
!is_merc_quest &&
!is_global_merc_quest &&
!is_item_quest &&
!is_spell_quest
) {
@@ -2080,7 +2106,8 @@ void PerlembParser::ExportEventVariables(
"killed_bot_id",
killed->IsBot() ? killed->CastToBot()->GetBotID() : 0
);
ExportVar(package_name.c_str(), "killed_npc_id", killed->IsNPC() ? killed->GetNPCTypeID() : 0);
ExportVar(package_name.c_str(), "killed_merc_id", killed->IsMerc() ? killed->CastToMerc()->GetMercenaryID() : 0);
ExportVar(package_name.c_str(), "killed_npc_id", !killed->IsMerc() && killed->IsNPC() ? killed->GetNPCTypeID() : 0);
}
}
break;
@@ -2356,6 +2383,7 @@ void PerlembParser::ExportEventVariables(
case EVENT_DESPAWN: {
ExportVar(package_name.c_str(), "despawned_entity_id", npc_mob->GetID());
ExportVar(package_name.c_str(), "despawned_bot_id", npc_mob->IsBot() ? npc_mob->CastToBot()->GetBotID() : 0);
ExportVar(package_name.c_str(), "despawned_merc_id", npc_mob->IsMerc() ? npc_mob->CastToMerc()->GetMercenaryID() : 0);
ExportVar(package_name.c_str(), "despawned_npc_id", npc_mob->IsNPC() ? npc_mob->GetNPCTypeID() : 0);
break;
}
@@ -2488,6 +2516,28 @@ void PerlembParser::ExportEventVariables(
break;
}
case EVENT_READ_ITEM: {;
ExportVar(package_name.c_str(), "item_id", extra_data);
ExportVar(package_name.c_str(), "text_file", data);
if (extra_pointers && extra_pointers->size() == 7) {
ExportVar(package_name.c_str(), "book_text", std::any_cast<std::string>(extra_pointers->at(0)).c_str());
ExportVar(package_name.c_str(), "can_cast", std::any_cast<int8>(extra_pointers->at(1)));
ExportVar(package_name.c_str(), "can_scribe", std::any_cast<int8>(extra_pointers->at(2)));
ExportVar(package_name.c_str(), "slot_id", std::any_cast<int16>(extra_pointers->at(3)));
ExportVar(package_name.c_str(), "target_id", std::any_cast<int>(extra_pointers->at(4)));
ExportVar(package_name.c_str(), "type", std::any_cast<uint8>(extra_pointers->at(5)));
ExportVar(
package_name.c_str(),
"item",
"QuestItem",
std::any_cast<EQ::ItemInstance*>(extra_pointers->at(6))
);
}
break;
}
default: {
break;
}
@@ -2614,4 +2664,124 @@ int PerlembParser::EventGlobalBot(
);
}
void PerlembParser::LoadMercScript(std::string filename)
{
if (!perl || merc_quest_status_ != questUnloaded) {
return;
}
try {
perl->eval_file("qst_merc", filename.c_str());
} catch (std::string e) {
AddError(
fmt::format(
"Error Compiling Merc Quest File [{}] Error [{}]",
filename,
e
)
);
merc_quest_status_ = questFailedToLoad;
return;
}
merc_quest_status_ = questLoaded;
}
void PerlembParser::LoadGlobalMercScript(std::string filename)
{
if (!perl || global_merc_quest_status_ != questUnloaded) {
return;
}
try {
perl->eval_file("qst_global_merc", filename.c_str());
} catch (std::string e) {
AddError(
fmt::format(
"Error Compiling Global Merc Quest File [{}] Error [{}]",
filename,
e
)
);
global_merc_quest_status_ = questFailedToLoad;
return;
}
global_merc_quest_status_ = questLoaded;
}
bool PerlembParser::MercHasQuestSub(QuestEventID event_id)
{
if (
!perl ||
merc_quest_status_ != questLoaded ||
event_id >= _LargestEventID
) {
return false;
}
return perl->SubExists("qst_merc", QuestEventSubroutines[event_id]);
}
bool PerlembParser::GlobalMercHasQuestSub(QuestEventID event_id)
{
if (
!perl ||
global_merc_quest_status_ != questLoaded ||
event_id >= _LargestEventID
) {
return false;
}
return (perl->SubExists("qst_global_merc", QuestEventSubroutines[event_id]));
}
int PerlembParser::EventMerc(
QuestEventID event_id,
Merc* merc,
Mob* mob,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
return EventCommon(
event_id,
0,
data.c_str(),
merc,
nullptr,
nullptr,
mob,
extra_data,
false,
extra_pointers
);
}
int PerlembParser::EventGlobalMerc(
QuestEventID event_id,
Merc* merc,
Mob* mob,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
return EventCommon(
event_id,
0,
data.c_str(),
merc,
nullptr,
nullptr,
mob,
extra_data,
true,
extra_pointers
);
}
#endif
+32
View File
@@ -118,6 +118,24 @@ public:
std::vector<std::any>* extra_pointers
);
virtual int EventMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
);
virtual int EventGlobalMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
);
virtual bool HasQuestSub(uint32 npc_id, QuestEventID event_id);
virtual bool HasGlobalQuestSub(QuestEventID event_id);
virtual bool PlayerHasQuestSub(QuestEventID event_id);
@@ -126,6 +144,8 @@ public:
virtual bool ItemHasQuestSub(EQ::ItemInstance* inst, QuestEventID event_id);
virtual bool BotHasQuestSub(QuestEventID event_id);
virtual bool GlobalBotHasQuestSub(QuestEventID event_id);
virtual bool MercHasQuestSub(QuestEventID event_id);
virtual bool GlobalMercHasQuestSub(QuestEventID event_id);
virtual void LoadNPCScript(std::string filename, int npc_id);
virtual void LoadGlobalNPCScript(std::string filename);
@@ -135,6 +155,8 @@ public:
virtual void LoadSpellScript(std::string filename, uint32 spell_id);
virtual void LoadBotScript(std::string filename);
virtual void LoadGlobalBotScript(std::string filename);
virtual void LoadMercScript(std::string filename);
virtual void LoadGlobalMercScript(std::string filename);
virtual void AddVar(std::string name, std::string val);
virtual std::string GetVar(std::string name);
@@ -182,6 +204,8 @@ private:
bool& is_global_player_quest,
bool& is_bot_quest,
bool& is_global_bot_quest,
bool& is_merc_quest,
bool& is_global_merc_quest,
bool& is_global_npc_quest,
bool& is_item_quest,
bool& is_spell_quest,
@@ -197,6 +221,8 @@ private:
bool& is_global_player_quest,
bool& is_bot_quest,
bool& is_global_bot_quest,
bool& is_merc_quest,
bool& is_global_merc_quest,
bool& is_global_npc_quest,
bool& is_item_quest,
bool& is_spell_quest,
@@ -216,6 +242,8 @@ private:
bool is_global_player_quest,
bool is_bot_quest,
bool is_global_bot_quest,
bool is_merc_quest,
bool is_global_merc_quest,
bool is_global_npc_quest,
bool is_item_quest,
bool is_spell_quest,
@@ -230,6 +258,8 @@ private:
bool is_global_player_quest,
bool is_bot_quest,
bool is_global_bot_quest,
bool is_merc_quest,
bool is_global_merc_quest,
bool is_global_npc_quest,
bool is_item_quest,
bool is_spell_quest,
@@ -263,6 +293,8 @@ private:
PerlQuestStatus global_player_quest_status_;
PerlQuestStatus bot_quest_status_;
PerlQuestStatus global_bot_quest_status_;
PerlQuestStatus merc_quest_status_;
PerlQuestStatus global_merc_quest_status_;
SV* _empty_sv;
+12
View File
@@ -5978,6 +5978,16 @@ bool Perl__aretaskscompleted(perl::array task_ids)
return quest_manager.aretaskscompleted(v);
}
void Perl__SpawnCircle(uint32 npc_id, float x, float y, float z, float heading, float radius, uint32 points)
{
quest_manager.SpawnCircle(npc_id, glm::vec4(x, y, z, heading), radius, points);
}
void Perl__SpawnGrid(uint32 npc_id, float x, float y, float z, float heading, float spacing, uint32 spawn_count)
{
quest_manager.SpawnGrid(npc_id, glm::vec4(x, y, z, heading), spacing, spawn_count);
}
void perl_register_quest()
{
perl::interpreter perl(PERL_GET_THX);
@@ -6287,6 +6297,8 @@ void perl_register_quest()
package.add("SendMail", &Perl__SendMail);
package.add("SetAutoLoginCharacterNameByAccountID", &Perl__SetAutoLoginCharacterNameByAccountID);
package.add("SetRunning", &Perl__SetRunning);
package.add("SpawnCircle", &Perl__SpawnCircle);
package.add("SpawnGrid", &Perl__SpawnGrid);
package.add("activespeakactivity", &Perl__activespeakactivity);
package.add("activespeaktask", &Perl__activespeaktask);
package.add("activetasksinset", &Perl__activetasksinset);
+83 -52
View File
@@ -696,7 +696,7 @@ void EntityList::AddNPC(NPC *npc, bool send_spawn_packet, bool dont_queue)
npc_list.emplace(std::pair<uint16, NPC *>(npc->GetID(), npc));
mob_list.emplace(std::pair<uint16, Mob *>(npc->GetID(), npc));
entity_list.ScanCloseMobs(npc->close_mobs, npc, true);
entity_list.ScanCloseMobs(npc);
if (parse->HasQuestSub(npc->GetNPCTypeID(), EVENT_SPAWN)) {
parse->EventNPC(EVENT_SPAWN, npc, nullptr, "", 0);
@@ -776,6 +776,10 @@ void EntityList::AddMerc(Merc *merc, bool SendSpawnPacket, bool dontqueue)
merc_list.emplace(std::pair<uint16, Merc *>(merc->GetID(), merc));
mob_list.emplace(std::pair<uint16, Mob *>(merc->GetID(), merc));
if (parse->MercHasQuestSub(EVENT_SPAWN)) {
parse->EventMerc(EVENT_SPAWN, merc, nullptr, "", 0);
}
}
}
@@ -1742,8 +1746,7 @@ void EntityList::QueueCloseClients(
}
float distance_squared = distance * distance;
for (auto &e : GetCloseMobList(sender, distance)) {
for (auto &e : sender->GetCloseMobList(distance)) {
Mob *mob = e.second;
if (!mob) {
continue;
@@ -2886,7 +2889,7 @@ bool EntityList::RemoveMobFromCloseLists(Mob *mob)
entity_id
);
it->second->close_mobs.erase(entity_id);
it->second->m_close_mobs.erase(entity_id);
++it;
}
@@ -2911,49 +2914,40 @@ void EntityList::RemoveAuraFromMobs(Mob *aura)
}
}
/**
* The purpose of this system is so that we cache relevant entities that are "close"
*
* In general; it becomes incredibly expensive to run zone-wide checks against every single mob in the zone when in reality
* we only care about entities closest to us
*
* A very simple example of where this is relevant is Aggro, the below example is skewed because the overall implementation
* of Aggro was also tweaked in conjunction with close lists. We also scan more aggressively when entities are moving (1-6 seconds)
* versus 60 seconds when idle. We also have entities that are moving add themselves to those closest to them so that their close
* lists remain always up to date
*
* Before: Aggro checks for NPC to Client aggro | (40 clients in zone) x (525 npcs) x 2 (times a second) = 2,520,000 checks a minute
* After: Aggro checks for NPC to Client aggro | (40 clients in zone) x (20-30 npcs) x 2 (times a second) = 144,000 checks a minute (This is actually far less today)
*
* Places in the code where this logic makes a huge impact
*
* Aggro checks (zone wide -> close)
* Aura processing (zone wide -> close)
* AE Taunt (zone wide -> close)
* AOE Spells (zone wide -> close)
* Bard Pulse AOE (zone wide -> close)
* Mass Group Buff (zone wide -> close)
* AE Attack (zone wide -> close)
* Packet QueueCloseClients (zone wide -> close)
* Check Close Beneficial Spells (Buffs; should I heal other npcs) (zone wide -> close)
* AI Yell for Help (NPC Assist other NPCs) (zone wide -> close)
*
* All of the above makes a tremendous impact on the bottom line of cpu cycle performance because we run an order of magnitude
* less checks by focusing our hot path logic down to a very small subset of relevant entities instead of looping an entire
* entity list (zone wide)
*
* @param close_mobs
* @param scanning_mob
*/
void EntityList::ScanCloseMobs(
std::unordered_map<uint16, Mob *> &close_mobs,
Mob *scanning_mob,
bool add_self_to_other_lists
)
// The purpose of this system is so that we cache relevant entities that are "close"
//
// In general; it becomes incredibly expensive to run zone-wide checks against every single mob in the zone when in reality
// we only care about entities closest to us
//
// A very simple example of where this is relevant is Aggro, the below example is skewed because the overall implementation
// of Aggro was also tweaked in conjunction with close lists. We also scan more aggressively when entities are moving (1-6 seconds)
// versus 60 seconds when idle. We also have entities that are moving add themselves to those closest to them so that their close
// lists remain always up to date
//
// Before: Aggro checks for NPC to Client aggro | (40 clients in zone) x (525 npcs) x 2 (times a second) = 2,520,000 checks a minute
// After: Aggro checks for NPC to Client aggro | (40 clients in zone) x (20-30 npcs) x 2 (times a second) = 144,000 checks a minute (This is // tually far less today)
//
// Places in the code where this logic makes a huge impact
//
// Aggro checks (zone wide -> close)
// Aura processing (zone wide -> close)
// AE Taunt (zone wide -> close)
// AOE Spells (zone wide -> close)
// Bard Pulse AOE (zone wide -> close)
// Mass Group Buff (zone wide -> close)
// AE Attack (zone wide -> close)
// Packet QueueCloseClients (zone wide -> close)
// Check Close Beneficial Spells (Buffs; should I heal other npcs) (zone wide -> close)
// AI Yell for Help (NPC Assist other NPCs) (zone wide -> close)
//
// All of the above makes a tremendous impact on the bottom line of cpu cycle performance because we run an order of magnitude
// less checks by focusing our hot path logic down to a very small subset of relevant entities instead of looping an entire
// entity list (zone wide)
void EntityList::ScanCloseMobs(Mob *scanning_mob)
{
float scan_range = RuleI(Range, MobCloseScanDistance) * RuleI(Range, MobCloseScanDistance);
close_mobs.clear();
scanning_mob->m_close_mobs.clear();
for (auto &e : mob_list) {
auto mob = e.second;
@@ -2963,12 +2957,13 @@ void EntityList::ScanCloseMobs(
float distance = DistanceSquared(scanning_mob->GetPosition(), mob->GetPosition());
if (distance <= scan_range || mob->GetAggroRange() >= scan_range) {
close_mobs.emplace(std::pair<uint16, Mob *>(mob->GetID(), mob));
scanning_mob->m_close_mobs.emplace(std::pair<uint16, Mob *>(mob->GetID(), mob));
if (add_self_to_other_lists && scanning_mob->GetID() > 0) {
// add self to other mobs close list
if (scanning_mob->GetID() > 0) {
bool has_mob = false;
for (auto &cm: mob->close_mobs) {
for (auto &cm: mob->m_close_mobs) {
if (scanning_mob->GetID() == cm.first) {
has_mob = true;
break;
@@ -2976,7 +2971,7 @@ void EntityList::ScanCloseMobs(
}
if (!has_mob) {
mob->close_mobs.insert(std::pair<uint16, Mob *>(scanning_mob->GetID(), scanning_mob));
mob->m_close_mobs.insert(std::pair<uint16, Mob *>(scanning_mob->GetID(), scanning_mob));
}
}
}
@@ -2985,7 +2980,7 @@ void EntityList::ScanCloseMobs(
LogAIScanCloseDetail(
"[{}] Scanning Close List | list_size [{}] moving [{}]",
scanning_mob->GetCleanName(),
close_mobs.size(),
scanning_mob->m_close_mobs.size(),
scanning_mob->IsMoving() ? "true" : "false"
);
}
@@ -4448,7 +4443,7 @@ void EntityList::QuestJournalledSayClose(
buf.WriteInt32(0);
if (RuleB(Chat, QuestDialogueUsesDialogueWindow)) {
for (auto &e : GetCloseMobList(sender, (dist * dist))) {
for (auto &e : sender->GetCloseMobList(dist)) {
Mob *mob = e.second;
if (!mob) {
continue;
@@ -5651,7 +5646,7 @@ std::vector<Mob*> EntityList::GetTargetsForVirusEffect(Mob *spreader, Mob *origi
std::vector<Mob *> spreader_list = {};
bool is_detrimental_spell = IsDetrimentalSpell(spell_id);
for (auto &it : entity_list.GetCloseMobList(spreader, range)) {
for (auto &it : spreader->GetCloseMobList(range)) {
Mob *mob = it.second;
if (!mob) {
continue;
@@ -5781,7 +5776,7 @@ void EntityList::ReloadMerchants() {
std::unordered_map<uint16, Mob *> &EntityList::GetCloseMobList(Mob *mob, float distance)
{
if (distance <= RuleI(Range, MobCloseScanDistance)) {
return mob->close_mobs;
return mob->m_close_mobs;
}
return mob_list;
@@ -5946,3 +5941,39 @@ void EntityList::DamageArea(
}
}
}
std::vector<NPC*> EntityList::GetNPCsByIDs(std::vector<uint32> npc_ids)
{
std::vector<NPC*> v;
for (const auto& e : GetNPCList()) {
const auto& n = std::find(npc_ids.begin(), npc_ids.end(), e.second->GetNPCTypeID());
if (e.second) {
if (n != npc_ids.end()) {
continue;
}
v.emplace_back(e.second);
}
}
return v;
}
std::vector<NPC*> EntityList::GetExcludedNPCsByIDs(std::vector<uint32> npc_ids)
{
std::vector<NPC*> v;
for (const auto& e : GetNPCList()) {
const auto& n = std::find(npc_ids.begin(), npc_ids.end(), e.second->GetNPCTypeID());
if (e.second) {
if (n == npc_ids.end()) {
continue;
}
v.emplace_back(e.second);
}
}
return v;
}
+4 -5
View File
@@ -560,17 +560,16 @@ public:
std::unordered_map<uint16, Mob *> &GetCloseMobList(Mob *mob, float distance = 0.0f);
std::vector<NPC*> GetNPCsByIDs(std::vector<uint32> npc_ids);
std::vector<NPC*> GetExcludedNPCsByIDs(std::vector<uint32> npc_ids);
void DepopAll(int NPCTypeID, bool StartSpawnTimer = true);
uint16 GetFreeID();
void RefreshAutoXTargets(Client *c);
void RefreshClientXTargets(Client *c);
void SendAlternateAdvancementStats();
void ScanCloseMobs(
std::unordered_map<uint16, Mob *> &close_mobs,
Mob *scanning_mob,
bool add_self_to_other_lists = false
);
void ScanCloseMobs(Mob *scanning_mob);
void GetTrapInfo(Client* c);
bool IsTrapGroupSpawned(uint32 trap_id, uint8 group);
+1
View File
@@ -144,6 +144,7 @@ typedef enum {
EVENT_ENTITY_VARIABLE_UPDATE,
EVENT_AA_LOSS,
EVENT_SPELL_BLOCKED,
EVENT_READ_ITEM,
// Add new events before these or Lua crashes
EVENT_SPELL_EFFECT_BOT,
+1 -1
View File
@@ -527,7 +527,7 @@ void Client::AddEXP(ExpSource exp_source, uint64 in_add_exp, uint8 conlevel, boo
// Are we also doing linear AA acceleration?
if (RuleB(AA, ModernAAScalingEnabled) && aaexp > 0)
{
aaexp = ScaleAAXPBasedOnCurrentAATotal(GetAAPoints(), aaexp);
aaexp = ScaleAAXPBasedOnCurrentAATotal(GetSpentAA() + GetAAPoints(), aaexp);
}
// Check for AA XP Cap
+6 -4
View File
@@ -7,15 +7,16 @@ void command_grantaa(Client *c, const Seperator *sep)
return;
}
const uint8 unlock_level = sep->IsNumber(1) ? static_cast<uint8>(Strings::ToUnsignedInt(sep->arg[1])) : 0;
const uint8 unlock_level = sep->IsNumber(1) ? static_cast<uint8>(Strings::ToUnsignedInt(sep->arg[1])) : 0;
const bool skip_grant_only = sep->IsNumber(2) ? Strings::ToBool(sep->arg[2]) : false;
auto t = c->GetTarget()->CastToClient();
t->GrantAllAAPoints(unlock_level);
t->GrantAllAAPoints(unlock_level, skip_grant_only);
c->Message(
Chat::White,
fmt::format(
"Successfully granted all Alternate Advancements for {}{}.",
"Successfully granted all Alternate Advancements for {}{}{}.",
c->GetTargetDescription(t),
(
unlock_level ?
@@ -24,7 +25,8 @@ void command_grantaa(Client *c, const Seperator *sep)
unlock_level
) :
""
)
),
skip_grant_only ? "except for grant only AAs" : ""
).c_str()
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ void SetMOTD(Client *c, const Seperator *sep)
auto m = (ServerMotd_Struct *) pack->pBuffer;
strn0cpy(m->myname, c->GetName(), sizeof(m->myname));
strn0cpy(m->motd, message.c_str(), sizeof(m->motd));
strn0cpy(m->motd, !message.empty() ? message.c_str() : "", sizeof(m->motd));
worldserver.SendPacket(pack);
safe_delete(pack);
+12 -7
View File
@@ -2117,15 +2117,20 @@ void Group::UnDelegateMarkNPC(const char *OldNPCMarkerName)
void Group::SaveGroupLeaderAA()
{
// Stores the Group Leaders Leadership AA data from the Player Profile as a blob in the group_leaders table.
// This is done so that group members not in the same zone as the Leader still have access to this information.
const uint32 group_id = GetID();
std::string aa((char *) &LeaderAbilities, sizeof(GroupLeadershipAA_Struct));
auto results = GroupLeadersRepository::UpdateLeadershipAA(database, aa, GetID());
if (!group_id) {
return;
}
if (!results) {
LogError("Unable to store GroupLeadershipAA for group_id: [{}]", GetID());
}
// Stores the Group Leaders Leadership AA data from the Player Profile as a blob in the group_leaders table.
// This is done so that group members not in the same zone as the Leader still have access to this information.
std::string aa((char*) &LeaderAbilities, sizeof(GroupLeadershipAA_Struct));
if (!GroupLeadersRepository::UpdateLeadershipAA(database, aa, group_id)) {
LogError("Unable to store GroupLeadershipAA for group_id: [{}]", group_id);
}
}
void Group::UnMarkNPC(uint16 ID)
+6 -1
View File
@@ -3431,6 +3431,11 @@ void Client::SetBandolier(const EQApplicationPacket *app)
}
}
}
if (RuleI(Character, BandolierSwapDelay) > 0) {
bandolier_throttle_timer.Start(RuleI(Character, BandolierSwapDelay));
}
// finally, recalculate any stat bonuses from the item change
CalcBonuses();
}
@@ -4913,4 +4918,4 @@ bool Client::FindNumberOfFreeInventorySlotsWithSizeCheck(std::vector<BuyerLineTr
}
}
return false;
};
};
+7
View File
@@ -3216,6 +3216,12 @@ void Lua_Client::GrantAllAAPoints(uint8 unlock_level)
self->GrantAllAAPoints(unlock_level);
}
void Lua_Client::GrantAllAAPoints(uint8 unlock_level, bool skip_grant_only)
{
Lua_Safe_Call_Void();
self->GrantAllAAPoints(unlock_level, skip_grant_only);
}
void Lua_Client::AddEbonCrystals(uint32 amount)
{
Lua_Safe_Call_Void();
@@ -3699,6 +3705,7 @@ luabind::scope lua_register_client() {
.def("GoFish", (void(Lua_Client::*)(void))&Lua_Client::GoFish)
.def("GrantAllAAPoints", (void(Lua_Client::*)(void))&Lua_Client::GrantAllAAPoints)
.def("GrantAllAAPoints", (void(Lua_Client::*)(uint8))&Lua_Client::GrantAllAAPoints)
.def("GrantAllAAPoints", (void(Lua_Client::*)(uint8,bool))&Lua_Client::GrantAllAAPoints)
.def("GrantAlternateAdvancementAbility", (bool(Lua_Client::*)(int, int))&Lua_Client::GrantAlternateAdvancementAbility)
.def("GrantAlternateAdvancementAbility", (bool(Lua_Client::*)(int, int, bool))&Lua_Client::GrantAlternateAdvancementAbility)
.def("GuildID", (uint32(Lua_Client::*)(void))&Lua_Client::GuildID)
+1
View File
@@ -487,6 +487,7 @@ public:
void SetBucket(std::string bucket_name, std::string bucket_value, std::string expiration);
void GrantAllAAPoints();
void GrantAllAAPoints(uint8 unlock_level);
void GrantAllAAPoints(uint8 unlock_level, bool skip_grant_only);
void AddEbonCrystals(uint32 amount);
void AddRadiantCrystals(uint32 amount);
void RemoveEbonCrystals(uint32 amount);
+8
View File
@@ -12,6 +12,7 @@
#include "lua_door.h"
#include "lua_bot.h"
#include "lua_merc.h"
bool Lua_Entity::IsClient() {
Lua_Safe_Call_Bool();
@@ -140,6 +141,12 @@ Lua_Bot Lua_Entity::CastToBot() {
return Lua_Bot(b);
}
Lua_Merc Lua_Entity::CastToMerc() {
void *d = GetLuaPtrData();
Merc *m = reinterpret_cast<Merc*>(d);
return Lua_Merc(m);
}
luabind::scope lua_register_entity() {
return luabind::class_<Lua_Entity>("Entity")
.def(luabind::constructor<>())
@@ -149,6 +156,7 @@ luabind::scope lua_register_entity() {
.def("CastToClient", &Lua_Entity::CastToClient)
.def("CastToCorpse", &Lua_Entity::CastToCorpse)
.def("CastToDoor", &Lua_Entity::CastToDoor)
.def("CastToMerc", &Lua_Entity::CastToMerc)
.def("CastToMob", &Lua_Entity::CastToMob)
.def("CastToNPC", &Lua_Entity::CastToNPC)
.def("CastToObject", &Lua_Entity::CastToObject)
+2
View File
@@ -7,6 +7,7 @@
class Entity;
class Lua_Client;
class Lua_Bot;
class Lua_Merc;
class Lua_NPC;
class Lua_Mob;
struct Lua_HateList;
@@ -59,6 +60,7 @@ public:
Lua_Object CastToObject();
Lua_Door CastToDoor();
Lua_Bot CastToBot();
Lua_Merc CastToMerc();
};
#endif
+63
View File
@@ -16,6 +16,7 @@
#include "lua_spawn.h"
#include "lua_bot.h"
#include "lua_merc.h"
struct Lua_Mob_List {
std::vector<Lua_Mob> entries;
@@ -764,6 +765,66 @@ void Lua_EntityList::MassGroupBuff(Lua_Mob caster, Lua_Mob center, uint16 spell_
self->MassGroupBuff(caster, center, spell_id, affect_caster);
}
Lua_NPC_List Lua_EntityList::GetNPCsByExcludedIDs(luabind::adl::object table)
{
Lua_Safe_Call_Class(Lua_NPC_List);
Lua_NPC_List ret;
if (luabind::type(table) != LUA_TTABLE) {
return ret;
}
if (d_) {
auto self = reinterpret_cast<NativeType*>(d_);
std::vector<uint32> ids;
int index = 1;
while (luabind::type(table[index]) != LUA_TNIL) {
ids.emplace_back(luabind::object_cast<uint32>(table[index]));
index++;
}
const auto& l = self->GetExcludedNPCsByIDs(ids);
for (const auto& e : l) {
ret.entries.emplace_back(Lua_NPC(e));
}
}
return ret;
}
Lua_NPC_List Lua_EntityList::GetNPCsByIDs(luabind::adl::object table)
{
Lua_Safe_Call_Class(Lua_NPC_List);
Lua_NPC_List ret;
if (luabind::type(table) != LUA_TTABLE) {
return ret;
}
if (d_) {
auto self = reinterpret_cast<NativeType*>(d_);
std::vector<uint32> ids;
int index = 1;
while (luabind::type(table[index]) != LUA_TNIL) {
ids.emplace_back(luabind::object_cast<uint32>(table[index]));
index++;
}
const auto& l = self->GetNPCsByIDs(ids);
for (const auto& e : l) {
ret.entries.emplace_back(Lua_NPC(e));
}
}
return ret;
}
luabind::scope lua_register_entity_list() {
return luabind::class_<Lua_EntityList>("EntityList")
.def(luabind::constructor<>())
@@ -829,6 +890,8 @@ luabind::scope lua_register_entity_list() {
.def("GetNPCByNPCTypeID", (Lua_NPC(Lua_EntityList::*)(int))&Lua_EntityList::GetNPCByNPCTypeID)
.def("GetNPCBySpawnID", (Lua_NPC(Lua_EntityList::*)(int))&Lua_EntityList::GetNPCBySpawnID)
.def("GetNPCList", (Lua_NPC_List(Lua_EntityList::*)(void))&Lua_EntityList::GetNPCList)
.def("GetNPCsByExcludedIDs", (Lua_NPC_List(Lua_EntityList::*)(luabind::adl::object))&Lua_EntityList::GetNPCsByExcludedIDs)
.def("GetNPCsByIDs", (Lua_NPC_List(Lua_EntityList::*)(luabind::adl::object))&Lua_EntityList::GetNPCsByIDs)
.def("GetObjectByDBID", (Lua_Object(Lua_EntityList::*)(uint32))&Lua_EntityList::GetObjectByDBID)
.def("GetObjectByID", (Lua_Object(Lua_EntityList::*)(int))&Lua_EntityList::GetObjectByID)
.def("GetObjectList", (Lua_Object_List(Lua_EntityList::*)(void))&Lua_EntityList::GetObjectList)
+3 -1
View File
@@ -8,6 +8,7 @@ class EntityList;
class Lua_Mob;
class Lua_Client;
class Lua_Bot;
class Lua_Merc;
class Lua_NPC;
class Lua_Door;
class Lua_Corpse;
@@ -156,7 +157,8 @@ public:
void AreaTaunt(Lua_Client caster, float range, int bonus_hate);
void MassGroupBuff(Lua_Mob caster, Lua_Mob center, uint16 spell_id);
void MassGroupBuff(Lua_Mob caster, Lua_Mob center, uint16 spell_id, bool affect_caster);
Lua_NPC_List GetNPCsByIDs(luabind::adl::object npc_ids);
Lua_NPC_List GetNPCsByExcludedIDs(luabind::adl::object npc_ids);
};
#endif
+15 -4
View File
@@ -5635,6 +5635,16 @@ int lua_are_tasks_completed(luabind::object task_ids)
return quest_manager.aretaskscompleted(v);
}
void lua_spawn_circle(uint32 npc_id, float x, float y, float z, float heading, float radius, uint32 points)
{
quest_manager.SpawnCircle(npc_id, glm::vec4(x, y, z, heading), radius, points);
}
void lua_spawn_grid(uint32 npc_id, float x, float y, float z, float heading, float spacing, uint32 spawn_count)
{
quest_manager.SpawnGrid(npc_id, glm::vec4(x, y, z, heading), spacing, spawn_count);
}
#define LuaCreateNPCParse(name, c_type, default_value) do { \
cur = table[#name]; \
if(luabind::type(cur) != LUA_TNIL) { \
@@ -6442,6 +6452,8 @@ luabind::scope lua_register_general() {
luabind::def("send_parcel", &lua_send_parcel),
luabind::def("get_zone_uptime", &lua_get_zone_uptime),
luabind::def("are_tasks_completed", &lua_are_tasks_completed),
luabind::def("spawn_circle", &lua_spawn_circle),
luabind::def("spawn_grid", &lua_spawn_grid),
/*
Cross Zone
*/
@@ -6583,7 +6595,7 @@ luabind::scope lua_register_general() {
luabind::def("cross_zone_reset_activity_by_guild_id", &lua_cross_zone_reset_activity_by_guild_id),
luabind::def("cross_zone_reset_activity_by_expedition_id", &lua_cross_zone_reset_activity_by_expedition_id),
luabind::def("cross_zone_reset_activity_by_client_name", &lua_cross_zone_reset_activity_by_client_name),
luabind::def("cross_zone_set_entity_variable_by_client_name", &lua_cross_zone_set_entity_variable_by_client_name),
luabind::def("cross_zone_set_entity_variable_by_char_id", &lua_cross_zone_set_entity_variable_by_char_id),
luabind::def("cross_zone_set_entity_variable_by_group_id", &lua_cross_zone_set_entity_variable_by_group_id),
luabind::def("cross_zone_set_entity_variable_by_raid_id", &lua_cross_zone_set_entity_variable_by_raid_id),
luabind::def("cross_zone_set_entity_variable_by_guild_id", &lua_cross_zone_set_entity_variable_by_guild_id),
@@ -6772,7 +6784,6 @@ luabind::scope lua_register_random() {
)];
}
luabind::scope lua_register_events() {
return luabind::class_<Events>("Event")
.enum_("constants")
@@ -6907,7 +6918,8 @@ luabind::scope lua_register_events() {
luabind::value("entity_variable_delete", static_cast<int>(EVENT_ENTITY_VARIABLE_DELETE)),
luabind::value("entity_variable_set", static_cast<int>(EVENT_ENTITY_VARIABLE_SET)),
luabind::value("entity_variable_update", static_cast<int>(EVENT_ENTITY_VARIABLE_UPDATE)),
luabind::value("aa_loss", static_cast<int>(EVENT_AA_LOSS))
luabind::value("aa_loss", static_cast<int>(EVENT_AA_LOSS)),
luabind::value("read", static_cast<int>(EVENT_READ_ITEM))
)];
}
@@ -8007,7 +8019,6 @@ luabind::scope lua_register_journal_mode() {
)];
}
luabind::scope lua_register_exp_source() {
return luabind::class_<ExpSource>("ExpSource")
.enum_("constants")
+2
View File
@@ -21,8 +21,10 @@ luabind::scope lua_register_rules_const();
luabind::scope lua_register_rulei();
luabind::scope lua_register_ruler();
luabind::scope lua_register_ruleb();
luabind::scope lua_register_rules();
luabind::scope lua_register_journal_speakmode();
luabind::scope lua_register_journal_mode();
luabind::scope lua_register_exp_source();
#endif
#endif
+229
View File
@@ -0,0 +1,229 @@
#ifdef LUA_EQEMU
#include "lua.hpp"
#include <luabind/luabind.hpp>
#include "merc.h"
#include "lua_client.h"
#include "lua_merc.h"
#include "lua_group.h"
#include "lua_item.h"
#include "lua_iteminst.h"
#include "lua_mob.h"
uint32 Lua_Merc::GetCostFormula()
{
Lua_Safe_Call_Int();
return self->GetCostFormula();
}
Lua_Group Lua_Merc::GetGroup()
{
Lua_Safe_Call_Class(Lua_Group);
return self->GetGroup();
}
int Lua_Merc::GetHatedCount()
{
Lua_Safe_Call_Int();
return self->GetHatedCount();
}
float Lua_Merc::GetMaxMeleeRangeToTarget(Lua_Mob target)
{
Lua_Safe_Call_Real();
return self->GetMaxMeleeRangeToTarget(target);
}
uint32 Lua_Merc::GetMercenaryCharacterID()
{
Lua_Safe_Call_Int();
return self->GetMercenaryCharacterID();
}
uint32 Lua_Merc::GetMercenaryID()
{
Lua_Safe_Call_Int();
return self->GetMercenaryID();
}
uint32 Lua_Merc::GetMercenaryNameType()
{
Lua_Safe_Call_Int();
return self->GetMercNameType();
}
Lua_Client Lua_Merc::GetMercenaryOwner()
{
Lua_Safe_Call_Class(Lua_Client);
return Lua_Client(self->GetMercenaryOwner());
}
uint32 Lua_Merc::GetMercenarySubtype()
{
Lua_Safe_Call_Int();
return self->GetMercenarySubType();
}
uint32 Lua_Merc::GetMercenaryTemplateID()
{
Lua_Safe_Call_Int();
return self->GetMercenaryTemplateID();
}
uint32 Lua_Merc::GetMercenaryType()
{
Lua_Safe_Call_Int();
return self->GetMercenaryType();
}
Lua_Mob Lua_Merc::GetOwner()
{
Lua_Safe_Call_Class(Lua_Mob);
return Lua_Mob(self->GetOwner());
}
Lua_Mob Lua_Merc::GetOwnerOrSelf()
{
Lua_Safe_Call_Class(Lua_Mob);
return Lua_Mob(self->GetOwnerOrSelf());
}
uint8 Lua_Merc::GetProficiencyID()
{
Lua_Safe_Call_Int();
return self->GetProficiencyID();
}
uint8 Lua_Merc::GetStance()
{
Lua_Safe_Call_Int();
return self->GetStance();
}
uint8 Lua_Merc::GetTierID()
{
Lua_Safe_Call_Int();
return self->GetTierID();
}
bool Lua_Merc::HasOrMayGetAggro()
{
Lua_Safe_Call_Bool();
return self->HasOrMayGetAggro();
}
bool Lua_Merc::IsMercenaryCaster()
{
Lua_Safe_Call_Bool();
return self->IsMercCaster();
}
bool Lua_Merc::IsMercenaryCasterCombatRange(Lua_Mob target)
{
Lua_Safe_Call_Bool();
return self->IsMercCasterCombatRange(target);
}
bool Lua_Merc::IsSitting()
{
Lua_Safe_Call_Bool();
return self->IsSitting();
}
bool Lua_Merc::IsStanding()
{
Lua_Safe_Call_Bool();
return self->IsStanding();
}
void Lua_Merc::ScaleStats(int scale_percentage)
{
Lua_Safe_Call_Void();
self->ScaleStats(scale_percentage);
}
void Lua_Merc::ScaleStats(int scale_percentage, bool set_to_max)
{
Lua_Safe_Call_Void();
self->ScaleStats(scale_percentage, set_to_max);
}
void Lua_Merc::SendPayload(int payload_id, std::string payload_value)
{
Lua_Safe_Call_Void();
self->SendPayload(payload_id, payload_value);
}
void Lua_Merc::SetTarget(Lua_Mob target)
{
Lua_Safe_Call_Void();
self->SetTarget(target);
}
void Lua_Merc::Signal(int signal_id)
{
Lua_Safe_Call_Void();
self->Signal(signal_id);
}
void Lua_Merc::Sit()
{
Lua_Safe_Call_Void();
self->Sit();
}
void Lua_Merc::Stand()
{
Lua_Safe_Call_Void();
self->Stand();
}
bool Lua_Merc::Suspend()
{
Lua_Safe_Call_Bool();
return self->Suspend();
}
bool Lua_Merc::UseDiscipline(uint16 spell_id, uint16 target_id)
{
Lua_Safe_Call_Bool();
return self->UseDiscipline(spell_id, target_id);
}
luabind::scope lua_register_merc() {
return luabind::class_<Lua_Merc, Lua_Mob>("Merc")
.def(luabind::constructor<>())
.def("GetCostFormula", &Lua_Merc::GetCostFormula)
.def("GetGroup", &Lua_Merc::GetGroup)
.def("GetHatedCount", &Lua_Merc::GetHatedCount)
.def("GetMaxMeleeRangeToTarget", &Lua_Merc::GetMaxMeleeRangeToTarget)
.def("GetMercenaryCharacterID", &Lua_Merc::GetMercenaryCharacterID)
.def("GetMercenaryID", &Lua_Merc::GetMercenaryID)
.def("GetMercenaryNameType", &Lua_Merc::GetMercenaryNameType)
.def("GetMercenaryOwner", &Lua_Merc::GetMercenaryOwner)
.def("GetMercenarySubtype", &Lua_Merc::GetMercenarySubtype)
.def("GetMercenaryTemplateID", &Lua_Merc::GetMercenaryTemplateID)
.def("GetMercenaryType", &Lua_Merc::GetMercenaryType)
.def("GetOwner", &Lua_Merc::GetOwner)
.def("GetOwnerOrSelf", &Lua_Merc::GetOwnerOrSelf)
.def("GetProficiencyID", &Lua_Merc::GetProficiencyID)
.def("GetStance", &Lua_Merc::GetStance)
.def("GetTierID", &Lua_Merc::GetTierID)
.def("HasOrMayGetAggro", &Lua_Merc::HasOrMayGetAggro)
.def("IsMercenaryCaster", &Lua_Merc::IsMercenaryCaster)
.def("IsMercenaryCasterCombatRange", &Lua_Merc::IsMercenaryCasterCombatRange)
.def("IsSitting", &Lua_Merc::IsSitting)
.def("IsStanding", &Lua_Merc::IsStanding)
.def("ScaleStats", (void(Lua_Merc::*)(int))&Lua_Merc::ScaleStats)
.def("ScaleStats", (void(Lua_Merc::*)(int,bool))&Lua_Merc::ScaleStats)
.def("SendPayload", &Lua_Merc::SendPayload)
.def("SetTarget", &Lua_Merc::SetTarget)
.def("Signal", &Lua_Merc::Signal)
.def("Sit", &Lua_Merc::Sit)
.def("Stand", &Lua_Merc::Stand)
.def("Suspend", &Lua_Merc::Suspend)
.def("UseDiscipline", &Lua_Merc::UseDiscipline);
}
#endif
+63
View File
@@ -0,0 +1,63 @@
#ifndef EQEMU_LUA_MERC_H
#define EQEMU_LUA_MERC_H
#ifdef LUA_EQEMU
#include "lua_mob.h"
class Merc;
class Lua_Group;
class Lua_Merc;
class Lua_Mob;
namespace luabind {
struct scope;
}
luabind::scope lua_register_merc();
class Lua_Merc : public Lua_Mob
{
typedef Merc NativeType;
public:
Lua_Merc() { SetLuaPtrData(nullptr); }
Lua_Merc(Merc *d) { SetLuaPtrData(reinterpret_cast<Entity*>(d)); }
virtual ~Lua_Merc() { }
operator Merc*() {
return reinterpret_cast<Merc*>(GetLuaPtrData());
}
uint32 GetCostFormula();
Lua_Group GetGroup();
int GetHatedCount();
float GetMaxMeleeRangeToTarget(Lua_Mob target);
uint32 GetMercenaryCharacterID();
uint32 GetMercenaryID();
uint32 GetMercenaryNameType();
Lua_Client GetMercenaryOwner();
uint32 GetMercenarySubtype();
uint32 GetMercenaryTemplateID();
uint32 GetMercenaryType();
Lua_Mob GetOwner();
Lua_Mob GetOwnerOrSelf();
uint8 GetProficiencyID();
uint8 GetStance();
uint8 GetTierID();
bool HasOrMayGetAggro();
bool IsMercenaryCaster();
bool IsMercenaryCasterCombatRange(Lua_Mob target);
bool IsSitting();
bool IsStanding();
void ScaleStats(int scale_percentage);
void ScaleStats(int scale_percentage, bool set_to_max);
void SendPayload(int payload_id, std::string payload_value);
void SetTarget(Lua_Mob target);
void Signal(int signal_id);
void Sit();
void Stand();
bool Suspend();
bool UseDiscipline(uint16 spell_id, uint16 target_id);
};
#endif // LUA_EQEMU
#endif // EQEMU_LUA_MERC_H
+35
View File
@@ -3446,6 +3446,36 @@ void Lua_Mob::MassGroupBuff(Lua_Mob center, uint16 spell_id, bool affect_caster)
entity_list.MassGroupBuff(self, center, spell_id, affect_caster);
}
void Lua_Mob::BuffFadeBeneficial()
{
Lua_Safe_Call_Void();
self->BuffFadeBeneficial();
}
void Lua_Mob::BuffFadeDetrimental()
{
Lua_Safe_Call_Void();
self->BuffFadeDetrimental();
}
void Lua_Mob::BuffFadeDetrimentalByCaster(Lua_Mob caster)
{
Lua_Safe_Call_Void();
self->BuffFadeDetrimentalByCaster(caster);
}
void Lua_Mob::BuffFadeNonPersistDeath()
{
Lua_Safe_Call_Void();
self->BuffFadeNonPersistDeath();
}
void Lua_Mob::BuffFadeSongs()
{
Lua_Safe_Call_Void();
self->BuffFadeSongs();
}
luabind::scope lua_register_mob() {
return luabind::class_<Lua_Mob, Lua_Entity>("Mob")
.def(luabind::constructor<>())
@@ -3483,11 +3513,16 @@ luabind::scope lua_register_mob() {
.def("BuffCount", (uint32(Lua_Mob::*)(bool))&Lua_Mob::BuffCount)
.def("BuffCount", (uint32(Lua_Mob::*)(bool,bool))&Lua_Mob::BuffCount)
.def("BuffFadeAll", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeAll)
.def("BuffFadeBeneficial", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeBeneficial)
.def("BuffFadeByEffect", (void(Lua_Mob::*)(int))&Lua_Mob::BuffFadeByEffect)
.def("BuffFadeByEffect", (void(Lua_Mob::*)(int,int))&Lua_Mob::BuffFadeByEffect)
.def("BuffFadeBySlot", (void(Lua_Mob::*)(int))&Lua_Mob::BuffFadeBySlot)
.def("BuffFadeBySlot", (void(Lua_Mob::*)(int,bool))&Lua_Mob::BuffFadeBySlot)
.def("BuffFadeBySpellID", (void(Lua_Mob::*)(int))&Lua_Mob::BuffFadeBySpellID)
.def("BuffFadeDetrimental", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeDetrimental)
.def("BuffFadeDetrimentalByCaster", (void(Lua_Mob::*)(Lua_Mob))&Lua_Mob::BuffFadeDetrimentalByCaster)
.def("BuffFadeNonPersistDeath", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeNonPersistDeath)
.def("BuffFadeSongs", (void(Lua_Mob::*)(void))&Lua_Mob::BuffFadeSongs)
.def("CalculateDistance", (float(Lua_Mob::*)(double,double,double))&Lua_Mob::CalculateDistance)
.def("CalculateDistance", (float(Lua_Mob::*)(Lua_Mob))&Lua_Mob::CalculateDistance)
.def("CalculateHeadingToTarget", (double(Lua_Mob::*)(double,double))&Lua_Mob::CalculateHeadingToTarget)
+6
View File
@@ -10,6 +10,7 @@ class Lua_Item;
class Lua_ItemInst;
class Lua_StatBonuses;
class Lua_Bot;
class Lua_Merc;
class Lua_NPC;
class Lua_Client;
struct Lua_Mob_List;
@@ -605,6 +606,11 @@ public:
void AreaSpell(Lua_Mob center, uint16 spell_id, bool affect_caster, int16 resist_adjust, int max_targets);
void MassGroupBuff(Lua_Mob center, uint16 spell_id);
void MassGroupBuff(Lua_Mob center, uint16 spell_id, bool affect_caster);
void BuffFadeBeneficial();
void BuffFadeDetrimental();
void BuffFadeDetrimentalByCaster(Lua_Mob caster);
void BuffFadeNonPersistDeath();
void BuffFadeSongs();
};
#endif
+195 -2
View File
@@ -32,6 +32,7 @@
#include "lua_inventory.h"
#include "lua_item.h"
#include "lua_iteminst.h"
#include "lua_merc.h"
#include "lua_mob.h"
#include "lua_npc.h"
#include "lua_object.h"
@@ -184,7 +185,8 @@ const char *LuaEvents[_LargestEventID] = {
"event_entity_variable_set",
"event_entity_variable_update",
"event_aa_loss",
"event_spell_blocked"
"event_spell_blocked",
"event_read_item"
};
extern Zone *zone;
@@ -348,6 +350,7 @@ LuaParser::LuaParser() {
PlayerArgumentDispatch[EVENT_ENTITY_VARIABLE_UPDATE] = handle_player_entity_variable;
PlayerArgumentDispatch[EVENT_AA_LOSS] = handle_player_aa_loss;
PlayerArgumentDispatch[EVENT_SPELL_BLOCKED] = handle_player_spell_blocked;
PlayerArgumentDispatch[EVENT_READ_ITEM] = handle_player_read_item;
ItemArgumentDispatch[EVENT_ITEM_CLICK] = handle_item_click;
ItemArgumentDispatch[EVENT_ITEM_CLICK_CAST] = handle_item_click;
@@ -1277,6 +1280,7 @@ void LuaParser::MapFunctions(lua_State *L) {
lua_register_npc(),
lua_register_client(),
lua_register_bot(),
lua_register_merc(),
lua_register_inventory(),
lua_register_inventory_where(),
lua_register_iteminst(),
@@ -1308,11 +1312,13 @@ void LuaParser::MapFunctions(lua_State *L) {
lua_register_rulei(),
lua_register_ruler(),
lua_register_ruleb(),
lua_register_rules(),
lua_register_journal_speakmode(),
lua_register_journal_mode(),
lua_register_expedition(),
lua_register_expedition_lock_messages(),
lua_register_buff()
lua_register_buff(),
lua_register_exp_source()
)];
} catch(std::exception &ex) {
@@ -1833,3 +1839,190 @@ void LuaParser::LoadBotScript(std::string filename) {
void LuaParser::LoadGlobalBotScript(std::string filename) {
LoadScript(filename, "global_bot");
}
int LuaParser::EventMerc(
QuestEventID evt,
Merc *merc,
Mob *init,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
) {
evt = ConvertLuaEvent(evt);
if (evt >= _LargestEventID) {
return 0;
}
if (!merc) {
return 0;
}
if (!MercHasQuestSub(evt)) {
return 0;
}
return _EventMerc("merc", evt, merc, init, data, extra_data, extra_pointers);
}
int LuaParser::EventGlobalMerc(
QuestEventID evt,
Merc *merc,
Mob *init,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
) {
evt = ConvertLuaEvent(evt);
if (evt >= _LargestEventID) {
return 0;
}
if (!merc) {
return 0;
}
if (!GlobalMercHasQuestSub(evt)) {
return 0;
}
return _EventMerc("global_merc", evt, merc, init, data, extra_data, extra_pointers);
}
int LuaParser::_EventMerc(
std::string package_name,
QuestEventID evt,
Merc *merc,
Mob *init,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers,
luabind::adl::object *l_func
) {
const char *sub_name = LuaEvents[evt];
int start = lua_gettop(L);
try {
int npop = 2;
PushErrorHandler(L);
if(l_func != nullptr) {
l_func->push(L);
} else {
lua_getfield(L, LUA_REGISTRYINDEX, package_name.c_str());
lua_getfield(L, -1, sub_name);
npop = 3;
}
lua_createtable(L, 0, 0);
//push self
Lua_Merc l_merc(merc);
luabind::adl::object l_merc_o = luabind::adl::object(L, l_merc);
l_merc_o.push(L);
lua_setfield(L, -2, "self");
auto arg_function = NPCArgumentDispatch[evt];
arg_function(this, L, merc, init, data, extra_data, extra_pointers);
auto* c = (init && init->IsClient()) ? init->CastToClient() : nullptr;
quest_manager.StartQuest(merc, c);
if(lua_pcall(L, 1, 1, start + 1)) {
std::string error = lua_tostring(L, -1);
AddError(error);
quest_manager.EndQuest();
lua_pop(L, npop);
return 0;
}
quest_manager.EndQuest();
if(lua_isnumber(L, -1)) {
int ret = static_cast<int>(lua_tointeger(L, -1));
lua_pop(L, npop);
return ret;
}
lua_pop(L, npop);
} catch(std::exception &ex) {
AddError(
fmt::format(
"Lua Exception | [{}] for Merc [{}] in [{}]: {}",
sub_name,
merc->GetID(),
package_name,
ex.what()
)
);
//Restore our stack to the best of our ability
int end = lua_gettop(L);
int n = end - start;
if(n > 0) {
lua_pop(L, n);
}
}
return 0;
}
int LuaParser::DispatchEventMerc(
QuestEventID evt,
Merc *merc,
Mob *init,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
) {
evt = ConvertLuaEvent(evt);
if (evt >= _LargestEventID) {
return 0;
}
std::string package_name = "merc";
auto iter = lua_encounter_events_registered.find(package_name);
if (iter == lua_encounter_events_registered.end()) {
return 0;
}
int ret = 0;
auto riter = iter->second.begin();
while (riter != iter->second.end()) {
if (riter->event_id == evt) {
package_name = fmt::format("encounter_{}", riter->encounter_name);
int i = _EventMerc(package_name, evt, merc, init, data, extra_data, extra_pointers, &riter->lua_reference);
if (i != 0) {
ret = i;
}
}
++riter;
}
return ret;
}
bool LuaParser::MercHasQuestSub(QuestEventID evt) {
evt = ConvertLuaEvent(evt);
if (evt >= _LargestEventID) {
return false;
}
const char *subname = LuaEvents[evt];
return HasFunction(subname, "merc");
}
bool LuaParser::GlobalMercHasQuestSub(QuestEventID evt) {
evt = ConvertLuaEvent(evt);
if (evt >= _LargestEventID) {
return false;
}
const char *subname = LuaEvents[evt];
return HasFunction(subname, "global_merc");
}
void LuaParser::LoadMercScript(std::string filename) {
LoadScript(filename, "merc");
}
void LuaParser::LoadGlobalMercScript(std::string filename) {
LoadScript(filename, "global_merc");
}
+38
View File
@@ -109,6 +109,22 @@ public:
uint32 extra_data,
std::vector<std::any> *extra_pointers
);
virtual int EventMerc(
QuestEventID evt,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
);
virtual int EventGlobalMerc(
QuestEventID evt,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
);
virtual bool HasQuestSub(uint32 npc_id, QuestEventID evt);
virtual bool HasGlobalQuestSub(QuestEventID evt);
@@ -120,6 +136,8 @@ public:
virtual bool HasEncounterSub(const std::string& package_name, QuestEventID evt);
virtual bool BotHasQuestSub(QuestEventID evt);
virtual bool GlobalBotHasQuestSub(QuestEventID evt);
virtual bool MercHasQuestSub(QuestEventID evt);
virtual bool GlobalMercHasQuestSub(QuestEventID evt);
virtual void LoadNPCScript(std::string filename, int npc_id);
virtual void LoadGlobalNPCScript(std::string filename);
@@ -130,6 +148,8 @@ public:
virtual void LoadEncounterScript(std::string filename, std::string encounter_name);
virtual void LoadBotScript(std::string filename);
virtual void LoadGlobalBotScript(std::string filename);
virtual void LoadMercScript(std::string filename);
virtual void LoadGlobalMercScript(std::string filename);
virtual void AddVar(std::string name, std::string val);
virtual std::string GetVar(std::string name);
@@ -179,6 +199,14 @@ public:
uint32 extra_data,
std::vector<std::any> *extra_pointers
);
virtual int DispatchEventMerc(
QuestEventID evt,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
);
static LuaParser* Instance() {
static LuaParser inst;
@@ -269,6 +297,16 @@ private:
std::vector<std::any> *extra_pointers,
luabind::adl::object *l_func = nullptr
);
int _EventMerc(
std::string package_name,
QuestEventID evt,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers,
luabind::adl::object* l_func = nullptr
);
void LoadScript(std::string filename, std::string package_name);
void MapFunctions(lua_State *L);
+55
View File
@@ -734,6 +734,18 @@ void handle_player_death(
lua_pushinteger(L, Strings::ToUnsignedInt(sep.arg[4]));
lua_setfield(L, -2, "killed_entity_id");
lua_pushinteger(L, Strings::ToUnsignedInt(sep.arg[5]));
lua_setfield(L, -2, "combat_start_time");
lua_pushinteger(L, Strings::ToUnsignedInt(sep.arg[6]));
lua_setfield(L, -2, "combat_end_time");
lua_pushinteger(L, Strings::ToBigInt(sep.arg[7]));
lua_setfield(L, -2, "damage_received");
lua_pushinteger(L, Strings::ToBigInt(sep.arg[8]));
lua_setfield(L, -2, "healing_received");
}
void handle_player_timer(
@@ -1745,6 +1757,49 @@ void handle_player_spell_blocked(
lua_setfield(L, -2, "cast_spell");
}
void handle_player_read_item(
QuestInterface *parse,
lua_State* L,
Client* client,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
)
{
lua_pushstring(L, data.c_str());
lua_setfield(L, -2, "text_file");
lua_pushinteger(L, extra_data);
lua_setfield(L, -2, "item_id");
if (extra_pointers) {
if (extra_pointers->size() == 7) {
lua_pushstring(L, std::any_cast<std::string>(extra_pointers->at(0)).c_str());
lua_setfield(L, -2, "book_text");
lua_pushboolean(L, std::any_cast<int8>(extra_pointers->at(1)));
lua_setfield(L, -2, "can_cast");
lua_pushboolean(L, std::any_cast<int8>(extra_pointers->at(2)));
lua_setfield(L, -2, "can_scribe");
lua_pushinteger(L, std::any_cast<int16>(extra_pointers->at(3)));
lua_setfield(L, -2, "slot_id");
lua_pushinteger(L, std::any_cast<int>(extra_pointers->at(4)));
lua_setfield(L, -2, "target_id");
lua_pushinteger(L, std::any_cast<uint8>(extra_pointers->at(5)));
lua_setfield(L, -2, "type");
Lua_ItemInst l_item(std::any_cast<EQ::ItemInstance*>(extra_pointers->at(6)));
luabind::adl::object l_item_o = luabind::adl::object(L, l_item);
l_item_o.push(L);
lua_setfield(L, -2, "item");
}
}
}
// Item
void handle_item_click(
QuestInterface *parse,
+10
View File
@@ -8,6 +8,7 @@ typedef void(*ItemArgumentHandler)(QuestInterface*, lua_State*, Client*, EQ::Ite
typedef void(*SpellArgumentHandler)(QuestInterface*, lua_State*, Mob*, Client*, uint32, std::string, uint32, std::vector<std::any>*);
typedef void(*EncounterArgumentHandler)(QuestInterface*, lua_State*, Encounter* encounter, std::string, uint32, std::vector<std::any>*);
typedef void(*BotArgumentHandler)(QuestInterface*, lua_State*, Bot*, Mob*, std::string, uint32, std::vector<std::any>*);
typedef void(*MercArgumentHandler)(QuestInterface*, lua_State*, Merc*, Mob*, std::string, uint32, std::vector<std::any>*);
// NPC
void handle_npc_event_say(
@@ -855,6 +856,15 @@ void handle_player_spell_blocked(
std::vector<std::any> *extra_pointers
);
void handle_player_read_item(
QuestInterface *parse,
lua_State* L,
Client* client,
std::string data,
uint32 extra_data,
std::vector<std::any> *extra_pointers
);
// Item
void handle_item_click(
QuestInterface *parse,
+14 -6
View File
@@ -31,12 +31,14 @@ Map::~Map() {
}
float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result) const {
if (!imp)
if (!imp) {
return BEST_Z_INVALID;
}
glm::vec3 tmp;
if(!result)
if (!result) {
result = &tmp;
}
start.z += RuleI(Map, FindBestZHeightAdjust);
glm::vec3 from(start.x, start.y, start.z);
@@ -45,16 +47,22 @@ float Map::FindBestZ(glm::vec3 &start, glm::vec3 *result) const {
bool hit = false;
hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance);
if(hit) {
if (hit && zone->newzone_data.underworld != 0.0f && result->z < zone->newzone_data.underworld) {
hit = false;
}
if (hit) {
return result->z;
}
// Find nearest Z above us
to.z = -BEST_Z_INVALID;
hit = imp->rm->raycast((const RmReal*)&from, (const RmReal*)&to, (RmReal*)result, nullptr, &hit_distance);
if (hit)
{
if (zone->newzone_data.max_z != 0.0f && result->z > zone->newzone_data.max_z) {
hit = false;
}
if (hit) {
return result->z;
}
+17 -10
View File
@@ -5,6 +5,7 @@
#include "entity.h"
#include "groups.h"
#include "mob.h"
#include "quest_parser_collection.h"
#include "zone.h"
#include "string_ids.h"
@@ -4078,12 +4079,6 @@ bool Merc::Death(Mob* killer_mob, int64 damage, uint16 spell, EQ::skills::SkillT
Save();
//no corpse, no exp if we're a merc.
//We'll suspend instead, since that's what live does.
//Not actually sure live supports 'depopping' merc corpses.
//if(entity_list.GetCorpseByID(GetID()))
// entity_list.GetCorpseByID(GetID())->Depop();
// If client is in zone, suspend merc, else depop it.
if (!Suspend()) {
Depop();
@@ -4671,7 +4666,6 @@ bool Merc::Spawn(Client *owner) {
//UpdateMercAppearance();
return true;
}
@@ -5189,9 +5183,6 @@ void Client::SpawnMerc(Merc* merc, bool setMaxStats) {
merc->SetStance(GetMercInfo().Stance);
Log(Logs::General, Logs::Mercenaries, "SpawnMerc Success for %s.", GetName());
return;
}
bool Merc::Suspend() {
@@ -5914,3 +5905,19 @@ uint32 Merc::CalcUpkeepCost(uint32 templateID , uint8 level, uint8 currency_type
return cost;
}
void Merc::Signal(int signal_id)
{
if (parse->MercHasQuestSub(EVENT_SIGNAL)) {
parse->EventMerc(EVENT_SIGNAL, this, nullptr, std::to_string(signal_id), 0);
}
}
void Merc::SendPayload(int payload_id, std::string payload_value)
{
if (parse->MercHasQuestSub(EVENT_PAYLOAD)) {
const auto& export_string = fmt::format("{} {}", payload_id, payload_value);
parse->EventMerc(EVENT_PAYLOAD, this, nullptr, export_string, 0);
}
}
+3
View File
@@ -146,6 +146,9 @@ public:
bool IsMedding() { return _medding; };
bool IsSuspended() { return _suspended; };
void Signal(int signal_id);
void SendPayload(int payload_id, std::string payload_value);
static uint32 CalcPurchaseCost( uint32 templateID , uint8 level, uint8 currency_type = 0);
static uint32 CalcUpkeepCost( uint32 templateID , uint8 level, uint8 currency_type = 0);
+95 -93
View File
@@ -128,8 +128,9 @@ Mob::Mob(
attack_anim_timer(500),
position_update_melee_push_timer(500),
hate_list_cleanup_timer(6000),
mob_close_scan_timer(6000),
mob_check_moving_timer(1000)
m_scan_close_mobs_timer(6000),
m_mob_check_moving_timer(1000),
bot_attack_flag_timer(10000)
{
mMovementManager = &MobMovementManager::Get();
mMovementManager->AddMob(this);
@@ -400,6 +401,10 @@ Mob::Mob(
pet_owner_npc = false;
pet_targetlock_id = 0;
//bot attack flag
bot_attack_flags.clear();
bot_attack_flag_timer.Disable();
attacked_count = 0;
mezzed = false;
stunned = false;
@@ -512,7 +517,7 @@ Mob::Mob(
m_manual_follow = false;
mob_close_scan_timer.Trigger();
m_scan_close_mobs_timer.Trigger();
SetCanOpenDoors(true);
@@ -565,7 +570,7 @@ Mob::~Mob()
entity_list.RemoveMobFromCloseLists(this);
entity_list.RemoveAuraFromMobs(this);
close_mobs.clear();
m_close_mobs.clear();
LeaveHealRotationTargetPool();
}
@@ -4063,7 +4068,8 @@ uint8 Mob::GetDefaultGender(uint16 in_race, uint8 in_gender) {
in_race == Race::Human2 ||
in_race == Race::ElvenGhost ||
in_race == Race::HumanGhost ||
in_race == Race::Coldain2
in_race == Race::Coldain2 ||
in_race == Race::Akheva
) {
if (in_gender >= Gender::Neuter) { // Male default for PC Races
return Gender::Male;
@@ -5012,7 +5018,7 @@ void Mob::Say(const char *format, ...)
int16 distance = 200;
if (RuleB(Chat, QuestDialogueUsesDialogueWindow)) {
for (auto &e : entity_list.GetCloseMobList(talker, (distance * distance))) {
for (auto &e : talker->GetCloseMobList(distance)) {
Mob *mob = e.second;
if (!mob) {
continue;
@@ -5508,37 +5514,13 @@ void Mob::SetTarget(Mob *mob)
target = mob;
entity_list.UpdateHoTT(this);
const auto has_target_change_event = (
parse->HasQuestSub(GetNPCTypeID(), EVENT_TARGET_CHANGE) ||
parse->PlayerHasQuestSub(EVENT_TARGET_CHANGE) ||
parse->BotHasQuestSub(EVENT_TARGET_CHANGE)
);
if (IsClient() && CastToClient()->admin > AccountStatus::GMMgmt) {
DisplayInfo(mob);
}
if (has_target_change_event) {
std::vector<std::any> args;
std::vector<std::any> args = { mob };
args.emplace_back(mob);
if (IsNPC()) {
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_TARGET_CHANGE)) {
parse->EventNPC(EVENT_TARGET_CHANGE, CastToNPC(), mob, "", 0, &args);
}
} else if (IsClient()) {
if (parse->PlayerHasQuestSub(EVENT_TARGET_CHANGE)) {
parse->EventPlayer(EVENT_TARGET_CHANGE, CastToClient(), "", 0, &args);
}
CastToClient()->SetBotPrecombat(false); // Any change in target will nullify this flag (target == mob checked above)
} else if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_TARGET_CHANGE)) {
parse->EventBot(EVENT_TARGET_CHANGE, CastToBot(), mob, "", 0, &args);
}
}
}
parse->EventMob(EVENT_TARGET_CHANGE, this, mob, [&]() { return ""; }, 0, &args);
if (IsPet() && GetOwner() && GetOwner()->IsClient()) {
GetOwner()->CastToClient()->UpdateXTargetType(MyPetTarget, mob);
@@ -5711,22 +5693,10 @@ bool Mob::ClearEntityVariables()
return false;
}
if (
(IsBot() && parse->BotHasQuestSub(EVENT_ENTITY_VARIABLE_DELETE)) ||
(IsClient() && parse->PlayerHasQuestSub(EVENT_ENTITY_VARIABLE_DELETE)) ||
(IsNPC() && parse->HasQuestSub(GetNPCTypeID(), EVENT_ENTITY_VARIABLE_DELETE))
) {
for (const auto& e : m_EntityVariables) {
std::vector<std::any> args = { e.first, e.second };
for (const auto& e : m_EntityVariables) {
std::vector<std::any> args = { e.first, e.second };
if (IsBot()) {
parse->EventBot(EVENT_ENTITY_VARIABLE_DELETE, CastToBot(), nullptr, "", 0, &args);
} else if (IsClient()) {
parse->EventPlayer(EVENT_ENTITY_VARIABLE_DELETE, CastToClient(), "", 0, &args);
} else if (IsNPC()) {
parse->EventNPC(EVENT_ENTITY_VARIABLE_DELETE, CastToNPC(), nullptr, "", 0, &args);
}
}
parse->EventMob(EVENT_ENTITY_VARIABLE_DELETE, this, nullptr, [&]() { return ""; }, 0, &args);
}
m_EntityVariables.clear();
@@ -5744,24 +5714,11 @@ bool Mob::DeleteEntityVariable(std::string variable_name)
return false;
}
std::vector<std::any> args = { v->first, v->second };
parse->EventMob(EVENT_ENTITY_VARIABLE_DELETE, this, nullptr, [&]() { return ""; }, 0, &args);
m_EntityVariables.erase(v);
if (
(IsBot() && parse->BotHasQuestSub(EVENT_ENTITY_VARIABLE_DELETE)) ||
(IsClient() && parse->PlayerHasQuestSub(EVENT_ENTITY_VARIABLE_DELETE)) ||
(IsNPC() && parse->HasQuestSub(GetNPCTypeID(), EVENT_ENTITY_VARIABLE_DELETE))
) {
std::vector<std::any> args = { v->first, v->second };
if (IsBot()) {
parse->EventBot(EVENT_ENTITY_VARIABLE_DELETE, CastToBot(), nullptr, "", 0, &args);
} else if (IsClient()) {
parse->EventPlayer(EVENT_ENTITY_VARIABLE_DELETE, CastToClient(), "", 0, &args);
} else if (IsNPC()) {
parse->EventNPC(EVENT_ENTITY_VARIABLE_DELETE, CastToNPC(), nullptr, "", 0, &args);
}
}
return true;
}
@@ -5808,32 +5765,16 @@ void Mob::SetEntityVariable(std::string variable_name, std::string variable_valu
return;
}
const QuestEventID event_id = (
!EntityVariableExists(variable_name) ?
EVENT_ENTITY_VARIABLE_SET :
EVENT_ENTITY_VARIABLE_UPDATE
);
std::vector<std::any> args;
if (
(IsBot() && parse->BotHasQuestSub(event_id)) ||
(IsClient() && parse->PlayerHasQuestSub(event_id)) ||
(IsNPC() && parse->HasQuestSub(GetNPCTypeID(), event_id))
) {
std::vector<std::any> args;
if (!EntityVariableExists(variable_name)) {
args = { variable_name, variable_value };
if (event_id != EVENT_ENTITY_VARIABLE_UPDATE) {
args = { variable_name, variable_value };
} else {
args = { variable_name, GetEntityVariable(variable_name), variable_value };
}
parse->EventMob(EVENT_ENTITY_VARIABLE_SET, this, nullptr, [&]() { return ""; }, 0, &args);
} else {
args = { variable_name, GetEntityVariable(variable_name), variable_value };
if (IsBot()) {
parse->EventBot(event_id, CastToBot(), nullptr, "", 0, &args);
} else if (IsClient()) {
parse->EventPlayer(event_id, CastToClient(), "", 0, &args);
} else if (IsNPC()) {
parse->EventNPC(event_id, CastToNPC(), nullptr, "", 0, &args);
}
parse->EventMob(EVENT_ENTITY_VARIABLE_UPDATE, this, nullptr, [&]() { return ""; }, 0, &args);
}
m_EntityVariables[variable_name] = variable_value;
@@ -6171,9 +6112,7 @@ int32 Mob::GetPositionalDmgTakenAmt(Mob *attacker)
void Mob::SetBottomRampageList()
{
auto &mob_list = entity_list.GetCloseMobList(this);
for (auto &e : mob_list) {
for (auto &e : GetCloseMobList()) {
auto mob = e.second;
if (!mob) {
continue;
@@ -6198,9 +6137,7 @@ void Mob::SetBottomRampageList()
void Mob::SetTopRampageList()
{
auto &mob_list = entity_list.GetCloseMobList(this);
for (auto &e : mob_list) {
for (auto &e : GetCloseMobList()) {
auto mob = e.second;
if (!mob) {
continue;
@@ -8614,7 +8551,7 @@ void Mob::SetExtraHaste(int haste, bool need_to_save)
bool Mob::IsCloseToBanker()
{
for (auto &e: entity_list.GetCloseMobList(this)) {
for (auto &e: GetCloseMobList()) {
auto mob = e.second;
if (mob && mob->IsNPC() && mob->GetClass() == Class::Banker) {
return true;
@@ -8623,3 +8560,68 @@ bool Mob::IsCloseToBanker()
return false;
}
bool Mob::HasBotAttackFlag(Mob* tar) {
if (!tar) {
return false;
}
std::vector<uint32> l = tar->GetBotAttackFlags();
for (uint32 e : l) {
if (IsBot() && e == CastToBot()->GetBotOwnerCharacterID()) {
return true;
}
if (IsClient() && e == CastToClient()->CharacterID()) {
return true;
}
}
return false;
}
const uint16 scan_close_mobs_timer_moving = 6000; // 6 seconds
const uint16 scan_close_mobs_timer_idle = 60000; // 60 seconds
void Mob::CheckScanCloseMobsMovingTimer()
{
LogAIScanCloseDetail(
"Mob [{}] {}moving, scan timer [{}]",
GetCleanName(),
IsMoving() ? "" : "NOT ",
m_scan_close_mobs_timer.GetRemainingTime()
);
// If the moving timer triggers, lets see if we are moving or idle to restart the appropriate
// dynamic timer
if (m_mob_check_moving_timer.Check()) {
// If the mob is still moving, restart the moving timer
if (moving) {
if (m_scan_close_mobs_timer.GetRemainingTime() > scan_close_mobs_timer_moving) {
LogAIScanCloseDetail("Mob [{}] Restarting with moving timer", GetCleanName());
m_scan_close_mobs_timer.Disable();
m_scan_close_mobs_timer.Start(scan_close_mobs_timer_moving);
m_scan_close_mobs_timer.Trigger();
}
}
// If the mob is not moving, restart the idle timer
else if (m_scan_close_mobs_timer.GetDuration() == scan_close_mobs_timer_moving) {
LogAIScanCloseDetail("Mob [{}] Restarting with idle timer", GetCleanName());
m_scan_close_mobs_timer.Disable();
m_scan_close_mobs_timer.Start(scan_close_mobs_timer_idle);
}
}
}
void Mob::ScanCloseMobProcess()
{
if (m_scan_close_mobs_timer.Check()) {
entity_list.ScanCloseMobs(this);
}
}
std::unordered_map<uint16, Mob *> &Mob::GetCloseMobList(float distance)
{
return entity_list.GetCloseMobList(this, distance);
}
+19 -3
View File
@@ -201,9 +201,12 @@ public:
void DisplayInfo(Mob *mob);
std::unordered_map<uint16, Mob *> close_mobs;
Timer mob_close_scan_timer;
Timer mob_check_moving_timer;
std::unordered_map<uint16, Mob *> m_close_mobs;
Timer m_scan_close_mobs_timer;
Timer m_mob_check_moving_timer;
// Bot attack flag
Timer bot_attack_flag_timer;
//Somewhat sorted: needs documenting!
@@ -444,6 +447,7 @@ public:
void BuffFadeBySlot(int slot, bool iRecalcBonuses = true);
void BuffFadeDetrimentalByCaster(Mob *caster);
void BuffFadeBySitModifier();
void BuffFadeSongs();
void BuffDetachCaster(Mob *caster);
bool IsAffectedByBuffByGlobalGroup(GlobalGroup group);
void BuffModifyDurationBySpellID(uint16 spell_id, int32 newDuration);
@@ -1102,6 +1106,11 @@ public:
bool invulnerable;
bool qglobal;
inline std::vector<uint32> GetBotAttackFlags() { return bot_attack_flags; }
inline void SetBotAttackFlag(uint32 value) { bot_attack_flags.push_back(value); }
inline void ClearBotAttackFlags() { bot_attack_flags.clear(); }
bool HasBotAttackFlag(Mob* tar);
virtual void SetAttackTimer();
inline void SetInvul(bool invul) { invulnerable=invul; }
inline bool GetInvul(void) { return invulnerable; }
@@ -1479,6 +1488,10 @@ public:
bool IsCloseToBanker();
void ScanCloseMobProcess();
std::unordered_map<uint16, Mob *> &GetCloseMobList(float distance = 0.0f);
void CheckScanCloseMobsMovingTimer();
protected:
void CommonDamage(Mob* other, int64 &damage, const uint16 spell_id, const EQ::skills::SkillType attack_skill, bool &avoidable, const int8 buffslot, const bool iBuffTic, eSpecialAttacks specal = eSpecialAttacks::None);
static uint16 GetProcID(uint16 spell_id, uint8 effect_index);
@@ -1863,6 +1876,9 @@ protected:
bool pet_owner_npc; // Flags pets as belonging to an NPC
uint32 pet_targetlock_id;
//bot attack flags
std::vector<uint32> bot_attack_flags;
glm::vec3 m_TargetRing;
GravityBehavior flymode;
+12 -45
View File
@@ -1391,29 +1391,8 @@ void Mob::AI_Process() {
StopNavigation();
}
}
else if (zone->CanDoCombat() && CastToNPC()->GetNPCAggro() && AI_scan_area_timer->Check()) {
/**
* NPC to NPC aggro (npc_aggro flag set)
*/
for (auto &close_mob : close_mobs) {
Mob *mob = close_mob.second;
if (mob->IsClient()) {
continue;
}
if (CheckWillAggro(mob)) {
AddToHateList(mob);
}
}
AI_scan_area_timer->Disable();
AI_scan_area_timer->Start(
RandomTimer(RuleI(NPC, NPCToNPCAggroTimerMin), RuleI(NPC, NPCToNPCAggroTimerMax)),
false
);
else if (zone->CanDoCombat() && IsNPC() && CastToNPC()->GetNPCAggro() && AI_scan_area_timer->Check()) {
CastToNPC()->DoNpcToNpcAggroScan();
}
else if (AI_movement_timer->Check() && !IsRooted()) {
if (IsPet()) {
@@ -1776,6 +1755,8 @@ void Mob::AI_Event_Engaged(Mob *attacker, bool yell_for_help)
SetAppearance(eaStanding);
parse->EventBotMerc(EVENT_COMBAT, this, attacker, [&] { return "1"; });
if (IsNPC()) {
CastToNPC()->AIautocastspell_timer->Start(300, false);
@@ -1814,12 +1795,6 @@ void Mob::AI_Event_Engaged(Mob *attacker, bool yell_for_help)
}
}
}
if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_COMBAT)) {
parse->EventBot(EVENT_COMBAT, CastToBot(), attacker, "1", 0);
}
}
}
// Note: Hate list may not be actually clear until after this function call completes
@@ -1840,27 +1815,19 @@ void Mob::AI_Event_NoLongerEngaged() {
StopNavigation();
ClearRampage();
parse->EventBotMercNPC(EVENT_COMBAT, this, nullptr, [&]() { return "0"; });
if (IsNPC()) {
SetPrimaryAggro(false);
SetAssistAggro(false);
if (CastToNPC()->GetCombatEvent() && GetHP() > 0) {
if (entity_list.GetNPCByID(GetID())) {
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_COMBAT)) {
parse->EventNPC(EVENT_COMBAT, CastToNPC(), nullptr, "0", 0);
}
const uint32 emote_id = CastToNPC()->GetEmoteID();
if (emote_id) {
CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::LeaveCombat, emote_id);
}
m_combat_record.Stop();
CastToNPC()->SetCombatEvent(false);
const uint32 emote_id = CastToNPC()->GetEmoteID();
if (emote_id) {
CastToNPC()->DoNPCEmote(EQ::constants::EmoteEventTypes::LeaveCombat, emote_id);
}
}
} else if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_COMBAT)) {
parse->EventBot(EVENT_COMBAT, CastToBot(), nullptr, "0", 0);
m_combat_record.Stop();
CastToNPC()->SetCombatEvent(false);
}
}
}
-136
View File
@@ -127,11 +127,6 @@ public:
}
/**
* @param mob_movement_manager
* @param mob
* @return
*/
virtual bool Process(MobMovementManager *mob_movement_manager, Mob *mob)
{
if (!mob->IsAIControlled()) {
@@ -286,11 +281,6 @@ public:
}
/**
* @param mob_movement_manager
* @param mob
* @return
*/
virtual bool Process(MobMovementManager *mob_movement_manager, Mob *mob)
{
if (!mob->IsAIControlled()) {
@@ -707,33 +697,21 @@ void MobMovementManager::Process()
}
}
/**
* @param mob
*/
void MobMovementManager::AddMob(Mob *mob)
{
_impl->Entries.insert(std::make_pair(mob, MobMovementEntry()));
}
/**
* @param mob
*/
void MobMovementManager::RemoveMob(Mob *mob)
{
_impl->Entries.erase(mob);
}
/**
* @param client
*/
void MobMovementManager::AddClient(Client *client)
{
_impl->Clients.push_back(client);
}
/**
* @param client
*/
void MobMovementManager::RemoveClient(Client *client)
{
auto iter = _impl->Clients.begin();
@@ -747,11 +725,6 @@ void MobMovementManager::RemoveClient(Client *client)
}
}
/**
* @param who
* @param to
* @param mob_movement_mode
*/
void MobMovementManager::RotateTo(Mob *who, float to, MobMovementMode mob_movement_mode)
{
auto iter = _impl->Entries.find(who);
@@ -764,13 +737,6 @@ void MobMovementManager::RotateTo(Mob *who, float to, MobMovementMode mob_moveme
PushRotateTo(ent.second, who, to, mob_movement_mode);
}
/**
* @param who
* @param x
* @param y
* @param z
* @param heading
*/
void MobMovementManager::Teleport(Mob *who, float x, float y, float z, float heading)
{
auto iter = _impl->Entries.find(who);
@@ -781,13 +747,6 @@ void MobMovementManager::Teleport(Mob *who, float x, float y, float z, float hea
PushTeleportTo(ent.second, x, y, z, heading);
}
/**
* @param who
* @param x
* @param y
* @param z
* @param mode
*/
void MobMovementManager::NavigateTo(Mob *who, float x, float y, float z, MobMovementMode mode)
{
if (IsPositionEqualWithinCertainZ(glm::vec3(x, y, z), glm::vec3(who->GetX(), who->GetY(), who->GetZ()), 6.0f)) {
@@ -824,9 +783,6 @@ void MobMovementManager::NavigateTo(Mob *who, float x, float y, float z, MobMove
}
}
/**
* @param who
*/
void MobMovementManager::StopNavigation(Mob *who)
{
auto iter = _impl->Entries.find(who);
@@ -852,16 +808,6 @@ void MobMovementManager::StopNavigation(Mob *who)
PushStopMoving(ent.second);
}
/**
* @param mob
* @param delta_x
* @param delta_y
* @param delta_z
* @param delta_heading
* @param anim
* @param range
* @param single_client
*/
void MobMovementManager::SendCommandToClients(
Mob *mob,
float delta_x,
@@ -961,10 +907,6 @@ void MobMovementManager::SendCommandToClients(
}
}
/**
* @param in
* @return
*/
float MobMovementManager::FixHeading(float in)
{
auto h = in;
@@ -979,9 +921,6 @@ float MobMovementManager::FixHeading(float in)
return h;
}
/**
* @param client
*/
void MobMovementManager::DumpStats(Client *client)
{
auto current_time = static_cast<double>(Timer::GetCurrentTime()) / 1000.0;
@@ -1062,13 +1001,6 @@ void MobMovementManager::FillCommandStruct(
}
}
/**
* @param who
* @param x
* @param y
* @param z
* @param mob_movement_mode
*/
void MobMovementManager::UpdatePath(Mob *who, float x, float y, float z, MobMovementMode mob_movement_mode)
{
Mob *target=who->GetTarget();
@@ -1114,13 +1046,6 @@ void MobMovementManager::UpdatePath(Mob *who, float x, float y, float z, MobMove
}
}
/**
* @param who
* @param x
* @param y
* @param z
* @param mode
*/
void MobMovementManager::UpdatePathGround(Mob *who, float x, float y, float z, MobMovementMode mode)
{
PathfinderOptions opts;
@@ -1253,13 +1178,6 @@ void MobMovementManager::UpdatePathGround(Mob *who, float x, float y, float z, M
}
}
/**
* @param who
* @param x
* @param y
* @param z
* @param movement_mode
*/
void MobMovementManager::UpdatePathUnderwater(Mob *who, float x, float y, float z, MobMovementMode movement_mode)
{
auto eiter = _impl->Entries.find(who);
@@ -1368,13 +1286,6 @@ void MobMovementManager::UpdatePathUnderwater(Mob *who, float x, float y, float
}
}
/**
* @param who
* @param x
* @param y
* @param z
* @param mode
*/
void MobMovementManager::UpdatePathBoat(Mob *who, float x, float y, float z, MobMovementMode mode)
{
auto eiter = _impl->Entries.find(who);
@@ -1386,48 +1297,21 @@ void MobMovementManager::UpdatePathBoat(Mob *who, float x, float y, float z, Mob
PushStopMoving(ent.second);
}
/**
* @param ent
* @param x
* @param y
* @param z
* @param heading
*/
void MobMovementManager::PushTeleportTo(MobMovementEntry &ent, float x, float y, float z, float heading)
{
ent.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new TeleportToCommand(x, y, z, heading)));
}
/**
* @param ent
* @param x
* @param y
* @param z
* @param mob_movement_mode
*/
void MobMovementManager::PushMoveTo(MobMovementEntry &ent, float x, float y, float z, MobMovementMode mob_movement_mode)
{
ent.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new MoveToCommand(x, y, z, mob_movement_mode)));
}
/**
* @param ent
* @param x
* @param y
* @param z
* @param mob_movement_mode
*/
void MobMovementManager::PushSwimTo(MobMovementEntry &ent, float x, float y, float z, MobMovementMode mob_movement_mode)
{
ent.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new SwimToCommand(x, y, z, mob_movement_mode)));
}
/**
* @param ent
* @param who
* @param to
* @param mob_movement_mode
*/
void MobMovementManager::PushRotateTo(MobMovementEntry &ent, Mob *who, float to, MobMovementMode mob_movement_mode)
{
auto from = FixHeading(who->GetHeading());
@@ -1450,41 +1334,21 @@ void MobMovementManager::PushRotateTo(MobMovementEntry &ent, Mob *who, float to,
ent.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new RotateToCommand(to, diff > 0 ? 1.0 : -1.0, mob_movement_mode)));
}
/**
* @param ent
* @param x
* @param y
* @param z
* @param mob_movement_mode
*/
void MobMovementManager::PushFlyTo(MobMovementEntry &ent, float x, float y, float z, MobMovementMode mob_movement_mode)
{
ent.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new FlyToCommand(x, y, z, mob_movement_mode)));
}
/**
* @param mob_movement_entry
*/
void MobMovementManager::PushStopMoving(MobMovementEntry &mob_movement_entry)
{
mob_movement_entry.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new StopMovingCommand()));
}
/**
* @param mob_movement_entry
*/
void MobMovementManager::PushEvadeCombat(MobMovementEntry &mob_movement_entry)
{
mob_movement_entry.Commands.emplace_back(std::unique_ptr<IMovementCommand>(new EvadeCombatCommand()));
}
/**
* @param who
* @param x
* @param y
* @param z
* @param mob_movement_mode
*/
void MobMovementManager::HandleStuckBehavior(Mob *who, float x, float y, float z, MobMovementMode mob_movement_mode)
{
LogDebug("Handle stuck behavior for {0} at ({1}, {2}, {3}) with movement_mode {4}", who->GetName(), x, y, z, static_cast<int>(mob_movement_mode));
+283 -65
View File
@@ -601,28 +601,8 @@ bool NPC::Process()
DepopSwarmPets();
}
if (mob_close_scan_timer.Check()) {
entity_list.ScanCloseMobs(close_mobs, this, IsMoving());
}
const uint16 npc_mob_close_scan_timer_moving = 6000;
const uint16 npc_mob_close_scan_timer_idle = 60000;
if (mob_check_moving_timer.Check()) {
if (moving) {
if (mob_close_scan_timer.GetRemainingTime() > npc_mob_close_scan_timer_moving) {
LogAIScanCloseDetail("NPC [{}] Restarting with moving timer", GetCleanName());
mob_close_scan_timer.Disable();
mob_close_scan_timer.Start(npc_mob_close_scan_timer_moving);
mob_close_scan_timer.Trigger();
}
}
else if (mob_close_scan_timer.GetDuration() == npc_mob_close_scan_timer_moving) {
LogAIScanCloseDetail("NPC [{}] Restarting with idle timer", GetCleanName());
mob_close_scan_timer.Disable();
mob_close_scan_timer.Start(npc_mob_close_scan_timer_idle);
}
}
ScanCloseMobProcess();
CheckScanCloseMobsMovingTimer();
if (hp_regen_per_second > 0 && hp_regen_per_second_timer.Check()) {
if (GetHP() < GetMaxHP()) {
@@ -799,6 +779,11 @@ bool NPC::Process()
}
}
if (bot_attack_flag_timer.Check()) {
bot_attack_flag_timer.Disable();
ClearBotAttackFlags();
}
AI_Process();
return true;
@@ -848,22 +833,10 @@ void NPC::Depop(bool start_spawn_timer) {
DoNPCEmote(EQ::constants::EmoteEventTypes::OnDespawn, emoteid);
}
if (IsNPC()) {
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_DESPAWN)) {
parse->EventNPC(EVENT_DESPAWN, this, nullptr, "", 0);
}
parse->EventBotMercNPC(EVENT_DESPAWN, this, nullptr);
if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_DESPAWN_ZONE)) {
DispatchZoneControllerEvent(EVENT_DESPAWN_ZONE, this, "", 0, nullptr);
}
} else if (IsBot()) {
if (parse->BotHasQuestSub(EVENT_DESPAWN)) {
parse->EventBot(EVENT_DESPAWN, CastToBot(), nullptr, "", 0);
}
if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_DESPAWN_ZONE)) {
DispatchZoneControllerEvent(EVENT_DESPAWN_ZONE, this, "", 0, nullptr);
}
if (parse->HasQuestSub(ZONE_CONTROLLER_NPC_ID, EVENT_DESPAWN_ZONE)) {
DispatchZoneControllerEvent(EVENT_DESPAWN_ZONE, this, "", 0, nullptr);
}
p_depop = true;
@@ -2932,31 +2905,252 @@ void NPC::DoNPCEmote(uint8 event_, uint32 emote_id, Mob* t)
bool NPC::CanTalk()
{
//Races that should be able to talk. (Races up to Titanium)
uint16 TalkRace[473] =
{1,2,3,4,5,6,7,8,9,10,11,12,0,0,15,16,0,18,19,20,0,0,23,0,25,0,0,0,0,0,0,
32,0,0,0,0,0,0,39,40,0,0,0,44,0,0,0,0,49,0,51,0,53,54,55,56,57,58,0,0,0,
62,0,64,65,66,67,0,0,70,71,0,0,0,0,0,77,78,79,0,81,82,0,0,0,86,0,0,0,90,
0,92,93,94,95,0,0,98,99,0,101,0,103,0,0,0,0,0,0,110,111,112,0,0,0,0,0,0,
0,0,0,0,123,0,0,126,0,128,0,130,131,0,0,0,0,136,137,0,139,140,0,0,0,144,
0,0,0,0,0,150,151,152,153,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,183,184,0,0,187,188,189,0,0,0,0,0,195,196,0,198,0,0,0,202,0,
0,205,0,0,208,0,0,0,0,0,0,0,0,217,0,219,0,0,0,0,0,0,226,0,0,229,230,0,0,
0,0,235,236,0,238,239,240,241,242,243,244,0,246,247,0,0,0,251,0,0,254,255,
256,257,0,0,0,0,0,0,0,0,266,267,0,0,270,271,0,0,0,0,0,277,278,0,0,0,0,283,
284,0,286,0,288,289,290,0,0,0,0,295,296,297,298,299,300,0,0,0,304,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,320,0,322,323,324,325,0,0,0,0,330,331,332,333,334,335,
336,337,338,339,340,341,342,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,359,360,361,362,
0,364,365,366,0,368,369,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,385,386,0,0,0,0,0,392,
393,394,395,396,397,398,0,400,402,0,0,0,0,406,0,408,0,0,411,0,413,0,0,0,417,
0,0,420,0,0,0,0,425,0,0,0,0,0,0,0,433,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,458,0,0,0,0,0,0,0,0,467,0,0,470,0,0,473};
if (TalkRace[GetRace() - 1] > 0)
return true;
return false;
switch (GetRace()) {
case Race::Human:
case Race::Barbarian:
case Race::Erudite:
case Race::WoodElf:
case Race::HighElf:
case Race::DarkElf:
case Race::HalfElf:
case Race::Dwarf:
case Race::Troll:
case Race::Ogre:
case Race::Halfling:
case Race::Gnome:
case Race::Werewolf:
case Race::Brownie:
case Race::Centaur:
case Race::Giant:
case Race::Trakanon:
case Race::VenrilSathir:
case Race::Kerran:
case Race::Fairy:
case Race::Ghost:
case Race::Gnoll:
case Race::Goblin:
case Race::FreeportGuard:
case Race::LavaDragon:
case Race::LizardMan:
case Race::Minotaur:
case Race::Orc:
case Race::HumanBeggar:
case Race::Pixie:
case Race::Drachnid:
case Race::SolusekRo:
case Race::Tunare:
case Race::Treant:
case Race::Vampire:
case Race::StatueOfRallosZek:
case Race::HighpassCitizen:
case Race::Zombie:
case Race::QeynosCitizen:
case Race::NeriakCitizen:
case Race::EruditeCitizen:
case Race::Bixie:
case Race::RivervaleCitizen:
case Race::Scarecrow:
case Race::Sphinx:
case Race::HalasCitizen:
case Race::GrobbCitizen:
case Race::OggokCitizen:
case Race::KaladimCitizen:
case Race::CazicThule:
case Race::ElfVampire:
case Race::Denizen:
case Race::Efreeti:
case Race::PhinigelAutropos:
case Race::Mermaid:
case Race::Harpy:
case Race::Fayguard:
case Race::Innoruuk:
case Race::Djinn:
case Race::InvisibleMan:
case Race::Iksar:
case Race::VahShir:
case Race::Sarnak:
case Race::Xalgoz:
case Race::Yeti:
case Race::IksarCitizen:
case Race::ForestGiant:
case Race::Burynai:
case Race::Erollisi:
case Race::Tribunal:
case Race::Bertoxxulous:
case Race::Bristlebane:
case Race::Ratman:
case Race::Coldain:
case Race::VeliousDragon:
case Race::Siren:
case Race::FrostGiant:
case Race::StormGiant:
case Race::BlackAndWhiteDragon:
case Race::GhostDragon:
case Race::PrismaticDragon:
case Race::Grimling:
case Race::KhatiSha:
case Race::Vampire2:
case Race::Shissar:
case Race::VampireVolatalis:
case Race::Shadel:
case Race::Netherbian:
case Race::Akhevan:
case Race::Wretch:
case Race::LordInquisitorSeru:
case Race::VahShirKing:
case Race::VahShirGuard:
case Race::TeleportMan:
case Race::Werewolf2:
case Race::Nymph:
case Race::Dryad:
case Race::Treant2:
case Race::TarewMarr:
case Race::SolusekRo2:
case Race::GuardOfJustice:
case Race::SolusekRoGuard:
case Race::BertoxxulousNew:
case Race::TribunalNew:
case Race::TerrisThule:
case Race::KnightOfPestilence:
case Race::Lepertoloth:
case Race::Pusling:
case Race::WaterMephit:
case Race::NightmareGoblin:
case Race::Karana:
case Race::Saryrn:
case Race::FenninRo:
case Race::SoulDevourer:
case Race::NewRallosZek:
case Race::VallonZek:
case Race::TallonZek:
case Race::AirMephit:
case Race::EarthMephit:
case Race::FireMephit:
case Race::NightmareMephit:
case Race::Zebuxoruk:
case Race::MithanielMarr:
case Race::UndeadKnight:
case Race::Rathe:
case Race::Xegony:
case Race::Fiend:
case Race::Quarm:
case Race::Efreeti2:
case Race::Valorian2:
case Race::AnimatedArmor:
case Race::UndeadFootman:
case Race::RallosOgre:
case Race::Froglok2:
case Race::TrollCrewMember:
case Race::PirateDeckhand:
case Race::BrokenSkullPirate:
case Race::PirateGhost:
case Race::OneArmedPirate:
case Race::SpiritmasterNadox:
case Race::BrokenSkullTaskmaster:
case Race::GnomePirate:
case Race::DarkElfPirate:
case Race::OgrePirate:
case Race::HumanPirate:
case Race::EruditePirate:
case Race::UndeadVampire:
case Race::Vampire3:
case Race::RujarkianOrc:
case Race::BoneGolem:
case Race::SandElf:
case Race::MasterVampire:
case Race::MasterOrc:
case Race::Mummy:
case Race::NewGoblin:
case Race::Nihil:
case Race::Trusik:
case Race::Ukun:
case Race::Ixt:
case Race::Ikaav:
case Race::Aneuk:
case Race::Kyv:
case Race::Noc:
case Race::Ratuk:
case Race::Huvul:
case Race::Mastruq:
case Race::MataMuram:
case Race::Succubus:
case Race::Pyrilen:
case Race::Dragorn:
case Race::Gelidran:
case Race::Minotaur2:
case Race::CrystalShard:
case Race::Goblin2:
case Race::Giant2:
case Race::Orc2:
case Race::Werewolf3:
case Race::Shiliskin:
case Race::Minotaur3:
case Race::Fairy2:
case Race::Bolvirk:
case Race::Elddar:
case Race::ForestGiant2:
case Race::BoneGolem2:
case Race::Scrykin:
case Race::Treant3:
case Race::Vampire4:
case Race::AyonaeRo:
case Race::SullonZek:
case Race::Bixie2:
case Race::Centaur2:
case Race::Drakkin:
case Race::Giant3:
case Race::Gnoll2:
case Race::GiantShade:
case Race::Harpy2:
case Race::Satyr:
case Race::Dynleth:
case Race::Kedge:
case Race::Kerran2:
case Race::Shissar2:
case Race::Siren2:
case Race::Sphinx2:
case Race::Human2:
case Race::Brownie2:
case Race::Exoskeleton:
case Race::Minotaur4:
case Race::Scarecrow2:
case Race::Wereorc:
case Race::ElvenGhost:
case Race::HumanGhost:
case Race::Burynai2:
case Race::Dracolich:
case Race::IksarGhost:
case Race::Mephit:
case Race::Sarnak2:
case Race::Gnoll3:
case Race::GodOfDiscord:
case Race::Ogre2:
case Race::Giant4:
case Race::Apexus:
case Race::Bellikos:
case Race::BrellsFirstCreation:
case Race::Brell:
case Race::Coldain2:
case Race::Coldain3:
case Race::Telmira:
case Race::MorellThule:
case Race::Amygdalan:
case Race::Sandman:
case Race::RoyalGuard:
case Race::CazicThule2:
case Race::Erudite2:
case Race::Alaran:
case Race::AlaranGhost:
case Race::Ratman2:
case Race::Akheva:
case Race::Luclin:
case Race::Luclin2:
case Race::Luclin3:
case Race::Luclin4:
return true;
default:
return false;
}
}
//this is called with 'this' as the mob being looked at, and
@@ -3322,7 +3516,7 @@ bool NPC::AICheckCloseBeneficialSpells(
/**
* Check through close range mobs
*/
for (auto & close_mob : entity_list.GetCloseMobList(caster, cast_range)) {
for (auto & close_mob : caster->GetCloseMobList(cast_range)) {
Mob *mob = close_mob.second;
if (!mob) {
continue;
@@ -3401,8 +3595,8 @@ void NPC::AIYellForHelp(Mob *sender, Mob *attacker)
GetID()
);
for (auto &close_mob : entity_list.GetCloseMobList(sender)) {
Mob *mob = close_mob.second;
for (auto &close_mob: sender->GetCloseMobList()) {
Mob *mob = close_mob.second;
if (!mob) {
continue;
}
@@ -4014,3 +4208,27 @@ void NPC::DescribeSpecialAbilities(Client* c)
c->Message(Chat::White, e.c_str());
}
}
void NPC::DoNpcToNpcAggroScan()
{
for (auto &close_mob : GetCloseMobList(GetAggroRange())) {
Mob *mob = close_mob.second;
if (!mob) {
continue;
}
if (!mob->IsNPC()) {
continue;
}
if (CheckWillAggro(mob)) {
AddToHateList(mob);
}
}
AI_scan_area_timer->Disable();
AI_scan_area_timer->Start(
RandomTimer(RuleI(NPC, NPCToNPCAggroTimerMin), RuleI(NPC, NPCToNPCAggroTimerMax)),
false
);
}
+1
View File
@@ -557,6 +557,7 @@ public:
bool CanPathTo(float x, float y, float z);
void DoNpcToNpcAggroScan();
protected:
void HandleRoambox();
+12
View File
@@ -3031,6 +3031,11 @@ void Perl_Client_GrantAllAAPoints(Client* self, uint8 unlock_level)
self->GrantAllAAPoints(unlock_level);
}
void Perl_Client_GrantAllAAPoints(Client* self, uint8 unlock_level, bool skip_grant_only)
{
self->GrantAllAAPoints(unlock_level, skip_grant_only);
}
void Perl_Client_AddEbonCrystals(Client* self, uint32 amount)
{
self->AddEbonCrystals(amount);
@@ -3202,6 +3207,11 @@ void Perl_Client_AreaTaunt(Client* self, float range, int bonus_hate)
entity_list.AETaunt(self, range, bonus_hate);
}
Merc* Perl_Client_GetMerc(Client* self)
{
return self->GetMerc();
}
void perl_register_client()
{
perl::interpreter perl(PERL_GET_THX);
@@ -3433,6 +3443,7 @@ void perl_register_client()
package.add("GetLearnedDisciplines", &Perl_Client_GetLearnedDisciplines);
package.add("GetLockoutExpeditionUUID", &Perl_Client_GetLockoutExpeditionUUID);
package.add("GetMaxEndurance", &Perl_Client_GetMaxEndurance);
package.add("GetMerc", &Perl_Client_GetMerc);
package.add("GetMemmedSpells", &Perl_Client_GetMemmedSpells);
package.add("GetModCharacterFactionLevel", &Perl_Client_GetModCharacterFactionLevel);
package.add("GetMoney", &Perl_Client_GetMoney);
@@ -3471,6 +3482,7 @@ void perl_register_client()
package.add("GoFish", &Perl_Client_GoFish);
package.add("GrantAllAAPoints", (void(*)(Client*))&Perl_Client_GrantAllAAPoints);
package.add("GrantAllAAPoints", (void(*)(Client*, uint8))&Perl_Client_GrantAllAAPoints);
package.add("GrantAllAAPoints", (void(*)(Client*, uint8, bool))&Perl_Client_GrantAllAAPoints);
package.add("GrantAlternateAdvancementAbility", (bool(*)(Client*, int, int))&Perl_Client_GrantAlternateAdvancementAbility);
package.add("GrantAlternateAdvancementAbility", (bool(*)(Client*, int, int, bool))&Perl_Client_GrantAlternateAdvancementAbility);
package.add("GuildID", &Perl_Client_GuildID);
+40
View File
@@ -739,6 +739,44 @@ void Perl_EntityList_MassGroupBuff(EntityList* self, Mob* caster, Mob* center, u
self->MassGroupBuff(caster, center, spell_id, affect_caster);
}
perl::array Perl_EntityList_GetNPCsByExcludedIDs(EntityList* self, perl::array npc_ids)
{
std::vector<uint32> ids;
for (int i = 0; i < npc_ids.size(); i++) {
ids.emplace_back(npc_ids[i]);
}
const auto& l = self->GetExcludedNPCsByIDs(ids);
perl::array npcs;
for (const auto& e : l) {
npcs.push_back(e);
}
return npcs;
}
perl::array Perl_EntityList_GetNPCsByIDs(EntityList* self, perl::array npc_ids)
{
std::vector<uint32> ids;
for (int i = 0; i < npc_ids.size(); i++) {
ids.emplace_back(npc_ids[i]);
}
const auto& l = self->GetNPCsByIDs(ids);
perl::array npcs;
for (const auto& e : l) {
npcs.push_back(e);
}
return npcs;
}
void perl_register_entitylist()
{
perl::interpreter perl(PERL_GET_THX);
@@ -804,6 +842,8 @@ void perl_register_entitylist()
package.add("GetNPCByNPCTypeID", &Perl_EntityList_GetNPCByNPCTypeID);
package.add("GetNPCBySpawnID", &Perl_EntityList_GetNPCBySpawnID);
package.add("GetNPCList", &Perl_EntityList_GetNPCList);
package.add("GetNPCsByExcludedIDs", &Perl_EntityList_GetNPCsByExcludedIDs);
package.add("GetNPCsByIDs", &Perl_EntityList_GetNPCsByIDs);
package.add("GetObjectByDBID", &Perl_EntityList_GetObjectByDBID);
package.add("GetObjectByID", &Perl_EntityList_GetObjectByID);
package.add("GetObjectList", &Perl_EntityList_GetObjectList);
+195
View File
@@ -0,0 +1,195 @@
#include "../common/features.h"
#ifdef EMBPERL_XS_CLASSES
#include "../common/global_define.h"
#include "embperl.h"
#include "merc.h"
uint32 Perl_Merc_GetCostFormula(Merc* self)
{
return self->GetCostFormula();
}
Group* Perl_Merc_GetGroup(Merc* self)
{
return self->GetGroup();
}
int Perl_Merc_GetHatedCount(Merc* self)
{
return self->GetHatedCount();
}
float Perl_Merc_GetMaxMeleeRangeToTarget(Merc* self, Mob* target)
{
return self->GetMaxMeleeRangeToTarget(target);
}
uint32 Perl_Merc_GetMercenaryCharacterID(Merc* self)
{
return self->GetMercenaryCharacterID();
}
uint32 Perl_Merc_GetMercenaryID(Merc* self)
{
return self->GetMercenaryID();
}
uint32 Perl_Merc_GetMercenaryNameType(Merc* self)
{
return self->GetMercNameType();
}
Client* Perl_Merc_GetMercenaryOwner(Merc* self)
{
return self->GetMercenaryOwner();
}
uint32 Perl_Merc_GetMercenarySubtype(Merc* self)
{
return self->GetMercenarySubType();
}
uint32 Perl_Merc_GetMercenaryTemplateID(Merc* self)
{
return self->GetMercenaryTemplateID();
}
uint32 Perl_Merc_GetMercenaryType(Merc* self)
{
return self->GetMercenaryType();
}
Mob* Perl_Merc_GetOwner(Merc* self)
{
return self->GetOwner();
}
Mob* Perl_Merc_GetOwnerOrSelf(Merc* self)
{
return self->GetOwnerOrSelf();
}
uint8 Perl_Merc_GetProficiencyID(Merc* self)
{
return self->GetProficiencyID();
}
uint8 Perl_Merc_GetStance(Merc* self)
{
return self->GetStance();
}
uint8 Perl_Merc_GetTierID(Merc* self)
{
return self->GetTierID();
}
bool Perl_Merc_HasOrMayGetAggro(Merc* self)
{
return self->HasOrMayGetAggro();
}
bool Perl_Merc_IsMercenaryCaster(Merc* self)
{
return self->IsMercCaster();
}
bool Perl_Merc_IsMercenaryCasterCombatRange(Merc* self, Mob* target)
{
return self->IsMercCasterCombatRange(target);
}
bool Perl_Merc_IsSitting(Merc* self)
{
return self->IsSitting();
}
bool Perl_Merc_IsStanding(Merc* self)
{
return self->IsStanding();
}
void Perl_Merc_ScaleStats(Merc* self, int scale_percentage)
{
self->ScaleStats(scale_percentage);
}
void Perl_Merc_ScaleStats(Merc* self, int scale_percentage, bool set_to_max)
{
self->ScaleStats(scale_percentage, set_to_max);
}
void Perl_Merc_SendPayload(Merc* self, int payload_id, std::string payload_value)
{
self->SendPayload(payload_id, payload_value);
}
void Perl_Merc_SetTarget(Merc* self, Mob* target)
{
self->SetTarget(target);
}
void Perl_Merc_Signal(Merc* self, int signal_id)
{
self->Signal(signal_id);
}
void Perl_Merc_Sit(Merc* self)
{
self->Sit();
}
void Perl_Merc_Stand(Merc* self)
{
self->Stand();
}
bool Perl_Merc_Suspend(Merc* self)
{
return self->Suspend();
}
bool Perl_Merc_UseDiscipline(Merc* self, uint16 spell_id, uint16 target_id)
{
return self->UseDiscipline(spell_id, target_id);
}
void perl_register_merc()
{
perl::interpreter state(PERL_GET_THX);
auto package = state.new_class<Merc>("Merc");
package.add_base_class("NPC");
package.add("GetCostFormula", &Perl_Merc_GetCostFormula);
package.add("GetGroup", &Perl_Merc_GetGroup);
package.add("GetHatedCount", &Perl_Merc_GetHatedCount);
package.add("GetMaxMeleeRangeToTarget", &Perl_Merc_GetMaxMeleeRangeToTarget);
package.add("GetMercenaryCharacterID", &Perl_Merc_GetMercenaryCharacterID);
package.add("GetMercenaryID", &Perl_Merc_GetMercenaryID);
package.add("GetMercenaryNameType", &Perl_Merc_GetMercenaryNameType);
package.add("GetMercenaryOwner", &Perl_Merc_GetMercenaryOwner);
package.add("GetMercenarySubtype", &Perl_Merc_GetMercenarySubtype);
package.add("GetMercenaryTemplateID", &Perl_Merc_GetMercenaryTemplateID);
package.add("GetMercenaryType", &Perl_Merc_GetMercenaryType);
package.add("GetOwner", &Perl_Merc_GetOwner);
package.add("GetOwnerOrSelf", &Perl_Merc_GetOwnerOrSelf);
package.add("GetProficiencyID", &Perl_Merc_GetProficiencyID);
package.add("GetStance", &Perl_Merc_GetStance);
package.add("GetTierID", &Perl_Merc_GetTierID);
package.add("HasOrMayGetAggro", &Perl_Merc_HasOrMayGetAggro);
package.add("IsMercenaryCaster", &Perl_Merc_IsMercenaryCaster);
package.add("IsMercenaryCasterCombatRange", &Perl_Merc_IsMercenaryCasterCombatRange);
package.add("IsSitting", &Perl_Merc_IsSitting);
package.add("IsStanding", &Perl_Merc_IsStanding);
package.add("ScaleStats", (void(*)(Merc*, int))&Perl_Merc_ScaleStats);
package.add("ScaleStats", (void(*)(Merc*, int, bool))&Perl_Merc_ScaleStats);
package.add("SendPayload", &Perl_Merc_SendPayload);
package.add("SetTarget", &Perl_Merc_SetTarget);
package.add("Signal", &Perl_Merc_Signal);
package.add("Sit", &Perl_Merc_Sit);
package.add("Stand", &Perl_Merc_Stand);
package.add("Suspend", &Perl_Merc_Suspend);
package.add("UseDiscipline", &Perl_Merc_UseDiscipline);
}
#endif //EMBPERL_XS_CLASSES
+30
View File
@@ -3543,6 +3543,31 @@ void Perl_Mob_MassGroupBuff(Mob* self, Mob* center, uint16 spell_id, bool affect
entity_list.MassGroupBuff(self, center, spell_id, affect_caster);
}
void Perl_Mob_BuffFadeBeneficial(Mob* self)
{
self->BuffFadeBeneficial();
}
void Perl_Mob_BuffFadeDetrimental(Mob* self)
{
self->BuffFadeDetrimental();
}
void Perl_Mob_BuffFadeDetrimentalByCaster(Mob* self, Mob* caster)
{
self->BuffFadeDetrimentalByCaster(caster);
}
void Perl_Mob_BuffFadeNonPersistDeath(Mob* self)
{
self->BuffFadeNonPersistDeath();
}
void Perl_Mob_BuffFadeSongs(Mob* self)
{
self->BuffFadeSongs();
}
void perl_register_mob()
{
perl::interpreter perl(PERL_GET_THX);
@@ -3578,11 +3603,16 @@ void perl_register_mob()
package.add("BuffCount", (uint32(*)(Mob*, bool))&Perl_Mob_BuffCount);
package.add("BuffCount", (uint32(*)(Mob*, bool, bool))&Perl_Mob_BuffCount);
package.add("BuffFadeAll", &Perl_Mob_BuffFadeAll);
package.add("BuffFadeBeneficial", &Perl_Mob_BuffFadeBeneficial);
package.add("BuffFadeByEffect", (void(*)(Mob*, int))&Perl_Mob_BuffFadeByEffect);
package.add("BuffFadeByEffect", (void(*)(Mob*, int, int))&Perl_Mob_BuffFadeByEffect);
package.add("BuffFadeBySlot", (void(*)(Mob*, int))&Perl_Mob_BuffFadeBySlot);
package.add("BuffFadeBySlot", (void(*)(Mob*, int, bool))&Perl_Mob_BuffFadeBySlot);
package.add("BuffFadeBySpellID", &Perl_Mob_BuffFadeBySpellID);
package.add("BuffFadeDetrimental", &Perl_Mob_BuffFadeDetrimental);
package.add("BuffFadeDetrimentalByCaster", &Perl_Mob_BuffFadeDetrimentalByCaster);
package.add("BuffFadeNonPersistDeath", &Perl_Mob_BuffFadeNonPersistDeath);
package.add("BuffFadeSongs", &Perl_Mob_BuffFadeSongs);
package.add("CalculateDistance", (float(*)(Mob*, float, float, float))&Perl_Mob_CalculateDistance);
package.add("CalculateDistance", (float(*)(Mob*, Mob*))&Perl_Mob_CalculateDistance);
package.add("CalculateHeadingToTarget", &Perl_Mob_CalculateHeadingToTarget);
+48
View File
@@ -139,6 +139,30 @@ public:
return 0;
}
virtual int EventMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
return 0;
}
virtual int EventGlobalMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
return 0;
}
virtual bool HasQuestSub(uint32 npc_id, QuestEventID event_id)
{
return false;
@@ -189,6 +213,16 @@ public:
return false;
}
virtual bool MercHasQuestSub(QuestEventID event_id)
{
return false;
}
virtual bool GlobalMercHasQuestSub(QuestEventID event_id)
{
return false;
}
virtual void LoadNPCScript(std::string filename, int npc_id) { }
virtual void LoadGlobalNPCScript(std::string filename) { }
virtual void LoadPlayerScript(std::string filename) { }
@@ -198,6 +232,8 @@ public:
virtual void LoadEncounterScript(std::string filename, std::string encounter_name) { }
virtual void LoadBotScript(std::string filename) { }
virtual void LoadGlobalBotScript(std::string filename) { }
virtual void LoadMercScript(std::string filename) { }
virtual void LoadGlobalMercScript(std::string filename) { }
virtual int DispatchEventNPC(
QuestEventID event_id,
@@ -260,6 +296,18 @@ public:
return 0;
}
virtual int DispatchEventMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
return 0;
}
virtual void AddVar(std::string name, std::string val) { }
virtual std::string GetVar(std::string name)
{
+301
View File
@@ -47,6 +47,8 @@ QuestParserCollection::QuestParserCollection()
_global_npc_quest_status = QuestUnloaded;
_bot_quest_status = QuestUnloaded;
_global_bot_quest_status = QuestUnloaded;
_merc_quest_status = QuestUnloaded;
_global_merc_quest_status = QuestUnloaded;
}
QuestParserCollection::~QuestParserCollection() { }
@@ -94,6 +96,8 @@ void QuestParserCollection::ReloadQuests(bool reset_timers)
_global_npc_quest_status = QuestUnloaded;
_bot_quest_status = QuestUnloaded;
_global_bot_quest_status = QuestUnloaded;
_merc_quest_status = QuestUnloaded;
_global_merc_quest_status = QuestUnloaded;
_spell_quest_status.clear();
_item_quest_status.clear();
@@ -379,6 +383,49 @@ bool QuestParserCollection::BotHasQuestSub(QuestEventID event_id)
return BotHasQuestSubLocal(event_id) || BotHasQuestSubGlobal(event_id);
}
bool QuestParserCollection::MercHasQuestSubLocal(QuestEventID event_id)
{
if (_merc_quest_status == QuestUnloaded) {
std::string filename;
auto qi = GetQIByMercQuest(filename);
if (qi) {
_merc_quest_status = qi->GetIdentifier();
qi->LoadMercScript(filename);
return qi->MercHasQuestSub(event_id);
}
} else if (_merc_quest_status != QuestFailedToLoad) {
auto iter = _interfaces.find(_merc_quest_status);
return iter->second->MercHasQuestSub(event_id);
}
return false;
}
bool QuestParserCollection::MercHasQuestSubGlobal(QuestEventID event_id)
{
if (_global_merc_quest_status == QuestUnloaded) {
std::string filename;
auto qi = GetQIByGlobalMercQuest(filename);
if (qi) {
_global_merc_quest_status = qi->GetIdentifier();
qi->LoadGlobalMercScript(filename);
return qi->GlobalMercHasQuestSub(event_id);
}
} else if (_global_merc_quest_status != QuestFailedToLoad) {
auto iter = _interfaces.find(_global_merc_quest_status);
return iter->second->GlobalMercHasQuestSub(event_id);
}
return false;
}
bool QuestParserCollection::MercHasQuestSub(QuestEventID event_id)
{
return MercHasQuestSubLocal(event_id) || MercHasQuestSubGlobal(event_id);
}
int QuestParserCollection::EventNPC(
QuestEventID event_id,
NPC* npc,
@@ -793,6 +840,86 @@ int QuestParserCollection::EventBotGlobal(
return 0;
}
int QuestParserCollection::EventMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
const int local_return = EventMercLocal(event_id, merc, init, data, extra_data, extra_pointers);
const int global_return = EventMercGlobal(event_id, merc, init, data, extra_data, extra_pointers);
const int default_return = DispatchEventMerc(event_id, merc, init, data, extra_data, extra_pointers);
if (local_return != 0) {
return local_return;
} else if (global_return != 0) {
return global_return;
} else if (default_return != 0) {
return default_return;
}
return 0;
}
int QuestParserCollection::EventMercLocal(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (_merc_quest_status == QuestUnloaded) {
std::string filename;
auto qi = GetQIByMercQuest(filename);
if (qi) {
_merc_quest_status = qi->GetIdentifier();
qi->LoadMercScript(filename);
return qi->EventMerc(event_id, merc, init, data, extra_data, extra_pointers);
}
} else {
if (_merc_quest_status != QuestFailedToLoad) {
auto iter = _interfaces.find(_merc_quest_status);
return iter->second->EventMerc(event_id, merc, init, data, extra_data, extra_pointers);
}
}
return 0;
}
int QuestParserCollection::EventMercGlobal(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (_global_merc_quest_status == QuestUnloaded) {
std::string filename;
auto qi = GetQIByGlobalMercQuest(filename);
if (qi) {
_global_merc_quest_status = qi->GetIdentifier();
qi->LoadGlobalMercScript(filename);
return qi->EventGlobalMerc(event_id, merc, init, data, extra_data, extra_pointers);
}
} else {
if (_global_merc_quest_status != QuestFailedToLoad) {
auto iter = _interfaces.find(_global_merc_quest_status);
return iter->second->EventGlobalMerc(event_id, merc, init, data, extra_data, extra_pointers);
}
}
return 0;
}
QuestInterface* QuestParserCollection::GetQIByNPCQuest(uint32 npc_id, std::string& filename)
{
if (!zone) {
@@ -1188,6 +1315,81 @@ QuestInterface* QuestParserCollection::GetQIByGlobalBotQuest(std::string& filena
return nullptr;
}
QuestInterface* QuestParserCollection::GetQIByMercQuest(std::string& filename)
{
if (!zone || !zone->IsLoaded()) {
return nullptr;
}
const std::string& global_path = fmt::format(
"{}/{}",
path.GetQuestsPath(),
QUEST_GLOBAL_DIRECTORY
);
const std::string& zone_path = fmt::format(
"{}/{}",
path.GetQuestsPath(),
zone->GetShortName()
);
const std::string& zone_versioned_path = fmt::format(
"{}/{}/v{}",
path.GetQuestsPath(),
zone->GetShortName(),
zone->GetInstanceVersion()
);
std::vector<std::string> file_names = {
fmt::format("{}/merc", zone_versioned_path), // Local versioned by Instance Version ./quests/zone/v0/merc.ext
fmt::format("{}/merc_v{}", zone_path, zone->GetInstanceVersion()), // Local by Instance Version
fmt::format("{}/merc", zone_path), // Local
fmt::format("{}/merc", global_path) // Global
};
std::string file_name;
for (auto & file : file_names) {
for (auto* e: _load_precedence) {
file_name = fmt::format(
"{}.{}",
file,
_extensions.find(e->GetIdentifier())->second
);
if (File::Exists(file_name)) {
filename = file_name;
return e;
}
}
}
return nullptr;
}
QuestInterface* QuestParserCollection::GetQIByGlobalMercQuest(std::string& filename)
{
if (!zone) {
return nullptr;
}
std::string file_name;
for (auto* e: _load_precedence) {
file_name = fmt::format(
"{}/{}/global_merc.{}",
path.GetQuestsPath(),
QUEST_GLOBAL_DIRECTORY,
_extensions.find(e->GetIdentifier())->second
);
if (File::Exists(file_name)) {
filename = file_name;
return e;
}
}
return nullptr;
}
void QuestParserCollection::GetErrors(std::list<std::string>& quest_errors)
{
quest_errors.clear();
@@ -1303,6 +1505,27 @@ int QuestParserCollection::DispatchEventBot(
return ret;
}
int QuestParserCollection::DispatchEventMerc(
QuestEventID event_id,
Merc* merc,
Mob* init,
std::string data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
int ret = 0;
for (const auto& e: _load_precedence) {
int i = e->DispatchEventMerc(event_id, merc, init, data, extra_data, extra_pointers);
if (i != 0) {
ret = i;
}
}
return ret;
}
void QuestParserCollection::LoadPerlEventExportSettings(PerlEventExportSettings* s)
{
for (int i = 0; i < _LargestEventID; i++) {
@@ -1325,3 +1548,81 @@ void QuestParserCollection::LoadPerlEventExportSettings(PerlEventExportSettings*
LogInfo("Loaded [{}] Perl Event Export Settings", l.size());
}
int QuestParserCollection::EventBotMerc(
QuestEventID event_id,
Mob* e,
Mob* init,
std::function<std::string()> lazy_data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (e->IsBot() && BotHasQuestSub(event_id)) {
return EventBot(event_id, e->CastToBot(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsMerc() && MercHasQuestSub(event_id)) {
return EventMerc(event_id, e->CastToMerc(), init, lazy_data(), extra_data, extra_pointers);
}
return false; // No quest subscription found
}
int QuestParserCollection::EventMercNPC(
QuestEventID event_id,
Mob* e,
Mob* init,
std::function<std::string()> lazy_data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (e->IsMerc() && MercHasQuestSub(event_id)) {
return EventMerc(event_id, e->CastToMerc(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsNPC() && HasQuestSub(e->GetNPCTypeID(), event_id)) {
return EventNPC(event_id, e->CastToNPC(), init, lazy_data(), extra_data, extra_pointers);
}
return false; // No quest subscription found
}
int QuestParserCollection::EventBotMercNPC(
QuestEventID event_id,
Mob* e,
Mob* init,
std::function<std::string()> lazy_data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (e->IsBot() && BotHasQuestSub(event_id)) {
return EventBot(event_id, e->CastToBot(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsMerc() && MercHasQuestSub(event_id)) {
return EventMerc(event_id, e->CastToMerc(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsNPC() && HasQuestSub(e->GetNPCTypeID(), event_id)) {
return EventNPC(event_id, e->CastToNPC(), init, lazy_data(), extra_data, extra_pointers);
}
return false; // No quest subscription found
}
int QuestParserCollection::EventMob(
QuestEventID event_id,
Mob* e,
Mob* init,
std::function<std::string()> lazy_data,
uint32 extra_data,
std::vector<std::any>* extra_pointers
)
{
if (e->IsClient() && PlayerHasQuestSub(event_id)) {
return EventPlayer(event_id, e->CastToClient(), lazy_data(), extra_data, extra_pointers);
} else if (e->IsBot() && BotHasQuestSub(event_id)) {
return EventBot(event_id, e->CastToBot(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsMerc() && MercHasQuestSub(event_id)) {
return EventMerc(event_id, e->CastToMerc(), init, lazy_data(), extra_data, extra_pointers);
} else if (e->IsNPC() && HasQuestSub(e->GetNPCTypeID(), event_id)) {
return EventNPC(event_id, e->CastToNPC(), init, lazy_data(), extra_data, extra_pointers);
}
return false; // No quest subscription found
}

Some files were not shown because too many files have changed in this diff Show More