Compare commits

...

80 Commits

Author SHA1 Message Date
Chris Miles faa6819f80 . 2025-06-14 00:03:28 -05:00
Chris Miles 4cc1d99322 Update zone.h 2025-06-13 23:52:34 -05:00
Chris Miles a0ff9d67a1 [Fix] Bulk Send Corpses after Idle State (#4910) 2025-06-09 14:31:28 -05:00
Chris Miles befee1c729 [Quests] Support Multiple Quest, Plugin, and Lua Module Paths (#4906)
* [Quests] Add Support for Multiple Load Paths

* Adjust load paths

* plugin != m_lua_module_directories
2025-06-09 12:49:46 -05:00
nytmyr 3d70063a68 [Doors] Fix door saving for versions (#4905)
* [Doors] Fix door saving for versions

- Door saving wasn't saving to the proper version on `#door save`

* Update doors.cpp

---------

Co-authored-by: Chris Miles <akkadius1@gmail.com>
2025-06-09 12:47:56 -05:00
JJ 4e28bcf85e Bump libuv to 1.50.0 (#4898) 2025-06-09 12:40:37 -05:00
JJ 687d10960a [Logs] Fix output for tasks in logs (#4907) 2025-06-09 12:32:45 -05:00
Chris Miles 567d46c3d6 [Performance] Auto Idle / AFK (#4903)
* [Performance] AFK Client Packet Filtering

* Player feedback

* Update client_packet.cpp

* Fixes

* Streamline updates to SetAFK

* Decouple idling and AFK and manual AFK

* Reset clock timer when we take AFK or idle off

* Exclude bard songs in non combat zones from resetting timer

* GM exclusion adjustments
2025-05-22 13:08:32 -05:00
Chris Miles 53cc2de459 [World API] Input Validation (#4904)
* [World API] Input Validation

* Update eqemu_api_world_data_service.cpp

* Add db ping to player events processor, move back into main thread
2025-05-22 13:08:17 -05:00
Chris Miles cb866cba31 [Release] 23.7.0 (#4902) 2025-05-19 12:31:37 -05:00
JJ e2162c08da [CLI] Add custom database version output (#4901)
* Add custom database version output to CLI from #4892

* Spacing alignment
2025-05-18 11:28:10 -05:00
Chris Miles e657953b8f [Netcode] Resend Logic Adjustments (#4900)
* Timeout adjustment

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Stuff

* Update daybreak_connection.h

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp

* Update daybreak_connection.cpp
2025-05-18 11:19:37 -05:00
JJ eb366e67b7 [CLI] Fix MySQL check in database dumper (#4897) 2025-05-16 18:56:38 -05:00
JJ 5b728a42f7 Update zonelist.h (#4896) 2025-05-16 18:17:38 -04:00
Chris Miles a1d414d64c [Netcode] Resend Logic Adjustments (#4895)
* [Netcode] Resend Logic Adjustments

* Update daybreak_connection.h
2025-05-16 16:46:07 -05:00
Chris Miles 907029ed76 [Database] Remove Transaction Wrapped Character Save (#4894) 2025-05-16 16:07:45 -05:00
Chris Miles 894f22fba0 [Fix] Deadlock on failed #copycharacter commands (#4887) 2025-05-16 13:39:34 -05:00
Chris Miles 7ec09d7e0f [Player Events] Add rule to ignore configured GM commands (#4888) 2025-05-16 13:39:24 -05:00
Chris Miles c82266790a [Zone State] Load New Spawn2 Data When Present (#4889) 2025-05-16 13:39:16 -05:00
Chris Miles ec31fddbae [Logging] Auto Update Log Category Names (#4890) 2025-05-16 13:39:08 -05:00
Chris Miles fb49ce2404 [Rules] Auto Update Rule Notes from Source (#4891)
* [Rules] Auto Update Rule Notes from Source

* Update rulesys.cpp
2025-05-16 13:38:59 -05:00
Chris Miles 276b7e238a [Database] Add Custom Database Migrations for Operators (#4892)
* [Database] Add Custom Database Migrations for Operators

* Changes

* Update database_update_manifest_custom.cpp
2025-05-16 13:38:51 -05:00
Chris Miles c7a463420b [World] Fix Rarer Reload Deadlock (#4893) 2025-05-16 13:38:42 -05:00
Chris Miles a56bb52808 Update README.md 2025-05-15 15:29:05 -05:00
Chris Miles 0bbdb58679 Update README.md 2025-05-15 15:26:07 -05:00
Alex King f29478c105 [Commands] Add #zonevariable Command (#4882)
* [Commands] Add #zonevariable Command

* Update zonevariable.cpp

* Argument safety

---------

Co-authored-by: Chris Miles <akkadius1@gmail.com>
2025-05-14 20:53:43 -05:00
Chris Miles 888a88f966 [Release] 23.6.0 (#4885) 2025-05-14 20:50:49 -05:00
Alex King 3b617a6652 [Quest API] Add Last Login and First Login Flags to EVENT_CONNECT (#4866)
* [Quest API] Add Last Login and First Login Flags to EVENT_CONNECT

* Push

* Update base_character_data_repository.h

* Update version field

---------

Co-authored-by: Chris Miles <akkadius1@gmail.com>
2025-05-14 20:46:25 -05:00
Alex King c36c336bc7 [Performance] Store Player Title Sets in Client Memory (#4836)
* [Performance] Store Player Title Sets in Zone Memory

* Move to client memory

* Update client_packet.cpp

* Update zonedb.cpp

* Save only when necessary

* Single Insert

* Add optional insert flag

* Update client.h

* Consolidation
2025-05-14 20:46:11 -05:00
zrix-eq 4a9779635d [Feature] Add Character:TradeskillUpMinChance rule (#4867) 2025-05-14 20:27:09 -05:00
Mitch Freeman 20da490bda [Feature] Enable spawn attribute for NPCTintID (#4871)
* Add scripting for NPCTintIndex

* AddNPCTintID

Add NPCTintID to spawn logic

* Update base_npc_types_repository.h

* Correct version.h
2025-05-14 20:26:35 -05:00
Mitch Freeman 1221e88d92 [Fix] Add trader/buyer cleanup actions (#4843)
* Add trader/buyer cleanup actions

Add trader/buyer db cleanup for
- on zone idle
- on client first login
- when world drops a zone connection
- in Client::ProcessMovePC

Cleanup several compiler warnings

* Formatting Updates
2025-05-14 20:24:59 -05:00
nytmyr a2b28b2e16 [Bots] Correct ^pull logic and add checks for Enchanter pets (#4827)
* [Bots] Fix bots getting stuck on ^pull

- Removed initial LoS check from bots and rely only on owner when told to ^pull. This prevents bots from getting stuck in a loop if they don't have LoS when trying to initiate the pull.

* [Bots] Add valid state checks to ^clickitem

- Bots previously weren't checking if they were in a valid state or not before trying to click an item when told (in a death state, held, feared, silenced, in group/raid, etc.)

* [Bots] Add valid state checks to ^clickitem

- Bots previously weren't checking if they were in a valid state or not before trying to click an item when told (in a death state, held, feared, silenced, in group/raid, etc.)

Miscommit

* undo

* Cleanup and Fixes

- Cleanup
- Fixed Animation Empathy checks and consolidates
- Enchanter pets will honor Animation Empathy rules all around
- Pets will properly guard if allowed when pulling
2025-05-14 20:21:25 -05:00
zimp-wow 3d607d352c [Fix] Fix Object Name Init, User Refs, and Client Sync on Close (#4861)
* [Bugfix] Fix uninitialized char* in object.cpp.

* [Bugfix] Clear object user and user tradeskill object on reset.

* [Bugfix] Send clear object packet on Object::Close()
2025-05-14 20:20:57 -05:00
JJ 83cd8119c8 [CLI] ETL Settings Output (#4873)
* Update etl_get_settings.cpp

* Update etl_get_settings.cpp

---------

Co-authored-by: Chris Miles <akkadius1@gmail.com>
2025-05-14 20:19:30 -05:00
nytmyr 4de8fbbd56 [Bots] Fix creation limit, spawn limit, level requirement checks (#4868)
* [Bots] Fix creation limit, spawn limit, level requirement checks

- Previously if buckets were being used to control any of these values and the appropriate rule was set to 0, unset class specific buckets would override the main limit buckets.
- For example, if `Bots:SpawnLimit` is set to `0` and a player has their `bot_spawn_limit` set to `5` but they don't have a class bucket set for the class they're attempting to spawn a Cleric, the unset `bot_spawn_limit_Cleric` would return 0 and prevent Clerics from being spawned.
- This affected spawn limits, creation limits and level requirements to use bots if controlled by buckets.
- `#gm on` is required to be on for those beyond the ruled min status requirements to bypass the limits.

Rewrote checks and tested every scenario of set unset rules/buckets.

* Cleanup, fix bot count

- Fixes QueryBotCount to not account for soft deleted bots (`-deleted-`)
2025-05-14 20:13:50 -05:00
nytmyr 780120036d [Bots] Move all spell_id instances to uint16 (#4876)
* [Bots] Move all spell_id instances to uint16

* Alignment
2025-05-14 19:58:18 -05:00
catapultam-habeo f3697e633c [Fix] Prevent Ranged Attack from being triggered at arbitrary rate (#4879) 2025-05-14 19:55:14 -05:00
hbingram 24f8d88333 [Fix] Fix breaking change to UF patches caused by Big Bags update (#4883)
* Fix breaking change to UF patches caused by Big Bags update

* Update comment

* Removed original line that was commented.
2025-05-14 19:51:48 -05:00
carolus21rex 21bd906a4d [Crash] Fix crash bug with pbae and quest scripts spawning mobs (#4884)
If a PBAE is in progress while a mob pops it can cause a crash. Forcing a copy before the ae has a chance to pop a mob should fix the weirdness
2025-05-14 19:50:12 -05:00
JJ 138612bc88 Only resume NPC if npc_id is ready (#4875) 2025-05-02 16:25:18 -04:00
dependabot[bot] 5919bb4dea Bump golang.org/x/net in /utils/scripts/build/should-release (#4864)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.36.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-02 16:24:44 -04:00
Alex King 99d249fefd [Bug Fix] Fix Crash with #task (#4874) 2025-04-30 19:15:53 -04:00
Mitch Freeman c08f286817 AddUntargetableToSpawn (#4872)
Adds the db entry for untargetable to the spawn struct
2025-04-28 17:50:57 -04:00
nytmyr cd003ff0b7 [Cleanup] Fix typo in QueryNameAvailablity (#4869) 2025-04-28 17:49:03 -04:00
nytmyr 1a539f6656 [Hotfix] Fix #copycharacter command (#4860)
This was failing due to a column change in `inventory` from `charid` to `character_id` and would result in no inventory for the new character.
2025-04-28 17:48:30 -04:00
nytmyr 7e7fb7b758 [Bots] Prevent non-taunters from potentially fleeing mob on TargetReflection (#4859)
- Casters could endlessly flee a mob if they had target reflection and were too close.
- Prevents all bots that aren't taunting from adjusting if they're too close and have target reflection. This is safer than limiting it to just casters and ranged bots to prevent situations where melee bots might not be able to outrun the mob to get to their melee range and continually flee.
- WE'LL GET THIS RIGHT EVENTUALLY /sigh
2025-04-28 17:46:12 -04:00
Mitch Freeman 5ae87b40e2 Add some evolvingitem crash checks seen over the past few months. (#4870) 2025-04-28 17:45:15 -04:00
Mitch Freeman 0ec07daebb Fix an edge case for sending a no drop item within a parcel (#4865) 2025-04-28 17:43:31 -04:00
Mitch Freeman 9869da2a0a Resolve guild messaging for incorrect guild tags when creating/deleting guilds. (#4863) 2025-04-28 17:43:04 -04:00
Mitch Freeman 617eb4432b Resolve empty trader lists in the bazaar window. This resolves a typo in the GetDistinctTraders routine. (#4862) 2025-04-28 17:41:45 -04:00
Chris Miles a2b2a6a5cf [Release] 23.5.0 (#4858) 2025-04-10 02:11:20 -05:00
Chris Miles 43a5bff84a [Performance] Network Ring Buffers (#4857)
* [Performance] Network Ring Buffers

* Cursor versus linear scan (wtf GPT)
2025-04-10 02:02:25 -05:00
KayenEQ e983d07228 [Spells] Fear resistance effects edge case fixes and support for SPA 102 as an AA (#4848)
* Clean up for fear related bonus variables

SPA 181 FearResistChance  (% chance to outright resist fear) and SPA 102 Fearless (on live used only on pets for  full fear immunity if presents)

The way were calculating the bonuses was mixing the variables we used for each together in a way that could cause conflicts when combined with negate spell effect.

While doing some live testing was able to confirm SPA102 when cast on pets will terminate the fear on the next tick which is consistent with how we have the mechanic coded.

* Update spells.cpp

---------

Co-authored-by: Akkadius <akkadius1@gmail.com>
2025-04-09 23:49:55 -05:00
catapultam-habeo 90db12483a [Feature] Add rule to consume command text from any channel (#4839)
* vanilla implementation

* fix typo
2025-04-09 22:45:09 -05:00
nytmyr ff71cfbd5b [Bots] Positioning rewrite (#4856)
* [Bots] Positioning rewrite

- Fixes positioning issues where bots could run back and forth or stay at the target's feet.
- Adds checks to ensure bots don't spam positioning updates and complete their initial positioning.
- Cleans up logic and makes it easier to read.

* Cleanup
2025-04-09 22:39:59 -05:00
Mitch Freeman 08bb9de437 [Fix] Add the bazaar search limit to query (#4829) 2025-04-09 21:44:19 -05:00
Chris Miles 16e341906d [Fix] Zone State Spawn2 Location Restore (#4844)
* [Fix] Zone State Spawn2 Location Restore

* Update zone_save_state.cpp

* Update zone_save_state.cpp

* Update zone_save_state.cpp

* Update zone_save_state.cpp
2025-04-09 21:43:18 -05:00
Mitch Freeman 216b3a039f [Fix] Bazaar Search window not working in a DZ (#4828) 2025-04-09 21:33:48 -05:00
nytmyr b813cf71bb [Bots] Add valid state checks to ^clickitem (#4830)
* [Bots] Add valid state checks to ^clickitem

- Bots previously weren't checking if they were in a valid state or not before trying to click an item when told (in a death state, held, feared, silenced, in group/raid, etc.)

* Add error output on no bots selected.=
2025-04-09 21:33:11 -05:00
nytmyr 48ecd1222f [Bots] Restore old buff overwrite blocking (#4832)
- [#4756](https://github.com/EQEmu/Server/pull/4756) changed the way overwriting worked for buffs. It allowed stronger spells to overwrite lesser spells if they were already on the target.
- However, this is a positive change but has its drawbacks with spells of similar value. It could cause an endless buff loop where they constantly replaced one another.

Overall, the con of endless buffing outweighs the benefit of automatic overwriting lesser spells and the control should be left to only spell lists via `^spells` for bot owners as it was before to determine which buffs bots will cast.
2025-04-09 21:32:57 -05:00
nytmyr e758b407e9 [Bots] Flag all buffs with SE_DamageShield as Damage Shield (#4833)
- Certain resist spells have a DS bonus on them and can be casted as a resist rather than a damage shield. This forces anything with a damage shield as a damage shield buff.
2025-04-09 21:32:38 -05:00
KayenEQ 758dd1875e [Spells] Update to SPA 180 SE_ResistSpellChance to not block unresistable spells. (#4847)
* SPA180 bypassed by field no_resist (field209)

SPA 180 SE_ResistSpellChance provides chance to resist spells outright. This should not be checked to resist a spell if spell has (field209 / no_resist) set to 1.

* Update spells.cpp

confirmed on live unresistable spells are not blocked by sanctification disc or related AA's
2025-04-09 21:31:27 -05:00
KayenEQ 8e2961dda5 [Spells] Update to SPA 378 SE_SpellEffectResistChance (#4845)
* SPA 378 update

Update to SPA 378

SE_SpellEffectResistChance provides chance to resist specific spell effects.

This updates allows support for multiple resistance checks against different effects in same spell.

Example. If a spell has a silence and a blind effect and the target has an AA that gives 10% chance to resist silence and 5% chance to resist blind. It will roll against effect respectively each giving a chance to resist the spell.

* Update spells.cpp

Unresistable spells (resisttype = 0) do not ever resist a spell even with SPA378

Example: Pheonix Charm in Plane of Fire, or Storm Comet in Bastion of Thunder. Parsed both and never got a resist despite having the AA that has effect specific resists which both those spells match.
2025-04-09 21:23:39 -05:00
JJ 5522eda6e4 [Cleanup] Update link for legacy EQEmu loginserver account setup (#4826) 2025-04-09 21:15:22 -05:00
Chris Miles ac1469bac2 [Performance] Pre-Compute CLE Server Lists (#4838)
* [Performance] Pre-Compute CLE Server Lists

* Remove debug
2025-04-09 21:11:52 -05:00
zimp-wow c2989e019a [Bugfix] Prevent depops from blocking new spawns. (#4841) 2025-04-09 21:11:05 -05:00
JJ e16b481ba2 [Cleanup] Remove queryserv dump flag (#4842)
Since queryserv tables are deprecated, there is no longer a need to use this flag
2025-04-09 21:10:46 -05:00
zimp-wow 4fc0ffd173 [Bugfix] Load zone variables before encounter_load. (#4846) 2025-04-09 20:59:19 -05:00
Chris Miles b883888a19 [Performance] Character Save Optimizations (#4851) 2025-04-09 20:56:24 -05:00
Chris Miles 50ad97aa0b [Fix] Databuckets Account Cache Loading (#4855) 2025-04-09 20:56:11 -05:00
zimp-wow dca892e258 [Bugfix] Prevent final shutdown from persisting incomplete state. (#4849)
* [Bugfix] Prevent final shutdown from persisting incomplete state.

* [Bugfix] Change shutdown parameters to member variable.
2025-04-09 20:55:56 -05:00
Alex f9fe4ea2ec [Fix] FixHeading Infinite Loop Fix (#4854)
Co-authored-by: KimLS <KimLS@peqtgc.com>
2025-04-09 20:55:35 -05:00
Chris Miles cc30c72538 [API] World API Optimizations (#4850) 2025-04-09 20:55:13 -05:00
JJ d1fd40cd85 [Database] Fix manifest for helmtexture in horses table (#4852)
* Update database_update_manifest.cpp

* Update database_update_manifest.cpp
2025-04-09 20:53:41 -05:00
Chris Miles f3af458cb3 [Netcode] Fix Stale Client Edge Case (#4853)
* [Netcode] Fix Stale Client Edge Case

* [Netcode] Make sure we always set m_close_time
2025-04-09 20:52:46 -05:00
Akkadius a093d04594 [Hotfix] Remove QS Tables From Export 2025-04-09 20:17:28 -05:00
zimp-wow 115df81400 [Bug Fix] Fix missing timer_name check on Mob::StopTimer (#4840) 2025-04-03 20:40:13 -04:00
Chris Miles 5babc864b9 [Fix] Regression in World SendEmoteMessageRaw (#4837) 2025-04-03 11:17:09 -05:00
Chris Miles 60a2dd8616 [Crash] Fix rarer exception crash issue in PlayerEventLogs::ProcessBatchQueue (#4835) 2025-04-02 19:22:40 -05:00
142 changed files with 3719 additions and 1837 deletions
+152
View File
@@ -1,3 +1,155 @@
## [23.7.0] 5/19/2025
### CLI
* Add custom database version output ([#4901](https://github.com/EQEmu/Server/pull/4901)) @joligario 2025-05-18
* Fix MySQL check in database dumper ([#4897](https://github.com/EQEmu/Server/pull/4897)) @joligario 2025-05-16
### Commands
* Add #zonevariable Command ([#4882](https://github.com/EQEmu/Server/pull/4882)) @Kinglykrab 2025-05-15
### Database
* Add Custom Database Migrations for Operators ([#4892](https://github.com/EQEmu/Server/pull/4892)) @Akkadius 2025-05-16
* Remove Transaction Wrapped Character Save ([#4894](https://github.com/EQEmu/Server/pull/4894)) @Akkadius 2025-05-16
### Fixes
* Deadlock on failed #copycharacter commands ([#4887](https://github.com/EQEmu/Server/pull/4887)) @Akkadius 2025-05-16
### Logging
* Auto Update Log Category Names ([#4890](https://github.com/EQEmu/Server/pull/4890)) @Akkadius 2025-05-16
### Netcode
* Resend Logic Adjustments ([#4900](https://github.com/EQEmu/Server/pull/4900)) @Akkadius 2025-05-18
### Player Events
* Add rule to ignore configured GM commands ([#4888](https://github.com/EQEmu/Server/pull/4888)) @Akkadius 2025-05-16
### Rules
* Auto Update Rule Notes from Source ([#4891](https://github.com/EQEmu/Server/pull/4891)) @Akkadius 2025-05-16
### World
* Fix Rarer Reload Deadlock ([#4893](https://github.com/EQEmu/Server/pull/4893)) @Akkadius 2025-05-16
### Zone State
* Load New Spawn2 Data When Present ([#4889](https://github.com/EQEmu/Server/pull/4889)) @Akkadius 2025-05-16
## [23.6.0] 5/14/2025
### Bots
* Correct ^pull logic and add checks for Enchanter pets ([#4827](https://github.com/EQEmu/Server/pull/4827)) @nytmyr 2025-05-15
* Fix creation limit, spawn limit, level requirement checks ([#4868](https://github.com/EQEmu/Server/pull/4868)) @nytmyr 2025-05-15
* Move all spell_id instances to uint16 ([#4876](https://github.com/EQEmu/Server/pull/4876)) @nytmyr 2025-05-15
* Prevent non-taunters from potentially fleeing mob on TargetReflection ([#4859](https://github.com/EQEmu/Server/pull/4859)) @nytmyr 2025-04-28
### CLI
* ETL Settings Output ([#4873](https://github.com/EQEmu/Server/pull/4873)) @joligario 2025-05-15
### Code
* Fix typo in QueryNameAvailablity ([#4869](https://github.com/EQEmu/Server/pull/4869)) @nytmyr 2025-04-28
### Crash
* Fix crash bug with pbae and quest scripts spawning mobs ([#4884](https://github.com/EQEmu/Server/pull/4884)) @carolus21rex 2025-05-15
### Feature
* Add Character:TradeskillUpMinChance rule ([#4867](https://github.com/EQEmu/Server/pull/4867)) @zrix-eq 2025-05-15
* Enable spawn attribute for NPCTintID ([#4871](https://github.com/EQEmu/Server/pull/4871)) @neckkola 2025-05-15
### Fixes
* Add trader/buyer cleanup actions ([#4843](https://github.com/EQEmu/Server/pull/4843)) @neckkola 2025-05-15
* Fix #copycharacter command ([#4860](https://github.com/EQEmu/Server/pull/4860)) @nytmyr 2025-04-28
* Fix Crash with #task ([#4874](https://github.com/EQEmu/Server/pull/4874)) @Kinglykrab 2025-04-30
* Fix Object Name Init, User Refs, and Client Sync on Close ([#4861](https://github.com/EQEmu/Server/pull/4861)) @zimp-wow 2025-05-15
* Fix breaking change to UF patches caused by Big Bags update ([#4883](https://github.com/EQEmu/Server/pull/4883)) @hbingram 2025-05-15
* Prevent Ranged Attack from being triggered at arbitrary rate ([#4879](https://github.com/EQEmu/Server/pull/4879)) @catapultam-habeo 2025-05-15
### Performance
* Store Player Title Sets in Client Memory ([#4836](https://github.com/EQEmu/Server/pull/4836)) @Kinglykrab 2025-05-15
### Quest API
* Add Last Login and First Login Flags to EVENT_CONNECT ([#4866](https://github.com/EQEmu/Server/pull/4866)) @Kinglykrab 2025-05-15
## [23.5.0] 4/10/2025
### API
* World API Optimizations ([#4850](https://github.com/EQEmu/Server/pull/4850)) @Akkadius 2025-04-10
### Bots
* Add valid state checks to ^clickitem ([#4830](https://github.com/EQEmu/Server/pull/4830)) @nytmyr 2025-04-10
* Flag all buffs with SE_DamageShield as Damage Shield ([#4833](https://github.com/EQEmu/Server/pull/4833)) @nytmyr 2025-04-10
* Positioning rewrite ([#4856](https://github.com/EQEmu/Server/pull/4856)) @nytmyr 2025-04-10
* Restore old buff overwrite blocking ([#4832](https://github.com/EQEmu/Server/pull/4832)) @nytmyr 2025-04-10
### Bugfix
* Load zone variables before encounter_load. ([#4846](https://github.com/EQEmu/Server/pull/4846)) @zimp-wow 2025-04-10
* Prevent depops from blocking new spawns. ([#4841](https://github.com/EQEmu/Server/pull/4841)) @zimp-wow 2025-04-10
* Prevent final shutdown from persisting incomplete state. ([#4849](https://github.com/EQEmu/Server/pull/4849)) @zimp-wow 2025-04-10
### Code
* Remove queryserv dump flag ([#4842](https://github.com/EQEmu/Server/pull/4842)) @joligario 2025-04-10
* Update link for legacy EQEmu loginserver account setup ([#4826](https://github.com/EQEmu/Server/pull/4826)) @joligario 2025-04-10
### Crash
* Fix rarer exception crash issue in PlayerEventLogs::ProcessBatchQueue ([#4835](https://github.com/EQEmu/Server/pull/4835)) @Akkadius 2025-04-03
### Database
* Fix manifest for `helmtexture` in `horses` table ([#4852](https://github.com/EQEmu/Server/pull/4852)) @joligario 2025-04-10
### Feature
* Add rule to consume command text from any channel ([#4839](https://github.com/EQEmu/Server/pull/4839)) @catapultam-habeo 2025-04-10
### Fixes
* Add the bazaar search limit to query ([#4829](https://github.com/EQEmu/Server/pull/4829)) @neckkola 2025-04-10
* Backfill expire_at (not sure why this didn't make it in there to begin with) @Akkadius 2025-03-31
* Bazaar Search window not working in a DZ ([#4828](https://github.com/EQEmu/Server/pull/4828)) @neckkola 2025-04-10
* Databuckets Account Cache Loading ([#4855](https://github.com/EQEmu/Server/pull/4855)) @Akkadius 2025-04-10
* Fix missing timer_name check on Mob::StopTimer ([#4840](https://github.com/EQEmu/Server/pull/4840)) @zimp-wow 2025-04-04
* FixHeading Infinite Loop Fix ([#4854](https://github.com/EQEmu/Server/pull/4854)) @KimLS 2025-04-10
* Make sure we don't expire default value instances @Akkadius 2025-03-31
* Regression in World SendEmoteMessageRaw ([#4837](https://github.com/EQEmu/Server/pull/4837)) @Akkadius 2025-04-03
* Remove QS Tables From Export @Akkadius 2025-04-10
* Zone State Spawn2 Location Restore ([#4844](https://github.com/EQEmu/Server/pull/4844)) @Akkadius 2025-04-10
### Netcode
* Fix Stale Client Edge Case ([#4853](https://github.com/EQEmu/Server/pull/4853)) @Akkadius 2025-04-10
### Performance
* Character Save Optimizations ([#4851](https://github.com/EQEmu/Server/pull/4851)) @Akkadius 2025-04-10
* Network Ring Buffers ([#4857](https://github.com/EQEmu/Server/pull/4857)) @Akkadius 2025-04-10
* Pre-Compute CLE Server Lists ([#4838](https://github.com/EQEmu/Server/pull/4838)) @Akkadius 2025-04-10
### Spells
* Fear resistance effects edge case fixes and support for SPA 102 as an AA ([#4848](https://github.com/EQEmu/Server/pull/4848)) @KayenEQ 2025-04-10
* Update to SPA 180 SE_ResistSpellChance to not block unresistable spells. ([#4847](https://github.com/EQEmu/Server/pull/4847)) @KayenEQ 2025-04-10
* Update to SPA 378 SE_SpellEffectResistChance ([#4845](https://github.com/EQEmu/Server/pull/4845)) @KayenEQ 2025-04-10
## [23.4.0] 3/30/2025 ## [23.4.0] 3/30/2025
### API ### API
+123 -55
View File
@@ -1,79 +1,147 @@
# EQEmulator Core Server <h1 align="center">EQEmulator Server Platform</h1>
| Drone (Linux x64) | Drone (Windows x64) |
|:---:|:---:| <p align="center">
|[![Build Status](http://drone.akkadius.com/api/badges/EQEmu/Server/status.svg)](http://drone.akkadius.com/EQEmu/Server) |[![Build Status](http://drone.akkadius.com/api/badges/EQEmu/Server/status.svg)](http://drone.akkadius.com/EQEmu/Server) | <img src="https://github.com/user-attachments/assets/11942e15-b512-402d-a619-0543c7f1151e" style="border-radius: 10px">
</p>
<p align="center">
<b>EverQuest Emulator (EQEmu) - A Fan-Made Project Honoring the Legendary MMORPG</b>
</p>
<p align="center">
<a href="https://github.com/eqemu/server/graphs/contributors"><img src="https://img.shields.io/github/contributors/eqemu/server" alt="Contributors"></a>
<a href="https://discord.gg/QHsm7CD"><img src="https://img.shields.io/discord/212663220849213441?label=Discord&amp;logo=discord&amp;color=7289DA" alt="Discord"></a>
<a href="https://docs.eqemu.io"><img src="https://img.shields.io/badge/docs-MkDocs%20Powered-blueviolet" alt="Docs"></a>
<a href="./LICENSE"><img src="https://img.shields.io/github/license/EQEmu/Server" alt="License"></a>
<a href="https://github.com/eqemu/server/releases"><img src="https://img.shields.io/github/v/release/eqemu/server" alt="Latest Release"></a>
<a href="https://github.com/EQEmu/Server/releases"><img src="https://img.shields.io/github/release-date/EQEmu/Server" alt="Release Date"></a>
<img src="https://img.shields.io/github/downloads/eqemu/server/total.svg" alt="Github All Releases"></a>
<a href="http://drone.akkadius.com/EQEmu/Server"><img src="http://drone.akkadius.com/api/badges/EQEmu/Server/status.svg" alt="Build Status"></a>
<img src="https://img.shields.io/github/issues-pr-closed/eqemu/server" alt="GitHub Issues or Pull Requests">
<img src="https://img.shields.io/docker/pulls/akkadius/eqemu-server" alt="Docker Pulls">
<a href="http://drone.akkadius.com/EQEmu/Server"><img src="http://drone.akkadius.com/api/badges/EQEmu/Server/status.svg" alt="Build Status"></a> <img src="https://jb.gg/badges/official-plastic.svg" alt="Official">
</p>
*** ***
**EQEmulator is a custom completely from-scratch open source server implementation for EverQuest built mostly on C++** <p align="center">
* MySQL/MariaDB is used as the database engine (over 200+ tables) EQEmulator is a <b>passion-driven</b>, <b>open source server emulator</b> project dedicated to preserving and celebrating the groundbreaking world of <b>EverQuest</b>, the massively multiplayer online role-playing game originally developed by <b>Verant Interactive</b> and <b>Sony Online Entertainment (now Daybreak Game Company)</b>.
* Perl and LUA are both supported scripting languages for NPC/Player/Quest oriented events </p>
* Open source database (Project EQ) has content up to expansion OoW (included in server installs)
* Game server environments and databases can be heavily customized to create all new experiences
* Hundreds of Quests/events created and maintained by Project EQ
## Server Installs <p align="center">
| |Windows|Linux| For over two decades and continuing, EQEmulator has served as a <strong>fan tribute</strong>, providing tools and technology that allow players to explore, customize, and experience EverQuests iconic gameplay in new ways. This project exists solely out of <strong>deep admiration</strong> for the original developers, artists, designers, and visionaries who created one of the most influential online worlds of all time.
|:---:|:---:|:---:| </p>
|**Install Count**|![Windows Install Count](http://analytics.akkadius.com/?install_count&windows_count)|![Linux Install Count](http://analytics.akkadius.com/?install_count&linux_count)|
### > Windows
* [Install Guide](https://docs.eqemu.io/server/installation/server-installation-windows/) <p align="center">
We do not claim ownership of EverQuest or its assets. <strong>All credit and respect belong to the original creators and Daybreak Game Company</strong>, whose work continues to inspire generations of players and developers alike.
</p>
### > Debian/Ubuntu/CentOS/Fedora <p align="center">
EQEmulator has for over 20 years and always will be a <strong>fan-based, non-commercial open-source effort</strong> made by players, for players—preserving the legacy of EverQuest while empowering community-driven creativity, learning and joy that the game and its creators has so strongly inspired in us all.
</p>
* [Install Guide](https://docs.eqemu.io/server/installation/server-installation-linux/) ***
* You can use curl or wget to kick off the installer (whichever your OS has) <h3 align="center">
> curl -O https://raw.githubusercontent.com/EQEmu/Server/master/utils/scripts/linux_installer/install.sh install.sh && chmod 755 install.sh && ./install.sh Technical Overview & Reverse Engineering Effort
</h1>
> wget --no-check-certificate https://raw.githubusercontent.com/EQEmu/Server/master/utils/scripts/linux_installer/install.sh -O install.sh && chmod 755 install.sh && ./install.sh <p align="center">EQEmulator represents <strong>over two decades of collaborative reverse engineering</strong>, rebuilding the EverQuest server from the ground up without access to the original source code. This effort was achieved entirely through <strong>community-driven analysis, network protocol decoding, and in-game behavioral research</strong>.</p>
## Supported Clients <h1 align="center">
💡 How We Did It
</h1>
<p align="center">
<img src="https://github.com/user-attachments/assets/b6b48cf7-f64a-4497-9750-71f442a3d132" height="300px">
</p>
<p align="center">
<strong>Reverse Engineering</strong>
Every system, packet, opcode, and game mechanic has been reconstructed through countless hours of live packet sniffing, client disassembly, and in-game experimentation by dedicated contributors over the years.
</p>
<p align="center">
No proprietary code or server sources were ever used.
</p>
<p align="center">
All implementations are the result of clean-room engineering.
</p>
<h1 align="center">
🛠️ Technology Stack
</h1>
<p align="center">
<img src="https://github.com/user-attachments/assets/df5ea809-86c5-439d-a8fa-651fb04ba477" style="border-radius: 10px">
</p>
**C++ Core Engine**
* High-performance networking and gameplay logic built in C++
* Cross-platform support for Linux and Windows
**MySQL / MariaDB Backend**
* Fully structured schema with over 200+ tables
* Supports content customization, expansions, and custom worlds
**Scripting Engine**
* Native support for **Perl** and **Lua** scripting
* Powerfully extendable for quests, NPC behaviors, and custom events
**Open Source Content Database**
* Includes ProjectEQs world data up through *Dragons of Norrath*
* 100% customizable to create entirely new game worlds
<h1 align="center">
🚀 Why It Matters
</h1>
<p align="center">🧬 EQEmulator stands as a <strong>technical preservation project</strong>, ensuring that the magic of classic and custom EverQuest servers lives on for future generations of players, tinkerers, and game designers.
</p>
> We humbly acknowledge and thank the original developers at **Verant Interactive** and **Sony Online Entertainment (now Daybreak Game Company)** for creating one of the most influential online experiences in gaming history.
<h1 align="center">
🧑‍💻🖥️ Supported Clients
</h1>
|Titanium Edition|Secrets of Faydwer|Seeds of Destruction|Underfoot|Rain of Fear| |Titanium Edition|Secrets of Faydwer|Seeds of Destruction|Underfoot|Rain of Fear|
|:---:|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|:---:|
|<img src="http://i.imgur.com/hrwDxoM.jpg" height="150">|<img src="http://i.imgur.com/cRDW5tn.png" height="150">|<img src="http://i.imgur.com/V48kuVn.jpg" height="150">|<img src="http://i.imgur.com/IJQ0XMa.jpg" height="150">|<img src="http://i.imgur.com/OMpHkKa.png" height="100">| |<img src="http://i.imgur.com/hrwDxoM.jpg" height="150">|<img src="http://i.imgur.com/cRDW5tn.png" height="150">|<img src="http://i.imgur.com/V48kuVn.jpg" height="150">|<img src="http://i.imgur.com/IJQ0XMa.jpg" height="150">|<img src="http://i.imgur.com/OMpHkKa.png" height="100">|
## Bug Reports <img src="http://i.imgur.com/daf1Vjw.png" height="20"> ## 📚 Resources
* Please use the [issue tracker](https://github.com/EQEmu/Server/issues) provided by GitHub to send us bug
reports or feature requests.
* The [EQEmu Forums](http://www.eqemulator.org/forums/) are also a place to submit and get help with bugs.
## Contributions <img src="http://image.flaticon.com/icons/png/512/25/25231.png" width="20"> | Resource | Badges | Link |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| **EQEmulator Docs** | [![Docs](https://img.shields.io/badge/docs-MkDocs%20Powered-blueviolet)](https://docs.eqemu.io) | [docs.eqemu.io](https://docs.eqemu.io/) |
| **Discord Community**| [![Discord](https://img.shields.io/discord/212663220849213441?label=Discord&logo=discord&color=7289DA)](https://discord.gg/QHsm7CD) | [Join Discord](https://discord.gg/QHsm7CD) |
| **Latest Release** | [![Latest Release](https://img.shields.io/github/v/release/eqemu/server)](https://github.com/eqemu/server/releases) <br> [![Release Date](https://img.shields.io/github/release-date/EQEmu/Server)](https://github.com/EQEmu/Server/releases) <br> [![All Releases](https://img.shields.io/github/downloads/eqemu/server/total.svg)](https://github.com/eqemu/server/releases) | [View Releases](https://github.com/eqemu/server/releases) |
| **License** | [![License](https://img.shields.io/github/license/EQEmu/Server)](./LICENSE) | [View License](./LICENSE) |
| **Build Status** | [![Build Status](http://drone.akkadius.com/api/badges/EQEmu/Server/status.svg)](http://drone.akkadius.com/EQEmu/Server) | [View Build Status](http://drone.akkadius.com/EQEmu/Server) |
| **Docker Pulls** | [![Docker Pulls](https://img.shields.io/docker/pulls/akkadius/eqemu-server)](https://hub.docker.com/r/akkadius/eqemu-server) | [Docker Hub](https://hub.docker.com/r/akkadius/eqemu-server) |
| **Contributions** | [![GitHub PRs](https://img.shields.io/github/issues-pr-closed/eqemu/server)](https://github.com/eqemu/server/pulls?q=is%3Apr+is%3Aclosed) | [Closed PRs & Issues](https://github.com/eqemu/server/pulls?q=is%3Apr+is%3Aclosed) |
* The preferred way to contribute is to fork the repo and submit a pull request on ## 🛠️ Getting Started
GitHub. If you need help with your changes, you can always post on the forums or
try Discord. You can also post unified diffs (`git diff` should do the trick) on the
[Server Code Submissions](http://www.eqemulator.org/forums/forumdisplay.php?f=669)
forum, although pull requests will be much quicker and easier on all parties.
## Contact <img src="http://gamerescape.com/wp-content/uploads/2015/06/discord.png" height="20"> If you want to set up your own EQEmulator server, please refer to the current [server installation guides](https://docs.eqemu.io/#server-installation). We've had 100,000s of players and developers use our guides to set up their own servers, and we hope you will too!
- Discord Channel: https://discord.gg/QHsm7CD ## 🗂️ Related Repositories
- **User Discord Channel**: `#general`
- **Developer Discord Channel**: `#eqemucoders`
## Resources | Repository | Description |
- [EQEmulator Forums](http://www.eqemulator.org/forums) |--------------------|----------------------------------------------------------------------------------|
- [EQEmulator Wiki](https://docs.eqemu.io/) | [ProjectEQ Quests](https://github.com/ProjectEQ/projecteqquests) | Official quests and event scripts for ProjectEQ |
| [Maps](https://github.com/Akkadius/EQEmuMaps) | EQEmu-compatible zone maps |
| [Installer Resources](https://github.com/Akkadius/EQEmuInstall) | Scripts and assets for setting up EQEmu servers |
| [Zone Utilities](https://github.com/EQEmu/zone-utilities) | Utilities for parsing, rendering, and manipulating EQ zone files |
## Related Repositories
* [ProjectEQ Quests](https://github.com/ProjectEQ/projecteqquests)
* [Maps](https://github.com/Akkadius/EQEmuMaps)
* [Installer Resources](https://github.com/Akkadius/EQEmuInstall)
* [Zone Utilities](https://github.com/EQEmu/zone-utilities) - Various utilities and libraries for parsing, rendering and manipulating EQ Zone files.
## Other License Info
* The server code and utilities are released under **GPLv3**
* We also include some small libraries for convienence that may be under different licensing
* SocketLib - GPL LibXML
* zlib - zlib license
* MariaDB/MySQL - GPL
* GPL Perl - GPL / ActiveState (under the assumption that this is a free project)
* CPPUnit - GLP StringUtilities - Apache
* LUA - MIT
## Contributors ## Contributors
+5
View File
@@ -17,6 +17,7 @@ SET(common_sources
database.cpp database.cpp
database_instances.cpp database_instances.cpp
database/database_update_manifest.cpp database/database_update_manifest.cpp
database/database_update_manifest_custom.cpp
database/database_update_manifest_bots.cpp database/database_update_manifest_bots.cpp
database/database_update.cpp database/database_update.cpp
dbcore.cpp dbcore.cpp
@@ -671,6 +672,7 @@ SET(common_headers
net/console_server_connection.h net/console_server_connection.h
net/crc32.h net/crc32.h
net/daybreak_connection.h net/daybreak_connection.h
net/daybreak_pooling.h
net/daybreak_structs.h net/daybreak_structs.h
net/dns.h net/dns.h
net/endian.h net/endian.h
@@ -682,6 +684,7 @@ SET(common_headers
net/servertalk_server.h net/servertalk_server.h
net/servertalk_server_connection.h net/servertalk_server_connection.h
net/tcp_connection.h net/tcp_connection.h
net/tcp_connection_pooling.h
net/tcp_server.h net/tcp_server.h
net/websocket_server.h net/websocket_server.h
net/websocket_server_connection.h net/websocket_server_connection.h
@@ -742,6 +745,7 @@ SOURCE_GROUP(Net FILES
net/crc32.h net/crc32.h
net/daybreak_connection.cpp net/daybreak_connection.cpp
net/daybreak_connection.h net/daybreak_connection.h
net/daybreak_pooling.h
net/daybreak_structs.h net/daybreak_structs.h
net/dns.h net/dns.h
net/endian.h net/endian.h
@@ -762,6 +766,7 @@ SOURCE_GROUP(Net FILES
net/servertalk_server_connection.h net/servertalk_server_connection.h
net/tcp_connection.cpp net/tcp_connection.cpp
net/tcp_connection.h net/tcp_connection.h
net/tcp_connection_pooling.h
net/tcp_server.cpp net/tcp_server.cpp
net/tcp_server.h net/tcp_server.h
net/websocket_server.cpp net/websocket_server.cpp
+2 -1
View File
@@ -279,7 +279,8 @@ Bazaar::GetSearchResults(
trader_items_ids, trader_items_ids,
std::string(search.item_name), std::string(search.item_name),
field_criteria_items, field_criteria_items,
where_criteria_items where_criteria_items,
search.max_results
); );
if (item_results.empty()) { if (item_results.empty()) {
+25 -7
View File
@@ -1095,13 +1095,13 @@ void Database::SetLFP(uint32 character_id, bool is_lfp)
CharacterDataRepository::UpdateOne(*this, e); CharacterDataRepository::UpdateOne(*this, e);
} }
void Database::SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 first_logon) void Database::SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 ingame)
{ {
auto e = CharacterDataRepository::FindOne(*this, character_id); auto e = CharacterDataRepository::FindOne(*this, character_id);
e.firstlogon = first_logon; e.ingame = ingame;
e.lfg = is_lfg ? 1 : 0; e.lfg = is_lfg ? 1 : 0;
e.lfp = is_lfp ? 1 : 0; e.lfp = is_lfp ? 1 : 0;
CharacterDataRepository::UpdateOne(*this, e); CharacterDataRepository::UpdateOne(*this, e);
} }
@@ -1115,11 +1115,11 @@ void Database::SetLFG(uint32 character_id, bool is_lfg)
CharacterDataRepository::UpdateOne(*this, e); CharacterDataRepository::UpdateOne(*this, e);
} }
void Database::SetFirstLogon(uint32 character_id, uint8 first_logon) void Database::SetIngame(uint32 character_id, uint8 ingame)
{ {
auto e = CharacterDataRepository::FindOne(*this, character_id); auto e = CharacterDataRepository::FindOne(*this, character_id);
e.firstlogon = first_logon; e.ingame = ingame;
CharacterDataRepository::UpdateOne(*this, e); CharacterDataRepository::UpdateOne(*this, e);
} }
@@ -1920,6 +1920,7 @@ bool Database::CopyCharacter(
std::vector<std::string> tables_to_zero_id = { std::vector<std::string> tables_to_zero_id = {
"keyring", "keyring",
"data_buckets", "data_buckets",
"character_evolving_items",
"character_instance_safereturns", "character_instance_safereturns",
"character_expedition_lockouts", "character_expedition_lockouts",
"character_instance_lockouts", "character_instance_lockouts",
@@ -1951,6 +1952,12 @@ bool Database::CopyCharacter(
) )
); );
if (!results.Success()) {
LogError("Transaction failed [{}] rolling back", results.ErrorMessage());
TransactionRollback();
return false;
}
std::vector<std::string> columns = {}; std::vector<std::string> columns = {};
int column_count = 0; int column_count = 0;
@@ -1969,6 +1976,12 @@ bool Database::CopyCharacter(
) )
); );
if (!results.Success()) {
LogError("Transaction failed [{}] rolling back", results.ErrorMessage());
TransactionRollback();
return false;
}
std::vector<std::vector<std::string>> new_rows; std::vector<std::vector<std::string>> new_rows;
for (auto row : results) { for (auto row : results) {
@@ -2036,13 +2049,18 @@ bool Database::CopyCharacter(
LogInfo("Copying table [{}] rows [{}]", table_name, Strings::Commify(rows_copied)); LogInfo("Copying table [{}] rows [{}]", table_name, Strings::Commify(rows_copied));
if (!insert.ErrorMessage().empty()) { if (!insert.ErrorMessage().empty()) {
LogError("Error copying table [{}] [{}]", table_name, insert.ErrorMessage());
TransactionRollback(); TransactionRollback();
return false; return false;
} }
} }
} }
TransactionCommit(); auto r = TransactionCommit();
if (!r.Success()) {
LogError("Transaction failed [{}] rolling back", r.ErrorMessage());
return false;
}
LogInfo( LogInfo(
"Character [{}] copied to [{}] total rows [{}]", "Character [{}] copied to [{}] total rows [{}]",
+1 -1
View File
@@ -263,7 +263,7 @@ public:
bool SaveTime(int8 minute, int8 hour, int8 day, int8 month, int16 year); bool SaveTime(int8 minute, int8 hour, int8 day, int8 month, int16 year);
void ClearMerchantTemp(); void ClearMerchantTemp();
void ClearPTimers(uint32 character_id); void ClearPTimers(uint32 character_id);
void SetFirstLogon(uint32 character_id, uint8 first_logon); void SetIngame(uint32 character_id, uint8 ingame);
void SetLFG(uint32 character_id, bool is_lfg); void SetLFG(uint32 character_id, bool is_lfg);
void SetLFP(uint32 character_id, bool is_lfp); void SetLFP(uint32 character_id, bool is_lfp);
void SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 first_logon); void SetLoginFlags(uint32 character_id, bool is_lfp, bool is_lfg, uint8 first_logon);
+1 -1
View File
@@ -50,7 +50,7 @@ bool DatabaseDumpService::IsMySQLInstalled()
{ {
std::string version_output = GetMySQLVersion(); std::string version_output = GetMySQLVersion();
return version_output.find("mysql") != std::string::npos && version_output.find("Ver") != std::string::npos; return version_output.find("mysql") != std::string::npos && (version_output.find("Ver") != std::string::npos || version_output.find("from") != std::string::npos);
} }
/** /**
-1
View File
@@ -65,7 +65,6 @@ private:
bool dump_system_tables = false; bool dump_system_tables = false;
bool dump_content_tables = false; bool dump_content_tables = false;
bool dump_player_tables = false; bool dump_player_tables = false;
bool dump_query_server_tables = false;
bool dump_login_server_tables = false; bool dump_login_server_tables = false;
bool dump_with_no_data = false; bool dump_with_no_data = false;
bool dump_table_lock = false; bool dump_table_lock = false;
+37 -2
View File
@@ -7,6 +7,7 @@
#include "../http/httplib.h" #include "../http/httplib.h"
#include "database_update_manifest.cpp" #include "database_update_manifest.cpp"
#include "database_update_manifest_custom.cpp"
#include "database_update_manifest_bots.cpp" #include "database_update_manifest_bots.cpp"
#include "database_dump_service.h" #include "database_dump_service.h"
@@ -14,7 +15,7 @@ constexpr int BREAK_LENGTH = 70;
DatabaseVersion DatabaseUpdate::GetDatabaseVersions() DatabaseVersion DatabaseUpdate::GetDatabaseVersions()
{ {
auto results = m_database->QueryDatabase("SELECT `version`, `bots_version` FROM `db_version` LIMIT 1"); auto results = m_database->QueryDatabase("SELECT `version`, `bots_version`, `custom_version` FROM `db_version` LIMIT 1");
if (!results.Success() || !results.RowCount()) { if (!results.Success() || !results.RowCount()) {
LogError("Failed to read from [db_version] table!"); LogError("Failed to read from [db_version] table!");
return DatabaseVersion{}; return DatabaseVersion{};
@@ -25,6 +26,7 @@ DatabaseVersion DatabaseUpdate::GetDatabaseVersions()
return DatabaseVersion{ return DatabaseVersion{
.server_database_version = Strings::ToInt(r[0]), .server_database_version = Strings::ToInt(r[0]),
.bots_database_version = Strings::ToInt(r[1]), .bots_database_version = Strings::ToInt(r[1]),
.custom_database_version = Strings::ToInt(r[2]),
}; };
} }
@@ -33,6 +35,7 @@ DatabaseVersion DatabaseUpdate::GetBinaryDatabaseVersions()
return DatabaseVersion{ return DatabaseVersion{
.server_database_version = CURRENT_BINARY_DATABASE_VERSION, .server_database_version = CURRENT_BINARY_DATABASE_VERSION,
.bots_database_version = (RuleB(Bots, Enabled) ? CURRENT_BINARY_BOTS_DATABASE_VERSION : 0), .bots_database_version = (RuleB(Bots, Enabled) ? CURRENT_BINARY_BOTS_DATABASE_VERSION : 0),
.custom_database_version = CUSTOM_BINARY_DATABASE_VERSION,
}; };
} }
@@ -43,6 +46,7 @@ constexpr int LOOK_BACK_AMOUNT = 10;
// this check will take action // this check will take action
void DatabaseUpdate::CheckDbUpdates() void DatabaseUpdate::CheckDbUpdates()
{ {
InjectCustomVersionColumn();
InjectBotsVersionColumn(); InjectBotsVersionColumn();
auto v = GetDatabaseVersions(); auto v = GetDatabaseVersions();
auto b = GetBinaryDatabaseVersions(); auto b = GetBinaryDatabaseVersions();
@@ -59,6 +63,15 @@ void DatabaseUpdate::CheckDbUpdates()
m_database->QueryDatabase(fmt::format("UPDATE `db_version` SET `version` = {}", b.server_database_version)); m_database->QueryDatabase(fmt::format("UPDATE `db_version` SET `version` = {}", b.server_database_version));
} }
if (UpdateManifest(manifest_entries_custom, v.custom_database_version, b.custom_database_version)) {
LogInfo(
"Updates ran successfully, setting database version to [{}] from [{}]",
b.custom_database_version,
v.custom_database_version
);
m_database->QueryDatabase(fmt::format("UPDATE `db_version` SET `custom_version` = {}", b.custom_database_version));
}
if (b.bots_database_version > 0) { if (b.bots_database_version > 0) {
if (UpdateManifest(bot_manifest_entries, v.bots_database_version, b.bots_database_version)) { if (UpdateManifest(bot_manifest_entries, v.bots_database_version, b.bots_database_version)) {
LogInfo( LogInfo(
@@ -344,6 +357,16 @@ bool DatabaseUpdate::CheckVersionsUpToDate(DatabaseVersion v, DatabaseVersion b)
); );
} }
if (b.custom_database_version > 0) {
LogInfo(
"{:>8} | database [{}] binary [{}] {}",
"Custom",
v.custom_database_version,
b.custom_database_version,
(v.custom_database_version == b.custom_database_version) ? "up to date" : "checking updates"
);
}
LogInfo("{:>8} | [server.auto_database_updates] [<green>true]", "Config"); LogInfo("{:>8} | [server.auto_database_updates] [<green>true]", "Config");
LogInfo("{}", Strings::Repeat("-", BREAK_LENGTH)); LogInfo("{}", Strings::Repeat("-", BREAK_LENGTH));
@@ -353,7 +376,10 @@ bool DatabaseUpdate::CheckVersionsUpToDate(DatabaseVersion v, DatabaseVersion b)
// bots database version is optional, if not enabled then it is always up-to-date // bots database version is optional, if not enabled then it is always up-to-date
bool bots_up_to_date = RuleB(Bots, Enabled) ? v.bots_database_version >= b.bots_database_version : true; bool bots_up_to_date = RuleB(Bots, Enabled) ? v.bots_database_version >= b.bots_database_version : true;
return server_up_to_date && bots_up_to_date; // custom database version is optional, if not enabled then it is always up-to-date
bool custom_up_to_date = v.custom_database_version >= b.custom_database_version;
return server_up_to_date && bots_up_to_date && custom_up_to_date;
} }
// checks to see if there are pending updates // checks to see if there are pending updates
@@ -373,3 +399,12 @@ void DatabaseUpdate::InjectBotsVersionColumn()
m_database->QueryDatabase("ALTER TABLE db_version ADD bots_version int(11) DEFAULT '0' AFTER version"); m_database->QueryDatabase("ALTER TABLE db_version ADD bots_version int(11) DEFAULT '0' AFTER version");
} }
} }
void DatabaseUpdate::InjectCustomVersionColumn()
{
auto results = m_database->QueryDatabase("SHOW COLUMNS FROM `db_version` LIKE 'custom_version'");
if (!results.Success() || results.RowCount() == 0) {
LogInfo("Adding custom_version column to db_version table");
m_database->QueryDatabase("ALTER TABLE `db_version` ADD COLUMN `custom_version` INT(11) UNSIGNED NOT NULL DEFAULT 0");
}
}
+2
View File
@@ -17,6 +17,7 @@ struct ManifestEntry {
struct DatabaseVersion { struct DatabaseVersion {
int server_database_version; int server_database_version;
int bots_database_version; int bots_database_version;
int custom_database_version;
}; };
class DatabaseUpdate { class DatabaseUpdate {
@@ -38,6 +39,7 @@ private:
Database *m_content_database; Database *m_content_database;
static bool CheckVersionsUpToDate(DatabaseVersion v, DatabaseVersion b); static bool CheckVersionsUpToDate(DatabaseVersion v, DatabaseVersion b);
void InjectBotsVersionColumn(); void InjectBotsVersionColumn();
void InjectCustomVersionColumn();
}; };
+27 -2
View File
@@ -6942,8 +6942,8 @@ CREATE TABLE `character_pet_name` (
.version = 9310, .version = 9310,
.description = "2025_03_7_expand_horse_def.sql", .description = "2025_03_7_expand_horse_def.sql",
.check = "SHOW COLUMNS FROM `horses` LIKE 'helmtexture'", .check = "SHOW COLUMNS FROM `horses` LIKE 'helmtexture'",
.condition = "missing", .condition = "empty",
.match = "TINYINT(2)", .match = "",
.sql = R"( .sql = R"(
ALTER TABLE `horses` ALTER TABLE `horses`
ADD COLUMN `helmtexture` TINYINT(2) NOT NULL DEFAULT -1 AFTER `texture`; ADD COLUMN `helmtexture` TINYINT(2) NOT NULL DEFAULT -1 AFTER `texture`;
@@ -7084,6 +7084,31 @@ ADD COLUMN `expire_at` bigint(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `duration`;
UPDATE instance_list set expire_at = `start_time` + `duration`; -- backfill existing data UPDATE instance_list set expire_at = `start_time` + `duration`; -- backfill existing data
CREATE INDEX `idx_expire_at` ON `instance_list` (`expire_at`); CREATE INDEX `idx_expire_at` ON `instance_list` (`expire_at`);
)",
.content_schema_update = false
},
ManifestEntry{
.version = 9322,
.description = "2025_04_24_add_npc_tint_id.sql",
.check = "SHOW COLUMNS FROM `npc_types` LIKE 'npc_tint_id'",
.condition = "empty",
.match = "",
.sql = R"(
ALTER TABLE `npc_types`
ADD COLUMN `npc_tint_id` SMALLINT UNSIGNED NULL DEFAULT '0' AFTER `multiquest_enabled`;
)",
.content_schema_update = true
},
ManifestEntry{
.version = 9323,
.description = "2025_04_16_character_data_first_login.sql",
.check = "SHOW COLUMNS FROM `character_data` LIKE 'first_login'",
.condition = "empty",
.match = "",
.sql = R"(
ALTER TABLE `character_data`
CHANGE COLUMN `firstlogon` `ingame` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `xtargets`,
ADD COLUMN `first_login` int(11) UNSIGNED NOT NULL DEFAULT 0 AFTER `xtargets`;
)", )",
.content_schema_update = false .content_schema_update = false
}, },
@@ -0,0 +1,55 @@
#include "database_update.h"
std::vector<ManifestEntry> manifest_entries_custom = {
ManifestEntry{
.version = 1,
.description = "2025_05_16_new_database_check_test",
.check = "SHOW TABLES LIKE 'new_table'",
.condition = "empty",
.match = "",
.sql = R"(
CREATE TABLE `new_table` (
`id` int NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
)",
.content_schema_update = false,
},
// Used for testing
// ManifestEntry{
// .version = 9229,
// .description = "new_database_check_test",
// .check = "SHOW TABLES LIKE 'new_table'",
// .condition = "empty",
// .match = "",
// .sql = R"(
//CREATE TABLE `new_table` (
// `id` int NOT NULL AUTO_INCREMENT,
// PRIMARY KEY (`id`)
//);
//CREATE TABLE `new_table1` (
// `id` int NOT NULL AUTO_INCREMENT,
// PRIMARY KEY (`id`)
//);
//CREATE TABLE `new_table2` (
// `id` int NOT NULL AUTO_INCREMENT,
// PRIMARY KEY (`id`)
//);
//CREATE TABLE `new_table3` (
// `id` int NOT NULL AUTO_INCREMENT,
// PRIMARY KEY (`id`)
//);
//)",
// }
};
// see struct definitions for what each field does
// struct ManifestEntry {
// int version{}; // database version of the migration
// std::string description{}; // description of the migration ex: "add_new_table" or "add_index_to_table"
// std::string check{}; // query that checks against the condition
// std::string condition{}; // condition or "match_type" - Possible values [contains|match|missing|empty|not_empty]
// std::string match{}; // match field that is not always used, but works in conjunction with "condition" values [missing|match|contains]
// std::string sql{}; // the SQL DDL that gets ran when the condition is true
// };
+1 -1
View File
@@ -80,7 +80,7 @@ namespace DatabaseSchema {
{"guild_members", "char_id"}, {"guild_members", "char_id"},
{"guilds", "id"}, {"guilds", "id"},
{"instance_list_player", "id"}, {"instance_list_player", "id"},
{"inventory", "charid"}, {"inventory", "character_id"},
{"inventory_snapshots", "charid"}, {"inventory_snapshots", "charid"},
{"keyring", "char_id"}, {"keyring", "char_id"},
{"mail", "charid"}, {"mail", "charid"},
+2 -2
View File
@@ -189,9 +189,9 @@ void DBcore::TransactionBegin()
QueryDatabase("START TRANSACTION"); QueryDatabase("START TRANSACTION");
} }
void DBcore::TransactionCommit() MySQLRequestResult DBcore::TransactionCommit()
{ {
QueryDatabase("COMMIT"); return QueryDatabase("COMMIT");
} }
void DBcore::TransactionRollback() void DBcore::TransactionRollback()
+1 -1
View File
@@ -32,7 +32,7 @@ public:
MySQLRequestResult QueryDatabase(const std::string& query, bool retryOnFailureOnce = true); MySQLRequestResult QueryDatabase(const std::string& query, bool retryOnFailureOnce = true);
MySQLRequestResult QueryDatabaseMulti(const std::string &query); MySQLRequestResult QueryDatabaseMulti(const std::string &query);
void TransactionBegin(); void TransactionBegin();
void TransactionCommit(); MySQLRequestResult TransactionCommit();
void TransactionRollback(); void TransactionRollback();
std::string Escape(const std::string& s); std::string Escape(const std::string& s);
uint32 DoEscapeString(char *tobuf, const char *frombuf, uint32 fromlen); uint32 DoEscapeString(char *tobuf, const char *frombuf, uint32 fromlen);
+2
View File
@@ -324,6 +324,8 @@ union
bool guild_show; bool guild_show;
bool trader; bool trader;
bool buyer; bool buyer;
bool untargetable;
uint32 npc_tint_id;
}; };
struct PlayerState_Struct { struct PlayerState_Struct {
+15
View File
@@ -177,6 +177,21 @@ void EQEmuConfig::parse_config()
SharedMemDir = _root["server"]["directories"].get("shared_memory", "shared/").asString(); SharedMemDir = _root["server"]["directories"].get("shared_memory", "shared/").asString();
LogDir = _root["server"]["directories"].get("logs", "logs/").asString(); LogDir = _root["server"]["directories"].get("logs", "logs/").asString();
auto load_paths = [&](const std::string& key, std::vector<std::string>& target) {
const auto& paths = _root["server"]["directories"][key];
if (paths.isArray()) {
for (const auto& dir : paths) {
if (dir.isString()) {
target.push_back(dir.asString());
}
}
}
};
load_paths("quest_paths", m_quest_directories);
load_paths("plugin_paths", m_plugin_directories);
load_paths("lua_module_paths", m_lua_module_directories);
/** /**
* Logs * Logs
*/ */
+21
View File
@@ -120,6 +120,22 @@ class EQEmuConfig
const std::string &GetUCSHost() const; const std::string &GetUCSHost() const;
uint16 GetUCSPort() const; uint16 GetUCSPort() const;
std::vector<std::string> GetQuestDirectories() const
{
return m_quest_directories;
}
std::vector<std::string> GetPluginsDirectories() const
{
return m_plugin_directories;
}
std::vector<std::string> GetLuaModuleDirectories() const
{
return m_lua_module_directories;
}
// uint16 DynamicCount; // uint16 DynamicCount;
// map<string,uint16> StaticZones; // map<string,uint16> StaticZones;
@@ -133,6 +149,11 @@ class EQEmuConfig
Json::Value _root; Json::Value _root;
static std::string ConfigFile; static std::string ConfigFile;
std::vector<std::string> m_quest_directories = {};
std::vector<std::string> m_plugin_directories = {};
std::vector<std::string> m_lua_module_directories = {};
protected:
void parse_config(); void parse_config();
EQEmuConfig() EQEmuConfig()
+27 -8
View File
@@ -682,14 +682,33 @@ EQEmuLogSys *EQEmuLogSys::LoadLogDatabaseSettings(bool silent_load)
if (is_missing_in_database && !is_deprecated_category) { if (is_missing_in_database && !is_deprecated_category) {
LogInfo("Automatically adding new log category [{}] ({})", Logs::LogCategoryName[i], i); LogInfo("Automatically adding new log category [{}] ({})", Logs::LogCategoryName[i], i);
auto new_category = LogsysCategoriesRepository::NewEntity(); auto e = LogsysCategoriesRepository::NewEntity();
new_category.log_category_id = i; e.log_category_id = i;
new_category.log_category_description = Strings::Escape(Logs::LogCategoryName[i]); e.log_category_description = Strings::Escape(Logs::LogCategoryName[i]);
new_category.log_to_console = log_settings[i].log_to_console; e.log_to_console = log_settings[i].log_to_console;
new_category.log_to_gmsay = log_settings[i].log_to_gmsay; e.log_to_gmsay = log_settings[i].log_to_gmsay;
new_category.log_to_file = log_settings[i].log_to_file; e.log_to_file = log_settings[i].log_to_file;
new_category.log_to_discord = log_settings[i].log_to_discord; e.log_to_discord = log_settings[i].log_to_discord;
db_categories_to_add.emplace_back(new_category); db_categories_to_add.emplace_back(e);
}
// look to see if the category name is different in the database
auto it = std::find_if(
categories.begin(),
categories.end(),
[i](const auto &c) { return c.log_category_id == i; }
);
if (it != categories.end()) {
if (it->log_category_description != Logs::LogCategoryName[i]) {
LogInfo(
"Updating log category [{}] ({}) to new name [{}]",
it->log_category_description,
i,
Logs::LogCategoryName[i]
);
it->log_category_description = Logs::LogCategoryName[i];
LogsysCategoriesRepository::ReplaceOne(*m_database, *it);
}
} }
} }
+13 -9
View File
@@ -74,7 +74,7 @@ namespace Logs {
Spawns, Spawns,
Spells, Spells,
Status, // deprecated Status, // deprecated
TCPConnection, TCPConnection, // deprecated
Tasks, Tasks,
Tradeskills, Tradeskills,
Trading, Trading,
@@ -150,6 +150,8 @@ namespace Logs {
BotSpellTypeChecks, BotSpellTypeChecks,
NpcHandin, NpcHandin,
ZoneState, ZoneState,
NetClient,
NetTCP,
MaxCategoryID /* Don't Remove this */ MaxCategoryID /* Don't Remove this */
}; };
@@ -183,7 +185,7 @@ namespace Logs {
"Spawns", "Spawns",
"Spells", "Spells",
"Status (Deprecated)", "Status (Deprecated)",
"TCP Connection", "TCP Connection (Deprecated)",
"Tasks", "Tasks",
"Tradeskills", "Tradeskills",
"Trading", "Trading",
@@ -192,8 +194,8 @@ namespace Logs {
"Web Interface (Deprecated)", "Web Interface (Deprecated)",
"World Server (Deprecated)", "World Server (Deprecated)",
"Zone Server (Deprecated)", "Zone Server (Deprecated)",
"QueryErr", "MySQL Error",
"Query", "MySQL Query",
"Mercenaries", "Mercenaries",
"Quest Debug", "Quest Debug",
"Legacy Packet Logging (Deprecated)", "Legacy Packet Logging (Deprecated)",
@@ -209,15 +211,15 @@ namespace Logs {
"Traps", "Traps",
"NPC Roam Box", "NPC Roam Box",
"NPC Scaling", "NPC Scaling",
"MobAppearance", "Mob Appearance",
"Info", "Info",
"Warning", "Warning",
"Critical (Deprecated)", "Critical (Deprecated)",
"Emergency (Deprecated)", "Emergency (Deprecated)",
"Alert (Deprecated)", "Alert (Deprecated)",
"Notice (Deprecated)", "Notice (Deprecated)",
"AI Scan", "AI Scan Close",
"AI Yell", "AI Yell For Help",
"AI CastBeneficial", "AI CastBeneficial",
"AOE Cast", "AOE Cast",
"Entity Management", "Entity Management",
@@ -235,7 +237,7 @@ namespace Logs {
"DialogueWindow", "DialogueWindow",
"HTTP", "HTTP",
"Saylink", "Saylink",
"ChecksumVer", "Checksum Verification",
"CombatRecord", "CombatRecord",
"Hate", "Hate",
"Discord", "Discord",
@@ -258,7 +260,9 @@ namespace Logs {
"Bot Spell Checks", "Bot Spell Checks",
"Bot Spell Type Checks", "Bot Spell Type Checks",
"NpcHandin", "NpcHandin",
"ZoneState" "ZoneState",
"Net Server <-> Client",
"Net TCP"
}; };
} }
+20 -20
View File
@@ -261,26 +261,6 @@
OutF(LogSys, Logs::Detail, Logs::Spells, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ OutF(LogSys, Logs::Detail, Logs::Spells, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0) } while (0)
#define LogStatus(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::General, Logs::Status))\
OutF(LogSys, Logs::General, Logs::Status, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogStatusDetail(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::Detail, Logs::Status))\
OutF(LogSys, Logs::Detail, Logs::Status, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogTCPConnection(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::General, Logs::TCPConnection))\
OutF(LogSys, Logs::General, Logs::TCPConnection, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogTCPConnectionDetail(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::Detail, Logs::TCPConnection))\
OutF(LogSys, Logs::Detail, Logs::TCPConnection, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogTasks(message, ...) do {\ #define LogTasks(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::General, Logs::Tasks))\ if (LogSys.IsLogEnabled(Logs::General, Logs::Tasks))\
OutF(LogSys, Logs::General, Logs::Tasks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ OutF(LogSys, Logs::General, Logs::Tasks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
@@ -924,6 +904,26 @@
OutF(LogSys, Logs::Detail, Logs::ZoneState, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ OutF(LogSys, Logs::Detail, Logs::ZoneState, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0) } while (0)
#define LogNetClient(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::General, Logs::NetClient))\
OutF(LogSys, Logs::General, Logs::NetClient, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogNetClientDetail(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::Detail, Logs::NetClient))\
OutF(LogSys, Logs::Detail, Logs::NetClient, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogNetTCP(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::General, Logs::NetTCP))\
OutF(LogSys, Logs::General, Logs::NetTCP, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define LogNetTCPDetail(message, ...) do {\
if (LogSys.IsLogEnabled(Logs::Detail, Logs::NetTCP))\
OutF(LogSys, Logs::Detail, Logs::NetTCP, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
} while (0)
#define Log(debug_level, log_category, message, ...) do {\ #define Log(debug_level, log_category, message, ...) do {\
if (LogSys.IsLogEnabled(debug_level, log_category))\ if (LogSys.IsLogEnabled(debug_level, log_category))\
LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\
+16 -4
View File
@@ -15,9 +15,9 @@ const uint32 PROCESS_RETENTION_TRUNCATION_TIMER_INTERVAL = 60 * 60 * 1000; // 1
// general initialization routine // general initialization routine
void PlayerEventLogs::Init() void PlayerEventLogs::Init()
{ {
m_process_batch_events_timer.SetTimer(RuleI(Logging, BatchPlayerEventProcessIntervalSeconds) * 1000); m_process_batch_events_timer.SetTimer(RuleI(Logging, BatchPlayerEventProcessIntervalSeconds) * 1000);
m_process_retention_truncation_timer.SetTimer(PROCESS_RETENTION_TRUNCATION_TIMER_INTERVAL); m_process_retention_truncation_timer.SetTimer(PROCESS_RETENTION_TRUNCATION_TIMER_INTERVAL);
m_database_ping_timer.SetTimer(10 * 1000); // 10 seconds
ValidateDatabaseConnection(); ValidateDatabaseConnection();
@@ -181,9 +181,17 @@ void PlayerEventLogs::ProcessBatchQueue()
// Helper to deserialize event data // Helper to deserialize event data
auto Deserialize = [](const std::string &data, auto &out) { auto Deserialize = [](const std::string &data, auto &out) {
std::stringstream ss(data); if (!Strings::IsValidJson(data)) {
cereal::JSONInputArchive ar(ss); return;
out.serialize(ar); }
// cpp exceptions are terrible, don't ever use them
try {
std::stringstream ss(data);
cereal::JSONInputArchive ar(ss);
out.serialize(ar);
}
catch (const std::exception &e) {}
}; };
// Helper to assign ETL table ID // Helper to assign ETL table ID
@@ -908,6 +916,10 @@ std::string PlayerEventLogs::GetDiscordPayloadFromEvent(const PlayerEvent::Playe
// general process function, used in world or QS depending on rule Logging:PlayerEventsQSProcess // general process function, used in world or QS depending on rule Logging:PlayerEventsQSProcess
void PlayerEventLogs::Process() void PlayerEventLogs::Process()
{ {
if (m_database_ping_timer.Check()) {
m_database->ping();
}
if (m_process_batch_events_timer.Check() || if (m_process_batch_events_timer.Check() ||
m_record_batch_queue.size() >= RuleI(Logging, BatchPlayerEventProcessChunkSize)) { m_record_batch_queue.size() >= RuleI(Logging, BatchPlayerEventProcessChunkSize)) {
ProcessBatchQueue(); ProcessBatchQueue();
+1
View File
@@ -113,6 +113,7 @@ private:
std::map<PlayerEvent::EventType, EtlSettings> m_etl_settings{}; std::map<PlayerEvent::EventType, EtlSettings> m_etl_settings{};
// timers // timers
Timer m_database_ping_timer; // database ping timer
Timer m_process_batch_events_timer; // events processing timer Timer m_process_batch_events_timer; // events processing timer
Timer m_process_retention_truncation_timer; // timer for truncating events based on retention settings Timer m_process_retention_truncation_timer; // timer for truncating events based on retention settings
+23
View File
@@ -54,6 +54,10 @@ double EvolvingItemsManager::CalculateProgression(const uint64 current_amount, c
void EvolvingItemsManager::DoLootChecks(const uint32 char_id, const uint16 slot_id, const EQ::ItemInstance &inst) const void EvolvingItemsManager::DoLootChecks(const uint32 char_id, const uint16 slot_id, const EQ::ItemInstance &inst) const
{ {
if (!inst) {
return;
}
inst.SetEvolveEquipped(false); inst.SetEvolveEquipped(false);
if (inst.IsEvolving() && slot_id <= EQ::invslot::EQUIPMENT_END && slot_id >= EQ::invslot::EQUIPMENT_BEGIN) { if (inst.IsEvolving() && slot_id <= EQ::invslot::EQUIPMENT_END && slot_id >= EQ::invslot::EQUIPMENT_BEGIN) {
inst.SetEvolveEquipped(true); inst.SetEvolveEquipped(true);
@@ -87,6 +91,10 @@ void EvolvingItemsManager::DoLootChecks(const uint32 char_id, const uint16 slot_
uint32 EvolvingItemsManager::GetFinalItemID(const EQ::ItemInstance &inst) const uint32 EvolvingItemsManager::GetFinalItemID(const EQ::ItemInstance &inst) const
{ {
if (!inst) {
return 0;
}
const auto start_iterator = std::ranges::find_if( const auto start_iterator = std::ranges::find_if(
evolving_items_manager.GetEvolvingItemsCache().cbegin(), evolving_items_manager.GetEvolvingItemsCache().cbegin(),
evolving_items_manager.GetEvolvingItemsCache().cend(), evolving_items_manager.GetEvolvingItemsCache().cend(),
@@ -116,6 +124,10 @@ uint32 EvolvingItemsManager::GetFinalItemID(const EQ::ItemInstance &inst) const
uint32 EvolvingItemsManager::GetNextEvolveItemID(const EQ::ItemInstance &inst) const uint32 EvolvingItemsManager::GetNextEvolveItemID(const EQ::ItemInstance &inst) const
{ {
if (!inst) {
return 0;
}
int8 const current_level = inst.GetEvolveLvl(); int8 const current_level = inst.GetEvolveLvl();
const auto iterator = std::ranges::find_if( const auto iterator = std::ranges::find_if(
@@ -191,6 +203,10 @@ uint64 EvolvingItemsManager::GetTotalEarnedXP(const EQ::ItemInstance &inst)
EvolveGetNextItem EvolvingItemsManager::GetNextItemByXP(const EQ::ItemInstance &inst_in, const int64 in_xp) EvolveGetNextItem EvolvingItemsManager::GetNextItemByXP(const EQ::ItemInstance &inst_in, const int64 in_xp)
{ {
EvolveGetNextItem ets{}; EvolveGetNextItem ets{};
if (!inst_in) {
return ets;
}
const auto evolve_items = GetEvolveIDItems(inst_in.GetEvolveLoreID()); const auto evolve_items = GetEvolveIDItems(inst_in.GetEvolveLoreID());
uint32 max_transfer_level = 0; uint32 max_transfer_level = 0;
int64 xp = in_xp; int64 xp = in_xp;
@@ -235,6 +251,9 @@ EvolveTransfer EvolvingItemsManager::DetermineTransferResults(
) )
{ {
EvolveTransfer ets{}; EvolveTransfer ets{};
if (!inst_from || !inst_to) {
return ets;
}
auto evolving_details_inst_from = evolving_items_manager.GetEvolveItemDetails(inst_from.GetID()); auto evolving_details_inst_from = evolving_items_manager.GetEvolveItemDetails(inst_from.GetID());
auto evolving_details_inst_to = evolving_items_manager.GetEvolveItemDetails(inst_to.GetID()); auto evolving_details_inst_to = evolving_items_manager.GetEvolveItemDetails(inst_to.GetID());
@@ -295,6 +314,10 @@ uint32 EvolvingItemsManager::GetFirstItemInLoreGroupByItemID(const uint32 item_i
void EvolvingItemsManager::LoadPlayerEvent(const EQ::ItemInstance &inst, PlayerEvent::EvolveItem &e) void EvolvingItemsManager::LoadPlayerEvent(const EQ::ItemInstance &inst, PlayerEvent::EvolveItem &e)
{ {
if (!inst) {
return;
}
e.item_id = inst.GetID(); e.item_id = inst.GetID();
e.item_name = inst.GetItem() ? inst.GetItem()->Name : std::string(); e.item_name = inst.GetItem() ? inst.GetItem()->Name : std::string();
e.level = inst.GetEvolveLvl(); e.level = inst.GetEvolveLvl();
+2 -2
View File
@@ -53,11 +53,11 @@ public:
ItemsEvolvingDetailsRepository::ItemsEvolvingDetails GetEvolveItemDetails(uint64 id); ItemsEvolvingDetailsRepository::ItemsEvolvingDetails GetEvolveItemDetails(uint64 id);
EvolveTransfer DetermineTransferResults(const EQ::ItemInstance& inst_from, const EQ::ItemInstance& inst_to); EvolveTransfer DetermineTransferResults(const EQ::ItemInstance& inst_from, const EQ::ItemInstance& inst_to);
EvolveGetNextItem GetNextItemByXP(const EQ::ItemInstance &inst_in, int64 in_xp); EvolveGetNextItem GetNextItemByXP(const EQ::ItemInstance &inst_in, int64 in_xp);
std::map<uint32, ItemsEvolvingDetailsRepository::ItemsEvolvingDetails>& GetEvolvingItemsCache() { return evolving_items_cache; } std::map<uint32, ItemsEvolvingDetailsRepository::ItemsEvolvingDetails>& GetEvolvingItemsCache() { return m_evolving_items_cache; }
std::vector<ItemsEvolvingDetailsRepository::ItemsEvolvingDetails> GetEvolveIDItems(uint32 evolve_id); std::vector<ItemsEvolvingDetailsRepository::ItemsEvolvingDetails> GetEvolveIDItems(uint32 evolve_id);
private: private:
std::map<uint32, ItemsEvolvingDetailsRepository::ItemsEvolvingDetails> evolving_items_cache; std::map<uint32, ItemsEvolvingDetailsRepository::ItemsEvolvingDetails> m_evolving_items_cache;
Database * m_db; Database * m_db;
Database * m_content_db; Database * m_content_db;
}; };
+142 -104
View File
@@ -1,12 +1,16 @@
#include "daybreak_connection.h" #include "daybreak_connection.h"
#include "../event/event_loop.h" #include "../event/event_loop.h"
#include "../event/task.h"
#include "../data_verification.h" #include "../data_verification.h"
#include "crc32.h" #include "crc32.h"
#include "../eqemu_logsys.h"
#include <zlib.h> #include <zlib.h>
#include <fmt/format.h> #include <fmt/format.h>
#include <sstream>
// observed client receive window is 300 packets, 140KB
constexpr size_t MAX_CLIENT_RECV_PACKETS_PER_WINDOW = 300;
constexpr size_t MAX_CLIENT_RECV_BYTES_PER_WINDOW = 140 * 1024;
// buffer pools
SendBufferPool send_buffer_pool;
EQ::Net::DaybreakConnectionManager::DaybreakConnectionManager() EQ::Net::DaybreakConnectionManager::DaybreakConnectionManager()
{ {
@@ -53,16 +57,22 @@ void EQ::Net::DaybreakConnectionManager::Attach(uv_loop_t *loop)
uv_ip4_addr("0.0.0.0", m_options.port, &recv_addr); uv_ip4_addr("0.0.0.0", m_options.port, &recv_addr);
int rc = uv_udp_bind(&m_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR); int rc = uv_udp_bind(&m_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);
rc = uv_udp_recv_start(&m_socket, rc = uv_udp_recv_start(
[](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { &m_socket,
buf->base = new char[suggested_size]; [](uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
memset(buf->base, 0, suggested_size); if (suggested_size > 65536) {
buf->len = suggested_size; buf->base = new char[suggested_size];
}, buf->len = suggested_size;
return;
}
static thread_local char temp_buf[65536];
buf->base = temp_buf;
buf->len = 65536;
},
[](uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) { [](uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) {
DaybreakConnectionManager *c = (DaybreakConnectionManager*)handle->data; DaybreakConnectionManager *c = (DaybreakConnectionManager*)handle->data;
if (nread < 0 || addr == nullptr) { if (nread < 0 || addr == nullptr) {
delete[] buf->base;
return; return;
} }
@@ -70,7 +80,10 @@ void EQ::Net::DaybreakConnectionManager::Attach(uv_loop_t *loop)
uv_ip4_name((const sockaddr_in*)addr, endpoint, 16); uv_ip4_name((const sockaddr_in*)addr, endpoint, 16);
auto port = ntohs(((const sockaddr_in*)addr)->sin_port); auto port = ntohs(((const sockaddr_in*)addr)->sin_port);
c->ProcessPacket(endpoint, port, buf->base, nread); c->ProcessPacket(endpoint, port, buf->base, nread);
delete[] buf->base;
if (buf->len > 65536) {
delete[] buf->base;
}
}); });
m_attached = loop; m_attached = loop;
@@ -310,7 +323,7 @@ EQ::Net::DaybreakConnection::DaybreakConnection(DaybreakConnectionManager *owner
m_last_session_stats = Clock::now(); m_last_session_stats = Clock::now();
m_outgoing_budget = owner->m_options.outgoing_data_rate; m_outgoing_budget = owner->m_options.outgoing_data_rate;
LogNetcode("New session [{}] with encode key [{}]", m_connect_code, HostToNetwork(m_encode_key)); LogNetClient("New session [{}] with encode key [{}]", m_connect_code, HostToNetwork(m_encode_key));
} }
//new connection made as client //new connection made as client
@@ -342,16 +355,16 @@ EQ::Net::DaybreakConnection::~DaybreakConnection()
void EQ::Net::DaybreakConnection::Close() void EQ::Net::DaybreakConnection::Close()
{ {
if (m_status == StatusConnected) { if (m_status != StatusDisconnected && m_status != StatusDisconnecting) {
FlushBuffer(); FlushBuffer();
SendDisconnect(); SendDisconnect();
}
if (m_status != StatusDisconnecting) {
m_close_time = Clock::now(); m_close_time = Clock::now();
ChangeStatus(StatusDisconnecting);
}
else {
ChangeStatus(StatusDisconnecting);
} }
ChangeStatus(StatusDisconnecting);
} }
void EQ::Net::DaybreakConnection::QueuePacket(Packet &p) void EQ::Net::DaybreakConnection::QueuePacket(Packet &p)
@@ -634,7 +647,7 @@ void EQ::Net::DaybreakConnection::ProcessDecodedPacket(const Packet &p)
p.PutSerialize(0, reply); p.PutSerialize(0, reply);
InternalSend(p); InternalSend(p);
LogNetcode("[OP_SessionRequest] Session [{}] started with encode key [{}]", m_connect_code, HostToNetwork(m_encode_key)); LogNetClient("[OP_SessionRequest] Session [{}] started with encode key [{}]", m_connect_code, HostToNetwork(m_encode_key));
} }
break; break;
@@ -653,7 +666,7 @@ void EQ::Net::DaybreakConnection::ProcessDecodedPacket(const Packet &p)
m_max_packet_size = reply.max_packet_size; m_max_packet_size = reply.max_packet_size;
ChangeStatus(StatusConnected); ChangeStatus(StatusConnected);
LogNetcode( LogNetClient(
"[OP_SessionResponse] Session [{}] refresh with encode key [{}]", "[OP_SessionResponse] Session [{}] refresh with encode key [{}]",
m_connect_code, m_connect_code,
HostToNetwork(m_encode_key) HostToNetwork(m_encode_key)
@@ -782,7 +795,7 @@ void EQ::Net::DaybreakConnection::ProcessDecodedPacket(const Packet &p)
SendDisconnect(); SendDisconnect();
} }
LogNetcode( LogNetClient(
"[OP_SessionDisconnect] Session [{}] disconnect with encode key [{}]", "[OP_SessionDisconnect] Session [{}] disconnect with encode key [{}]",
m_connect_code, m_connect_code,
HostToNetwork(m_encode_key) HostToNetwork(m_encode_key)
@@ -852,7 +865,7 @@ bool EQ::Net::DaybreakConnection::ValidateCRC(Packet &p)
} }
if (p.Length() < (size_t)m_crc_bytes) { if (p.Length() < (size_t)m_crc_bytes) {
LogNetcode("Session [{}] ignored packet (crc bytes invalid on session)", m_connect_code); LogNetClient("Session [{}] ignored packet (crc bytes invalid on session)", m_connect_code);
return false; return false;
} }
@@ -1043,7 +1056,7 @@ void EQ::Net::DaybreakConnection::Decompress(Packet &p, size_t offset, size_t le
return; return;
} }
static uint8_t new_buffer[4096]; static thread_local uint8_t new_buffer[4096];
uint8_t *buffer = (uint8_t*)p.Data() + offset; uint8_t *buffer = (uint8_t*)p.Data() + offset;
uint32_t new_length = 0; uint32_t new_length = 0;
@@ -1064,7 +1077,7 @@ void EQ::Net::DaybreakConnection::Decompress(Packet &p, size_t offset, size_t le
void EQ::Net::DaybreakConnection::Compress(Packet &p, size_t offset, size_t length) void EQ::Net::DaybreakConnection::Compress(Packet &p, size_t offset, size_t length)
{ {
uint8_t new_buffer[2048] = { 0 }; static thread_local uint8_t new_buffer[2048] = { 0 };
uint8_t *buffer = (uint8_t*)p.Data() + offset; uint8_t *buffer = (uint8_t*)p.Data() + offset;
uint32_t new_length = 0; uint32_t new_length = 0;
bool send_uncompressed = true; bool send_uncompressed = true;
@@ -1091,10 +1104,6 @@ void EQ::Net::DaybreakConnection::ProcessResend()
} }
} }
// observed client receive window is 300 packets, 140KB
constexpr size_t MAX_CLIENT_RECV_PACKETS_PER_WINDOW = 300;
constexpr size_t MAX_CLIENT_RECV_BYTES_PER_WINDOW = 140 * 1024;
void EQ::Net::DaybreakConnection::ProcessResend(int stream) void EQ::Net::DaybreakConnection::ProcessResend(int stream)
{ {
if (m_status == DbProtocolStatus::StatusDisconnected) { if (m_status == DbProtocolStatus::StatusDisconnected) {
@@ -1117,34 +1126,57 @@ void EQ::Net::DaybreakConnection::ProcessResend(int stream)
auto &first_packet = s->sent_packets.begin()->second; auto &first_packet = s->sent_packets.begin()->second;
auto time_since_first_sent = std::chrono::duration_cast<std::chrono::milliseconds>(now - first_packet.first_sent).count(); auto time_since_first_sent = std::chrono::duration_cast<std::chrono::milliseconds>(now - first_packet.first_sent).count();
if (time_since_first_sent >= m_owner->m_options.resend_timeout) {
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
auto first_sent_ms = std::chrono::duration_cast<std::chrono::milliseconds>(first_packet.first_sent.time_since_epoch()).count();
LogNetClient(
"Closing connection for m_endpoint [{}] m_port [{}] time_since_first_sent [{}] >= m_owner->m_options.resend_timeout [{}] now [{}] first_packet.first_sent [{}]",
m_endpoint,
m_port,
time_since_first_sent,
m_owner->m_options.resend_timeout,
now_ms,
first_sent_ms
);
Close();
return;
}
if (m_last_ack - now > std::chrono::milliseconds(1000)) {
LogNetClient(
"Resetting m_acked_since_last_resend flag for m_endpoint [{}] m_port [{}]",
m_endpoint,
m_port
);
m_acked_since_last_resend = true;
}
// make sure that the first_packet in the list first_sent time is within the resend_delay and now // make sure that the first_packet in the list first_sent time is within the resend_delay and now
// if it is not, then we need to resend all packets in the list // if it is not, then we need to resend all packets in the list
if (time_since_first_sent <= first_packet.resend_delay && !m_acked_since_last_resend) { if (time_since_first_sent <= first_packet.resend_delay && !m_acked_since_last_resend) {
LogNetcodeDetail( LogNetClientDetail(
"Not resending packets for stream [{}] time since first sent [{}] resend delay [{}] m_acked_since_last_resend [{}]", "Not resending packets for m_endpoint [{}] m_port [{}] packets [{}] time_first_sent [{}] resend_delay [{}] m_acked_since_last_resend [{}]",
stream, m_endpoint,
m_port,
s->sent_packets.size(),
time_since_first_sent, time_since_first_sent,
first_packet.resend_delay, first_packet.resend_delay,
m_acked_since_last_resend m_acked_since_last_resend
); );
return; return;
} }
if (time_since_first_sent >= m_owner->m_options.resend_timeout) {
Close();
return;
}
} }
if (LogSys.IsLogEnabled(Logs::Detail, Logs::Netcode)) { if (LogSys.IsLogEnabled(Logs::General, Logs::NetClient)) {
size_t total_size = 0; size_t total_size = 0;
for (auto &e: s->sent_packets) { for (auto &e: s->sent_packets) {
total_size += e.second.packet.Length(); total_size += e.second.packet.Length();
} }
LogNetcodeDetail( LogNetClientDetail(
"Resending packets for stream [{}] packet count [{}] total packet size [{}] m_acked_since_last_resend [{}]", "Resending packets for m_endpoint [{}] m_port [{}] packet count [{}] total packet size [{}] m_acked_since_last_resend [{}]",
stream, m_endpoint,
m_port,
s->sent_packets.size(), s->sent_packets.size(),
total_size, total_size,
m_acked_since_last_resend m_acked_since_last_resend
@@ -1154,10 +1186,13 @@ void EQ::Net::DaybreakConnection::ProcessResend(int stream)
for (auto &e: s->sent_packets) { for (auto &e: s->sent_packets) {
if (m_resend_packets_sent >= MAX_CLIENT_RECV_PACKETS_PER_WINDOW || if (m_resend_packets_sent >= MAX_CLIENT_RECV_PACKETS_PER_WINDOW ||
m_resend_bytes_sent >= MAX_CLIENT_RECV_BYTES_PER_WINDOW) { m_resend_bytes_sent >= MAX_CLIENT_RECV_BYTES_PER_WINDOW) {
LogNetcodeDetail( LogNetClient(
"Stopping resend because we hit thresholds m_resend_packets_sent [{}] max [{}] m_resend_bytes_sent [{}] max [{}]", "Stopping resend because we hit thresholds for m_endpoint [{}] m_port [{}] m_resend_packets_sent [{}] max [{}] in_queue [{}] m_resend_bytes_sent [{}] max [{}]",
m_endpoint,
m_port,
m_resend_packets_sent, m_resend_packets_sent,
MAX_CLIENT_RECV_PACKETS_PER_WINDOW, MAX_CLIENT_RECV_PACKETS_PER_WINDOW,
s->sent_packets.size(),
m_resend_bytes_sent, m_resend_bytes_sent,
MAX_CLIENT_RECV_BYTES_PER_WINDOW MAX_CLIENT_RECV_BYTES_PER_WINDOW
); );
@@ -1194,11 +1229,11 @@ void EQ::Net::DaybreakConnection::ProcessResend(int stream)
} }
m_acked_since_last_resend = false; m_acked_since_last_resend = false;
m_last_ack = now;
} }
void EQ::Net::DaybreakConnection::Ack(int stream, uint16_t seq) void EQ::Net::DaybreakConnection::Ack(int stream, uint16_t seq)
{ {
auto now = Clock::now(); auto now = Clock::now();
auto s = &m_streams[stream]; auto s = &m_streams[stream];
auto iter = s->sent_packets.begin(); auto iter = s->sent_packets.begin();
@@ -1214,12 +1249,14 @@ void EQ::Net::DaybreakConnection::Ack(int stream, uint16_t seq)
m_rolling_ping = (m_rolling_ping * 2 + round_time) / 3; m_rolling_ping = (m_rolling_ping * 2 + round_time) / 3;
iter = s->sent_packets.erase(iter); iter = s->sent_packets.erase(iter);
m_acked_since_last_resend = true;
} }
else { else {
++iter; ++iter;
} }
} }
m_acked_since_last_resend = true;
m_last_ack = now;
} }
void EQ::Net::DaybreakConnection::OutOfOrderAck(int stream, uint16_t seq) void EQ::Net::DaybreakConnection::OutOfOrderAck(int stream, uint16_t seq)
@@ -1237,6 +1274,9 @@ void EQ::Net::DaybreakConnection::OutOfOrderAck(int stream, uint16_t seq)
s->sent_packets.erase(iter); s->sent_packets.erase(iter);
} }
m_acked_since_last_resend = true;
m_last_ack = now;
} }
void EQ::Net::DaybreakConnection::UpdateDataBudget(double budget_add) void EQ::Net::DaybreakConnection::UpdateDataBudget(double budget_add)
@@ -1333,99 +1373,97 @@ void EQ::Net::DaybreakConnection::SendKeepAlive()
InternalSend(p); InternalSend(p);
} }
void EQ::Net::DaybreakConnection::InternalSend(Packet &p) void EQ::Net::DaybreakConnection::InternalSend(Packet &p) {
{
if (m_owner->m_options.outgoing_data_rate > 0.0) { if (m_owner->m_options.outgoing_data_rate > 0.0) {
auto new_budget = m_outgoing_budget - (p.Length() / 1024.0); auto new_budget = m_outgoing_budget - (p.Length() / 1024.0);
if (new_budget <= 0.0) { if (new_budget <= 0.0) {
m_stats.dropped_datarate_packets++; m_stats.dropped_datarate_packets++;
return; return;
} } else {
else {
m_outgoing_budget = new_budget; m_outgoing_budget = new_budget;
} }
} }
m_last_send = Clock::now(); m_last_send = Clock::now();
auto send_func = [](uv_udp_send_t* req, int status) { auto pooled_opt = send_buffer_pool.acquire();
delete[](char*)req->data; if (!pooled_opt) {
delete req; m_stats.dropped_datarate_packets++;
}; return;
}
auto [send_req, data, ctx] = *pooled_opt;
ctx->pool = &send_buffer_pool; // set pool pointer
sockaddr_in send_addr{};
uv_ip4_addr(m_endpoint.c_str(), m_port, &send_addr);
uv_buf_t send_buffers[1];
if (PacketCanBeEncoded(p)) { if (PacketCanBeEncoded(p)) {
m_stats.bytes_before_encode += p.Length(); m_stats.bytes_before_encode += p.Length();
DynamicPacket out; DynamicPacket out;
out.PutPacket(0, p); out.PutPacket(0, p);
for (int i = 0; i < 2; ++i) { for (auto &m_encode_passe: m_encode_passes) {
switch (m_encode_passes[i]) { switch (m_encode_passe) {
case EncodeCompression: case EncodeCompression:
if (out.GetInt8(0) == 0) if (out.GetInt8(0) == 0) {
Compress(out, DaybreakHeader::size(), out.Length() - DaybreakHeader::size()); Compress(out, DaybreakHeader::size(), out.Length() - DaybreakHeader::size());
else } else {
Compress(out, 1, out.Length() - 1); Compress(out, 1, out.Length() - 1);
break; }
case EncodeXOR: break;
if (out.GetInt8(0) == 0) case EncodeXOR:
Encode(out, DaybreakHeader::size(), out.Length() - DaybreakHeader::size()); if (out.GetInt8(0) == 0) {
else Encode(out, DaybreakHeader::size(), out.Length() - DaybreakHeader::size());
Encode(out, 1, out.Length() - 1); } else {
break; Encode(out, 1, out.Length() - 1);
default: }
break; break;
default:
break;
} }
} }
AppendCRC(out); AppendCRC(out);
uv_udp_send_t *send_req = new uv_udp_send_t;
memset(send_req, 0, sizeof(*send_req));
sockaddr_in send_addr;
uv_ip4_addr(m_endpoint.c_str(), m_port, &send_addr);
uv_buf_t send_buffers[1];
char *data = new char[out.Length()];
memcpy(data, out.Data(), out.Length()); memcpy(data, out.Data(), out.Length());
send_buffers[0] = uv_buf_init(data, out.Length()); send_buffers[0] = uv_buf_init(data, out.Length());
send_req->data = send_buffers[0].base; } else {
memcpy(data, p.Data(), p.Length());
m_stats.sent_bytes += out.Length(); send_buffers[0] = uv_buf_init(data, p.Length());
m_stats.sent_packets++;
if (m_owner->m_options.simulated_out_packet_loss && m_owner->m_options.simulated_out_packet_loss >= m_owner->m_rand.Int(0, 100)) {
delete[](char*)send_req->data;
delete send_req;
return;
}
uv_udp_send(send_req, &m_owner->m_socket, send_buffers, 1, (sockaddr*)&send_addr, send_func);
return;
} }
m_stats.bytes_before_encode += p.Length();
uv_udp_send_t *send_req = new uv_udp_send_t;
sockaddr_in send_addr;
uv_ip4_addr(m_endpoint.c_str(), m_port, &send_addr);
uv_buf_t send_buffers[1];
char *data = new char[p.Length()];
memcpy(data, p.Data(), p.Length());
send_buffers[0] = uv_buf_init(data, p.Length());
send_req->data = send_buffers[0].base;
m_stats.sent_bytes += p.Length(); m_stats.sent_bytes += p.Length();
m_stats.sent_packets++; m_stats.sent_packets++;
if (m_owner->m_options.simulated_out_packet_loss && m_owner->m_options.simulated_out_packet_loss >= m_owner->m_rand.Int(0, 100)) { if (m_owner->m_options.simulated_out_packet_loss &&
delete[](char*)send_req->data; m_owner->m_options.simulated_out_packet_loss >= m_owner->m_rand.Int(0, 100)) {
delete send_req; send_buffer_pool.release(ctx);
return; return;
} }
uv_udp_send(send_req, &m_owner->m_socket, send_buffers, 1, (sockaddr*)&send_addr, send_func); int send_result = uv_udp_send(
send_req, &m_owner->m_socket, send_buffers, 1, (sockaddr *)&send_addr,
[](uv_udp_send_t *req, int status) {
auto *ctx = reinterpret_cast<EmbeddedContext *>(req->data);
if (!ctx) {
std::cerr << "Error: send_req->data is null in callback!" << std::endl;
return;
}
if (status < 0) {
std::cerr << "uv_udp_send failed: " << uv_strerror(status) << std::endl;
}
ctx->pool->release(ctx);
}
);
if (send_result < 0) {
std::cerr << "uv_udp_send() failed: " << uv_strerror(send_result) << std::endl;
send_buffer_pool.release(ctx);
}
} }
void EQ::Net::DaybreakConnection::InternalQueuePacket(Packet &p, int stream_id, bool reliable) void EQ::Net::DaybreakConnection::InternalQueuePacket(Packet &p, int stream_id, bool reliable)
+2
View File
@@ -3,6 +3,7 @@
#include "../random.h" #include "../random.h"
#include "packet.h" #include "packet.h"
#include "daybreak_structs.h" #include "daybreak_structs.h"
#include "daybreak_pooling.h"
#include <uv.h> #include <uv.h>
#include <chrono> #include <chrono>
#include <functional> #include <functional>
@@ -185,6 +186,7 @@ namespace EQ
size_t m_resend_packets_sent = 0; size_t m_resend_packets_sent = 0;
size_t m_resend_bytes_sent = 0; size_t m_resend_bytes_sent = 0;
bool m_acked_since_last_resend = false; bool m_acked_since_last_resend = false;
Timestamp m_last_ack;
struct DaybreakSentPacket struct DaybreakSentPacket
{ {
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include <optional>
#include <atomic>
#include <memory>
#include <array>
#include <vector>
#include <mutex>
#include <iostream>
#include "../eqemu_logsys.h"
#include <uv.h>
constexpr size_t UDP_BUFFER_SIZE = 512;
struct EmbeddedContext {
size_t pool_index;
class SendBufferPool* pool;
};
class SendBufferPool {
public:
explicit SendBufferPool(size_t initial_capacity = 64)
: m_capacity(initial_capacity), m_head(0)
{
LogNetClient("[SendBufferPool] Initializing with capacity [{}]", (int)m_capacity);
m_pool.reserve(m_capacity);
m_locks = std::make_unique<std::atomic_bool[]>(m_capacity);
for (size_t i = 0; i < m_capacity; ++i) {
auto* req = new PooledUdpSend();
req->context.pool_index = i;
req->context.pool = this;
req->uv_req.data = &req->context;
m_pool.emplace_back(std::unique_ptr<PooledUdpSend>(req));
m_locks[i].store(false, std::memory_order_relaxed);
}
}
std::optional<std::tuple<uv_udp_send_t*, char*, EmbeddedContext*>> acquire() {
size_t cap = m_capacity.load(std::memory_order_acquire);
for (size_t i = 0; i < cap; ++i) {
size_t index = m_head.fetch_add(1, std::memory_order_relaxed) % cap;
bool expected = false;
if (m_locks[index].compare_exchange_strong(expected, true)) {
auto* req = m_pool[index].get();
LogNetClientDetail("[SendBufferPool] Acquired [{}]", index);
return std::make_tuple(&req->uv_req, req->buffer.data(), &req->context);
}
}
LogNetClient("[SendBufferPool] Growing from [{}] to [{}]", cap, cap * 2);
grow();
return acquireAfterGrowth();
}
void release(EmbeddedContext* ctx) {
if (!ctx || ctx->pool != this || ctx->pool_index >= m_capacity.load(std::memory_order_acquire)) {
LogNetClient("[SendBufferPool] Invalid context release [{}]", ctx ? ctx->pool_index : -1);
return;
}
m_locks[ctx->pool_index].store(false, std::memory_order_release);
LogNetClientDetail("[SendBufferPool] Released [{}]", ctx->pool_index);
}
private:
struct PooledUdpSend {
uv_udp_send_t uv_req;
std::array<char, UDP_BUFFER_SIZE> buffer;
EmbeddedContext context;
};
std::vector<std::unique_ptr<PooledUdpSend>> m_pool;
std::unique_ptr<std::atomic_bool[]> m_locks;
std::atomic<size_t> m_capacity;
std::atomic<size_t> m_head;
std::mutex m_grow_mutex;
void grow() {
std::lock_guard<std::mutex> lock(m_grow_mutex);
size_t old_cap = m_capacity.load(std::memory_order_acquire);
size_t new_cap = old_cap * 2;
m_pool.reserve(new_cap);
for (size_t i = old_cap; i < new_cap; ++i) {
auto* req = new PooledUdpSend();
req->context.pool_index = i;
req->context.pool = this;
req->uv_req.data = &req->context;
m_pool.emplace_back(std::unique_ptr<PooledUdpSend>(req));
}
auto new_locks = std::make_unique<std::atomic_bool[]>(new_cap);
for (size_t i = 0; i < old_cap; ++i) {
new_locks[i].store(m_locks[i].load(std::memory_order_acquire));
}
for (size_t i = old_cap; i < new_cap; ++i) {
new_locks[i].store(false, std::memory_order_relaxed);
}
m_locks = std::move(new_locks);
m_capacity.store(new_cap, std::memory_order_release);
LogNetClient("[SendBufferPool] Grew to [{}] from [{}]", new_cap, old_cap);
}
std::optional<std::tuple<uv_udp_send_t*, char*, EmbeddedContext*>> acquireAfterGrowth() {
size_t cap = m_capacity.load(std::memory_order_acquire);
for (size_t i = 0; i < cap; ++i) {
size_t index = m_head.fetch_add(1, std::memory_order_relaxed) % cap;
bool expected = false;
if (m_locks[index].compare_exchange_strong(expected, true)) {
auto* req = m_pool[index].get();
LogNetClient("[SendBufferPool] Acquired after grow [{}]", index);
return std::make_tuple(&req->uv_req, req->buffer.data(), &req->context);
}
}
return std::nullopt;
}
};
+1
View File
@@ -171,3 +171,4 @@ namespace EQ
}; };
} }
} }
+3 -3
View File
@@ -62,15 +62,15 @@ void EQ::Net::ServertalkClient::Connect()
m_connecting = true; m_connecting = true;
EQ::Net::TCPConnection::Connect(m_addr, m_port, false, [this](std::shared_ptr<EQ::Net::TCPConnection> connection) { EQ::Net::TCPConnection::Connect(m_addr, m_port, false, [this](std::shared_ptr<EQ::Net::TCPConnection> connection) {
if (connection == nullptr) { if (connection == nullptr) {
LogF(Logs::General, Logs::TCPConnection, "Error connecting to {0}:{1}, attempting to reconnect...", m_addr, m_port); LogNetTCP("Error connecting to {0}:{1}, attempting to reconnect...", m_addr, m_port);
m_connecting = false; m_connecting = false;
return; return;
} }
LogF(Logs::General, Logs::TCPConnection, "Connected to {0}:{1}", m_addr, m_port); LogNetTCP("Connected to {0}:{1}", m_addr, m_port);
m_connection = connection; m_connection = connection;
m_connection->OnDisconnect([this](EQ::Net::TCPConnection *c) { m_connection->OnDisconnect([this](EQ::Net::TCPConnection *c) {
LogF(Logs::General, Logs::TCPConnection, "Connection lost to {0}:{1}, attempting to reconnect...", m_addr, m_port); LogNetTCP("Connection lost to {0}:{1}, attempting to reconnect...", m_addr, m_port);
m_connection.reset(); m_connection.reset();
}); });
@@ -58,15 +58,15 @@ void EQ::Net::ServertalkLegacyClient::Connect()
m_connecting = true; m_connecting = true;
EQ::Net::TCPConnection::Connect(m_addr, m_port, false, [this](std::shared_ptr<EQ::Net::TCPConnection> connection) { EQ::Net::TCPConnection::Connect(m_addr, m_port, false, [this](std::shared_ptr<EQ::Net::TCPConnection> connection) {
if (connection == nullptr) { if (connection == nullptr) {
LogF(Logs::General, Logs::TCPConnection, "Error connecting to {0}:{1}, attempting to reconnect...", m_addr, m_port); LogNetTCP("Error connecting to {0}:{1}, attempting to reconnect...", m_addr, m_port);
m_connecting = false; m_connecting = false;
return; return;
} }
LogF(Logs::General, Logs::TCPConnection, "Connected to {0}:{1}", m_addr, m_port); LogNetTCP("Connected to {0}:{1}", m_addr, m_port);
m_connection = connection; m_connection = connection;
m_connection->OnDisconnect([this](EQ::Net::TCPConnection *c) { m_connection->OnDisconnect([this](EQ::Net::TCPConnection *c) {
LogF(Logs::General, Logs::TCPConnection, "Connection lost to {0}:{1}, attempting to reconnect...", m_addr, m_port); LogNetTCP("Connection lost to {0}:{1}, attempting to reconnect...", m_addr, m_port);
m_connection.reset(); m_connection.reset();
}); });
+108 -55
View File
@@ -1,5 +1,8 @@
#include "tcp_connection.h" #include "tcp_connection.h"
#include "../event/event_loop.h" #include "../event/event_loop.h"
#include <iostream>
WriteReqPool tcp_write_pool;
void on_close_handle(uv_handle_t* handle) { void on_close_handle(uv_handle_t* handle) {
delete (uv_tcp_t *)handle; delete (uv_tcp_t *)handle;
@@ -64,36 +67,37 @@ void EQ::Net::TCPConnection::Connect(const std::string &addr, int port, bool ipv
}); });
} }
void EQ::Net::TCPConnection::Start() { void EQ::Net::TCPConnection::Start()
uv_read_start((uv_stream_t*)m_socket, [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { {
buf->base = new char[suggested_size]; uv_read_start(
buf->len = suggested_size; (uv_stream_t *) m_socket, [](uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
}, [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { if (suggested_size > 65536) {
buf->base = new char[suggested_size];
buf->len = suggested_size;
return;
}
TCPConnection *connection = (TCPConnection*)stream->data; static thread_local char temp_buf[65536];
buf->base = temp_buf;
buf->len = 65536;
}, [](uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
auto *connection = (TCPConnection *) stream->data;
if (nread > 0) { if (nread > 0) {
connection->Read(buf->base, nread); connection->Read(buf->base, nread);
}
else if (nread == UV_EOF) {
connection->Disconnect();
}
else if (nread < 0) {
connection->Disconnect();
}
if (buf->base) { if (buf->len > 65536) {
delete[] buf->base; delete [] buf->base;
} }
} }
else if (nread == UV_EOF) { );
connection->Disconnect();
if (buf->base) {
delete[] buf->base;
}
}
else if (nread < 0) {
connection->Disconnect();
if (buf->base) {
delete[] buf->base;
}
}
});
} }
void EQ::Net::TCPConnection::OnRead(std::function<void(TCPConnection*, const unsigned char*, size_t)> cb) void EQ::Net::TCPConnection::OnRead(std::function<void(TCPConnection*, const unsigned char*, size_t)> cb)
@@ -130,43 +134,92 @@ void EQ::Net::TCPConnection::Read(const char *data, size_t count)
} }
} }
void EQ::Net::TCPConnection::Write(const char *data, size_t count) void EQ::Net::TCPConnection::Write(const char* data, size_t count) {
{ if (!m_socket || !data || count == 0) {
if (!m_socket) { std::cerr << "TCPConnection::Write - Invalid socket or data\n";
return; return;
} }
struct WriteBaton if (count <= TCP_BUFFER_SIZE) {
{ // Fast path: use pooled request with embedded buffer
TCPConnection *connection; auto req_opt = tcp_write_pool.acquire();
char *buffer; if (!req_opt) {
}; std::cerr << "TCPConnection::Write - Out of write requests\n";
return;
WriteBaton *baton = new WriteBaton;
baton->connection = this;
baton->buffer = new char[count];
uv_write_t *write_req = new uv_write_t;
memset(write_req, 0, sizeof(uv_write_t));
write_req->data = baton;
uv_buf_t send_buffers[1];
memcpy(baton->buffer, data, count);
send_buffers[0] = uv_buf_init(baton->buffer, count);
uv_write(write_req, (uv_stream_t*)m_socket, send_buffers, 1, [](uv_write_t* req, int status) {
WriteBaton *baton = (WriteBaton*)req->data;
delete[] baton->buffer;
delete req;
if (status < 0) {
baton->connection->Disconnect();
} }
delete baton; TCPWriteReq* write_req = *req_opt;
});
// Fill buffer and set context
memcpy(write_req->buffer.data(), data, count);
write_req->connection = this;
write_req->magic = 0xC0FFEE;
uv_buf_t buf = uv_buf_init(write_req->buffer.data(), static_cast<unsigned int>(count));
int result = uv_write(
&write_req->req,
reinterpret_cast<uv_stream_t*>(m_socket),
&buf,
1,
[](uv_write_t* req, int status) {
auto* full_req = reinterpret_cast<TCPWriteReq*>(req);
if (full_req->magic != 0xC0FFEE) {
std::cerr << "uv_write callback - invalid magic, skipping release\n";
return;
}
tcp_write_pool.release(full_req);
if (status < 0 && full_req->connection) {
std::cerr << "uv_write failed: " << uv_strerror(status) << std::endl;
full_req->connection->Disconnect();
}
}
);
if (result < 0) {
std::cerr << "uv_write() failed immediately: " << uv_strerror(result) << std::endl;
tcp_write_pool.release(write_req);
}
} else {
// Slow path: allocate heap buffer for large write
LogNetTCP("[TCPConnection] Large write of [{}] bytes, using heap buffer", count);
char* heap_buffer = new char[count];
memcpy(heap_buffer, data, count);
uv_write_t* write_req = new uv_write_t;
write_req->data = heap_buffer;
uv_buf_t buf = uv_buf_init(heap_buffer, static_cast<unsigned int>(count));
int result = uv_write(
write_req,
reinterpret_cast<uv_stream_t*>(m_socket),
&buf,
1,
[](uv_write_t* req, int status) {
char* data = static_cast<char*>(req->data);
delete[] data;
delete req;
if (status < 0) {
std::cerr << "uv_write (large) failed: " << uv_strerror(status) << std::endl;
}
}
);
if (result < 0) {
std::cerr << "uv_write() (large) failed immediately: " << uv_strerror(result) << std::endl;
delete[] heap_buffer;
delete write_req;
}
}
} }
std::string EQ::Net::TCPConnection::LocalIP() const std::string EQ::Net::TCPConnection::LocalIP() const
{ {
sockaddr_storage addr; sockaddr_storage addr;
+1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "tcp_connection_pooling.h"
#include <functional> #include <functional>
#include <string> #include <string>
#include <memory> #include <memory>
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include "../eqemu_logsys.h"
#include <vector>
#include <array>
#include <atomic>
#include <memory>
#include <optional>
#include <mutex>
#include <uv.h>
#include <iostream>
namespace EQ { namespace Net { class TCPConnection; } }
constexpr size_t TCP_BUFFER_SIZE = 8192;
struct TCPWriteReq {
uv_write_t req{};
std::array<char, TCP_BUFFER_SIZE> buffer{};
size_t buffer_index{};
EQ::Net::TCPConnection* connection{};
uint32_t magic = 0xC0FFEE;
};
class WriteReqPool {
public:
explicit WriteReqPool(size_t initial_capacity = 512)
: m_capacity(initial_capacity), m_head(0) {
initialize_pool(m_capacity);
}
std::optional<TCPWriteReq*> acquire() {
size_t cap = m_capacity.load(std::memory_order_acquire);
for (size_t i = 0; i < cap; ++i) {
size_t index = m_head.fetch_add(1, std::memory_order_relaxed) % cap;
bool expected = false;
if (m_locks[index].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
LogNetTCPDetail("[WriteReqPool] Acquired buffer index [{}]", index);
return m_reqs[index].get();
}
}
LogNetTCP("[WriteReqPool] Growing from [{}] to [{}]", cap, cap * 2);
grow();
return acquireAfterGrow();
}
void release(TCPWriteReq* req) {
if (!req) return;
const size_t index = req->buffer_index;
const size_t cap = m_capacity.load(std::memory_order_acquire);
if (index >= cap || m_reqs[index].get() != req) {
std::cerr << "WriteReqPool::release - Invalid or stale pointer (index=" << index << ")\n";
return;
}
m_locks[index].store(false, std::memory_order_release);
LogNetTCPDetail("[WriteReqPool] Released buffer index [{}]", index);
}
private:
std::vector<std::unique_ptr<TCPWriteReq>> m_reqs;
std::unique_ptr<std::atomic_bool[]> m_locks;
std::atomic<size_t> m_capacity;
std::atomic<size_t> m_head;
std::mutex m_grow_mutex;
void initialize_pool(size_t count) {
m_reqs.reserve(count);
m_locks = std::make_unique<std::atomic_bool[]>(count);
for (size_t i = 0; i < count; ++i) {
auto req = std::make_unique<TCPWriteReq>();
req->buffer_index = i;
req->req.data = req.get(); // optional: for use in libuv callbacks
m_locks[i].store(false, std::memory_order_relaxed);
m_reqs.emplace_back(std::move(req));
}
m_capacity.store(count, std::memory_order_release);
}
void grow() {
std::lock_guard<std::mutex> lock(m_grow_mutex);
const size_t old_cap = m_capacity.load(std::memory_order_acquire);
const size_t new_cap = old_cap * 2;
m_reqs.reserve(new_cap);
for (size_t i = old_cap; i < new_cap; ++i) {
auto req = std::make_unique<TCPWriteReq>();
req->buffer_index = i;
req->req.data = req.get(); // optional
m_reqs.emplace_back(std::move(req));
}
auto new_locks = std::make_unique<std::atomic_bool[]>(new_cap);
for (size_t i = 0; i < old_cap; ++i) {
new_locks[i].store(m_locks[i].load(std::memory_order_acquire));
}
for (size_t i = old_cap; i < new_cap; ++i) {
new_locks[i].store(false, std::memory_order_relaxed);
}
m_locks = std::move(new_locks);
m_capacity.store(new_cap, std::memory_order_release);
}
std::optional<TCPWriteReq*> acquireAfterGrow() {
const size_t cap = m_capacity.load(std::memory_order_acquire);
for (size_t i = 0; i < cap; ++i) {
bool expected = false;
if (m_locks[i].compare_exchange_strong(expected, true, std::memory_order_acquire)) {
LogNetTCP("[WriteReqPool] Acquired buffer index [{}] after grow", i);
return m_reqs[i].get();
}
}
return std::nullopt;
}
};
+7 -7
View File
@@ -4688,7 +4688,7 @@ namespace RoF2
Bitfields->linkdead = 0; Bitfields->linkdead = 0;
Bitfields->showhelm = emu->showhelm; Bitfields->showhelm = emu->showhelm;
Bitfields->trader = emu->trader ? 1 : 0; Bitfields->trader = emu->trader ? 1 : 0;
Bitfields->targetable = 1; Bitfields->targetable = emu->NPC ? emu->untargetable : 1;
Bitfields->targetable_with_hotkey = emu->targetable_with_hotkey ? 1 : 0; Bitfields->targetable_with_hotkey = emu->targetable_with_hotkey ? 1 : 0;
Bitfields->showname = ShowName; Bitfields->showname = ShowName;
@@ -4839,13 +4839,13 @@ namespace RoF2
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->petOwnerId); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->petOwnerId);
VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // FindBits MQ2 name VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // FindBits MQ2 name
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->PlayerState); VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->PlayerState);
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // NpcTintIndex VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->npc_tint_id); // NpcTintIndex
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // PrimaryTintIndex VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // PrimaryTintIndex
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // SecondaryTintIndex VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // SecondaryTintIndex
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // These do something with OP_WeaponEquip1 VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // These do something with OP_WeaponEquip1
VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // ^ VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // ^
if ((emu->NPC == 0) || (emu->race <= Race::Gnome) || (emu->race == Race::Iksar) || if ((emu->NPC == 0) || (emu->race <= Race::Gnome) || (emu->race == Race::Iksar) ||
(emu->race == Race::VahShir) || (emu->race == Race::Froglok2) || (emu->race == Race::Drakkin) (emu->race == Race::VahShir) || (emu->race == Race::Froglok2) || (emu->race == Race::Drakkin)
+11 -11
View File
@@ -4908,12 +4908,12 @@ namespace UF
UFSlot = serverSlot - 2; UFSlot = serverSlot - 2;
} }
else if (serverSlot <= EQ::invbag::GENERAL_BAGS_8_END && serverSlot >= EQ::invbag::GENERAL_BAGS_BEGIN) { else if (serverSlot <= EQ::invbag::GENERAL_BAGS_END && serverSlot >= EQ::invbag::GENERAL_BAGS_BEGIN) {
UFSlot = serverSlot + 11; UFSlot = serverSlot - (EQ::invbag::GENERAL_BAGS_BEGIN - invbag::GENERAL_BAGS_BEGIN)/*3748*/ - ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((serverSlot - EQ::invbag::GENERAL_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT)); // + 11;
} }
else if (serverSlot <= EQ::invbag::CURSOR_BAG_END && serverSlot >= EQ::invbag::CURSOR_BAG_BEGIN) { else if (serverSlot <= EQ::invbag::CURSOR_BAG_END && serverSlot >= EQ::invbag::CURSOR_BAG_BEGIN) {
UFSlot = serverSlot - 9; UFSlot = serverSlot - (EQ::invbag::CURSOR_BAG_BEGIN - invbag::CURSOR_BAG_BEGIN)/*5668*/; // - 9;
} }
else if (serverSlot <= EQ::invslot::TRIBUTE_END && serverSlot >= EQ::invslot::TRIBUTE_BEGIN) { else if (serverSlot <= EQ::invslot::TRIBUTE_END && serverSlot >= EQ::invslot::TRIBUTE_BEGIN) {
@@ -4933,7 +4933,7 @@ namespace UF
} }
else if (serverSlot <= EQ::invbag::BANK_BAGS_END && serverSlot >= EQ::invbag::BANK_BAGS_BEGIN) { else if (serverSlot <= EQ::invbag::BANK_BAGS_END && serverSlot >= EQ::invbag::BANK_BAGS_BEGIN) {
UFSlot = serverSlot + 1; UFSlot = serverSlot - (EQ::invbag::BANK_BAGS_BEGIN - invbag::BANK_BAGS_BEGIN) - ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((serverSlot - EQ::invbag::BANK_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT)); // + 1;
} }
else if (serverSlot <= EQ::invslot::SHARED_BANK_END && serverSlot >= EQ::invslot::SHARED_BANK_BEGIN) { else if (serverSlot <= EQ::invslot::SHARED_BANK_END && serverSlot >= EQ::invslot::SHARED_BANK_BEGIN) {
@@ -4941,7 +4941,7 @@ namespace UF
} }
else if (serverSlot <= EQ::invbag::SHARED_BANK_BAGS_END && serverSlot >= EQ::invbag::SHARED_BANK_BAGS_BEGIN) { else if (serverSlot <= EQ::invbag::SHARED_BANK_BAGS_END && serverSlot >= EQ::invbag::SHARED_BANK_BAGS_BEGIN) {
UFSlot = serverSlot + 1; UFSlot = serverSlot - (EQ::invbag::SHARED_BANK_BAGS_BEGIN - invbag::SHARED_BANK_BAGS_BEGIN) - ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((serverSlot - EQ::invbag::SHARED_BANK_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT)); // + 1;
} }
else if (serverSlot <= EQ::invslot::TRADE_END && serverSlot >= EQ::invslot::TRADE_BEGIN) { else if (serverSlot <= EQ::invslot::TRADE_END && serverSlot >= EQ::invslot::TRADE_BEGIN) {
@@ -4949,7 +4949,7 @@ namespace UF
} }
else if (serverSlot <= EQ::invbag::TRADE_BAGS_END && serverSlot >= EQ::invbag::TRADE_BAGS_BEGIN) { else if (serverSlot <= EQ::invbag::TRADE_BAGS_END && serverSlot >= EQ::invbag::TRADE_BAGS_BEGIN) {
UFSlot = serverSlot; UFSlot = serverSlot - (EQ::invbag::TRADE_BAGS_BEGIN - invbag::TRADE_BAGS_BEGIN) - ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((serverSlot - EQ::invbag::TRADE_BAGS_BEGIN) / EQ::invbag::SLOT_COUNT)); // + 0;
} }
else if (serverSlot <= EQ::invslot::WORLD_END && serverSlot >= EQ::invslot::WORLD_BEGIN) { else if (serverSlot <= EQ::invslot::WORLD_END && serverSlot >= EQ::invslot::WORLD_BEGIN) {
@@ -4991,11 +4991,11 @@ namespace UF
} }
else if (ufSlot <= invbag::GENERAL_BAGS_END && ufSlot >= invbag::GENERAL_BAGS_BEGIN) { else if (ufSlot <= invbag::GENERAL_BAGS_END && ufSlot >= invbag::GENERAL_BAGS_BEGIN) {
ServerSlot = ufSlot - 11; ServerSlot = ufSlot + (EQ::invbag::GENERAL_BAGS_BEGIN - invbag::GENERAL_BAGS_BEGIN)/*3748*/ + ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((ufSlot - invbag::GENERAL_BAGS_BEGIN) / invbag::SLOT_COUNT)); // - 11;
} }
else if (ufSlot <= invbag::CURSOR_BAG_END && ufSlot >= invbag::CURSOR_BAG_BEGIN) { else if (ufSlot <= invbag::CURSOR_BAG_END && ufSlot >= invbag::CURSOR_BAG_BEGIN) {
ServerSlot = ufSlot + 9; ServerSlot = ufSlot + (EQ::invbag::CURSOR_BAG_BEGIN - invbag::CURSOR_BAG_BEGIN)/*5668*/; // + 9;
} }
else if (ufSlot <= invslot::TRIBUTE_END && ufSlot >= invslot::TRIBUTE_BEGIN) { else if (ufSlot <= invslot::TRIBUTE_END && ufSlot >= invslot::TRIBUTE_BEGIN) {
@@ -5015,7 +5015,7 @@ namespace UF
} }
else if (ufSlot <= invbag::BANK_BAGS_END && ufSlot >= invbag::BANK_BAGS_BEGIN) { else if (ufSlot <= invbag::BANK_BAGS_END && ufSlot >= invbag::BANK_BAGS_BEGIN) {
ServerSlot = ufSlot - 1; ServerSlot = ufSlot + (EQ::invbag::BANK_BAGS_BEGIN - invbag::BANK_BAGS_BEGIN) + ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((ufSlot - invbag::BANK_BAGS_BEGIN) / invbag::SLOT_COUNT)); // - 1;
} }
else if (ufSlot <= invslot::SHARED_BANK_END && ufSlot >= invslot::SHARED_BANK_BEGIN) { else if (ufSlot <= invslot::SHARED_BANK_END && ufSlot >= invslot::SHARED_BANK_BEGIN) {
@@ -5023,7 +5023,7 @@ namespace UF
} }
else if (ufSlot <= invbag::SHARED_BANK_BAGS_END && ufSlot >= invbag::SHARED_BANK_BAGS_BEGIN) { else if (ufSlot <= invbag::SHARED_BANK_BAGS_END && ufSlot >= invbag::SHARED_BANK_BAGS_BEGIN) {
ServerSlot = ufSlot - 1; ServerSlot = ufSlot + (EQ::invbag::SHARED_BANK_BAGS_BEGIN - invbag::SHARED_BANK_BAGS_BEGIN) + ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((ufSlot - invbag::SHARED_BANK_BAGS_BEGIN) / invbag::SLOT_COUNT)); // - 1;
} }
else if (ufSlot <= invslot::TRADE_END && ufSlot >= invslot::TRADE_BEGIN) { else if (ufSlot <= invslot::TRADE_END && ufSlot >= invslot::TRADE_BEGIN) {
@@ -5031,7 +5031,7 @@ namespace UF
} }
else if (ufSlot <= invbag::TRADE_BAGS_END && ufSlot >= invbag::TRADE_BAGS_BEGIN) { else if (ufSlot <= invbag::TRADE_BAGS_END && ufSlot >= invbag::TRADE_BAGS_BEGIN) {
ServerSlot = ufSlot; ServerSlot = ufSlot + (EQ::invbag::TRADE_BAGS_BEGIN - invbag::TRADE_BAGS_BEGIN) + ((EQ::invbag::SLOT_COUNT - invbag::SLOT_COUNT) * ((ufSlot - invbag::TRADE_BAGS_BEGIN) / invbag::SLOT_COUNT)); // - 0;
} }
else if (ufSlot <= invslot::WORLD_END && ufSlot >= invslot::WORLD_BEGIN) { else if (ufSlot <= invslot::WORLD_END && ufSlot >= invslot::WORLD_BEGIN) {
+43 -22
View File
@@ -48,10 +48,23 @@ void PathManager::LoadPaths()
return dir; return dir;
}; };
auto load_many_paths_fallback = [&](const std::vector<std::string>& dirs, const std::string& fallback, std::vector<std::string>& target) {
target.clear();
if (!dirs.empty()) {
for (const auto& path : dirs) {
target.push_back(resolve_path(path));
}
} else {
target.push_back(resolve_path(fallback));
}
};
load_many_paths_fallback(c->GetQuestDirectories(), c->QuestDir, m_quests_paths);
load_many_paths_fallback(c->GetPluginsDirectories(), c->PluginDir, m_plugin_paths);
load_many_paths_fallback(c->GetLuaModuleDirectories(), c->LuaModuleDir, m_lua_module_paths);
// resolve all paths
m_maps_path = resolve_path(c->MapDir, {"maps", "Maps"}); m_maps_path = resolve_path(c->MapDir, {"maps", "Maps"});
m_quests_path = resolve_path(c->QuestDir);
m_plugins_path = resolve_path(c->PluginDir);
m_lua_modules_path = resolve_path(c->LuaModuleDir);
m_lua_mods_path = resolve_path("mods"); m_lua_mods_path = resolve_path("mods");
m_patch_path = resolve_path(c->PatchDir); m_patch_path = resolve_path(c->PatchDir);
m_opcode_path = resolve_path(c->OpcodeDir); m_opcode_path = resolve_path(c->OpcodeDir);
@@ -62,13 +75,10 @@ void PathManager::LoadPaths()
std::vector<std::pair<std::string, std::string>> paths = { std::vector<std::pair<std::string, std::string>> paths = {
{"server", m_server_path}, {"server", m_server_path},
{"logs", m_log_path}, {"logs", m_log_path},
{"lua mods", m_lua_mods_path},
{"lua_modules", m_lua_modules_path},
{"maps", m_maps_path}, {"maps", m_maps_path},
{"lua mods", m_lua_mods_path},
{"patches", m_patch_path}, {"patches", m_patch_path},
{"opcode", m_opcode_path}, {"opcode", m_opcode_path},
{"plugins", m_plugins_path},
{"quests", m_quests_path},
{"shared_memory", m_shared_memory_path} {"shared_memory", m_shared_memory_path}
}; };
@@ -83,6 +93,17 @@ void PathManager::LoadPaths()
LogInfo("{:>{}} > [{:<{}}]", name, name_width, in_path, path_width); LogInfo("{:>{}} > [{:<{}}]", name, name_width, in_path, path_width);
} }
} }
auto log_paths = [&](const std::string& label, const std::vector<std::string>& paths) {
if (!paths.empty()) {
LogInfo("{:>{}} > [{:<{}}]", label, name_width - 1, Strings::Join(paths, ";"), path_width);
}
};
log_paths("quests", m_quests_paths);
log_paths("plugins", m_plugin_paths);
log_paths("lua_modules", m_lua_module_paths);
LogInfo("{}", Strings::Repeat("-", break_length)); LogInfo("{}", Strings::Repeat("-", break_length));
} }
@@ -96,21 +117,26 @@ const std::string &PathManager::GetMapsPath() const
return m_maps_path; return m_maps_path;
} }
const std::string &PathManager::GetQuestsPath() const
{
return m_quests_path;
}
const std::string &PathManager::GetPluginsPath() const
{
return m_plugins_path;
}
const std::string &PathManager::GetSharedMemoryPath() const const std::string &PathManager::GetSharedMemoryPath() const
{ {
return m_shared_memory_path; return m_shared_memory_path;
} }
std::vector<std::string> PathManager::GetQuestPaths() const
{
return m_quests_paths;
}
std::vector<std::string> PathManager::GetPluginPaths() const
{
return m_plugin_paths;
}
std::vector<std::string> PathManager::GetLuaModulePaths() const
{
return m_lua_module_paths;
}
const std::string &PathManager::GetLogPath() const const std::string &PathManager::GetLogPath() const
{ {
return m_log_path; return m_log_path;
@@ -126,11 +152,6 @@ const std::string &PathManager::GetOpcodePath() const
return m_opcode_path; return m_opcode_path;
} }
const std::string &PathManager::GetLuaModulesPath() const
{
return m_lua_modules_path;
}
const std::string &PathManager::GetLuaModsPath() const const std::string &PathManager::GetLuaModsPath() const
{ {
return m_lua_mods_path; return m_lua_mods_path;
+18 -12
View File
@@ -3,6 +3,7 @@
#include <string> #include <string>
#include <vector>
class PathManager { class PathManager {
public: public:
@@ -14,22 +15,27 @@ public:
[[nodiscard]] const std::string &GetMapsPath() const; [[nodiscard]] const std::string &GetMapsPath() const;
[[nodiscard]] const std::string &GetPatchPath() const; [[nodiscard]] const std::string &GetPatchPath() const;
[[nodiscard]] const std::string &GetOpcodePath() 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; [[nodiscard]] const std::string &GetServerPath() const;
[[nodiscard]] const std::string &GetSharedMemoryPath() const; [[nodiscard]] const std::string &GetSharedMemoryPath() const;
[[nodiscard]] std::vector<std::string> GetQuestPaths() const;
[[nodiscard]] std::vector<std::string> GetPluginPaths() const;
[[nodiscard]] std::vector<std::string> GetLuaModulePaths() const;
private: private:
std::string m_log_path; std::string m_log_path;
std::string m_lua_mods_path; std::string m_lua_mods_path;
std::string m_lua_modules_path; std::string m_maps_path;
std::string m_maps_path; std::string m_patch_path;
std::string m_patch_path; std::string m_opcode_path;
std::string m_opcode_path; std::string m_quests_path;
std::string m_plugins_path; std::vector<std::string> m_quests_paths;
std::string m_quests_path; std::vector<std::string> m_plugin_paths;
std::string m_server_path; std::vector<std::string> m_lua_module_paths;
std::string m_shared_memory_path;
private:
std::string m_server_path;
std::string m_shared_memory_path;
}; };
extern PathManager path; extern PathManager path;
@@ -115,7 +115,8 @@ public:
uint8_t lfg; uint8_t lfg;
std::string mailkey; std::string mailkey;
uint8_t xtargets; uint8_t xtargets;
int8_t firstlogon; uint8_t ingame;
uint32_t first_login;
uint32_t e_aa_effects; uint32_t e_aa_effects;
uint32_t e_percent_to_aa; uint32_t e_percent_to_aa;
uint32_t e_expended_aa_spent; uint32_t e_expended_aa_spent;
@@ -230,7 +231,8 @@ public:
"lfg", "lfg",
"mailkey", "mailkey",
"xtargets", "xtargets",
"firstlogon", "ingame",
"first_login",
"e_aa_effects", "e_aa_effects",
"e_percent_to_aa", "e_percent_to_aa",
"e_expended_aa_spent", "e_expended_aa_spent",
@@ -341,7 +343,8 @@ public:
"lfg", "lfg",
"mailkey", "mailkey",
"xtargets", "xtargets",
"firstlogon", "ingame",
"first_login",
"e_aa_effects", "e_aa_effects",
"e_percent_to_aa", "e_percent_to_aa",
"e_expended_aa_spent", "e_expended_aa_spent",
@@ -486,7 +489,8 @@ public:
e.lfg = 0; e.lfg = 0;
e.mailkey = ""; e.mailkey = "";
e.xtargets = 5; e.xtargets = 5;
e.firstlogon = 0; e.ingame = 0;
e.first_login = 0;
e.e_aa_effects = 0; e.e_aa_effects = 0;
e.e_percent_to_aa = 0; e.e_percent_to_aa = 0;
e.e_expended_aa_spent = 0; e.e_expended_aa_spent = 0;
@@ -627,15 +631,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0; e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : ""; e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5; e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0; e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0; e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0; e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0; e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0; e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0; e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0; e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10); e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0; e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
return e; return e;
} }
@@ -764,15 +769,16 @@ public:
v.push_back(columns[93] + " = " + std::to_string(e.lfg)); v.push_back(columns[93] + " = " + std::to_string(e.lfg));
v.push_back(columns[94] + " = '" + Strings::Escape(e.mailkey) + "'"); v.push_back(columns[94] + " = '" + Strings::Escape(e.mailkey) + "'");
v.push_back(columns[95] + " = " + std::to_string(e.xtargets)); v.push_back(columns[95] + " = " + std::to_string(e.xtargets));
v.push_back(columns[96] + " = " + std::to_string(e.firstlogon)); v.push_back(columns[96] + " = " + std::to_string(e.ingame));
v.push_back(columns[97] + " = " + std::to_string(e.e_aa_effects)); v.push_back(columns[97] + " = " + std::to_string(e.first_login));
v.push_back(columns[98] + " = " + std::to_string(e.e_percent_to_aa)); v.push_back(columns[98] + " = " + std::to_string(e.e_aa_effects));
v.push_back(columns[99] + " = " + std::to_string(e.e_expended_aa_spent)); v.push_back(columns[99] + " = " + std::to_string(e.e_percent_to_aa));
v.push_back(columns[100] + " = " + std::to_string(e.aa_points_spent_old)); v.push_back(columns[100] + " = " + std::to_string(e.e_expended_aa_spent));
v.push_back(columns[101] + " = " + std::to_string(e.aa_points_old)); v.push_back(columns[101] + " = " + std::to_string(e.aa_points_spent_old));
v.push_back(columns[102] + " = " + std::to_string(e.e_last_invsnapshot)); v.push_back(columns[102] + " = " + std::to_string(e.aa_points_old));
v.push_back(columns[103] + " = FROM_UNIXTIME(" + (e.deleted_at > 0 ? std::to_string(e.deleted_at) : "null") + ")"); v.push_back(columns[103] + " = " + std::to_string(e.e_last_invsnapshot));
v.push_back(columns[104] + " = " + std::to_string(e.illusion_block)); v.push_back(columns[104] + " = FROM_UNIXTIME(" + (e.deleted_at > 0 ? std::to_string(e.deleted_at) : "null") + ")");
v.push_back(columns[105] + " = " + std::to_string(e.illusion_block));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@@ -890,7 +896,8 @@ public:
v.push_back(std::to_string(e.lfg)); v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'"); v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets)); v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon)); v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects)); v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa)); v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent)); v.push_back(std::to_string(e.e_expended_aa_spent));
@@ -1024,7 +1031,8 @@ public:
v.push_back(std::to_string(e.lfg)); v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'"); v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets)); v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon)); v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects)); v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa)); v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent)); v.push_back(std::to_string(e.e_expended_aa_spent));
@@ -1162,15 +1170,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0; e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : ""; e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5; e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0; e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0; e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0; e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0; e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0; e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0; e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0; e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10); e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0; e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
all_entries.push_back(e); all_entries.push_back(e);
} }
@@ -1291,15 +1300,16 @@ public:
e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0; e.lfg = row[93] ? static_cast<uint8_t>(strtoul(row[93], nullptr, 10)) : 0;
e.mailkey = row[94] ? row[94] : ""; e.mailkey = row[94] ? row[94] : "";
e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5; e.xtargets = row[95] ? static_cast<uint8_t>(strtoul(row[95], nullptr, 10)) : 5;
e.firstlogon = row[96] ? static_cast<int8_t>(atoi(row[96])) : 0; e.ingame = row[96] ? static_cast<uint8_t>(strtoul(row[96], nullptr, 10)) : 0;
e.e_aa_effects = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0; e.first_login = row[97] ? static_cast<uint32_t>(strtoul(row[97], nullptr, 10)) : 0;
e.e_percent_to_aa = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0; e.e_aa_effects = row[98] ? static_cast<uint32_t>(strtoul(row[98], nullptr, 10)) : 0;
e.e_expended_aa_spent = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0; e.e_percent_to_aa = row[99] ? static_cast<uint32_t>(strtoul(row[99], nullptr, 10)) : 0;
e.aa_points_spent_old = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0; e.e_expended_aa_spent = row[100] ? static_cast<uint32_t>(strtoul(row[100], nullptr, 10)) : 0;
e.aa_points_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0; e.aa_points_spent_old = row[101] ? static_cast<uint32_t>(strtoul(row[101], nullptr, 10)) : 0;
e.e_last_invsnapshot = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0; e.aa_points_old = row[102] ? static_cast<uint32_t>(strtoul(row[102], nullptr, 10)) : 0;
e.deleted_at = strtoll(row[103] ? row[103] : "-1", nullptr, 10); e.e_last_invsnapshot = row[103] ? static_cast<uint32_t>(strtoul(row[103], nullptr, 10)) : 0;
e.illusion_block = row[104] ? static_cast<uint8_t>(strtoul(row[104], nullptr, 10)) : 0; e.deleted_at = strtoll(row[104] ? row[104] : "-1", nullptr, 10);
e.illusion_block = row[105] ? static_cast<uint8_t>(strtoul(row[105], nullptr, 10)) : 0;
all_entries.push_back(e); all_entries.push_back(e);
} }
@@ -1470,7 +1480,8 @@ public:
v.push_back(std::to_string(e.lfg)); v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'"); v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets)); v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon)); v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects)); v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa)); v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent)); v.push_back(std::to_string(e.e_expended_aa_spent));
@@ -1597,7 +1608,8 @@ public:
v.push_back(std::to_string(e.lfg)); v.push_back(std::to_string(e.lfg));
v.push_back("'" + Strings::Escape(e.mailkey) + "'"); v.push_back("'" + Strings::Escape(e.mailkey) + "'");
v.push_back(std::to_string(e.xtargets)); v.push_back(std::to_string(e.xtargets));
v.push_back(std::to_string(e.firstlogon)); v.push_back(std::to_string(e.ingame));
v.push_back(std::to_string(e.first_login));
v.push_back(std::to_string(e.e_aa_effects)); v.push_back(std::to_string(e.e_aa_effects));
v.push_back(std::to_string(e.e_percent_to_aa)); v.push_back(std::to_string(e.e_percent_to_aa));
v.push_back(std::to_string(e.e_expended_aa_spent)); v.push_back(std::to_string(e.e_expended_aa_spent));
@@ -149,6 +149,7 @@ public:
uint8_t keeps_sold_items; uint8_t keeps_sold_items;
uint8_t is_parcel_merchant; uint8_t is_parcel_merchant;
uint8_t multiquest_enabled; uint8_t multiquest_enabled;
uint16_t npc_tint_id;
}; };
static std::string PrimaryKey() static std::string PrimaryKey()
@@ -289,6 +290,7 @@ public:
"keeps_sold_items", "keeps_sold_items",
"is_parcel_merchant", "is_parcel_merchant",
"multiquest_enabled", "multiquest_enabled",
"npc_tint_id",
}; };
} }
@@ -425,6 +427,7 @@ public:
"keeps_sold_items", "keeps_sold_items",
"is_parcel_merchant", "is_parcel_merchant",
"multiquest_enabled", "multiquest_enabled",
"npc_tint_id",
}; };
} }
@@ -595,6 +598,7 @@ public:
e.keeps_sold_items = 1; e.keeps_sold_items = 1;
e.is_parcel_merchant = 0; e.is_parcel_merchant = 0;
e.multiquest_enabled = 0; e.multiquest_enabled = 0;
e.npc_tint_id = 0;
return e; return e;
} }
@@ -761,6 +765,7 @@ public:
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1; e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0; e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0;
e.npc_tint_id = row[130] ? static_cast<uint16_t>(strtoul(row[130], nullptr, 10)) : 0;
return e; return e;
} }
@@ -923,6 +928,7 @@ public:
v.push_back(columns[127] + " = " + std::to_string(e.keeps_sold_items)); v.push_back(columns[127] + " = " + std::to_string(e.keeps_sold_items));
v.push_back(columns[128] + " = " + std::to_string(e.is_parcel_merchant)); v.push_back(columns[128] + " = " + std::to_string(e.is_parcel_merchant));
v.push_back(columns[129] + " = " + std::to_string(e.multiquest_enabled)); v.push_back(columns[129] + " = " + std::to_string(e.multiquest_enabled));
v.push_back(columns[130] + " = " + std::to_string(e.npc_tint_id));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@@ -1074,6 +1080,7 @@ public:
v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.is_parcel_merchant));
v.push_back(std::to_string(e.multiquest_enabled)); v.push_back(std::to_string(e.multiquest_enabled));
v.push_back(std::to_string(e.npc_tint_id));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@@ -1233,6 +1240,7 @@ public:
v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.is_parcel_merchant));
v.push_back(std::to_string(e.multiquest_enabled)); v.push_back(std::to_string(e.multiquest_enabled));
v.push_back(std::to_string(e.npc_tint_id));
insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
} }
@@ -1396,6 +1404,7 @@ public:
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1; e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0; e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0;
e.npc_tint_id = row[130] ? static_cast<uint16_t>(strtoul(row[130], nullptr, 10)) : 0;
all_entries.push_back(e); all_entries.push_back(e);
} }
@@ -1550,6 +1559,7 @@ public:
e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1; e.keeps_sold_items = row[127] ? static_cast<uint8_t>(strtoul(row[127], nullptr, 10)) : 1;
e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0; e.is_parcel_merchant = row[128] ? static_cast<uint8_t>(strtoul(row[128], nullptr, 10)) : 0;
e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast<uint8_t>(strtoul(row[129], nullptr, 10)) : 0;
e.npc_tint_id = row[130] ? static_cast<uint16_t>(strtoul(row[130], nullptr, 10)) : 0;
all_entries.push_back(e); all_entries.push_back(e);
} }
@@ -1754,6 +1764,7 @@ public:
v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.is_parcel_merchant));
v.push_back(std::to_string(e.multiquest_enabled)); v.push_back(std::to_string(e.multiquest_enabled));
v.push_back(std::to_string(e.npc_tint_id));
auto results = db.QueryDatabase( auto results = db.QueryDatabase(
fmt::format( fmt::format(
@@ -1906,6 +1917,7 @@ public:
v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.keeps_sold_items));
v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.is_parcel_merchant));
v.push_back(std::to_string(e.multiquest_enabled)); v.push_back(std::to_string(e.multiquest_enabled));
v.push_back(std::to_string(e.npc_tint_id));
insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")");
} }
+51 -14
View File
@@ -106,13 +106,8 @@ public:
return false; return false;
} }
auto buy_lines = BaseBuyerBuyLinesRepository::GetWhere( auto buy_lines =
db, BaseBuyerBuyLinesRepository::GetWhere(db, fmt::format("`buyer_id` = {}", buyer.front().id));
fmt::format("`buyer_id` = '{}'", buyer.front().id)
);
if (buy_lines.empty()) {
return false;
}
std::vector<std::string> buy_line_ids{}; std::vector<std::string> buy_line_ids{};
for (auto const &bl: buy_lines) { for (auto const &bl: buy_lines) {
@@ -121,23 +116,65 @@ public:
DeleteWhere(db, fmt::format("`char_id` = '{}';", char_id)); DeleteWhere(db, fmt::format("`char_id` = '{}';", char_id));
if (buy_line_ids.empty()) { if (buy_line_ids.empty()) {
return false; return true;
} }
BaseBuyerBuyLinesRepository::DeleteWhere( BaseBuyerBuyLinesRepository::DeleteWhere(
db, db, fmt::format("`id` IN({})", Strings::Implode(", ", buy_line_ids))
fmt::format("`id` IN({})", Strings::Implode(", ", buy_line_ids))
); );
BaseBuyerTradeItemsRepository::DeleteWhere( BaseBuyerTradeItemsRepository::DeleteWhere(
db, db, fmt::format("`buyer_buy_lines_id` IN({})", Strings::Implode(", ", buy_line_ids))
fmt::format(
"`buyer_buy_lines_id` IN({})",
Strings::Implode(", ", buy_line_ids))
); );
} }
return true; return true;
} }
static bool DeleteBuyers(Database &db, uint32 char_zone_id, uint32 char_zone_instance_id)
{
auto buyers = GetWhere(
db,
fmt::format(
"`char_zone_id` = {} AND `char_zone_instance_id` = {}", char_zone_id, char_zone_instance_id
)
);
if (buyers.empty()) {
return false;
}
std::vector<std::string> buyer_ids{};
std::vector<std::string> buy_line_ids{};
for (auto const &b: buyers) {
buyer_ids.push_back(std::to_string(b.id));
}
auto buy_lines = BaseBuyerBuyLinesRepository::GetWhere(
db, fmt::format("`buyer_id` IN({})", Strings::Implode(", ", buyer_ids))
);
if (!buy_lines.empty()) {
for (auto const &bl: buy_lines) {
buy_line_ids.push_back(std::to_string(bl.id));
}
}
DeleteWhere(db, fmt::format("`id` IN({});", Strings::Implode(", ", buyer_ids)));
if (buy_line_ids.empty()) {
return true;
}
BaseBuyerBuyLinesRepository::DeleteWhere(
db,
fmt::format("`id` IN({})", Strings::Implode(", ", buy_line_ids))
);
BaseBuyerTradeItemsRepository::DeleteWhere(
db,
fmt::format("`buyer_buy_lines_id` IN({})", Strings::Implode(", ", buy_line_ids))
);
return true;
}
}; };
#endif //EQEMU_BUYER_REPOSITORY_H #endif //EQEMU_BUYER_REPOSITORY_H
+23 -10
View File
@@ -54,17 +54,30 @@ public:
{ {
BulkTraders_Struct all_entries{}; BulkTraders_Struct all_entries{};
std::vector<DistinctTraders_Struct> distinct_traders; std::vector<DistinctTraders_Struct> distinct_traders;
MySQLRequestResult results;
auto results = db.QueryDatabase(fmt::format( if (RuleB(Bazaar, UseAlternateBazaarSearch)) {
"SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name " results = db.QueryDatabase(fmt::format(
"FROM trader AS t " "SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name "
"JOIN character_data AS c ON t.char_id = c.id " "FROM trader AS t "
"WHERE t.char_zone_instance_id = {} " "JOIN character_data AS c ON t.char_id = c.id "
"ORDER BY t.char_zone_instance_id ASC " "WHERE t.char_zone_instance_id = {} "
"LIMIT {}", "ORDER BY t.char_zone_instance_id ASC "
char_zone_instance_id, "LIMIT {}",
max_results) char_zone_instance_id,
); max_results)
);
}
else {
results = db.QueryDatabase(fmt::format(
"SELECT DISTINCT(t.char_id), t.char_zone_id, t.char_zone_instance_id, t.char_entity_id, c.name "
"FROM trader AS t "
"JOIN character_data AS c ON t.char_id = c.id "
"ORDER BY t.char_zone_instance_id ASC "
"LIMIT {}",
max_results)
);
}
distinct_traders.reserve(results.RowCount()); distinct_traders.reserve(results.RowCount());
+13
View File
@@ -502,6 +502,19 @@ bool RuleManager::UpdateInjectedRules(Database *db, const std::string &rule_set_
} }
} }
// update rules in the database where the description is different
for (auto &e : RuleValuesRepository::All(*db)) {
auto i = rule_data.find(e.rule_name);
if (i != rule_data.end()) {
// if notes are different, update them
if (i->second.second != nullptr && *i->second.second != e.notes) {
LogInfo("Updating rule [{}] notes to [{}]", i->first, *i->second.second);
e.notes = *i->second.second;
RuleValuesRepository::ReplaceOne(*db, e);
}
}
}
if (injected_rule_entries.size()) { if (injected_rule_entries.size()) {
if (!RuleValuesRepository::InjectRules(*db, injected_rule_entries)) { if (!RuleValuesRepository::InjectRules(*db, injected_rule_entries)) {
return false; return false;
+11 -6
View File
@@ -156,6 +156,7 @@ RULE_REAL(Character, TradeskillUpPottery, 4.0, "Pottery skillup rate adjustment.
RULE_REAL(Character, TradeskillUpResearch, 1.0, "Research skillup rate adjustment. Lower is faster") RULE_REAL(Character, TradeskillUpResearch, 1.0, "Research skillup rate adjustment. Lower is faster")
RULE_REAL(Character, TradeskillUpTinkering, 2.0, "Tinkering skillup rate adjustment. Lower is faster") RULE_REAL(Character, TradeskillUpTinkering, 2.0, "Tinkering skillup rate adjustment. Lower is faster")
RULE_REAL(Character, TradeskillUpTailoring, 2.0, "Tailoring skillup rate adjustment. Lower is faster") RULE_REAL(Character, TradeskillUpTailoring, 2.0, "Tailoring skillup rate adjustment. Lower is faster")
RULE_REAL(Character, TradeskillUpMinChance, 2.5, "Determines the minimum percentage chance to gain a skill increase from a tradeskill. Cannot go below 2.5")
RULE_BOOL(Character, MarqueeHPUpdates, false, "Will show health percentage in center of screen if health lesser than 100%") RULE_BOOL(Character, MarqueeHPUpdates, false, "Will show health percentage in center of screen if health lesser than 100%")
RULE_INT(Character, IksarCommonTongue, 95, "Starting value for Common Tongue for Iksars") RULE_INT(Character, IksarCommonTongue, 95, "Starting value for Common Tongue for Iksars")
RULE_INT(Character, OgreCommonTongue, 95, "Starting value for Common Tongue for Ogres") RULE_INT(Character, OgreCommonTongue, 95, "Starting value for Common Tongue for Ogres")
@@ -232,6 +233,12 @@ RULE_BOOL(Character, SneakAlwaysSucceedOver100, false, "When sneak skill is over
RULE_INT(Character, BandolierSwapDelay, 0, "Bandolier swap delay in milliseconds, default is 0") RULE_INT(Character, BandolierSwapDelay, 0, "Bandolier swap delay in milliseconds, default is 0")
RULE_BOOL(Character, EnableHackedFastCampForGM, false, "Enables hacked fast camp for GM clients, if the GM doesn't have a hacked client they'll camp like normal") RULE_BOOL(Character, EnableHackedFastCampForGM, false, "Enables hacked fast camp for GM clients, if the GM doesn't have a hacked client they'll camp like normal")
RULE_BOOL(Character, AlwaysAllowNameChange, false, "Enable this option to allow /changename to work without enabling a name change via scripts.") RULE_BOOL(Character, AlwaysAllowNameChange, false, "Enable this option to allow /changename to work without enabling a name change via scripts.")
RULE_BOOL(Character, EnableAutoAFK, true, "Enable or disable the auto AFK feature, cuts down on packet spam")
RULE_BOOL(Character, AutoIdleFilterPackets, true, "Enable or disable filtering packets when auto AFK is enabled, heavily cuts down on packet spam in zones with lots of players")
RULE_INT(Character, SecondsBeforeIdleCombatZone, 600, "Seconds before a player is considered idle in combat zones (600 = 10 minutes)")
RULE_INT(Character, SecondsBeforeIdleNonCombatZone, 60, "Seconds before a player is considered idle in non-combat zones (60 = 1 minute)")
RULE_INT(Character, SecondsBeforeAFKCombatZone, 1800, "Seconds before a player is considered AFK in combat zones (1800 = 30 minutes)")
RULE_INT(Character, SecondsBeforeAFKNonCombatZone, 600, "Seconds before a player is considered AFK in non-combat zones (600 = 10 minutes)")
RULE_CATEGORY_END() RULE_CATEGORY_END()
RULE_CATEGORY(Mercs) RULE_CATEGORY(Mercs)
@@ -347,6 +354,7 @@ RULE_STRING(World, SupportedClients, "RoF2", "Comma-delimited list of clients to
RULE_STRING(World, CustomFilesKey, "", "Enable if the server requires custom files and sends a key to validate. Empty string to disable. Example: eqcustom_v1") RULE_STRING(World, CustomFilesKey, "", "Enable if the server requires custom files and sends a key to validate. Empty string to disable. Example: eqcustom_v1")
RULE_STRING(World, CustomFilesUrl, "github.com/knervous/eqnexus/releases", "URL to display at character select if client is missing custom files") RULE_STRING(World, CustomFilesUrl, "github.com/knervous/eqnexus/releases", "URL to display at character select if client is missing custom files")
RULE_INT(World, CustomFilesAdminLevel, 20, "Admin level at which custom file key is not required when CustomFilesKey is specified") RULE_INT(World, CustomFilesAdminLevel, 20, "Admin level at which custom file key is not required when CustomFilesKey is specified")
RULE_BOOL(World, RealTimeCalculateGuilds, false, "(Temp feature flag) If true, guilds will be calculated in real time instead of at zone boot. This is a performance hit but allows for more dynamic guilds.")
RULE_CATEGORY_END() RULE_CATEGORY_END()
RULE_CATEGORY(Zone) RULE_CATEGORY(Zone)
@@ -857,12 +865,6 @@ RULE_BOOL(Bots, BotArcheryConsumesAmmo, true, "Set to false to disable Archery A
RULE_BOOL(Bots, BotThrowingConsumesAmmo, true, "Set to false to disable Throwing Ammo Consumption") RULE_BOOL(Bots, BotThrowingConsumesAmmo, true, "Set to false to disable Throwing Ammo Consumption")
RULE_INT(Bots, StackSizeMin, 20, "20 Default. -1 to disable and use default max stack size. Minimum stack size to give a bot (Arrows/Throwing).") RULE_INT(Bots, StackSizeMin, 20, "20 Default. -1 to disable and use default max stack size. Minimum stack size to give a bot (Arrows/Throwing).")
RULE_INT(Bots, HasOrMayGetAggroThreshold, 90, "90 Default. Percent threshold of total hate where bots will stop casting spells that generate hate if they are set to try to not pull aggro via spells.") RULE_INT(Bots, HasOrMayGetAggroThreshold, 90, "90 Default. Percent threshold of total hate where bots will stop casting spells that generate hate if they are set to try to not pull aggro via spells.")
RULE_REAL(Bots, LowerMeleeDistanceMultiplier, 0.35, "Closest % of the hit box a melee bot will get to the target. Default 0.35")
RULE_REAL(Bots, LowerTauntingMeleeDistanceMultiplier, 0.25, "Closest % of the hit box a taunting melee bot will get to the target. Default 0.25")
RULE_REAL(Bots, LowerMaxMeleeRangeDistanceMultiplier, 0.80, "Closest % of the hit box a max melee range melee bot will get to the target. Default 0.80")
RULE_REAL(Bots, UpperMeleeDistanceMultiplier, 0.55, "Furthest % of the hit box a melee bot will get from the target. Default 0.55")
RULE_REAL(Bots, UpperTauntingMeleeDistanceMultiplier, 0.45, "Furthest % of the hit box a taunting melee bot will get from the target. Default 0.45")
RULE_REAL(Bots, UpperMaxMeleeRangeDistanceMultiplier, 0.95, "Furthest % of the hit box a max melee range melee bot will get from the target. Default 0.95")
RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, true, "If true, when bots are at max melee distance, special abilities including taunt will be disabled. Default True.") RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, true, "If true, when bots are at max melee distance, special abilities including taunt will be disabled. Default True.")
RULE_INT(Bots, MinJitterTimer, 500, "Minimum ms between bot movement jitter checks.") RULE_INT(Bots, MinJitterTimer, 500, "Minimum ms between bot movement jitter checks.")
RULE_INT(Bots, MaxJitterTimer, 2500, "Maximum ms between bot movement jitter checks. Set to 0 to disable timer checks.") RULE_INT(Bots, MaxJitterTimer, 2500, "Maximum ms between bot movement jitter checks. Set to 0 to disable timer checks.")
@@ -883,6 +885,7 @@ RULE_INT(Bots, MinStatusToBypassSpawnLimit, 100, "Minimum status to bypass spawn
RULE_INT(Bots, MinStatusBypassSpawnLimit, 120, "Spawn limit with status bypass. Default 120.") RULE_INT(Bots, MinStatusBypassSpawnLimit, 120, "Spawn limit with status bypass. Default 120.")
RULE_INT(Bots, MinStatusToBypassCreateLimit, 100, "Minimum status to bypass create limit. Default 100.") RULE_INT(Bots, MinStatusToBypassCreateLimit, 100, "Minimum status to bypass create limit. Default 100.")
RULE_INT(Bots, MinStatusBypassCreateLimit, 120, "Create limit with status bypass. Default 120.") RULE_INT(Bots, MinStatusBypassCreateLimit, 120, "Create limit with status bypass. Default 120.")
RULE_INT(Bots, MinStatusToBypassBotLevelRequirement, 100, "Minimum status to bypass level requirement for bots. Default 100.")
RULE_BOOL(Bots, EnableBotTGB, true, "If enabled bots will cast group buffs as TGB.") RULE_BOOL(Bots, EnableBotTGB, true, "If enabled bots will cast group buffs as TGB.")
RULE_BOOL(Bots, DoResponseAnimations, true, "If enabled bots will do animations to certain responses or commands.") RULE_BOOL(Bots, DoResponseAnimations, true, "If enabled bots will do animations to certain responses or commands.")
RULE_INT(Bots, DefaultFollowDistance, 20, "Default 20. Distance a bot will follow behind.") RULE_INT(Bots, DefaultFollowDistance, 20, "Default 20. Distance a bot will follow behind.")
@@ -928,6 +931,7 @@ RULE_BOOL(Chat, AutoInjectSaylinksToSay, true, "Automatically injects saylinks i
RULE_BOOL(Chat, AutoInjectSaylinksToClientMessage, true, "Automatically injects saylinks into dialogue that has [brackets in them]") RULE_BOOL(Chat, AutoInjectSaylinksToClientMessage, true, "Automatically injects saylinks into dialogue that has [brackets in them]")
RULE_BOOL(Chat, QuestDialogueUsesDialogueWindow, false, "Pipes all quest dialogue to dialogue window") RULE_BOOL(Chat, QuestDialogueUsesDialogueWindow, false, "Pipes all quest dialogue to dialogue window")
RULE_BOOL(Chat, DialogueWindowAnimatesNPCsIfNoneSet, true, "If there is no animation specified in the dialogue window markdown then it will choose a random greet animation such as wave or salute") RULE_BOOL(Chat, DialogueWindowAnimatesNPCsIfNoneSet, true, "If there is no animation specified in the dialogue window markdown then it will choose a random greet animation such as wave or salute")
RULE_BOOL(Chat, AlwaysCaptureCommandText, false, "Consume command text (# and ^ by default), regardless of which channel it is sent to")
RULE_CATEGORY_END() RULE_CATEGORY_END()
RULE_CATEGORY(Merchant) RULE_CATEGORY(Merchant)
@@ -1075,6 +1079,7 @@ RULE_CATEGORY(Logging)
RULE_BOOL(Logging, PrintFileFunctionAndLine, false, "Ex: [World Server] [net.cpp::main:309] Loading variables...") RULE_BOOL(Logging, PrintFileFunctionAndLine, false, "Ex: [World Server] [net.cpp::main:309] Loading variables...")
RULE_BOOL(Logging, WorldGMSayLogging, true, "Relay worldserver logging to zone processes via GM say output") RULE_BOOL(Logging, WorldGMSayLogging, true, "Relay worldserver logging to zone processes via GM say output")
RULE_BOOL(Logging, PlayerEventsQSProcess, false, "Have query server process player events instead of world. Useful when wanting to use a dedicated server and database for processing player events on separate disk") RULE_BOOL(Logging, PlayerEventsQSProcess, false, "Have query server process player events instead of world. Useful when wanting to use a dedicated server and database for processing player events on separate disk")
RULE_STRING(Logging, PlayerEventsIgnoreGMCommands, "help,show", "This is a comma delimited list of commands to ignore when recording GM command player events.")
RULE_INT(Logging, BatchPlayerEventProcessIntervalSeconds, 5, "This is the interval in which player events are processed in world or qs") RULE_INT(Logging, BatchPlayerEventProcessIntervalSeconds, 5, "This is the interval in which player events are processed in world or qs")
RULE_INT(Logging, BatchPlayerEventProcessChunkSize, 10000, "This is the cap of events that can be inserted into the queue before a force flush. This is to keep from hitting MySQL max_allowed_packet and killing the connection") RULE_INT(Logging, BatchPlayerEventProcessChunkSize, 10000, "This is the cap of events that can be inserted into the queue before a force flush. This is to keep from hitting MySQL max_allowed_packet and killing the connection")
RULE_CATEGORY_END() RULE_CATEGORY_END()
+1 -1
View File
@@ -48,7 +48,7 @@ enum class SharedTaskRequestGroupType {
struct ServerSharedTaskRequest_Struct { struct ServerSharedTaskRequest_Struct {
uint32 requested_character_id; uint32 requested_character_id;
uint32 requested_task_id; uint32 requested_task_id;
uint32 requested_npc_type_id; // original task logic passthrough uint32 requested_npc_entity_id; // original task logic passthrough
uint32 accept_time; uint32 accept_time;
}; };
+3 -2
View File
@@ -25,7 +25,7 @@
// Build variables // Build variables
// these get injected during the build pipeline // these get injected during the build pipeline
#define CURRENT_VERSION "23.4.0-dev" // always append -dev to the current version for custom-builds #define CURRENT_VERSION "23.7.0-dev" // always append -dev to the current version for custom-builds
#define LOGIN_VERSION "0.8.0" #define LOGIN_VERSION "0.8.0"
#define COMPILE_DATE __DATE__ #define COMPILE_DATE __DATE__
#define COMPILE_TIME __TIME__ #define COMPILE_TIME __TIME__
@@ -42,8 +42,9 @@
* Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt
*/ */
#define CURRENT_BINARY_DATABASE_VERSION 9321 #define CURRENT_BINARY_DATABASE_VERSION 9323
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054
#define CUSTOM_BINARY_DATABASE_VERSION 0
#endif #endif
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "eqemu-server", "name": "eqemu-server",
"version": "23.4.0", "version": "23.7.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/EQEmu/Server.git" "url": "https://github.com/EQEmu/Server.git"
+2 -2
View File
@@ -10,7 +10,7 @@ require (
require ( require (
github.com/golang/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
golang.org/x/crypto v0.35.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
) )
+4 -4
View File
@@ -10,12 +10,12 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-2
View File
@@ -33,7 +33,6 @@ bash -c "${world_bin} database:dump --login-tables --drop-table-syntax-only --du
bash -c "${world_bin} database:dump --player-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_player.sql" bash -c "${world_bin} database:dump --player-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_player.sql"
bash -c "${world_bin} database:dump --system-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_system.sql" bash -c "${world_bin} database:dump --system-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_system.sql"
bash -c "${world_bin} database:dump --state-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_state.sql" bash -c "${world_bin} database:dump --state-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_state.sql"
bash -c "${world_bin} database:dump --query-serv-tables --drop-table-syntax-only --dump-output-to-console > ${dump_path}drop_tables_queryserv.sql"
############################################# #############################################
# generate "create_" table files # generate "create_" table files
@@ -45,7 +44,6 @@ bash -c "${world_bin} database:dump --login-tables --table-structure-only --dump
bash -c "${world_bin} database:dump --player-tables --table-structure-only --dump-output-to-console | sed 's/ AUTO_INCREMENT=[0-9]*\b//g' > ${dump_path}create_tables_player.sql" bash -c "${world_bin} database:dump --player-tables --table-structure-only --dump-output-to-console | sed 's/ AUTO_INCREMENT=[0-9]*\b//g' > ${dump_path}create_tables_player.sql"
bash -c "${world_bin} database:dump --state-tables --table-structure-only --dump-output-to-console | sed 's/ AUTO_INCREMENT=[0-9]*\b//g' > ${dump_path}create_tables_state.sql" bash -c "${world_bin} database:dump --state-tables --table-structure-only --dump-output-to-console | sed 's/ AUTO_INCREMENT=[0-9]*\b//g' > ${dump_path}create_tables_state.sql"
bash -c "${world_bin} database:dump --static-instance-data --dump-output-to-console >> ${dump_path}create_tables_state.sql" bash -c "${world_bin} database:dump --static-instance-data --dump-output-to-console >> ${dump_path}create_tables_state.sql"
bash -c "${world_bin} database:dump --query-serv-tables --table-structure-only --dump-output-to-console | sed 's/ AUTO_INCREMENT=[0-9]*\b//g' > ${dump_path}create_tables_queryserv.sql"
# with content # with content
bash -c "${world_bin} database:dump --content-tables --dump-output-to-console > ${dump_path}create_tables_content.sql" bash -c "${world_bin} database:dump --content-tables --dump-output-to-console > ${dump_path}create_tables_content.sql"
+3 -2
View File
@@ -12,8 +12,9 @@ void WorldserverCLI::DatabaseVersion(int argc, char **argv, argh::parser &cmd, s
Json::Value v; Json::Value v;
v["database_version"] = CURRENT_BINARY_DATABASE_VERSION; v["database_version"] = CURRENT_BINARY_DATABASE_VERSION;
v["bots_database_version"] = RuleB(Bots, Enabled) ? CURRENT_BINARY_BOTS_DATABASE_VERSION : 0; v["bots_database_version"] = RuleB(Bots, Enabled) ? CURRENT_BINARY_BOTS_DATABASE_VERSION : 0;
v["custom_database_version"] = CUSTOM_BINARY_DATABASE_VERSION;
std::stringstream payload; std::stringstream payload;
payload << v; payload << v;
+1 -1
View File
@@ -16,7 +16,7 @@ void WorldserverCLI::EtlGetSettings(int argc, char **argv, argh::parser &cmd, st
auto event_settings = player_event_logs.GetSettings(); auto event_settings = player_event_logs.GetSettings();
auto etl_details = player_event_logs.GetEtlSettings(); auto etl_details = player_event_logs.GetEtlSettings();
for (auto i = 0; i < PlayerEvent::EventType::MAX; i++) { for (int i = PlayerEvent::GM_COMMAND; i < PlayerEvent::EventType::MAX; i++) {
player_events["event_id"] = event_settings[i].id; player_events["event_id"] = event_settings[i].id;
player_events["enabled"] = event_settings[i].event_enabled ? true : false; player_events["enabled"] = event_settings[i].event_enabled ? true : false;
player_events["retention"] = event_settings[i].retention_days; player_events["retention"] = event_settings[i].retention_days;
+6 -5
View File
@@ -11,11 +11,12 @@ void WorldserverCLI::Version(int argc, char **argv, argh::parser &cmd, std::stri
Json::Value j; Json::Value j;
j["bots_database_version"] = CURRENT_BINARY_BOTS_DATABASE_VERSION; j["bots_database_version"] = CURRENT_BINARY_BOTS_DATABASE_VERSION;
j["compile_date"] = COMPILE_DATE; j["compile_date"] = COMPILE_DATE;
j["compile_time"] = COMPILE_TIME; j["compile_time"] = COMPILE_TIME;
j["database_version"] = CURRENT_BINARY_DATABASE_VERSION; j["custom_database_version"] = CUSTOM_BINARY_DATABASE_VERSION;
j["server_version"] = CURRENT_VERSION; j["database_version"] = CURRENT_BINARY_DATABASE_VERSION;
j["server_version"] = CURRENT_VERSION;
std::stringstream payload; std::stringstream payload;
payload << j; payload << j;
+145 -102
View File
@@ -42,11 +42,16 @@ extern ZSList zoneserver_list;
uint32 numplayers = 0; //this really wants to be a member variable of ClientList... uint32 numplayers = 0; //this really wants to be a member variable of ClientList...
ClientList::ClientList() ClientList::ClientList()
: CLStale_timer(10000) : CLStale_timer(10000),
m_poll_cache_timer(6000)
{ {
NextCLEID = 1; NextCLEID = 1;
m_tick = std::make_unique<EQ::Timer>(5000, true, std::bind(&ClientList::OnTick, this, std::placeholders::_1)); m_tick = std::make_unique<EQ::Timer>(5000, true, std::bind(&ClientList::OnTick, this, std::placeholders::_1));
// pre-allocate / pin memory for the zone server caches
m_gm_zone_server_ids.reserve(512);
m_guild_zone_server_ids.reserve(1024);
} }
ClientList::~ClientList() { ClientList::~ClientList() {
@@ -57,6 +62,10 @@ void ClientList::Process() {
if (CLStale_timer.Check()) if (CLStale_timer.Check())
CLCheckStale(); CLCheckStale();
if (m_poll_cache_timer.Check()) {
RebuildZoneServerCaches();
}
LinkedListIterator<Client*> iterator(list); LinkedListIterator<Client*> iterator(list);
iterator.Reset(); iterator.Reset();
@@ -384,6 +393,7 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s
} }
else { else {
cle->Update(zoneserver, scl); cle->Update(zoneserver, scl);
AddToZoneServerCaches(cle);
} }
return; return;
} }
@@ -458,6 +468,7 @@ void ClientList::ClientUpdate(ZoneServer *zoneserver, ServerClientList_Struct *s
); );
clientlist.Insert(cle); clientlist.Insert(cle);
AddToZoneServerCaches(cle);
zoneserver->ChangeWID(scl->charid, cle->GetID()); zoneserver->ChangeWID(scl->charid, cle->GetID());
} }
@@ -1608,7 +1619,7 @@ void ClientList::OnTick(EQ::Timer *t)
/** /**
* @param response * @param response
*/ */
void ClientList::GetClientList(Json::Value &response) void ClientList::GetClientList(Json::Value &response, bool full_list)
{ {
LinkedListIterator<ClientListEntry *> Iterator(clientlist); LinkedListIterator<ClientListEntry *> Iterator(clientlist);
@@ -1619,62 +1630,68 @@ void ClientList::GetClientList(Json::Value &response)
Json::Value row; Json::Value row;
row["account_id"] = cle->AccountID(); row["id"] = cle->GetID();
row["account_name"] = cle->AccountName(); row["name"] = cle->name();
row["admin"] = cle->Admin(); row["level"] = cle->level();
row["id"] = cle->GetID(); row["ip"] = cle->GetIP();
row["ip"] = cle->GetIP(); row["gm"] = cle->GetGM();
row["loginserver_account_id"] = cle->LSAccountID(); row["race"] = cle->race();
row["loginserver_id"] = cle->LSID(); row["class"] = cle->class_();
row["loginserver_name"] = cle->LSName(); row["client_version"] = cle->GetClientVersion();
row["online"] = cle->Online(); row["admin"] = cle->Admin();
row["world_admin"] = cle->WorldAdmin(); row["account_id"] = cle->AccountID();
row["account_name"] = cle->AccountName();
row["character_id"] = cle->CharID();
row["anon"] = cle->Anon();
row["guild_id"] = cle->GuildID();
if (full_list) {
row["loginserver_account_id"] = cle->LSAccountID();
row["loginserver_id"] = cle->LSID();
row["loginserver_name"] = cle->LSName();
row["online"] = cle->Online();
row["world_admin"] = cle->WorldAdmin();
row["guild_rank"] = cle->GuildRank();
row["guild_tribute_opt_in"] = cle->GuildTributeOptIn();
row["instance"] = cle->instance();
row["is_local_client"] = cle->IsLocalClient();
row["lfg"] = cle->LFG();
row["lfg_comments"] = cle->GetLFGComments();
row["lfg_from_level"] = cle->GetLFGFromLevel();
row["lfg_match_filter"] = cle->GetLFGMatchFilter();
row["lfg_to_level"] = cle->GetLFGToLevel();
row["tells_off"] = cle->TellsOff();
row["zone"] = cle->zone();
}
auto server = cle->Server(); auto server = cle->Server();
if (server) { if (server) {
row["server"]["client_address"] = server->GetCAddress(); row["server"]["zone_id"] = server->GetZoneID();
row["server"]["client_local_address"] = server->GetCLocalAddress(); row["server"]["zone_long_name"] = server->GetZoneLongName();
row["server"]["client_port"] = server->GetCPort(); row["server"]["zone_name"] = server->GetZoneName();
row["server"]["compile_time"] = server->GetCompileTime(); row["server"]["zone_os_pid"] = server->GetZoneOSProcessID();
row["server"]["id"] = server->GetID(); row["server"]["id"] = server->GetID();
row["server"]["instance_id"] = server->GetInstanceID();
row["server"]["ip"] = server->GetIP(); if (full_list) {
row["server"]["is_booting"] = server->IsBootingUp(); row["server"]["client_address"] = server->GetCAddress();
row["server"]["launch_name"] = server->GetLaunchName(); row["server"]["client_local_address"] = server->GetCLocalAddress();
row["server"]["launched_name"] = server->GetLaunchedName(); row["server"]["client_port"] = server->GetCPort();
row["server"]["number_players"] = server->NumPlayers(); row["server"]["compile_time"] = server->GetCompileTime();
row["server"]["port"] = server->GetPort(); row["server"]["instance_id"] = server->GetInstanceID();
row["server"]["previous_zone_id"] = server->GetPrevZoneID(); row["server"]["ip"] = server->GetIP();
row["server"]["static_zone"] = server->IsStaticZone(); row["server"]["is_booting"] = server->IsBootingUp();
row["server"]["uui"] = server->GetUUID(); row["server"]["launch_name"] = server->GetLaunchName();
row["server"]["zone_id"] = server->GetZoneID(); row["server"]["launched_name"] = server->GetLaunchedName();
row["server"]["zone_long_name"] = server->GetZoneLongName(); row["server"]["number_players"] = server->NumPlayers();
row["server"]["zone_name"] = server->GetZoneName(); row["server"]["port"] = server->GetPort();
row["server"]["zone_os_pid"] = server->GetZoneOSProcessID(); row["server"]["previous_zone_id"] = server->GetPrevZoneID();
row["server"]["static_zone"] = server->IsStaticZone();
row["server"]["uui"] = server->GetUUID();
}
} }
else { else {
row["server"] = Json::Value(); row["server"] = Json::Value();
} }
row["anon"] = cle->Anon();
row["character_id"] = cle->CharID();
row["class"] = cle->class_();
row["client_version"] = cle->GetClientVersion();
row["gm"] = cle->GetGM();
row["guild_id"] = cle->GuildID();
row["guild_rank"] = cle->GuildRank();
row["guild_tribute_opt_in"] = cle->GuildTributeOptIn();
row["instance"] = cle->instance();
row["is_local_client"] = cle->IsLocalClient();
row["level"] = cle->level();
row["lfg"] = cle->LFG();
row["lfg_comments"] = cle->GetLFGComments();
row["lfg_from_level"] = cle->GetLFGFromLevel();
row["lfg_match_filter"] = cle->GetLFGMatchFilter();
row["lfg_to_level"] = cle->GetLFGToLevel();
row["name"] = cle->name();
row["race"] = cle->race();
row["tells_off"] = cle->TellsOff();
row["zone"] = cle->zone();
response.append(row); response.append(row);
@@ -1851,71 +1868,97 @@ std::map<uint32, ClientListEntry *> ClientList::GetGuildClientsWithTributeOptIn(
return guild_members; return guild_members;
} }
#include <unordered_set> void ClientList::RebuildZoneServerCaches()
{
// Clear without freeing memory (buckets stay allocated)
m_gm_zone_server_ids.clear();
m_guild_zone_server_ids.clear();
LinkedListIterator<ClientListEntry*> iterator(clientlist);
iterator.Reset();
while (iterator.MoreElements()) {
ClientListEntry* cle = iterator.GetData();
if (cle->Online() != CLE_Status::InZone || !cle->Server()) {
iterator.Advance();
continue;
}
uint32_t server_id = cle->Server()->GetID();
// Track GM zone server
if (cle->GetGM()) {
m_gm_zone_server_ids.insert(server_id);
}
// Track guild zone servers
if (cle->GuildID() > 0) {
auto& guild_set = m_guild_zone_server_ids[cle->GuildID()];
guild_set.insert(server_id);
}
iterator.Advance();
}
}
std::vector<uint32_t> ClientList::GetGuildZoneServers(uint32 guild_id) std::vector<uint32_t> ClientList::GetGuildZoneServers(uint32 guild_id)
{ {
std::vector<uint32_t> zone_server_ids; if (RuleB(World, RealTimeCalculateGuilds)) {
std::unordered_set<uint32_t> seen_ids; std::vector<uint32_t> zone_server_ids;
std::unordered_set<uint32_t> seen_ids;
LinkedListIterator<ClientListEntry *> iterator(clientlist); LinkedListIterator<ClientListEntry *> iterator(clientlist);
iterator.Reset(); iterator.Reset();
while (iterator.MoreElements()) { while (iterator.MoreElements()) {
ClientListEntry *cle = iterator.GetData(); ClientListEntry *cle = iterator.GetData();
if (cle->Online() != CLE_Status::InZone) { if (cle->Online() != CLE_Status::InZone) {
iterator.Advance(); iterator.Advance();
continue; continue;
}
if (!cle->Server()) {
iterator.Advance();
continue;
}
if (cle->GuildID() == guild_id) {
uint32_t id = cle->Server()->GetID();
if (seen_ids.insert(id).second) {
zone_server_ids.emplace_back(id);
} }
if (!cle->Server()) {
iterator.Advance();
continue;
}
if (cle->GuildID() == guild_id) {
uint32_t id = cle->Server()->GetID();
if (seen_ids.insert(id).second) {
zone_server_ids.emplace_back(id);
}
}
iterator.Advance();
} }
iterator.Advance(); return zone_server_ids;
} }
return zone_server_ids; auto it = m_guild_zone_server_ids.find(guild_id);
if (it == m_guild_zone_server_ids.end()) {
return {};
}
return {it->second.begin(), it->second.end()};
} }
std::vector<uint32_t> ClientList::GetZoneServersWithGMs() void ClientList::AddToZoneServerCaches(ClientListEntry* cle)
{ {
std::vector<uint32_t> zone_server_ids; if (!cle || cle->Online() != CLE_Status::InZone || !cle->Server()) {
std::unordered_set<uint32_t> seen_ids; return;
LinkedListIterator<ClientListEntry *> iterator(clientlist);
iterator.Reset();
while (iterator.MoreElements()) {
ClientListEntry *cle = iterator.GetData();
if (cle->Online() != CLE_Status::InZone) {
iterator.Advance();
continue;
}
if (!cle->Server()) {
iterator.Advance();
continue;
}
if (cle->Admin() > 0) {
uint32_t id = cle->Server()->GetID();
if (seen_ids.insert(id).second) {
zone_server_ids.emplace_back(id);
}
}
iterator.Advance();
} }
return zone_server_ids; uint32_t server_id = cle->Server()->GetID();
// Add GM zone server if applicable
if (cle->GetGM()) {
m_gm_zone_server_ids.insert(server_id);
}
// Add guild zone server if applicable
if (cle->GuildID() > 0) {
m_guild_zone_server_ids[cle->GuildID()].insert(server_id);
}
} }
+15 -3
View File
@@ -60,15 +60,13 @@ public:
void CLCheckStale(); void CLCheckStale();
void CLEKeepAlive(uint32 numupdates, uint32* wid); void CLEKeepAlive(uint32 numupdates, uint32* wid);
void CLEAdd(uint32 login_server_id, const char* login_server_name, const char* login_name, const char* login_key, int16 world_admin = AccountStatus::Player, uint32 ip_address = 0, uint8 is_local=0); void CLEAdd(uint32 login_server_id, const char* login_server_name, const char* login_name, const char* login_key, int16 world_admin = AccountStatus::Player, uint32 ip_address = 0, uint8 is_local=0);
std::vector<uint32_t> GetGuildZoneServers(uint32 guild_id);
std::vector<uint32_t> GetZoneServersWithGMs();
void UpdateClientGuild(uint32 char_id, uint32 guild_id); void UpdateClientGuild(uint32 char_id, uint32 guild_id);
bool IsAccountInGame(uint32 iLSID); bool IsAccountInGame(uint32 iLSID);
int GetClientCount(); int GetClientCount();
void GetClients(const char *zone_name, std::vector<ClientListEntry *> &into); void GetClients(const char *zone_name, std::vector<ClientListEntry *> &into);
void GetClientList(Json::Value &response); void GetClientList(Json::Value &response, bool full_list = false);
void GetGuildClientList(Json::Value& response, uint32 guild_id); void GetGuildClientList(Json::Value& response, uint32 guild_id);
void SendCharacterMessage(uint32_t character_id, int chat_type, const std::string& message); void SendCharacterMessage(uint32_t character_id, int chat_type, const std::string& message);
@@ -78,6 +76,15 @@ public:
void SendCharacterMessageID(const std::string& character_name, int chat_type, int eqstr_id, std::initializer_list<std::string> args = {}); void SendCharacterMessageID(const std::string& character_name, int chat_type, int eqstr_id, std::initializer_list<std::string> args = {});
void SendCharacterMessageID(ClientListEntry* character, int chat_type, int eqstr_id, std::initializer_list<std::string> args = {}); void SendCharacterMessageID(ClientListEntry* character, int chat_type, int eqstr_id, std::initializer_list<std::string> args = {});
void AddToZoneServerCaches(ClientListEntry* cle);
void RebuildZoneServerCaches();
std::vector<uint32_t> GetGuildZoneServers(uint32 guild_id);
inline std::vector<uint32_t> GetZoneServersWithGMs()
{
return {m_gm_zone_server_ids.begin(), m_gm_zone_server_ids.end()};
}
private: private:
void OnTick(EQ::Timer *t); void OnTick(EQ::Timer *t);
inline uint32 GetNextCLEID() { return NextCLEID++; } inline uint32 GetNextCLEID() { return NextCLEID++; }
@@ -92,6 +99,11 @@ private:
std::unique_ptr<EQ::Timer> m_tick; std::unique_ptr<EQ::Timer> m_tick;
// Zone server routing caches
Timer m_poll_cache_timer;
std::unordered_set<uint32_t> m_gm_zone_server_ids;
std::unordered_map<uint32_t, std::unordered_set<uint32_t>> m_guild_zone_server_ids;
}; };
#endif /*CLIENTLIST_H_*/ #endif /*CLIENTLIST_H_*/
+15 -3
View File
@@ -95,9 +95,22 @@ void ConsoleApi(
BenchTimer timer; BenchTimer timer;
timer.reset(); timer.reset();
EQEmuApiWorldDataService::get(response, args); std::string method = args.empty() ? "" : args[0];
std::string method = args[0]; if (method.empty()) {
root["execution_time"] = std::to_string(timer.elapsed());
root["method"] = method;
root["data"] = response;
root["error"] = "No method specified";
std::stringstream payload;
payload << root;
connection->SendLine(payload.str());
return;
}
// Safe to call now that args[0] is known to exist
EQEmuApiWorldDataService::get(response, args);
root["execution_time"] = std::to_string(timer.elapsed()); root["execution_time"] = std::to_string(timer.elapsed());
root["method"] = method; root["method"] = method;
@@ -105,7 +118,6 @@ void ConsoleApi(
std::stringstream payload; std::stringstream payload;
payload << root; payload << root;
connection->SendLine(payload.str()); connection->SendLine(payload.str());
} }
+26 -5
View File
@@ -24,6 +24,10 @@ void callGetZoneList(Json::Value &response)
for (auto &zone: zoneserver_list.getZoneServerList()) { for (auto &zone: zoneserver_list.getZoneServerList()) {
Json::Value row; Json::Value row;
if (!zone) {
continue;
}
if (!zone->IsConnected()) { if (!zone->IsConnected()) {
continue; continue;
} }
@@ -111,9 +115,17 @@ void callGetDatabaseSchema(Json::Value &response)
response.append(schema); response.append(schema);
} }
void callGetClientList(Json::Value &response) void callGetClientList(Json::Value &response, const std::vector<std::string> &args)
{ {
client_list.GetClientList(response); // if args has "full"
bool full_list = false;
if (args.size() > 1) {
if (args[1] == "full") {
full_list = true;
}
}
client_list.GetClientList(response, full_list);
} }
void getReloadTypes(Json::Value &response) void getReloadTypes(Json::Value &response)
@@ -127,6 +139,12 @@ void getReloadTypes(Json::Value &response)
} }
} }
void getServerCounts(Json::Value &response, const std::vector<std::string> &args)
{
response["zone_count"] = zoneserver_list.GetServerListCount();
response["client_count"] = client_list.GetClientCount();
}
void EQEmuApiWorldDataService::reload(Json::Value &r, const std::vector<std::string> &args) void EQEmuApiWorldDataService::reload(Json::Value &r, const std::vector<std::string> &args)
{ {
std::vector<std::string> commands{}; std::vector<std::string> commands{};
@@ -148,7 +166,8 @@ void EQEmuApiWorldDataService::reload(Json::Value &r, const std::vector<std::str
for (auto &t: ServerReload::GetTypes()) { for (auto &t: ServerReload::GetTypes()) {
if (std::to_string(t) == command || Strings::ToLower(ServerReload::GetName(t)) == command) { if (std::to_string(t) == command || Strings::ToLower(ServerReload::GetName(t)) == command) {
message(r, fmt::format("Reloading [{}] globally", ServerReload::GetName(t))); message(r, fmt::format("Reloading [{}] globally", ServerReload::GetName(t)));
zoneserver_list.SendServerReload(t, nullptr); LogInfo("Queueing reload of type [{}] to zones", ServerReload::GetName(t));
zoneserver_list.QueueServerReload(t);
} }
found_command = true; found_command = true;
} }
@@ -174,7 +193,7 @@ void EQEmuApiWorldDataService::get(Json::Value &r, const std::vector<std::string
callGetDatabaseSchema(r); callGetDatabaseSchema(r);
} }
if (m == "get_client_list") { if (m == "get_client_list") {
callGetClientList(r); callGetClientList(r, args);
} }
if (m == "get_reload_types") { if (m == "get_reload_types") {
getReloadTypes(r); getReloadTypes(r);
@@ -185,6 +204,9 @@ void EQEmuApiWorldDataService::get(Json::Value &r, const std::vector<std::string
if (m == "get_guild_details") { if (m == "get_guild_details") {
callGetGuildDetails(r, args); callGetGuildDetails(r, args);
} }
if (m == "get_server_counts") {
getServerCounts(r, args);
}
if (m == "lock_status") { if (m == "lock_status") {
r["locked"] = WorldConfig::get()->Locked; r["locked"] = WorldConfig::get()->Locked;
} }
@@ -192,7 +214,6 @@ void EQEmuApiWorldDataService::get(Json::Value &r, const std::vector<std::string
void EQEmuApiWorldDataService::callGetGuildDetails(Json::Value &response, const std::vector<std::string> &args) void EQEmuApiWorldDataService::callGetGuildDetails(Json::Value &response, const std::vector<std::string> &args)
{ {
std::string command = !args[1].empty() ? args[1] : ""; std::string command = !args[1].empty() ? args[1] : "";
if (command.empty()) { if (command.empty()) {
return; return;
+1 -1
View File
@@ -286,7 +286,7 @@ void LoginServer::ProcessLSFatalError(uint16_t opcode, EQ::Net::Packet &p)
if (error.find("Worldserver Account / Password INVALID") != std::string::npos) { if (error.find("Worldserver Account / Password INVALID") != std::string::npos) {
reason = "Usually this indicates you do not have a valid [account] and [password] (worldserver) account associated with your loginserver configuration. "; reason = "Usually this indicates you do not have a valid [account] and [password] (worldserver) account associated with your loginserver configuration. ";
if (fmt::format("{}", m_loginserver_address).find("login.eqemulator.net") != std::string::npos) { if (fmt::format("{}", m_loginserver_address).find("login.eqemulator.net") != std::string::npos) {
reason += "For Legacy EQEmulator connections, you need to register your server @ http://www.eqemulator.org/account/?LS"; reason += "For Legacy EQEmulator connections, you need to register your server @ https://www.eqemulator.org/index.php?pageid=ws_mgmt";
} }
} }
+8 -12
View File
@@ -182,7 +182,8 @@ int main(int argc, char **argv)
EQTimeTimer.Start(600000); EQTimeTimer.Start(600000);
Timer parcel_prune_timer(86400000); Timer parcel_prune_timer(86400000);
parcel_prune_timer.Start(86400000); parcel_prune_timer.Start(86400000);
Timer player_event_log_process(1000);
player_event_log_process.Start(1000);
// global loads // global loads
LogInfo("Loading launcher list"); LogInfo("Loading launcher list");
@@ -385,15 +386,6 @@ int main(int argc, char **argv)
player_event_logs.Init(); player_event_logs.Init();
} }
auto event_log_processor = std::jthread([](const std::stop_token& stoken) {
while (!stoken.stop_requested()) {
if (!RuleB(Logging, PlayerEventsQSProcess)) {
player_event_logs.Process();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
});
auto loop_fn = [&](EQ::Timer* t) { auto loop_fn = [&](EQ::Timer* t) {
Timer::SetCurrentTime(); Timer::SetCurrentTime();
@@ -487,6 +479,12 @@ int main(int argc, char **argv)
shared_task_manager.Process(); shared_task_manager.Process();
dynamic_zone_manager.Process(); dynamic_zone_manager.Process();
if (!RuleB(Logging, PlayerEventsQSProcess)) {
if (player_event_log_process.Check()) {
player_event_logs.Process();
}
}
if (InterserverTimer.Check()) { if (InterserverTimer.Check()) {
InterserverTimer.Start(); InterserverTimer.Start();
database.ping(); database.ping();
@@ -506,8 +504,6 @@ int main(int argc, char **argv)
EQ::EventLoop::Get().Run(); EQ::EventLoop::Get().Run();
event_log_processor.request_stop();
LogInfo("World main loop completed"); LogInfo("World main loop completed");
LogInfo("Shutting down zone connections (if any)"); LogInfo("Shutting down zone connections (if any)");
zoneserver_list.KillAll(); zoneserver_list.KillAll();
+4 -4
View File
@@ -677,10 +677,10 @@ void SharedTaskManager::SendAcceptNewSharedTaskPacket(
); );
auto d = reinterpret_cast<ServerSharedTaskRequest_Struct *>(p->pBuffer); auto d = reinterpret_cast<ServerSharedTaskRequest_Struct *>(p->pBuffer);
d->requested_character_id = character_id; d->requested_character_id = character_id;
d->requested_task_id = task_id; d->requested_task_id = task_id;
d->requested_npc_type_id = npc_context_id; d->requested_npc_entity_id = npc_context_id;
d->accept_time = accept_time; d->accept_time = accept_time;
// get requested character zone server // get requested character zone server
ClientListEntry *cle = client_list.FindCLEByCharacterID(character_id); ClientListEntry *cle = client_list.FindCLEByCharacterID(character_id);
+3 -3
View File
@@ -24,16 +24,16 @@ void SharedTaskWorldMessaging::HandleZoneMessage(ServerPacket *pack)
case ServerOP_SharedTaskRequest: { case ServerOP_SharedTaskRequest: {
auto *r = (ServerSharedTaskRequest_Struct *) pack->pBuffer; auto *r = (ServerSharedTaskRequest_Struct *) pack->pBuffer;
LogTasksDetail( LogTasksDetail(
"[ServerOP_SharedTaskRequest] Received request from character [{}] task_id [{}] npc_type_id [{}]", "[ServerOP_SharedTaskRequest] Received request from character [{}] task_id [{}] npc_entity_id [{}]",
r->requested_character_id, r->requested_character_id,
r->requested_task_id, r->requested_task_id,
r->requested_npc_type_id r->requested_npc_entity_id
); );
shared_task_manager.AttemptSharedTaskCreation( shared_task_manager.AttemptSharedTaskCreation(
r->requested_task_id, r->requested_task_id,
r->requested_character_id, r->requested_character_id,
r->requested_npc_type_id r->requested_npc_entity_id
); );
break; break;
+1 -1
View File
@@ -91,7 +91,7 @@ void WorldGuildManager::ProcessZonePacket(ServerPacket *pack) {
} }
//broadcast this packet to all zones. //broadcast this packet to all zones.
zoneserver_list.SendPacketToZonesWithGuild(s->guild_id, pack); zoneserver_list.SendPacketToBootedZones(pack);
break; break;
} }
+23 -1
View File
@@ -37,6 +37,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include "dynamic_zone_manager.h" #include "dynamic_zone_manager.h"
#include "ucs.h" #include "ucs.h"
#include "clientlist.h" #include "clientlist.h"
#include "../common/repositories/trader_repository.h"
#include "../common/repositories/buyer_repository.h"
extern uint32 numzones; extern uint32 numzones;
extern EQ::Random emu_random; extern EQ::Random emu_random;
@@ -84,6 +86,8 @@ void ZSList::Remove(const std::string &uuid)
while (iter != zone_server_list.end()) { while (iter != zone_server_list.end()) {
if ((*iter)->GetUUID().compare(uuid) == 0) { if ((*iter)->GetUUID().compare(uuid) == 0) {
auto port = (*iter)->GetCPort(); auto port = (*iter)->GetCPort();
(*iter)->CheckToClearTraderAndBuyerTables();
zone_server_list.erase(iter); zone_server_list.erase(iter);
if (port != 0) { if (port != 0) {
@@ -128,6 +132,16 @@ void ZSList::Process() {
).c_str() ).c_str()
); );
} }
if (!m_queued_reloads.empty()) {
m_queued_reloads_mutex.lock();
for (auto &type : m_queued_reloads) {
LogInfo("Sending reload of type [{}] to zones", ServerReload::GetName(type));
SendServerReload(type, nullptr);
}
m_queued_reloads.clear();
m_queued_reloads_mutex.unlock();
}
} }
bool ZSList::SendPacket(ServerPacket* pack) { bool ZSList::SendPacket(ServerPacket* pack) {
@@ -912,8 +926,9 @@ bool ZSList::SendPacketToZonesWithGuild(uint32 guild_id, ServerPacket* pack)
bool ZSList::SendPacketToZonesWithGMs(ServerPacket* pack) bool ZSList::SendPacketToZonesWithGMs(ServerPacket* pack)
{ {
auto servers = client_list.GetZoneServersWithGMs();
for (auto const &z: zone_server_list) { for (auto const &z: zone_server_list) {
for (auto const &server_id: client_list.GetZoneServersWithGMs()) { for (auto const &server_id: servers) {
if (z->GetID() == server_id && z->GetZoneID() > 0) { if (z->GetID() == server_id && z->GetZoneID() > 0) {
z->SendPacket(pack); z->SendPacket(pack);
} }
@@ -998,3 +1013,10 @@ void ZSList::SendServerReload(ServerReload::Type type, uchar *packet)
++counter; ++counter;
} }
} }
void ZSList::QueueServerReload(ServerReload::Type &type)
{
m_queued_reloads_mutex.lock();
m_queued_reloads.emplace_back(type);
m_queued_reloads_mutex.unlock();
}
+5
View File
@@ -9,6 +9,7 @@
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <deque> #include <deque>
#include <mutex>
class WorldTCPConnection; class WorldTCPConnection;
class ServerPacket; class ServerPacket;
@@ -72,8 +73,12 @@ public:
ZoneServer* FindByZoneID(uint32 ZoneID); ZoneServer* FindByZoneID(uint32 ZoneID);
const std::list<std::unique_ptr<ZoneServer>> &getZoneServerList() const; const std::list<std::unique_ptr<ZoneServer>> &getZoneServerList() const;
inline uint32_t GetServerListCount() { return zone_server_list.size(); }
void SendServerReload(ServerReload::Type type, uchar *packet = nullptr); void SendServerReload(ServerReload::Type type, uchar *packet = nullptr);
std::mutex m_queued_reloads_mutex;
std::vector<ServerReload::Type> m_queued_reloads = {};
void QueueServerReload(ServerReload::Type &type);
private: private:
void OnTick(EQ::Timer *t); void OnTick(EQ::Timer *t);
uint32 NextID; uint32 NextID;
+18
View File
@@ -50,6 +50,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#include "../common/repositories/guild_tributes_repository.h" #include "../common/repositories/guild_tributes_repository.h"
#include "../common/skill_caps.h" #include "../common/skill_caps.h"
#include "../common/server_reload_types.h" #include "../common/server_reload_types.h"
#include "../common/repositories/trader_repository.h"
#include "../common/repositories/buyer_repository.h"
extern ClientList client_list; extern ClientList client_list;
extern GroupLFPList LFPGroupList; extern GroupLFPList LFPGroupList;
@@ -1860,3 +1862,19 @@ void ZoneServer::IncomingClient(Client* client) {
SendPacket(pack); SendPacket(pack);
delete pack; delete pack;
} }
void ZoneServer::CheckToClearTraderAndBuyerTables()
{
if (GetZoneID() == Zones::BAZAAR) {
TraderRepository::DeleteWhere(
database,
fmt::format("`char_zone_id` = {} AND `char_zone_instance_id` = {}", GetZoneID(), GetInstanceID()
)
);
BuyerRepository::DeleteBuyers(database, GetZoneID(), GetInstanceID());
LogTradingDetail(
"Removed trader and buyer entries for Zone ID [{}] and Instance ID [{}]", GetZoneID(), GetInstanceID()
);
}
}
+1
View File
@@ -54,6 +54,7 @@ public:
inline const char* GetZoneName() const { return zone_name; } inline const char* GetZoneName() const { return zone_name; }
inline const char* GetZoneLongName() const { return long_name; } inline const char* GetZoneLongName() const { return long_name; }
inline std::string GetCurrentVersion() const { return CURRENT_VERSION; } inline std::string GetCurrentVersion() const { return CURRENT_VERSION; }
void CheckToClearTraderAndBuyerTables();
inline std::string GetCompileDate() const { return COMPILE_DATE; } inline std::string GetCompileDate() const { return COMPILE_DATE; }
const char* GetCompileTime() const{ return compiled; } const char* GetCompileTime() const{ return compiled; }
void SetCompile(char* in_compile){ strcpy(compiled,in_compile); } void SetCompile(char* in_compile){ strcpy(compiled,in_compile); }
+41 -26
View File
@@ -3068,11 +3068,11 @@ void Mob::AddToHateList(Mob* other, int64 hate /*= 0*/, int64 damage /*= 0*/, bo
else else
SetAssistAggro(true); SetAssistAggro(true);
bool wasengaged = IsEngaged(); bool was_engaged = IsEngaged();
Mob* owner = other->GetOwner(); Mob* owner = other->GetOwner();
Mob* mypet = GetPet(); Mob* my_pet = GetPet();
Mob* myowner = GetOwner(); Mob* my_owner = GetOwner();
Mob* targetmob = GetTarget(); Mob* target_mob = GetTarget();
bool on_hatelist = CheckAggro(other); bool on_hatelist = CheckAggro(other);
AddRampage(other); AddRampage(other);
@@ -3101,7 +3101,7 @@ void Mob::AddToHateList(Mob* other, int64 hate /*= 0*/, int64 damage /*= 0*/, bo
if (IsPet()) { if (IsPet()) {
if ((IsGHeld() || (IsHeld() && IsFocused())) && !on_hatelist) // we want them to be able to climb the hate list if ((IsGHeld() || (IsHeld() && IsFocused())) && !on_hatelist) // we want them to be able to climb the hate list
return; return;
if ((IsHeld() || IsPetStop() || IsPetRegroup()) && !wasengaged) // not 100% sure on stop/regroup kind of hard to test, but regroup is like "classic hold" if ((IsHeld() || IsPetStop() || IsPetRegroup()) && !was_engaged) // not 100% sure on stop/regroup kind of hard to test, but regroup is like "classic hold"
return; return;
} }
} }
@@ -3134,7 +3134,7 @@ void Mob::AddToHateList(Mob* other, int64 hate /*= 0*/, int64 damage /*= 0*/, bo
return; return;
} }
if (other == myowner) { if (other == my_owner) {
return; return;
} }
@@ -3236,26 +3236,39 @@ void Mob::AddToHateList(Mob* other, int64 hate /*= 0*/, int64 damage /*= 0*/, bo
} }
} }
if (mypet && !mypet->IsHeld() && !mypet->IsPetStop()) { // I have a pet, add other to it if (my_pet) {
if ( bool aggro_immunity = my_pet->GetSpecialAbility(SpecialAbility::AggroImmunity);
!mypet->IsFamiliar() && bool bot_aggro_immunity = IsBot() && my_pet->GetSpecialAbility(SpecialAbility::BotAggroImmunity);
!mypet->GetSpecialAbility(SpecialAbility::AggroImmunity) && bool client_aggro_immunity = IsClient() && my_pet->GetSpecialAbility(SpecialAbility::ClientAggroImmunity);
!(IsBot() && mypet->GetSpecialAbility(SpecialAbility::BotAggroImmunity)) && bool npc_aggro_immunity = IsNPC() && my_pet->GetSpecialAbility(SpecialAbility::NPCAggroImmunity);
!(IsClient() && mypet->GetSpecialAbility(SpecialAbility::ClientAggroImmunity)) && bool can_add_to_hatelist = !my_pet->IsFamiliar() &&
!(IsNPC() && mypet->GetSpecialAbility(SpecialAbility::NPCAggroImmunity)) !aggro_immunity &&
) { !bot_aggro_immunity &&
mypet->hate_list.AddEntToHateList(other, 0, 0, bFrenzy); !client_aggro_immunity &&
!npc_aggro_immunity;
if (can_add_to_hatelist) {
bool bot_with_controllable_pet = IsBot() && CastToBot()->HasControllablePet(BotAnimEmpathy::Attack);
if (!IsBot() || bot_with_controllable_pet) {
my_pet->hate_list.AddEntToHateList(other, 0, 0, bFrenzy);
}
} }
} }
else if (myowner) { // I am a pet, add other to owner if it's NPC/LD else if (my_owner) { // I am a pet, add other to owner if it's NPC/LD
if ( if (my_owner->IsAIControlled()) {
myowner->IsAIControlled() && bool aggro_immunity = my_owner->GetSpecialAbility(SpecialAbility::AggroImmunity);
!myowner->GetSpecialAbility(SpecialAbility::AggroImmunity) && bool bot_aggro_immunity = my_owner->IsBot() && GetSpecialAbility(SpecialAbility::BotAggroImmunity);
!(myowner->IsBot() && GetSpecialAbility(SpecialAbility::BotAggroImmunity)) && bool client_aggro_immunity = my_owner->IsClient() && GetSpecialAbility(SpecialAbility::ClientAggroImmunity);
!(myowner->IsClient() && GetSpecialAbility(SpecialAbility::ClientAggroImmunity)) && bool npc_aggro_immunity = my_owner->IsNPC() && GetSpecialAbility(SpecialAbility::NPCAggroImmunity);
!(myowner->IsNPC() && GetSpecialAbility(SpecialAbility::NPCAggroImmunity)) bool can_add_to_hatelist = !aggro_immunity &&
) { !bot_aggro_immunity &&
myowner->hate_list.AddEntToHateList(other, 0, 0, bFrenzy); !client_aggro_immunity &&
!npc_aggro_immunity;
if (can_add_to_hatelist) {
my_owner->hate_list.AddEntToHateList(other, 0, 0, bFrenzy);
}
} }
} }
@@ -3264,7 +3277,7 @@ void Mob::AddToHateList(Mob* other, int64 hate /*= 0*/, int64 damage /*= 0*/, bo
entity_list.AddTempPetsToHateList(this, other, bFrenzy); entity_list.AddTempPetsToHateList(this, other, bFrenzy);
} }
if (!wasengaged) { if (!was_engaged) {
if (IsNPC() && other->IsClient() && other->CastToClient()) { if (IsNPC() && other->IsClient() && other->CastToClient()) {
if (parse->HasQuestSub(GetNPCTypeID(), EVENT_AGGRO)) { if (parse->HasQuestSub(GetNPCTypeID(), EVENT_AGGRO)) {
parse->EventNPC(EVENT_AGGRO, CastToNPC(), other, "", 0); parse->EventNPC(EVENT_AGGRO, CastToNPC(), other, "", 0);
@@ -6677,7 +6690,9 @@ void Client::SetAttackTimer()
else else
speed = static_cast<int>(speed + ((hhe / 100.0f) * delay)); speed = static_cast<int>(speed + ((hhe / 100.0f) * delay));
} }
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), true, true);
bool reinit = !TimerToUse->Enabled();
TimerToUse->SetAtTrigger(std::max(RuleI(Combat, MinHastedDelay), speed), reinit, reinit);
if (i == EQ::invslot::slotPrimary) { if (i == EQ::invslot::slotPrimary) {
primary_speed = speed; primary_speed = speed;
+1 -1
View File
@@ -9,7 +9,7 @@
Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record) Aura::Aura(NPCType *type_data, Mob *owner, AuraRecord &record)
: NPC(type_data, 0, owner->GetPosition(), GravityBehavior::Flying), spell_id(record.spell_id), : NPC(type_data, 0, owner->GetPosition(), GravityBehavior::Flying), spell_id(record.spell_id),
distance(record.distance), distance(record.distance),
remove_timer(record.duration), movement_timer(100), process_timer(1000), aura_id(-1) remove_timer(record.duration), movement_timer(1000), process_timer(1000), aura_id(-1)
{ {
GiveNPCTypeData(type_data); // we will delete this later on GiveNPCTypeData(type_data); // we will delete this later on
m_owner = owner->GetID(); m_owner = owner->GetID();
+9 -12
View File
@@ -1165,10 +1165,6 @@ void Mob::ApplyAABonuses(const AA::Rank &rank, StatBonuses *newbon)
} }
case SE_ResistFearChance: { case SE_ResistFearChance: {
if (base_value == 100) // If we reach 100% in a single spell/item then we should be immune to
// negative fear resist effects until our immunity is over
newbon->Fearless = true;
newbon->ResistFearChance += base_value; // these should stack newbon->ResistFearChance += base_value; // these should stack
break; break;
} }
@@ -2474,9 +2470,6 @@ void Mob::ApplySpellsBonuses(uint16 spell_id, uint8 casterlevel, StatBonuses *ne
case SE_ResistFearChance: case SE_ResistFearChance:
{ {
if(effect_value == 100) // If we reach 100% in a single spell/item then we should be immune to negative fear resist effects until our immunity is over
new_bonus->Fearless = true;
new_bonus->ResistFearChance += effect_value; // these should stack new_bonus->ResistFearChance += effect_value; // these should stack
break; break;
} }
@@ -4689,11 +4682,7 @@ void Mob::NegateSpellEffectBonuses(uint16 spell_id)
break; break;
case SE_ResistFearChance: case SE_ResistFearChance:
if (negate_spellbonus) { if (negate_spellbonus) {spellbonuses.ResistFearChance = effect_value; }
spellbonuses.Fearless = false;
spellbonuses.ResistFearChance = effect_value;
}
if (negate_aabonus) { aabonuses.ResistFearChance = effect_value; } if (negate_aabonus) { aabonuses.ResistFearChance = effect_value; }
if (negate_itembonus) { itembonuses.ResistFearChance = effect_value; } if (negate_itembonus) { itembonuses.ResistFearChance = effect_value; }
break; break;
@@ -5331,6 +5320,14 @@ void Mob::NegateSpellEffectBonuses(uint16 spell_id)
spellbonuses.SEResist[e] = effect_value; spellbonuses.SEResist[e] = effect_value;
spellbonuses.SEResist[e + 1] = effect_value; spellbonuses.SEResist[e + 1] = effect_value;
} }
if (negate_itembonus) {
itembonuses.SEResist[e] = effect_value;
itembonuses.SEResist[e + 1] = effect_value;
}
if (negate_aabonus) {
aabonuses.SEResist[e] = effect_value;
aabonuses.SEResist[e + 1] = effect_value;
}
} }
break; break;
} }
+347 -287
View File
@@ -245,6 +245,8 @@ Bot::Bot(
EquipBot(); EquipBot();
m_combat_jitter_timer.Start();
if (GetClass() == Class::Rogue) { if (GetClass() == Class::Rogue) {
m_rogue_evade_timer.Start(); m_rogue_evade_timer.Start();
} }
@@ -2100,10 +2102,6 @@ void Bot::SetGuardMode() {
StopMoving(); StopMoving();
m_GuardPoint = GetPosition(); m_GuardPoint = GetPosition();
SetGuardFlag(); SetGuardFlag();
if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) {
GetPet()->StopMoving();
}
} }
void Bot::SetHoldMode() { void Bot::SetHoldMode() {
@@ -2186,8 +2184,7 @@ void Bot::AI_Process()
} }
if (HOLDING || (raid && r_group == RAID_GROUPLESS)) { if (HOLDING || (raid && r_group == RAID_GROUPLESS)) {
glm::vec3 Goal(0, 0, 0); TryNonCombatMovementChecks(bot_owner, follow_mob);
TryNonCombatMovementChecks(bot_owner, follow_mob, Goal);
return; return;
} }
@@ -2217,8 +2214,6 @@ void Bot::AI_Process()
} }
//ALT COMBAT (ACQUIRE HATE) //ALT COMBAT (ACQUIRE HATE)
glm::vec3 Goal(0, 0, 0);
// We have aggro to choose from // We have aggro to choose from
if (IsEngaged()) { if (IsEngaged()) {
if (rest_timer.Enabled()) { if (rest_timer.Enabled()) {
@@ -2271,7 +2266,7 @@ void Bot::AI_Process()
} }
// This causes conflicts with default pet handler (bounces between targets) // This causes conflicts with default pet handler (bounces between targets)
if (NOT_PULLING_BOT && NOT_RETURNING_BOT && HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { if (NOT_PULLING_BOT && NOT_RETURNING_BOT && HasControllablePet(BotAnimEmpathy::Attack)) {
// We don't add to hate list here because it's assumed to already be on the list // We don't add to hate list here because it's assumed to already be on the list
GetPet()->SetTarget(tar); GetPet()->SetTarget(tar);
} }
@@ -2285,23 +2280,23 @@ void Bot::AI_Process()
} }
// COMBAT RANGE CALCS // COMBAT RANGE CALCS
bool front_mob = InFrontMob(tar, GetX(), GetY()); bool front_mob = InFrontMob(tar, GetX(), GetY());
bool behind_mob = BehindMob(tar, GetX(), GetY()); bool behind_mob = BehindMob(tar, GetX(), GetY());
bool stop_melee_level = GetLevel() >= GetStopMeleeLevel(); bool stop_melee_level = GetLevel() >= GetStopMeleeLevel();
tar_distance = sqrt(tar_distance); // sqrt this for future calculations tar_distance = sqrt(tar_distance); // sqrt this for future calculations
// Item variables
const EQ::ItemInstance* p_item = GetBotItem(EQ::invslot::slotPrimary); const EQ::ItemInstance* p_item = GetBotItem(EQ::invslot::slotPrimary);
const EQ::ItemInstance* s_item = GetBotItem(EQ::invslot::slotSecondary); const EQ::ItemInstance* s_item = GetBotItem(EQ::invslot::slotSecondary);
CombatRangeInput input = { CombatRangeInput i = {
.target = tar, .target = tar,
.target_distance = tar_distance, .target_distance = tar_distance,
.stop_melee_level = stop_melee_level, .stop_melee_level = stop_melee_level,
.p_item = p_item, .p_item = p_item,
.s_item = s_item .s_item = s_item
}; };
CombatRangeOutput o = EvaluateCombatRange(input); CombatRangeOutput o = EvaluateCombatRange(i);
// Combat range variables // Combat range variables
bool at_combat_range = o.at_combat_range; bool at_combat_range = o.at_combat_range;
@@ -2311,20 +2306,36 @@ void Bot::AI_Process()
// PULLING FLAG (ACTIONABLE RANGE) // PULLING FLAG (ACTIONABLE RANGE)
if (PULLING_BOT || RETURNING_BOT) { if (PULLING_BOT) {
if (!TargetValidation(tar)) { return; } if (!TargetValidation(tar)) {
SetPullFlag(false);
SetPullingFlag(false);
bot_owner->SetBotPulling(false);
if (GetPet()) {
GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
}
if (RuleB(Bots, BotsRequireLoS) && !HasLoS()) {
return; return;
} }
if (at_combat_range) { if (!at_combat_range && RuleB(Bots, UseSpellPulling)) {
if ( uint16 pull_spell_id = RuleI(Bots, PullSpellID);
!tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) &&
if (IsValidSpell(pull_spell_id) && tar_distance <= spells[pull_spell_id].range) {
at_combat_range = true;
}
}
if (at_combat_range && DoLosChecks(tar)) {
bool ai_cast_successful = false;
bool can_range_attack = !tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) &&
RuleB(Bots, AllowRangedPulling) && RuleB(Bots, AllowRangedPulling) &&
IsBotRanged() && IsBotRanged() &&
ranged_timer.Check(false) ranged_timer.Check(false);
) {
if (can_range_attack) {
StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY()));
if (BotRangedAttack(tar) && CheckDoubleRangedAttack()) { if (BotRangedAttack(tar) && CheckDoubleRangedAttack()) {
@@ -2336,33 +2347,36 @@ void Bot::AI_Process()
return; return;
} }
if ( bool can_ai_spell_pull = RuleB(Bots, AllowAISpellPulling) &&
RuleB(Bots, AllowAISpellPulling) &&
!IsBotNonSpellFighter() && !IsBotNonSpellFighter() &&
AI_HasSpells() AI_HasSpells();
) {
if (can_ai_spell_pull) {
StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY()));
SetPullingSpell(true); SetPullingSpell(true);
AI_EngagedCastCheck(); ai_cast_successful = AI_EngagedCastCheck();
SetPullingSpell(false); SetPullingSpell(false);
return; if (ai_cast_successful) {
return;
}
} }
if (RuleB(Bots, UseSpellPulling)) {
uint16 pull_spell_id = RuleI(Bots, PullSpellID);
if (IsValidSpell(pull_spell_id) && tar_distance <= spells[pull_spell_id].range) {
StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY()));
SetPullingSpell(true);
CastSpell(pull_spell_id, tar->GetID());
SetPullingSpell(false);
}
}
return;
} }
if (RuleB(Bots, UseSpellPulling)) { TryPursueTarget(leash_distance);
uint16 spell_id = RuleI(Bots, PullSpellID);
if (tar_distance <= spells[spell_id].range) {
StopMoving();
SetPullingSpell(true);
CastSpell(spell_id, tar->GetID());
SetPullingSpell(false);
return;
}
}
TryPursueTarget(leash_distance, Goal);
return; return;
} }
@@ -2385,52 +2399,30 @@ void Bot::AI_Process()
(bot_owner->GetBotPulling() && NOT_RETURNING_BOT); (bot_owner->GetBotPulling() && NOT_RETURNING_BOT);
if (!other_bot_pulling && at_combat_range) { if (!other_bot_pulling && at_combat_range) {
bool jitter_cooldown = false; CombatPositioningInput cpi {
.tar = tar,
if (m_combat_jitter_timer.GetRemainingTime() > 1 && m_combat_jitter_timer.Enabled()) { .stop_melee_level = stop_melee_level,
jitter_cooldown = true; .tar_distance = tar_distance,
} .melee_distance_min = melee_distance_min,
.melee_distance = melee_distance,
if ( .melee_distance_max = melee_distance_max,
IsMoving() || .behind_mob = behind_mob,
GetCombatJitterFlag() || .front_mob = front_mob
GetCombatOutOfRangeJitterFlag() };
) {
if (
!GetCombatJitterFlag() ||
!IsMoving() ||
GetCombatOutOfRangeJitterFlag()
) {
StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY()));
}
if (DoCombatPositioning(cpi) && IsMoving()) {
return; return;
} }
if ( if (!IsSitting() && !IsFacingMob(tar)) {
!jitter_cooldown && FaceTarget(tar);
AI_movement_timer->Check() &&
(!spellend_timer.Enabled() || GetClass() == Class::Bard)
) {
DoCombatPositioning(tar, Goal, stop_melee_level, tar_distance, melee_distance_min, melee_distance, melee_distance_max, behind_mob, front_mob);
return; return;
} }
else {
if (!IsSitting() && !IsFacingMob(tar)) {
FaceTarget(tar);
return;
}
}
if (!IsBotNonSpellFighter() && AI_HasSpells() && AI_EngagedCastCheck()) { if (!IsBotNonSpellFighter() && AI_HasSpells() && AI_EngagedCastCheck()) {
return; return;
} }
if (IsMoving()) {
StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY()));
return;
}
if ( if (
!tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) && !tar->GetSpecialAbility(SpecialAbility::RangedAttackImmunity) &&
IsBotRanged() && IsBotRanged() &&
@@ -2479,7 +2471,7 @@ void Bot::AI_Process()
// ENGAGED NOT AT COMBAT RANGE // ENGAGED NOT AT COMBAT RANGE
else if (!other_bot_pulling && !TryPursueTarget(leash_distance, Goal)) { else if (!other_bot_pulling && !TryPursueTarget(leash_distance)) {
return; return;
} }
@@ -2492,7 +2484,7 @@ void Bot::AI_Process()
TryMeditate(); TryMeditate();
} }
else { // Out-of-combat behavior else { // Out-of-combat behavior
DoOutOfCombatChecks(bot_owner, follow_mob, Goal, leash_distance, fm_distance); DoOutOfCombatChecks(bot_owner, follow_mob, leash_distance, fm_distance);
} }
} }
@@ -2509,8 +2501,10 @@ bool Bot::TryBardMovementCasts() {// Basically, bard bots get a chance to cast i
return false; return false;
} }
bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, glm::vec3& Goal) {// Non-engaged movement checks bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob) {// Non-engaged movement checks
if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == Class::Bard)) { if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == Class::Bard)) {
glm::vec3 Goal(0, 0, 0);
if (GUARDING) { if (GUARDING) {
Goal = GetGuardPoint(); Goal = GetGuardPoint();
} }
@@ -2564,7 +2558,7 @@ bool Bot::TryIdleChecks(float fm_distance) {
return false; return false;
} }
void Bot::DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, glm::vec3& Goal, float leash_distance, float fm_distance) { void Bot::DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, float leash_distance, float fm_distance) {
SetAttackFlag(false); SetAttackFlag(false);
SetCombatRoundForAlerts(false); SetCombatRoundForAlerts(false);
SetAttackingFlag(false); SetAttackingFlag(false);
@@ -2572,6 +2566,12 @@ void Bot::DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, glm::vec3& Goa
if (PULLING_BOT || RETURNING_BOT || !bot_owner->GetBotPulling()) { if (PULLING_BOT || RETURNING_BOT || !bot_owner->GetBotPulling()) {
SetPullingFlag(false); SetPullingFlag(false);
SetReturningFlag(false); SetReturningFlag(false);
bot_owner->SetBotPulling(false);
if (GetPet()) {
GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
}
} }
if (TryAutoDefend(bot_owner, leash_distance) ) { if (TryAutoDefend(bot_owner, leash_distance) ) {
@@ -2580,14 +2580,7 @@ void Bot::DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, glm::vec3& Goa
SetTarget(nullptr); SetTarget(nullptr);
if ( if (HasControllablePet(BotAnimEmpathy::BackOff)) {
HasPet() &&
(
GetClass() != Class::Enchanter ||
GetPet()->GetPetType() != petAnimation ||
GetAA(aaAnimationEmpathy) >= 1
)
) {
GetPet()->WipeHateList(); GetPet()->WipeHateList();
GetPet()->SetTarget(nullptr); GetPet()->SetTarget(nullptr);
} }
@@ -2597,7 +2590,7 @@ void Bot::DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, glm::vec3& Goa
} }
// Ok to idle // Ok to idle
if (TryNonCombatMovementChecks(bot_owner, follow_mob, Goal)) { if (TryNonCombatMovementChecks(bot_owner, follow_mob)) {
return; return;
} }
@@ -2750,7 +2743,7 @@ bool Bot::TryAutoDefend(Client* bot_owner, float leash_distance) {
SetTarget(hater); SetTarget(hater);
SetAttackingFlag(); SetAttackingFlag();
if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { if (HasControllablePet(BotAnimEmpathy::Attack)) {
GetPet()->AddToHateList(hater, 1); GetPet()->AddToHateList(hater, 1);
GetPet()->SetTarget(hater); GetPet()->SetTarget(hater);
} }
@@ -2808,27 +2801,19 @@ bool Bot::TryMeditate() {
} }
// This code actually gets processed when we are too far away from target and have not engaged yet // This code actually gets processed when we are too far away from target and have not engaged yet
bool Bot::TryPursueTarget(float leash_distance, glm::vec3& Goal) { bool Bot::TryPursueTarget(float leash_distance) {
if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) { if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) {
if (GetTarget() && !IsRooted()) { if (GetTarget() && !IsRooted()) {
LogAIDetail("Pursuing [{}] while engaged", GetTarget()->GetCleanName()); LogAIDetail("Pursuing [{}] while engaged", GetTarget()->GetCleanName());
Goal = GetTarget()->GetPosition(); glm::vec3 Goal = GetTarget()->GetPosition();
if (DistanceSquared(m_Position, Goal) <= leash_distance) { if (DistanceSquared(m_Position, Goal) <= leash_distance) {
RunTo(Goal.x, Goal.y, Goal.z); RunTo(Goal.x, Goal.y, Goal.z);
SetCombatOutOfRangeJitter();
} else { } else {
WipeHateList(); WipeHateList();
SetTarget(nullptr); SetTarget(nullptr);
if ( if (HasControllablePet(BotAnimEmpathy::BackOff)) {
HasPet() &&
(
GetClass() != Class::Enchanter ||
GetPet()->GetPetType() != petAnimation ||
GetAA(aaAnimationEmpathy) >= 2
)
) {
GetPet()->WipeHateList(); GetPet()->WipeHateList();
GetPet()->SetTarget(nullptr); GetPet()->SetTarget(nullptr);
} }
@@ -3130,8 +3115,8 @@ CombatRangeOutput Bot::EvaluateCombatRange(const CombatRangeInput& input) {
bool is_backstab_weapon = input.p_item && input.p_item->GetItemBackstabDamage(); bool is_backstab_weapon = input.p_item && input.p_item->GetItemBackstabDamage();
if (IsTaunting()) { // Taunting bots if (IsTaunting()) { // Taunting bots
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerTauntingMeleeDistanceMultiplier); o.melee_distance_min = o.melee_distance_max * 0.25f;
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperTauntingMeleeDistanceMultiplier); o.melee_distance = o.melee_distance_max * 0.45f;
} }
else if (IsBotRanged()) { // Archers/Throwers else if (IsBotRanged()) { // Archers/Throwers
float min_distance = RuleI(Combat, MinRangedAttackDist); float min_distance = RuleI(Combat, MinRangedAttackDist);
@@ -3139,22 +3124,22 @@ CombatRangeOutput Bot::EvaluateCombatRange(const CombatRangeInput& input) {
float desired_range = GetBotDistanceRanged(); float desired_range = GetBotDistanceRanged();
max_distance = (max_distance == 0 ? desired_range : max_distance); // stay ranged even if items/ammo aren't correct max_distance = (max_distance == 0 ? desired_range : max_distance); // stay ranged even if items/ammo aren't correct
o.melee_distance_min = std::max(min_distance, (desired_range / 2)); o.melee_distance_min = std::max(min_distance, (desired_range * 0.75f));
o.melee_distance = std::min(max_distance, desired_range); o.melee_distance = std::min(max_distance, desired_range);
} }
else if (input.stop_melee_level) { // Casters else if (input.stop_melee_level) { // Casters
float desired_range = GetBotDistanceRanged(); float desired_range = GetBotDistanceRanged();
o.melee_distance_min = std::max(o.melee_distance_max, (desired_range / 2)); o.melee_distance_min = std::max(o.melee_distance_max, (desired_range * 0.75f));
o.melee_distance = std::max((o.melee_distance_max * 1.25f), desired_range); o.melee_distance = std::max((o.melee_distance_max * 1.25f), desired_range);
} }
else if (GetMaxMeleeRange()) { // Melee bots set to max melee range else if (GetMaxMeleeRange()) { // Melee bots set to max melee range
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerMaxMeleeRangeDistanceMultiplier); o.melee_distance_min = o.melee_distance_max * 0.80f;
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperMaxMeleeRangeDistanceMultiplier); o.melee_distance = o.melee_distance_max * 0.95f;
} }
else { // Regular melee else { // Regular melee
o.melee_distance_min = o.melee_distance_max * RuleR(Bots, LowerMeleeDistanceMultiplier); o.melee_distance_min = o.melee_distance_max * 0.30f;
o.melee_distance = o.melee_distance_max * RuleR(Bots, UpperMeleeDistanceMultiplier); o.melee_distance = o.melee_distance_max * 0.65f;
} }
o.at_combat_range = (input.target_distance <= o.melee_distance); o.at_combat_range = (input.target_distance <= o.melee_distance);
@@ -3213,7 +3198,8 @@ bool Bot::IsValidTarget(
bot_owner->SetBotPulling(false); bot_owner->SetBotPulling(false);
if (GetPet()) { if (GetPet()) {
GetPet()->SetPetOrder(m_previous_pet_order); GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
} }
} }
@@ -3247,7 +3233,8 @@ Mob* Bot::GetBotTarget(Client* bot_owner)
bot_owner->SetBotPulling(false); bot_owner->SetBotPulling(false);
if (GetPet()) { if (GetPet()) {
GetPet()->SetPetOrder(m_previous_pet_order); GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
} }
} }
@@ -3270,15 +3257,11 @@ bool Bot::TargetValidation(Mob* other) {
} }
bool Bot::ReturningFlagChecks(Client* bot_owner, Mob* leash_owner, float fm_distance) { bool Bot::ReturningFlagChecks(Client* bot_owner, Mob* leash_owner, float fm_distance) {
auto engage_range = (GetBotDistanceRanged() < 30 ? 30 : GetBotDistanceRanged()); bool target_check = !GetTarget() || Distance(GetPosition(), GetTarget()->GetPosition()) <= 75.0f;
bool returned_check = (NOT_GUARDING && fm_distance <= GetFollowDistance()) ||
(GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance());
if ( if (target_check && returned_check) { // Once we're back, clear blocking flags so everyone else can join in
(GetTarget() && Distance(GetPosition(), GetTarget()->GetPosition()) <= engage_range) &&
(
(NOT_GUARDING && fm_distance <= GetFollowDistance()) ||
(GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance())
)
) { // Once we're back, clear blocking flags so everyone else can join in
WipeHateList(); WipeHateList();
SetTarget(nullptr); SetTarget(nullptr);
SetPullingFlag(false); SetPullingFlag(false);
@@ -3286,9 +3269,10 @@ bool Bot::ReturningFlagChecks(Client* bot_owner, Mob* leash_owner, float fm_dist
bot_owner->SetBotPulling(false); bot_owner->SetBotPulling(false);
if (GetPet()) { if (GetPet()) {
GetPet()->SetPetOrder(m_previous_pet_order); GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
if (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1) { if (HasControllablePet(BotAnimEmpathy::BackOff)) {
GetPet()->WipeHateList(); GetPet()->WipeHateList();
GetPet()->SetTarget(nullptr); GetPet()->SetTarget(nullptr);
} }
@@ -3329,7 +3313,8 @@ bool Bot::PullingFlagChecks(Client* bot_owner) {
bot_owner->SetBotPulling(false); bot_owner->SetBotPulling(false);
if (GetPet()) { if (GetPet()) {
GetPet()->SetPetOrder(m_previous_pet_order); GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
} }
return false; return false;
@@ -3338,11 +3323,16 @@ bool Bot::PullingFlagChecks(Client* bot_owner) {
SetPullingFlag(false); SetPullingFlag(false);
SetReturningFlag(); SetReturningFlag();
if (HasPet() && Mob* my_pet = GetPet();
(GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) {
GetPet()->WipeHateList(); if (my_pet) {
GetPet()->SetTarget(nullptr); if (HasControllablePet(BotAnimEmpathy::BackOff)) {
my_pet->WipeHateList();
my_pet->SetTarget(nullptr);
} else {
my_pet->AddToHateList(GetTarget(), 1);
my_pet->SetTarget(GetTarget());
}
} }
if (GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive)) { if (GetPlayerState() & static_cast<uint32>(PlayerState::Aggressive)) {
@@ -3504,7 +3494,7 @@ Client* Bot::SetLeashOwner(Client* bot_owner, Group* bot_group, Raid* raid, uint
void Bot::SetOwnerTarget(Client* bot_owner) { void Bot::SetOwnerTarget(Client* bot_owner) {
if (GetPet() && (PULLING_BOT || RETURNING_BOT)) { if (GetPet() && (PULLING_BOT || RETURNING_BOT)) {
GetPet()->SetPetOrder(m_previous_pet_order); GetPet()->SetPetOrder(SPO_Follow);
} }
SetAttackFlag(false); SetAttackFlag(false);
@@ -3524,7 +3514,7 @@ void Bot::SetOwnerTarget(Client* bot_owner) {
SetTarget(attack_target); SetTarget(attack_target);
SetAttackingFlag(); SetAttackingFlag();
if (GetPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { if (HasControllablePet(BotAnimEmpathy::Attack)) {
GetPet()->WipeHateList(); GetPet()->WipeHateList();
GetPet()->AddToHateList(attack_target, 1); GetPet()->AddToHateList(attack_target, 1);
GetPet()->SetTarget(attack_target); GetPet()->SetTarget(attack_target);
@@ -3542,20 +3532,21 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) {
SetReturningFlag(false); SetReturningFlag(false);
bot_owner->SetBotPulling(false); bot_owner->SetBotPulling(false);
if (GetPet()) {
GetPet()->SetPetOrder(SPO_Follow);
GetPet()->CastToNPC()->SaveGuardSpot(true);
}
if (NOT_HOLDING && NOT_PASSIVE) { if (NOT_HOLDING && NOT_PASSIVE) {
auto pull_target = bot_owner->GetTarget(); auto pull_target = bot_owner->GetTarget();
if (pull_target) { if (pull_target) {
if (raid) { RaidGroupSay(
const auto msg = fmt::format("Pulling {}.", pull_target->GetCleanName()); fmt::format(
raid->RaidSay(msg.c_str(), GetCleanName(), 0, 100); "Pulling {}.",
} else { pull_target->GetCleanName()
RaidGroupSay( ).c_str()
fmt::format( );
"Pulling {}.",
pull_target->GetCleanName()
).c_str()
);
}
InterruptSpell(); InterruptSpell();
WipeHateList(); WipeHateList();
@@ -3564,12 +3555,15 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) {
SetPullingFlag(); SetPullingFlag();
bot_owner->SetBotPulling(); bot_owner->SetBotPulling();
if (HasPet() && (GetClass() != Class::Enchanter || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { if (GetPet()) {
GetPet()->WipeHateList(); GetPet()->WipeHateList();
GetPet()->SetTarget(nullptr); GetPet()->SetTarget(nullptr);
m_previous_pet_order = GetPet()->GetPetOrder();
GetPet()->CastToNPC()->SaveGuardSpot(GetPosition()); if (HasControllablePet(BotAnimEmpathy::Guard)) {
GetPet()->SetPetOrder(SPO_Guard); m_previous_pet_order = GetPet()->GetPetOrder();
GetPet()->CastToNPC()->SaveGuardSpot(GetPosition());
GetPet()->SetPetOrder(SPO_Guard);
}
} }
} }
} }
@@ -8737,6 +8731,86 @@ bool Bot::CheckCampSpawnConditions(Client* c) {
return true; return true;
} }
bool Bot::CheckHighEnoughLevelForBots(Client* c, uint8 bot_class) {
auto bot_character_level = c->GetBotRequiredLevel(bot_class);
bool not_high_enough_level = bot_character_level >= 0 && c->GetLevel() < bot_character_level;
if (not_high_enough_level) {
c->Message(
Chat::White,
fmt::format(
"You must be level {} to spawn {}bots.",
bot_character_level,
bot_class ? GetClassIDName(bot_class) : ""
).c_str()
);
return false;
}
return true;
}
bool Bot::CheckCreateLimit(Client* c, uint32 bot_count, uint8 bot_class) {
auto bot_creation_limit = c->GetBotCreationLimit(bot_class);
bool is_beyond_spawn_limit = bot_creation_limit >= 0 && bot_count >= bot_creation_limit;
if (is_beyond_spawn_limit) {
std::string message;
if (bot_creation_limit) {
message = fmt::format(
"You cannot create anymore than {} {}bot{}.",
bot_creation_limit,
bot_class ? GetClassIDName(bot_class) : "",
bot_creation_limit != 1 ? "s" : ""
);
} else {
message = fmt::format(
"You cannot create any {}bots.",
bot_class ? GetClassIDName(bot_class) : ""
);
}
c->Message(Chat::Yellow, message.c_str());
return false;
}
return true;
}
bool Bot::CheckSpawnLimit(Client* c, uint8 bot_class) {
auto bot_spawn_limit = c->GetBotSpawnLimit(bot_class);
auto spawned_bot_count = Bot::SpawnedBotCount(c->CharacterID(), bot_class);
bool is_beyond_spawn_limit = bot_spawn_limit >= 0 && spawned_bot_count >= bot_spawn_limit;
if (is_beyond_spawn_limit) {
std::string message;
if (bot_spawn_limit) {
message = fmt::format(
"You cannot have more than {} spawned {}bot{}.",
bot_spawn_limit,
bot_class ? GetClassIDName(bot_class) : "",
bot_spawn_limit != 1 ? "s" : ""
);
}
else {
message = fmt::format(
"You are not currently allowed to spawn any {}bots.",
bot_class ? GetClassIDName(bot_class) : ""
);
}
c->Message(Chat::White, message.c_str());
return false;
}
return true;
}
void Bot::AddBotStartingItems(uint16 race_id, uint8 class_id) void Bot::AddBotStartingItems(uint16 race_id, uint8 class_id)
{ {
if (!IsPlayerRace(race_id) || !IsPlayerClass(class_id)) { if (!IsPlayerRace(race_id) || !IsPlayerClass(class_id)) {
@@ -9738,7 +9812,7 @@ bool Bot::CastChecks(uint16 spell_id, Mob* tar, uint16 spell_type, bool precheck
) )
) )
&& &&
tar->CanBuffStack(spell_id, GetLevel(), false) < 0 tar->CanBuffStack(spell_id, GetLevel(), true) < 0
) { ) {
LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanBuffStack.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName()); LogBotSpellChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanBuffStack.'", GetCleanName(), GetSpellName(spell_id), tar->GetCleanName());
return false; return false;
@@ -11842,7 +11916,7 @@ bool Bot::IsValidSpellTypeBySpellID(uint16 spell_type, uint16 spell_id) {
return false; return false;
case BotSpellTypes::ResistBuffs: case BotSpellTypes::ResistBuffs:
case BotSpellTypes::PetResistBuffs: case BotSpellTypes::PetResistBuffs:
if (IsResistanceBuffSpell(spell_id)) { if (IsResistanceBuffSpell(spell_id) && !IsEffectInSpell(spell_id, SE_DamageShield)) {
return true; return true;
} }
@@ -11966,193 +12040,171 @@ void Bot::SetCastedSpellType(uint16 spell_type) {
_castedSpellType = spell_type; _castedSpellType = spell_type;
} }
void Bot::DoFaceCheckWithJitter(Mob* tar) {
if (!tar) {
return;
}
if (IsMoving()) {
return;
}
SetCombatJitter();
if (!IsFacingMob(tar)) {
FaceTarget(tar);
return;
}
return;
}
void Bot::DoFaceCheckNoJitter(Mob* tar) {
if (!tar) {
return;
}
if (IsMoving()) {
return;
}
if (!IsFacingMob(tar)) {
FaceTarget(tar);
return;
}
return;
}
void Bot::RunToGoalWithJitter(glm::vec3 Goal) { void Bot::RunToGoalWithJitter(glm::vec3 Goal) {
RunTo(Goal.x, Goal.y, Goal.z); RunTo(Goal.x, Goal.y, Goal.z);
SetCombatJitter(); SetCombatJitter();
} }
void Bot::SetCombatOutOfRangeJitter() {
SetCombatOutOfRangeJitterFlag();
if (RuleI(Bots, MaxJitterTimer) > 0) {
m_combat_jitter_timer.Start(zone->random.Int(RuleI(Bots, MinJitterTimer), RuleI(Bots, MaxJitterTimer)), true);
}
}
void Bot::SetCombatJitter() { void Bot::SetCombatJitter() {
SetCombatJitterFlag();
if (RuleI(Bots, MaxJitterTimer) > 0) { if (RuleI(Bots, MaxJitterTimer) > 0) {
m_combat_jitter_timer.Start(zone->random.Int(RuleI(Bots, MinJitterTimer), RuleI(Bots, MaxJitterTimer)), true); m_combat_jitter_timer.Start(zone->random.Int(RuleI(Bots, MinJitterTimer), RuleI(Bots, MaxJitterTimer)), true);
} }
} }
void Bot::DoCombatPositioning( bool Bot::DoCombatPositioning(const CombatPositioningInput& input)
Mob* tar, {
glm::vec3 Goal, bool adjustment_needed = false;
bool stop_melee_level, bool is_too_close = input.tar_distance < input.melee_distance_min;
float tar_distance, bool los_adjust = !HasRequiredLoSForPositioning(input.tar);
float melee_distance_min, bool behind_mob_set = !input.stop_melee_level &&
float melee_distance, !IsBotRanged() &&
float melee_distance_max, GetBehindMob(); // Don't want casters or ranged to find positions behind the target.
bool behind_mob, bool adjustment_allowed = !IsMoving() &&
bool front_mob m_combat_jitter_timer.Check() &&
) { (!spellend_timer.Enabled() || GetClass() == Class::Bard);
if (!tar->IsFeared()) {
bool is_too_close = tar_distance < melee_distance_min;
bool los_adjust = !HasRequiredLoSForPositioning(tar);
if (tar->IsRooted() && !IsTaunting()) { // Move non-taunting melee out of range
bool rooted_adjust = tar_distance <= melee_distance_max && HasTargetReflection();
if (rooted_adjust) { if (!IsMoving() && !IsSitting() && !IsFacingMob(input.tar)) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, (melee_distance_max + 1), (melee_distance_max * 1.25f), GetBehindMob(), !GetBehindMob())) { FaceTarget(input.tar);
RunToGoalWithJitter(Goal); }
return;
} FindPositionInput find_position_input = {
.tar = input.tar,
.distance_min = input.melee_distance_min,
.distance_max = input.melee_distance_max,
.behind_only = behind_mob_set,
.front_only = IsTaunting(),
.bypass_los = false,
};
bool is_melee = (!input.stop_melee_level && !IsBotRanged());
if (input.tar->IsRooted() && !IsTaunting()) { // Move non-taunting melee out of range
adjustment_needed =
(input.tar_distance <= input.melee_distance_max) &&
HasTargetReflection();
if (adjustment_needed && adjustment_allowed) {
find_position_input.distance_min = input.melee_distance_max + 1;
find_position_input.distance_max = input.melee_distance_max * 1.25f;
PlotBotPositionAroundTarget(find_position_input);
}
} else {
if (input.tar->IsFeared()) {
adjustment_needed = los_adjust;
if (adjustment_needed && adjustment_allowed) {
find_position_input.distance_min = input.melee_distance_min;
find_position_input.distance_max = input.melee_distance;
find_position_input.behind_only = false;
find_position_input.front_only = false;
PlotBotPositionAroundTarget(find_position_input);
} }
} }
else { else if (IsTaunting() || HasTargetReflection()) { // Taunting/Aggro adjustments
if (IsTaunting()) { // Taunting adjustments adjustment_needed =
bool taunting_adjust = (!front_mob || is_too_close || los_adjust); (IsTaunting() && is_too_close) ||
los_adjust ||
(is_melee && !input.front_mob);
if (taunting_adjust) { if (adjustment_needed && adjustment_allowed) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, false, true)) { find_position_input.distance_min = input.melee_distance_min;
RunToGoalWithJitter(Goal); find_position_input.distance_max = input.melee_distance;
find_position_input.behind_only = false;
find_position_input.front_only = true;
return; PlotBotPositionAroundTarget(find_position_input);
}
}
} }
else { } else {
if (tar->IsEnraged() && !stop_melee_level && !IsBotRanged()) { // Move non-taunting melee bots behind target during enrage if (input.tar->IsEnraged() && is_melee) { // Move non-taunting melee bots behind target during enrage
bool enraged_adjust = !behind_mob || is_too_close || los_adjust; adjustment_needed =
!behind_mob_set ||
is_too_close ||
los_adjust;
if (enraged_adjust) { if (adjustment_needed && adjustment_allowed) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, true)) { find_position_input.distance_min = input.melee_distance_min;
RunToGoalWithJitter(Goal); find_position_input.distance_max = input.melee_distance;
find_position_input.behind_only = true;
find_position_input.front_only = false;
return; PlotBotPositionAroundTarget(find_position_input);
}
}
} }
else { // Regular adjustments } else { // Regular adjustments
bool regular_adjust = adjustment_needed =
is_too_close || is_too_close ||
los_adjust || los_adjust ||
(!GetBehindMob() && !front_mob) || (behind_mob_set && !input.behind_mob);
(GetBehindMob() && !behind_mob);
if (regular_adjust) { if (adjustment_needed && adjustment_allowed) {
if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, GetBehindMob(), !GetBehindMob())) { find_position_input.distance_min = input.melee_distance_min;
RunToGoalWithJitter(Goal); find_position_input.distance_max = input.melee_distance;
return; PlotBotPositionAroundTarget(find_position_input);
}
}
} }
} }
} }
} }
DoFaceCheckNoJitter(tar); if (!adjustment_needed && IsMoving()) {
StopMoving();
}
return adjustment_needed;
} }
bool Bot::PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behind_only, bool front_only, bool bypass_los) { bool Bot::PlotBotPositionAroundTarget(const FindPositionInput& input) {
bool Result = false; bool Result = false;
if (target) { if (input.tar) {
float look_heading = 0; glm::vec3 temp_goal(0, 0, input.tar->GetZ());
glm::vec3 tar_position(input.tar->GetX(), input.tar->GetY(), input.tar->GetZ());
min_distance = min_distance; float look_heading = 0;
max_distance = max_distance; float best_z = 0;
float temp_x = 0; float tar_distance = 0;
float temp_y = 0; float desired_angle = 0;
float temp_z = target->GetZ(); const float offset = GetZOffset();
float best_z = 0; const uint16 max_iterations_allowed = 50;
auto offset = GetZOffset(); uint16 counter = 0;
const float tar_x = target->GetX();
const float tar_y = target->GetY();
float tar_distance = 0;
glm::vec3 temp_z_Position;
glm::vec4 temp_m_Position;
const uint16 max_iterations_allowed = 50;
uint16 counter = 0;
while (counter < max_iterations_allowed) { while (counter < max_iterations_allowed) {
temp_x = tar_x + zone->random.Real(-max_distance, max_distance); temp_goal.x = tar_position.x + zone->random.Real(-input.distance_max, input.distance_max);
temp_y = tar_y + zone->random.Real(-max_distance, max_distance); temp_goal.y = tar_position.y + zone->random.Real(-input.distance_max, input.distance_max);
best_z = GetFixedZ(temp_goal);
temp_z_Position.x = temp_x;
temp_z_Position.y = temp_y;
temp_z_Position.z = temp_z;
best_z = GetFixedZ(temp_z_Position);
if (best_z != BEST_Z_INVALID) { if (best_z != BEST_Z_INVALID) {
temp_z = best_z; temp_goal.z = best_z;
} }
else { else {
counter++; counter++;
continue; continue;
} }
temp_m_Position.x = temp_x; tar_distance = Distance(input.tar->GetPosition(), temp_goal);
temp_m_Position.y = temp_y;
temp_m_Position.z = temp_z;
tar_distance = Distance(target->GetPosition(), temp_m_Position); if (tar_distance > input.distance_max || tar_distance < std::max(input.distance_min, (input.distance_max * 0.75f))) {
if (tar_distance > max_distance || tar_distance < min_distance) {
counter++; counter++;
continue; continue;
} }
if (front_only && !InFrontMob(target, temp_x, temp_y)) { if (input.front_only && !InFrontMob(input.tar, temp_goal.x, temp_goal.y)) {
counter++; counter++;
continue; continue;
} }
else if (behind_only && !BehindMob(target, temp_x, temp_y)) { else if (input.behind_only && !BehindMob(input.tar, temp_goal.x, temp_goal.y)) {
counter++; counter++;
continue; continue;
} }
if (!bypass_los && CastToBot()->RequiresLoSForPositioning() && !CheckPositioningLosFN(target, temp_x, temp_y, temp_z)) { if (!input.bypass_los && CastToBot()->RequiresLoSForPositioning() && !CheckPositioningLosFN(input.tar, temp_goal.x, temp_goal.y, temp_goal.z)) {
counter++; counter++;
continue; continue;
} }
@@ -12161,9 +12213,7 @@ bool Bot::PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest,
} }
if (Result) { if (Result) {
x_dest = temp_x; RunToGoalWithJitter(temp_goal);
y_dest = temp_y;
z_dest = temp_z;
} }
} }
@@ -13404,3 +13454,13 @@ bool Bot::IsValidBotStance(uint8 stance) {
return false; return false;
} }
bool Bot::HasControllablePet(uint8 ranks_required) {
if (!GetPet()) {
return false;
}
return GetClass() != Class::Enchanter ||
GetPet()->GetPetType() != petAnimation ||
GetAA(aaAnimationEmpathy) >= ranks_required;
}
+14 -31
View File
@@ -39,9 +39,6 @@
constexpr uint32 BOT_KEEP_ALIVE_INTERVAL = 5000; // 5 seconds constexpr uint32 BOT_KEEP_ALIVE_INTERVAL = 5000; // 5 seconds
constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MIN = 1500; // 1.5 seconds
constexpr uint32 BOT_COMBAT_JITTER_INTERVAL_MAX = 3000; // 3 seconds
constexpr uint32 MAG_EPIC_1_0 = 28034; constexpr uint32 MAG_EPIC_1_0 = 28034;
extern WorldServer worldserver; extern WorldServer worldserver;
@@ -232,19 +229,10 @@ static std::map<uint16, std::string> botSubType_names = {
{ CommandedSubTypes::Selo, "Selo" } { CommandedSubTypes::Selo, "Selo" }
}; };
struct CombatRangeInput { namespace BotAnimEmpathy {
Mob* target; constexpr uint8 Guard = 1;
float target_distance; constexpr uint8 Attack = 2;
bool stop_melee_level; constexpr uint8 BackOff = 3;
const EQ::ItemInstance* p_item;
const EQ::ItemInstance* s_item;
};
struct CombatRangeOutput {
bool at_combat_range = false;
float melee_distance_min = 0.0f;
float melee_distance = 0.0f;
float melee_distance_max = 0.0f;
}; };
class Bot : public NPC { class Bot : public NPC {
@@ -577,7 +565,7 @@ public:
uint16 GetPetBotSpellType(uint16 spell_type); uint16 GetPetBotSpellType(uint16 spell_type);
// Movement checks // Movement checks
bool PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behind_only = false, bool front_only = false, bool bypass_los = false); bool PlotBotPositionAroundTarget(const FindPositionInput& input);
std::vector<Mob*> GetSpellTargetList(bool entire_raid = false); std::vector<Mob*> GetSpellTargetList(bool entire_raid = false);
void SetSpellTargetList(std::vector<Mob*> spell_target_list) { _spell_target_list = spell_target_list; } void SetSpellTargetList(std::vector<Mob*> spell_target_list) { _spell_target_list = spell_target_list; }
std::vector<Mob*> GetGroupSpellTargetList() { return _group_spell_target_list; } std::vector<Mob*> GetGroupSpellTargetList() { return _group_spell_target_list; }
@@ -765,7 +753,7 @@ public:
static BotSpell GetBestBotSpellForGroupCompleteHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); static BotSpell GetBestBotSpellForGroupCompleteHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal);
static BotSpell GetBestBotSpellForGroupHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal); static BotSpell GetBestBotSpellForGroupHeal(Bot* caster, Mob* tar, uint16 spell_type = BotSpellTypes::RegularHeal);
static Mob* GetFirstIncomingMobToMez(Bot* caster, int16 spell_id, uint16 spell_type, bool AE); static Mob* GetFirstIncomingMobToMez(Bot* caster, uint16 spell_id, uint16 spell_type, bool AE);
static BotSpell GetBestBotSpellForMez(Bot* caster, uint16 spell_type = BotSpellTypes::Mez); static BotSpell GetBestBotSpellForMez(Bot* caster, uint16 spell_type = BotSpellTypes::Mez);
static BotSpell GetBestBotMagicianPetSpell(Bot* caster, uint16 spell_type = BotSpellTypes::Pet); static BotSpell GetBestBotMagicianPetSpell(Bot* caster, uint16 spell_type = BotSpellTypes::Pet);
static std::string GetBotMagicianPetType(Bot* caster); static std::string GetBotMagicianPetType(Bot* caster);
@@ -804,6 +792,7 @@ public:
EQ::ItemInstance* GetBotItem(uint16 slot_id); EQ::ItemInstance* GetBotItem(uint16 slot_id);
bool GetSpawnStatus() { return _spawnStatus; } bool GetSpawnStatus() { return _spawnStatus; }
uint8 GetPetChooserID() { return _petChooserID; } uint8 GetPetChooserID() { return _petChooserID; }
bool HasControllablePet(uint8 ranks_required = 0);
bool IsBotRanged() { return _botRangedSetting; } bool IsBotRanged() { return _botRangedSetting; }
bool IsBotCharmer() { return _botCharmer; } bool IsBotCharmer() { return _botCharmer; }
bool IsBot() const override { return true; } bool IsBot() const override { return true; }
@@ -1102,15 +1091,8 @@ public:
bool CheckIfCasting(float fm_distance); bool CheckIfCasting(float fm_distance);
void HealRotationChecks(); void HealRotationChecks();
bool GetCombatJitterFlag() { return m_combat_jitter_flag; }
void SetCombatJitterFlag(bool flag = true) { m_combat_jitter_flag = flag; }
bool GetCombatOutOfRangeJitterFlag() { return m_combat_out_of_range_jitter_flag; }
void SetCombatOutOfRangeJitterFlag(bool flag = true) { m_combat_out_of_range_jitter_flag = flag; }
void SetCombatJitter(); void SetCombatJitter();
void SetCombatOutOfRangeJitter(); bool DoCombatPositioning(const CombatPositioningInput& input);
void DoCombatPositioning(Mob* tar, glm::vec3 Goal, bool stop_melee_level, float tar_distance, float melee_distance_min, float melee_distance, float melee_distance_max, bool behind_mob, bool front_mob);
void DoFaceCheckWithJitter(Mob* tar);
void DoFaceCheckNoJitter(Mob* tar);
void RunToGoalWithJitter(glm::vec3 Goal); void RunToGoalWithJitter(glm::vec3 Goal);
bool RequiresLoSForPositioning(); bool RequiresLoSForPositioning();
bool HasRequiredLoSForPositioning(Mob* tar); bool HasRequiredLoSForPositioning(Mob* tar);
@@ -1118,18 +1100,21 @@ public:
// Try Combat Methods // Try Combat Methods
bool TryEvade(Mob* tar); bool TryEvade(Mob* tar);
bool TryFacingTarget(Mob* tar); bool TryFacingTarget(Mob* tar);
bool TryPursueTarget(float leash_distance, glm::vec3& Goal); bool TryPursueTarget(float leash_distance);
bool TryMeditate(); bool TryMeditate();
bool TryAutoDefend(Client* bot_owner, float leash_distance); bool TryAutoDefend(Client* bot_owner, float leash_distance);
bool TryIdleChecks(float fm_distance); bool TryIdleChecks(float fm_distance);
bool TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, glm::vec3& Goal); bool TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob);
void DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, glm::vec3& Goal, float leash_distance, float fm_distance); void DoOutOfCombatChecks(Client* bot_owner, Mob* follow_mob, float leash_distance, float fm_distance);
bool TryBardMovementCasts(); bool TryBardMovementCasts();
bool BotRangedAttack(Mob* other, bool can_double_attack = false); bool BotRangedAttack(Mob* other, bool can_double_attack = false);
bool CheckDoubleRangedAttack(); bool CheckDoubleRangedAttack();
// Public "Refactor" Methods // Public "Refactor" Methods
static bool CheckCampSpawnConditions(Client* c); static bool CheckCampSpawnConditions(Client* c);
static bool CheckHighEnoughLevelForBots(Client* c, uint8 bot_class = Class::None);
static bool CheckCreateLimit(Client* c, uint32 bot_count, uint8 bot_class = Class::None);
static bool CheckSpawnLimit(Client* c, uint8 bot_class = Class::None);
protected: protected:
void BotMeditate(bool is_sitting); void BotMeditate(bool is_sitting);
@@ -1189,8 +1174,6 @@ private:
Timer m_auto_save_timer; Timer m_auto_save_timer;
Timer m_combat_jitter_timer; Timer m_combat_jitter_timer;
bool m_combat_jitter_flag;
bool m_combat_out_of_range_jitter_flag;
bool m_dirtyautohaters; bool m_dirtyautohaters;
bool m_guard_flag; bool m_guard_flag;
+12 -69
View File
@@ -468,7 +468,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas
bool available_flag = false; bool available_flag = false;
!database.botdb.QueryNameAvailablity(bot_name, available_flag); !database.botdb.QueryNameAvailability(bot_name, available_flag);
if (!available_flag) { if (!available_flag) {
bot_owner->Message( bot_owner->Message(
@@ -517,88 +517,31 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas
return bot_id; return bot_id;
} }
auto bot_creation_limit = bot_owner->GetBotCreationLimit(); if (!Bot::CheckHighEnoughLevelForBots(bot_owner)) {
auto bot_creation_limit_class = bot_owner->GetBotCreationLimit(bot_class); return bot_id;
}
if (!Bot::CheckHighEnoughLevelForBots(bot_owner, bot_class)) {
return bot_id;
}
uint32 bot_count = 0; uint32 bot_count = 0;
uint32 bot_class_count = 0; uint32 bot_class_count = 0;
if (!database.botdb.QueryBotCount(bot_owner->CharacterID(), bot_class, bot_count, bot_class_count)) { if (!database.botdb.QueryBotCount(bot_owner->CharacterID(), bot_class, bot_count, bot_class_count)) {
bot_owner->Message(Chat::Yellow, "Failed to query bot count."); bot_owner->Message(Chat::Yellow, "Failed to query bot count.");
return bot_id; return bot_id;
} }
if (bot_creation_limit >= 0 && bot_count >= bot_creation_limit) { if (!Bot::CheckCreateLimit(bot_owner, bot_count)) {
std::string message;
if (bot_creation_limit) {
message = fmt::format(
"You cannot create anymore than {} bot{}.",
bot_creation_limit,
bot_creation_limit != 1 ? "s" : ""
);
} else {
message = "You cannot create any bots.";
}
bot_owner->Message(Chat::Yellow, message.c_str());
return bot_id; return bot_id;
} }
if (bot_creation_limit_class >= 0 && bot_class_count >= bot_creation_limit_class) { if (!Bot::CheckCreateLimit(bot_owner, bot_class_count, bot_class)) {
std::string message;
if (bot_creation_limit_class) {
message = fmt::format(
"You cannot create anymore than {} {} bot{}.",
bot_creation_limit_class,
GetClassIDName(bot_class),
bot_creation_limit_class != 1 ? "s" : ""
);
} else {
message = fmt::format(
"You cannot create any {} bots.",
GetClassIDName(bot_class)
);
}
bot_owner->Message(Chat::Yellow, message.c_str());
return bot_id; return bot_id;
} }
auto bot_character_level = bot_owner->GetBotRequiredLevel();
if (
bot_character_level >= 0 &&
bot_owner->GetLevel() < bot_character_level
) {
bot_owner->Message(
Chat::Yellow,
fmt::format(
"You must be level {} to use bots.",
bot_character_level
).c_str()
);
return bot_id;
}
auto bot_character_level_class = bot_owner->GetBotRequiredLevel(bot_class);
if (
bot_character_level_class >= 0 &&
bot_owner->GetLevel() < bot_character_level_class
) {
bot_owner->Message(
Chat::Yellow,
fmt::format(
"You must be level {} to use {} bots.",
bot_character_level_class,
GetClassIDName(bot_class)
).c_str()
);
return bot_id;
}
auto my_bot = new Bot(Bot::CreateDefaultNPCTypeStructForBot(bot_name, "", bot_owner->GetLevel(), bot_race, bot_class, bot_gender), bot_owner); auto my_bot = new Bot(Bot::CreateDefaultNPCTypeStructForBot(bot_name, "", bot_owner->GetLevel(), bot_race, bot_class, bot_gender), bot_owner);
if (!my_bot->Save()) { if (!my_bot->Save()) {
+29 -121
View File
@@ -130,7 +130,7 @@ void bot_command_clone(Client *c, const Seperator *sep)
bool available_flag = false; bool available_flag = false;
!database.botdb.QueryNameAvailablity(bot_name, available_flag); !database.botdb.QueryNameAvailability(bot_name, available_flag);
if (!available_flag) { if (!available_flag) {
c->Message( c->Message(
@@ -144,55 +144,25 @@ void bot_command_clone(Client *c, const Seperator *sep)
return; return;
} }
auto bot_creation_limit = c->GetBotCreationLimit();
auto bot_creation_limit_class = c->GetBotCreationLimit(my_bot->GetClass());
uint32 bot_count = 0; uint32 bot_count = 0;
uint32 bot_class_count = 0; uint32 bot_class_count = 0;
if (!database.botdb.QueryBotCount(c->CharacterID(), my_bot->GetClass(), bot_count, bot_class_count)) { if (!database.botdb.QueryBotCount(c->CharacterID(), my_bot->GetClass(), bot_count, bot_class_count)) {
c->Message(Chat::White, "Failed to query bot count."); c->Message(Chat::Yellow, "Failed to query bot count.");
return; return;
} }
if (bot_creation_limit >= 0 && bot_count >= bot_creation_limit) { if (!Bot::CheckCreateLimit(c, bot_count)) {
std::string message;
if (bot_creation_limit) {
message = fmt::format(
"You have reached the maximum limit of {} bot{}.",
bot_creation_limit,
bot_creation_limit != 1 ? "s" : ""
);
} else {
message = "You cannot create any bots.";
}
c->Message(Chat::White, message.c_str());
return; return;
} }
if (bot_creation_limit_class >= 0 && bot_class_count >= bot_creation_limit_class) { if (!Bot::CheckCreateLimit(c, bot_class_count, my_bot->GetClass())) {
std::string message;
if (bot_creation_limit_class) {
message = fmt::format(
"You cannot create anymore than {} {} bot{}.",
bot_creation_limit_class,
GetClassIDName(my_bot->GetClass()),
bot_creation_limit_class != 1 ? "s" : ""
);
} else {
message = fmt::format(
"You cannot create any {} bots.",
GetClassIDName(my_bot->GetClass())
);
}
c->Message(Chat::White, message.c_str());
return; return;
} }
uint32 clone_id = 0; uint32 clone_id = 0;
if (!database.botdb.CreateCloneBot(my_bot->GetBotID(), bot_name, clone_id) || !clone_id) { if (!database.botdb.CreateCloneBot(my_bot->GetBotID(), bot_name, clone_id) || !clone_id) {
c->Message( c->Message(
Chat::White, Chat::White,
@@ -205,6 +175,7 @@ void bot_command_clone(Client *c, const Seperator *sep)
} }
int clone_stance = Stance::Passive; int clone_stance = Stance::Passive;
if (!database.botdb.LoadStance(my_bot->GetBotID(), clone_stance)) { if (!database.botdb.LoadStance(my_bot->GetBotID(), clone_stance)) {
c->Message( c->Message(
Chat::White, Chat::White,
@@ -729,6 +700,7 @@ void bot_command_list_bots(Client *c, const Seperator *sep)
return; return;
} }
int NO_BOT_LIMIT = -1;
bool Account = false; bool Account = false;
int seps = 1; int seps = 1;
uint32 filter_value[FilterCount]; uint32 filter_value[FilterCount];
@@ -867,7 +839,7 @@ void bot_command_list_bots(Client *c, const Seperator *sep)
for (uint8 class_id = Class::Warrior; class_id <= Class::Berserker; class_id++) { for (uint8 class_id = Class::Warrior; class_id <= Class::Berserker; class_id++) {
auto class_creation_limit = c->GetBotCreationLimit(class_id); auto class_creation_limit = c->GetBotCreationLimit(class_id);
if (class_creation_limit != overall_bot_creation_limit) { if (class_creation_limit != NO_BOT_LIMIT && class_creation_limit != overall_bot_creation_limit) {
c->Message( c->Message(
Chat::White, Chat::White,
fmt::format( fmt::format(
@@ -938,20 +910,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
return; return;
} }
auto bot_character_level = c->GetBotRequiredLevel(); if (!Bot::CheckHighEnoughLevelForBots(c)) {
if (
bot_character_level >= 0 &&
c->GetLevel() < bot_character_level &&
!c->GetGM()
) {
c->Message(
Chat::White,
fmt::format(
"You must be level {} to spawn bots.",
bot_character_level
).c_str()
);
return; return;
} }
@@ -959,27 +918,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
return; return;
} }
auto bot_spawn_limit = c->GetBotSpawnLimit(); if (!Bot::CheckSpawnLimit(c)) {
auto spawned_bot_count = Bot::SpawnedBotCount(c->CharacterID());
if (
bot_spawn_limit >= 0 &&
spawned_bot_count >= bot_spawn_limit &&
!c->GetGM()
) {
std::string message;
if (bot_spawn_limit) {
message = fmt::format(
"You cannot have more than {} spawned bot{}.",
bot_spawn_limit,
bot_spawn_limit != 1 ? "s" : ""
);
} else {
message = "You are not currently allowed to spawn any bots.";
}
c->Message(Chat::White, message.c_str());
return; return;
} }
@@ -1004,52 +943,6 @@ void bot_command_spawn(Client *c, const Seperator *sep)
return; return;
} }
auto bot_spawn_limit_class = c->GetBotSpawnLimit(bot_class);
auto spawned_bot_count_class = Bot::SpawnedBotCount(c->CharacterID(), bot_class);
if (
bot_spawn_limit_class >= 0 &&
spawned_bot_count_class >= bot_spawn_limit_class &&
!c->GetGM()
) {
std::string message;
if (bot_spawn_limit_class) {
message = fmt::format(
"You cannot have more than {} spawned {} bot{}.",
bot_spawn_limit_class,
GetClassIDName(bot_class),
bot_spawn_limit_class != 1 ? "s" : ""
);
} else {
message = fmt::format(
"You are not currently allowed to spawn any {} bots.",
GetClassIDName(bot_class)
);
}
c->Message(Chat::White, message.c_str());
return;
}
auto bot_character_level_class = c->GetBotRequiredLevel(bot_class);
if (
bot_character_level_class >= 0 &&
c->GetLevel() < bot_character_level_class &&
!c->GetGM()
) {
c->Message(
Chat::White,
fmt::format(
"You must be level {} to spawn {} bots.",
bot_character_level_class,
GetClassIDName(bot_class)
).c_str()
);
return;
}
if (!bot_id) { if (!bot_id) {
c->Message( c->Message(
Chat::White, Chat::White,
@@ -1061,6 +954,14 @@ void bot_command_spawn(Client *c, const Seperator *sep)
return; return;
} }
if (!Bot::CheckHighEnoughLevelForBots(c, bot_class)) {
return;
}
if (!Bot::CheckSpawnLimit(c, bot_class)) {
return;
}
if (entity_list.GetMobByBotID(bot_id)) { if (entity_list.GetMobByBotID(bot_id)) {
c->Message( c->Message(
Chat::White, Chat::White,
@@ -1069,6 +970,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
bot_name bot_name
).c_str() ).c_str()
); );
return; return;
} }
@@ -1083,6 +985,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
bot_id bot_id
).c_str() ).c_str()
); );
return; return;
} }
@@ -1097,6 +1000,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
); );
safe_delete(my_bot); safe_delete(my_bot);
return; return;
} }
@@ -1121,6 +1025,7 @@ void bot_command_spawn(Client *c, const Seperator *sep)
}; };
uint8 message_index = 0; uint8 message_index = 0;
if (c->GetBotOption(Client::booSpawnMessageClassSpecific)) { if (c->GetBotOption(Client::booSpawnMessageClassSpecific)) {
message_index = VALIDATECLASSID(my_bot->GetClass()); message_index = VALIDATECLASSID(my_bot->GetClass());
} }
@@ -1560,8 +1465,11 @@ void bot_command_summon(Client *c, const Seperator *sep)
continue; continue;
} }
bot_iter->GetPet()->WipeHateList(); if (bot_iter->HasControllablePet(BotAnimEmpathy::BackOff)) {
bot_iter->GetPet()->SetTarget(nullptr); bot_iter->GetPet()->WipeHateList();
bot_iter->GetPet()->SetTarget(nullptr);
}
bot_iter->GetPet()->Teleport(c->GetPosition()); bot_iter->GetPet()->Teleport(c->GetPosition());
} }
+7 -1
View File
@@ -48,9 +48,10 @@ void bot_command_click_item(Client* c, const Seperator* sep)
sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end());
Mob* tar = c->GetTarget(); Mob* tar = c->GetTarget();
bool is_success = false;
for (auto my_bot : sbl) { for (auto my_bot : sbl) {
if (my_bot->BotPassiveCheck()) { if (!my_bot->ValidStateCheck(c)) {
continue; continue;
} }
@@ -68,6 +69,11 @@ void bot_command_click_item(Client* c, const Seperator* sep)
continue; continue;
} }
is_success = true;
my_bot->TryItemClick(slot_id); my_bot->TryItemClick(slot_id);
} }
if (!is_success) {
c->Message(Chat::Yellow, "None of your bots are capable of doing that currently.");
}
} }
+30 -61
View File
@@ -5,8 +5,8 @@ void bot_command_pull(Client *c, const Seperator *sep)
if (helper_command_alias_fail(c, "bot_command_pull", sep->arg[0], "pull")) { if (helper_command_alias_fail(c, "bot_command_pull", sep->arg[0], "pull")) {
return; return;
} }
if (helper_is_help_or_usage(sep->arg[1])) {
if (helper_is_help_or_usage(sep->arg[1])) {
c->Message(Chat::White, "usage: <enemy_target> %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); c->Message(Chat::White, "usage: <enemy_target> %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]);
return; return;
} }
@@ -40,7 +40,7 @@ void bot_command_pull(Client *c, const Seperator *sep)
if ( if (
!target_mob || !target_mob ||
target_mob == c || target_mob->IsOfClientBotMerc() ||
!c->IsAttackAllowed(target_mob) !c->IsAttackAllowed(target_mob)
) { ) {
c->Message(Chat::White, "Your current target is not attackable!"); c->Message(Chat::White, "Your current target is not attackable!");
@@ -55,12 +55,16 @@ void bot_command_pull(Client *c, const Seperator *sep)
} }
if (target_mob->IsNPC() && target_mob->GetHateList().size()) { if (target_mob->IsNPC() && target_mob->GetHateList().size()) {
c->Message(Chat::White, "Your current target is already engaged!"); c->Message(Chat::White, "Your current target is already engaged!");
return; return;
} }
Bot* bot_puller = nullptr; Bot* bot_puller = nullptr;
Bot* backup_bot_puller = nullptr;
Bot* alternate_bot_puller = nullptr;
bool backup_puller_found = false;
bool alternate_puller_found = false;
for (auto bot_iter : sbl) { for (auto bot_iter : sbl) {
if (!bot_iter->ValidStateCheck(c)) { if (!bot_iter->ValidStateCheck(c)) {
@@ -72,72 +76,37 @@ void bot_command_pull(Client *c, const Seperator *sep)
case Class::Monk: case Class::Monk:
case Class::Bard: case Class::Bard:
case Class::Ranger: case Class::Ranger:
bot_puller = bot_iter; bot_iter->SetPullFlag();
break;
case Class::Warrior:
case Class::ShadowKnight:
case Class::Paladin:
case Class::Berserker:
case Class::Beastlord:
if (!bot_puller) {
bot_puller = bot_iter; return;
continue;
}
switch (bot_puller->GetClass()) {
case Class::Druid:
case Class::Shaman:
case Class::Cleric:
case Class::Wizard:
case Class::Necromancer:
case Class::Magician:
case Class::Enchanter:
bot_puller = bot_iter;
default:
continue;
}
continue;
case Class::Druid:
case Class::Shaman:
case Class::Cleric:
if (!bot_puller) {
bot_puller = bot_iter;
continue;
}
switch (bot_puller->GetClass()) {
case Class::Wizard:
case Class::Necromancer:
case Class::Magician:
case Class::Enchanter:
bot_puller = bot_iter;
default:
continue;
}
continue;
case Class::Wizard:
case Class::Necromancer:
case Class::Magician:
case Class::Enchanter:
if (!bot_puller) {
bot_puller = bot_iter;
}
continue;
default: default:
continue; break;
} }
if (!backup_puller_found) {
switch (bot_iter->GetClass()) {
case Class::Warrior:
case Class::ShadowKnight:
case Class::Paladin:
case Class::Berserker:
case Class::Beastlord:
backup_bot_puller = bot_iter;
backup_puller_found = true;
bot_puller = bot_iter; break;
default:
break;
}
}
break; if (!backup_puller_found && !alternate_puller_found) {
alternate_bot_puller = bot_iter;
alternate_puller_found = true;
}
} }
bot_puller = backup_bot_puller ? backup_bot_puller : alternate_bot_puller;
if (bot_puller) { if (bot_puller) {
bot_puller->SetPullFlag(); bot_puller->SetPullFlag();
} }
+6
View File
@@ -17,6 +17,12 @@ void bot_command_release(Client *c, const Seperator *sep)
sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end()); sbl.erase(std::remove(sbl.begin(), sbl.end(), nullptr), sbl.end());
for (auto bot_iter : sbl) { for (auto bot_iter : sbl) {
bot_iter->WipeHateList(); bot_iter->WipeHateList();
if (bot_iter->GetPet() && bot_iter->HasControllablePet(BotAnimEmpathy::BackOff)) {
bot_iter->GetPet()->WipeHateList();
bot_iter->GetPet()->SetTarget(nullptr);
}
bot_iter->SetPauseAI(false); bot_iter->SetPauseAI(false);
} }
+3 -3
View File
@@ -182,7 +182,7 @@ bool BotDatabase::LoadBotSpellCastingChances()
return true; return true;
} }
bool BotDatabase::QueryNameAvailablity(const std::string& bot_name, bool& available_flag) bool BotDatabase::QueryNameAvailability(const std::string& bot_name, bool& available_flag)
{ {
if ( if (
bot_name.empty() || bot_name.empty() ||
@@ -207,7 +207,7 @@ bool BotDatabase::QueryBotCount(const uint32 owner_id, int class_id, uint32& bot
bot_count = BotDataRepository::Count( bot_count = BotDataRepository::Count(
database, database,
fmt::format( fmt::format(
"`owner_id` = {}", "`owner_id` = {} AND `name` NOT LIKE '%-deleted-%'",
owner_id owner_id
) )
); );
@@ -216,7 +216,7 @@ bool BotDatabase::QueryBotCount(const uint32 owner_id, int class_id, uint32& bot
bot_class_count = BotDataRepository::Count( bot_class_count = BotDataRepository::Count(
database, database,
fmt::format( fmt::format(
"`owner_id` = {} AND `class` = {}", "`owner_id` = {} AND `class` = {} AND `name` NOT LIKE '%-deleted-%'",
owner_id, owner_id,
class_id class_id
) )
+1 -1
View File
@@ -48,7 +48,7 @@ public:
/* Bot functions */ /* Bot functions */
bool QueryNameAvailablity(const std::string& bot_name, bool& available_flag); bool QueryNameAvailability(const std::string& bot_name, bool& available_flag);
bool QueryBotCount(const uint32 owner_id, int class_id, uint32& bot_count, uint32& bot_class_count); bool QueryBotCount(const uint32 owner_id, int class_id, uint32& bot_count, uint32& bot_class_count);
bool LoadBotsList(const uint32 owner_id, std::list<BotsAvailableList>& bots_list, bool by_account = false); bool LoadBotsList(const uint32 owner_id, std::list<BotsAvailableList>& bots_list, bool by_account = false);
+38 -2
View File
@@ -21,6 +21,7 @@
#include "../common/types.h" #include "../common/types.h"
#include "../common/timer.h" #include "../common/timer.h"
#include "mob.h"
#include <sstream> #include <sstream>
@@ -67,7 +68,7 @@ struct BotSpellSetting {
struct BotSpells { struct BotSpells {
uint32 type; // 0 = never, must be one (and only one) of the defined values uint32 type; // 0 = never, must be one (and only one) of the defined values
int16 spellid; // <= 0 = no spell uint16 spellid; // <= 0 = no spell
int16 manacost; // -1 = use spdat, -2 = no cast time int16 manacost; // -1 = use spdat, -2 = no cast time
uint32 time_cancast; // when we can cast this spell next uint32 time_cancast; // when we can cast this spell next
int32 recast_delay; int32 recast_delay;
@@ -85,7 +86,7 @@ struct BotSpells {
struct BotSpells_wIndex { struct BotSpells_wIndex {
uint32 index; //index of AIBot_spells uint32 index; //index of AIBot_spells
uint32 type; // 0 = never, must be one (and only one) of the defined values uint32 type; // 0 = never, must be one (and only one) of the defined values
int16 spellid; // <= 0 = no spell uint16 spellid; // <= 0 = no spell
int16 manacost; // -1 = use spdat, -2 = no cast time int16 manacost; // -1 = use spdat, -2 = no cast time
uint32 time_cancast; // when we can cast this spell next uint32 time_cancast; // when we can cast this spell next
int32 recast_delay; int32 recast_delay;
@@ -151,4 +152,39 @@ struct BotSpellTypesByClass {
std::string description; std::string description;
}; };
struct CombatRangeInput {
Mob* target;
float target_distance;
bool stop_melee_level;
const EQ::ItemInstance* p_item;
const EQ::ItemInstance* s_item;
};
struct CombatRangeOutput {
bool at_combat_range = false;
float melee_distance_min = 0.0f;
float melee_distance = 0.0f;
float melee_distance_max = 0.0f;
};
struct CombatPositioningInput {
Mob* tar;
bool stop_melee_level;
float tar_distance;
float melee_distance_min;
float melee_distance;
float melee_distance_max;
bool behind_mob;
bool front_mob;
};
struct FindPositionInput {
Mob* tar;
float distance_min;
float distance_max;
bool behind_only;
bool front_only;
bool bypass_los;
};
#endif // BOT_STRUCTS #endif // BOT_STRUCTS
+1 -1
View File
@@ -1459,7 +1459,7 @@ BotSpell Bot::GetBestBotSpellForMez(Bot* caster, uint16 spell_type) {
return result; return result;
} }
Mob* Bot::GetFirstIncomingMobToMez(Bot* caster, int16 spell_id, uint16 spell_type, bool AE) { Mob* Bot::GetFirstIncomingMobToMez(Bot* caster, uint16 spell_id, uint16 spell_type, bool AE) {
Mob* result = nullptr; Mob* result = nullptr;
if (caster && caster->GetOwner()) { if (caster && caster->GetOwner()) {
+58 -17
View File
@@ -2,9 +2,10 @@ extern Zone *zone;
#include <cereal/archives/json.hpp> #include <cereal/archives/json.hpp>
#include <cereal/types/map.hpp> #include <cereal/types/map.hpp>
#include "../../common/repositories/npc_types_repository.h"
#include "../../corpse.h" #include "../../corpse.h"
#include "../../../common/repositories/npc_types_repository.h"
#include "../../../common/repositories/respawn_times_repository.h" #include "../../../common/repositories/respawn_times_repository.h"
#include "../../../common/repositories/zone_state_spawns_repository.h"
inline void ClearState() inline void ClearState()
{ {
@@ -38,7 +39,11 @@ inline void SetupStateZone()
SetupZone("soldungb"); SetupZone("soldungb");
zone->Process(); zone->Process();
// depop the zone controller // depop the zone controller
entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID)->Depop(); auto controller = entity_list.GetNPCByNPCTypeID(ZONE_CONTROLLER_NPC_ID);
if (controller != nullptr) {
controller->Depop();
}
entity_list.MobProcess(); // process the depop entity_list.MobProcess(); // process the depop
} }
@@ -352,8 +357,12 @@ inline void TestSpawns()
npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand);
} }
bool condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == 115; bool condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == entries;
RunTest("Spawns > All NPC's killed (0 NPCs) (115 Corpses)", true, condition); RunTest(
fmt::format("Spawns > All NPC's killed (0 NPCs) ([{}] Corpses)", entries),
true,
condition
);
std::vector<uint32_t> spawn2_ids = {}; std::vector<uint32_t> spawn2_ids = {};
@@ -377,11 +386,15 @@ inline void TestSpawns()
zone->Shutdown(); zone->Shutdown();
SetupStateZone(); SetupStateZone();
condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == 115; condition = (int) entity_list.GetNPCList().size() == 0 && (int) entity_list.GetCorpseList().size() == entries;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
} }
RunTest("Spawns > After restore (0 NPCs) (115 Corpses)", true, condition); RunTest(
fmt::format("Spawns > After restore (0 NPCs) ([{}] Corpses)", entries),
true,
condition
);
for (auto &e: entity_list.GetCorpseList()) { for (auto &e: entity_list.GetCorpseList()) {
auto c = e.second; auto c = e.second;
@@ -405,11 +418,15 @@ inline void TestSpawns()
zone->Process(); zone->Process();
condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 115; condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == entries;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
} }
RunTest("Spawns > After respawn (115 NPCs) (115 Corpses)", true, condition); RunTest(
fmt::format("Spawns > After respawn ([{}] NPCs) ([{}] Corpses)", entries, entries),
true,
condition
);
for (auto &c: entity_list.GetCorpseList()) { for (auto &c: entity_list.GetCorpseList()) {
c.second->DepopNPCCorpse(); c.second->DepopNPCCorpse();
@@ -417,11 +434,15 @@ inline void TestSpawns()
entity_list.CorpseProcess(); entity_list.CorpseProcess();
condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 0; condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 0;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
} }
RunTest("Spawns > After respawn (115 NPCs) (0 Corpses)", true, condition); RunTest(
fmt::format("Spawns > After respawn ([{}] NPCs) (0 Corpses)", entries),
true,
condition
);
// lets set NPC's up with a predictable loottable for testing // lets set NPC's up with a predictable loottable for testing
uint32_t loottable_id = SeedLootTable(); uint32_t loottable_id = SeedLootTable();
@@ -462,20 +483,28 @@ inline void TestSpawns()
npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand); npc->Death(npc, npc->GetHP() + 1, SPELL_UNKNOWN, EQ::skills::SkillHandtoHand);
} }
condition = (int) entity_list.GetNPCList().size() == 105 && (int) entity_list.GetCorpseList().size() == 10; condition = (int) entity_list.GetNPCList().size() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
} }
RunTest("Spawns > Kill 10 NPC's before save/restore (105 NPCs) (10 Corpses)", true, condition); RunTest(
fmt::format("Spawns > Kill 10 NPC's before save/restore ([{}] NPCs) (10 Corpses)", (entries - 10)),
true,
condition
);
zone->Shutdown(); zone->Shutdown();
SetupStateZone(); SetupStateZone();
condition = (int) entity_list.GetNPCList().size() == 105 && (int) entity_list.GetCorpseList().size() == 10; condition = (int) entity_list.GetNPCList().size() == (entries - 10) && (int) entity_list.GetCorpseList().size() == 10;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
} }
RunTest("Spawns > After restore (105 NPCs) (10 Corpses)", true, condition); RunTest(
fmt::format("Spawns > After restore ([{}] NPCs) (10 Corpses)", (entries - 10)),
true,
condition
);
// validate that all corpses and npc's have cloak of flames // validate that all corpses and npc's have cloak of flames
bool test_failed = false; bool test_failed = false;
@@ -572,6 +601,14 @@ inline void TestSpawns()
false false
); );
int max_respawn = 0;
const auto& l = RespawnTimesRepository::All(database);
for (const auto& e : l) {
if (e.duration > max_respawn) {
max_respawn = e.duration;
}
}
entity_list.MobProcess(); entity_list.MobProcess();
zone->Process(); zone->Process();
@@ -587,16 +624,20 @@ inline void TestSpawns()
npc->SetEntityVariable("previously_spawned", "true"); npc->SetEntityVariable("previously_spawned", "true");
} }
Timer::RollForward(302401); // longest respawn time in zone Timer::RollForward(max_respawn); // longest respawn time in zone
zone->Process(); zone->Process();
entity_list.MobProcess(); // processing depops entity_list.MobProcess(); // processing depops
condition = (int) entity_list.GetNPCList().size() == 115 && (int) entity_list.GetCorpseList().size() == 10; condition = (int) entity_list.GetNPCList().size() == entries && (int) entity_list.GetCorpseList().size() == 10;
if (!condition) { if (!condition) {
PrintEntityCounts(); PrintEntityCounts();
PrintZoneNpcs(); PrintZoneNpcs();
} }
RunTest("Spawns > After respawn, ensure we have expected entity counts (115 NPCs) (10 Corpses)", true, condition); RunTest(
fmt::format("Spawns > After respawn, ensure we have expected entity counts ([{}] NPCs) (10 Corpses)", entries),
true,
condition
);
entity_list.MobProcess(); // processing depops entity_list.MobProcess(); // processing depops
+108 -16
View File
@@ -226,8 +226,8 @@ Client::Client() : Mob(
last_reported_endurance_percent = 0; last_reported_endurance_percent = 0;
last_reported_mana_percent = 0; last_reported_mana_percent = 0;
gm_hide_me = false; gm_hide_me = false;
AFK = false; m_is_afk = false;
LFG = false; LFG = false;
LFGFromLevel = 0; LFGFromLevel = 0;
LFGToLevel = 0; LFGToLevel = 0;
LFGMatchFilter = false; LFGMatchFilter = false;
@@ -536,8 +536,8 @@ Client::Client(EQStreamInterface *ieqs) : Mob(
last_reported_endurance_percent = 0; last_reported_endurance_percent = 0;
last_reported_mana_percent = 0; last_reported_mana_percent = 0;
gm_hide_me = false; gm_hide_me = false;
AFK = false; m_is_afk = false;
LFG = false; LFG = false;
LFGFromLevel = 0; LFGFromLevel = 0;
LFGToLevel = 0; LFGToLevel = 0;
LFGMatchFilter = false; LFGMatchFilter = false;
@@ -995,6 +995,8 @@ bool Client::Save(uint8 iCommitNow) {
if(!ClientDataLoaded()) if(!ClientDataLoaded())
return false; return false;
BenchTimer timer;
/* Wrote current basics to PP for saves */ /* Wrote current basics to PP for saves */
if (!m_lock_save_position) { if (!m_lock_save_position) {
m_pp.x = m_Position.x; m_pp.x = m_Position.x;
@@ -1105,6 +1107,8 @@ bool Client::Save(uint8 iCommitNow) {
database.botdb.SaveBotSettings(this); database.botdb.SaveBotSettings(this);
} }
LogInfo("Save for [{}] took [{}]", GetCleanName(), timer.elapsed());
return true; return true;
} }
@@ -1171,6 +1175,10 @@ void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CO
return; return;
} }
if (RuleB(Character, AutoIdleFilterPackets) && m_is_idle && IsFilteredAFKPacket(app)) {
return;
}
if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) { if (client_state != CLIENT_CONNECTED && required_state == CLIENT_CONNECTED) {
AddPacket(app, ack_req); AddPacket(app, ack_req);
return; return;
@@ -1209,6 +1217,58 @@ void Client::ChannelMessageReceived(uint8 chan_num, uint8 language, uint8 lang_s
LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message); LogDebug("Client::ChannelMessageReceived() Channel:[{}] message:[{}]", chan_num, message);
if (RuleB(Chat, AlwaysCaptureCommandText)) {
if (message[0] == COMMAND_CHAR) {
if (command_dispatch(this, message, false) == -2) {
if (parse->PlayerHasQuestSub(EVENT_COMMAND)) {
int i = parse->EventPlayer(EVENT_COMMAND, this, message, 0);
if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Command '%s' not recognized.", message);
}
}
else if (parse->PlayerHasQuestSub(EVENT_SAY)) {
int i = parse->EventPlayer(EVENT_SAY, this, message, 0);
if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Command '%s' not recognized.", message);
}
}
else {
if (!RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Command '%s' not recognized.", message);
}
}
}
return;
}
if (message[0] == BOT_COMMAND_CHAR) {
if (RuleB(Bots, Enabled)) {
if (bot_command_dispatch(this, message) == -2) {
if (parse->PlayerHasQuestSub(EVENT_BOT_COMMAND)) {
int i = parse->EventPlayer(EVENT_BOT_COMMAND, this, message, 0);
if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Bot command '%s' not recognized.", message);
}
}
else if (parse->PlayerHasQuestSub(EVENT_SAY)) {
int i = parse->EventPlayer(EVENT_SAY, this, message, 0);
if (i == 0 && !RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Bot command '%s' not recognized.", message);
}
}
else {
if (!RuleB(Chat, SuppressCommandErrors)) {
Message(Chat::Red, "Bot command '%s' not recognized.", message);
}
}
}
} else {
Message(Chat::Red, "Bots are disabled on this server.");
}
return;
}
}
if (targetname == nullptr) { if (targetname == nullptr) {
targetname = (!GetTarget()) ? "" : GetTarget()->GetName(); targetname = (!GetTarget()) ? "" : GetTarget()->GetName();
} }
@@ -2472,7 +2532,7 @@ void Client::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho)
Mob::FillSpawnStruct(ns, ForWho); Mob::FillSpawnStruct(ns, ForWho);
// Populate client-specific spawn information // Populate client-specific spawn information
ns->spawn.afk = AFK; ns->spawn.afk = m_is_afk;
ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live ns->spawn.lfg = LFG; // afk and lfg are cleared on zoning on live
ns->spawn.anon = m_pp.anon; ns->spawn.anon = m_pp.anon;
ns->spawn.gm = GetGM() ? 1 : 0; ns->spawn.gm = GetGM() ? 1 : 0;
@@ -10783,15 +10843,37 @@ void Client::SetAnon(uint8 anon_flag) {
safe_delete(outapp); safe_delete(outapp);
} }
void Client::SetAFK(uint8 afk_flag) { void Client::SetAFK(uint8 afk_flag)
AFK = afk_flag; {
auto outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); if (!afk_flag) {
SpawnAppearance_Struct* spawn_appearance = (SpawnAppearance_Struct*)outapp->pBuffer; ResetAFKTimer();
spawn_appearance->spawn_id = GetID(); }
spawn_appearance->type = AppearanceType::AFK;
spawn_appearance->parameter = afk_flag; bool changed_afk_state = (m_is_afk && !afk_flag) || (!m_is_afk && afk_flag);
entity_list.QueueClients(this, outapp);
safe_delete(outapp); if (!changed_afk_state) {
return;
}
// set messaging based on the state
std::string you_are = "You are no longer AFK.";
if (!m_is_afk && afk_flag) {
you_are = "You are now AFK.";
}
// set the state
m_is_afk = afk_flag;
// inform of state change
Message(Chat::Yellow, you_are.c_str());
// send the spawn appearance packet to all clients
static EQApplicationPacket p(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct));
auto *s = (SpawnAppearance_Struct *) p.pBuffer;
s->spawn_id = GetID();
s->type = AppearanceType::AFK;
s->parameter = afk_flag;
entity_list.QueueClients(this, &p);
} }
void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) { void Client::SendToInstance(std::string instance_type, std::string zone_short_name, uint32 instance_version, float x, float y, float z, float heading, std::string instance_identifier, uint32 duration) {
@@ -12657,7 +12739,7 @@ void Client::SendTopLevelInventory()
} }
} }
void Client::CheckSendBulkNpcPositions() void Client::CheckSendBulkNpcPositions(bool force)
{ {
float distance_moved = DistanceNoZ(m_last_position_before_bulk_update, GetPosition()); float distance_moved = DistanceNoZ(m_last_position_before_bulk_update, GetPosition());
float update_range = RuleI(Range, MobCloseScanDistance); float update_range = RuleI(Range, MobCloseScanDistance);
@@ -12668,7 +12750,7 @@ void Client::CheckSendBulkNpcPositions()
int updated_count = 0; int updated_count = 0;
int skipped_count = 0; int skipped_count = 0;
if (is_ready_to_update) { if (is_ready_to_update || force) {
auto &mob_movement_manager = MobMovementManager::Get(); auto &mob_movement_manager = MobMovementManager::Get();
for (auto &e: entity_list.GetMobList()) { for (auto &e: entity_list.GetMobList()) {
@@ -12714,6 +12796,16 @@ void Client::CheckSendBulkNpcPositions()
updated_count++; updated_count++;
} }
if (force) {
static EQApplicationPacket p(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct));
auto *s = (PlayerPositionUpdateServer_Struct *) p.pBuffer;
for (auto &e: entity_list.GetCorpseList()) {
Corpse *c = e.second;
MakeSpawnUpdate(s);
QueuePacket(&p, false);
}
}
LogPositionUpdate( LogPositionUpdate(
"[{}] Sent [{}] bulk updated NPC positions, skipped [{}] distance_moved [{}] update_range [{}]", "[{}] Sent [{}] bulk updated NPC positions, skipped [{}] distance_moved [{}] update_range [{}]",
GetCleanName(), GetCleanName(),
+33 -7
View File
@@ -73,6 +73,7 @@ namespace EQ
#include "../common/guild_base.h" #include "../common/guild_base.h"
#include "../common/repositories/buyer_buy_lines_repository.h" #include "../common/repositories/buyer_buy_lines_repository.h"
#include "../common/repositories/character_evolving_items_repository.h" #include "../common/repositories/character_evolving_items_repository.h"
#include "../common/repositories/player_titlesets_repository.h"
#include "bot_structs.h" #include "bot_structs.h"
@@ -483,6 +484,9 @@ public:
virtual bool Save() { return Save(0); } virtual bool Save() { return Save(0); }
bool Save(uint8 iCommitNow); // 0 = delayed, 1=async now, 2=sync now bool Save(uint8 iCommitNow); // 0 = delayed, 1=async now, 2=sync now
inline void SaveCharacterData() {
database.SaveCharacterData(this, &m_pp, &m_epp);
};
/* New PP Save Functions */ /* New PP Save Functions */
bool SaveCurrency(){ return database.SaveCharacterCurrency(this->CharacterID(), &m_pp); } bool SaveCurrency(){ return database.SaveCharacterCurrency(this->CharacterID(), &m_pp); }
@@ -497,9 +501,19 @@ public:
void Kick(const std::string &reason); void Kick(const std::string &reason);
void WorldKick(); void WorldKick();
inline uint8 GetAnon() const { return m_pp.anon; } inline uint8 GetAnon() const { return m_pp.anon; }
inline uint8 GetAFK() const { return AFK; } inline uint8 GetAFK() const { return m_is_afk; }
void SetAnon(uint8 anon_flag); void SetAnon(uint8 anon_flag);
inline Client* ResetAFKTimer() {
if (!RuleB(Character, EnableAutoAFK)) {
return this;
}
m_afk_reset = true;
m_last_moved = std::chrono::steady_clock::now();
return this;
};
void SetAFK(uint8 afk_flag); void SetAFK(uint8 afk_flag);
inline bool IsIdle() { return m_is_idle; }
inline PlayerProfile_Struct& GetPP() { return m_pp; } inline PlayerProfile_Struct& GetPP() { return m_pp; }
inline ExtendedProfile_Struct& GetEPP() { return m_epp; } inline ExtendedProfile_Struct& GetEPP() { return m_epp; }
inline EQ::InventoryProfile& GetInv() { return m_inv; } inline EQ::InventoryProfile& GetInv() { return m_inv; }
@@ -1252,9 +1266,10 @@ public:
void ResetAllCastbarCooldowns(); void ResetAllCastbarCooldowns();
void ResetCastbarCooldownBySpellID(uint32 spell_id); void ResetCastbarCooldownBySpellID(uint32 spell_id);
bool CheckTitle(int titleset); bool CheckTitle(int title_set);
void EnableTitle(int titleset); void EnableTitle(int title_set, bool insert = true);
void RemoveTitle(int titleset); const std::vector<PlayerTitlesetsRepository::PlayerTitlesets>& GetTitles() { return m_player_title_sets; };
void RemoveTitle(int title_set);
void EnteringMessages(Client* client); void EnteringMessages(Client* client);
void SendRules(); void SendRules();
@@ -2057,7 +2072,8 @@ private:
uint8 LFGToLevel; uint8 LFGToLevel;
bool LFGMatchFilter; bool LFGMatchFilter;
char LFGComments[64]; char LFGComments[64];
bool AFK; bool m_is_afk = false;
bool m_is_manual_afk = false;
bool auto_attack; bool auto_attack;
bool auto_fire; bool auto_fire;
bool runmode; bool runmode;
@@ -2077,7 +2093,8 @@ private:
uint16 trader_id; uint16 trader_id;
uint16 customer_id; uint16 customer_id;
uint32 account_creation; uint32 account_creation;
uint8 firstlogon; bool first_login;
bool ingame;
uint32 mercid; // current merc uint32 mercid; // current merc
uint8 mercSlot; // selected merc slot uint8 mercSlot; // selected merc slot
time_t m_trader_transaction_date; time_t m_trader_transaction_date;
@@ -2210,7 +2227,12 @@ private:
glm::vec4 m_last_position_before_bulk_update; glm::vec4 m_last_position_before_bulk_update;
Timer m_client_bulk_npc_pos_update_timer; Timer m_client_bulk_npc_pos_update_timer;
Timer m_position_update_timer; Timer m_position_update_timer;
void CheckSendBulkNpcPositions(); void CheckSendBulkNpcPositions(bool force = false);
// afk
bool m_is_idle = false;
bool m_afk_reset = false; // used to trigger next-tic afk reset
std::chrono::steady_clock::time_point m_last_moved = std::chrono::steady_clock::now();
void BulkSendInventoryItems(); void BulkSendInventoryItems();
@@ -2254,6 +2276,7 @@ private:
bool m_exp_enabled; bool m_exp_enabled;
std::vector<EXPModifier> m_exp_modifiers; std::vector<EXPModifier> m_exp_modifiers;
std::vector<PlayerTitlesetsRepository::PlayerTitlesets> m_player_title_sets;
//Anti Spam Stuff //Anti Spam Stuff
Timer *KarmaUpdateTimer; Timer *KarmaUpdateTimer;
@@ -2403,6 +2426,9 @@ public:
const std::string &GetMailKey() const; const std::string &GetMailKey() const;
void ShowZoneShardMenu(); void ShowZoneShardMenu();
void Handle_OP_ChangePetName(const EQApplicationPacket *app); void Handle_OP_ChangePetName(const EQApplicationPacket *app);
bool IsFilteredAFKPacket(const EQApplicationPacket *p);
void CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p);
void SyncWorldPositionsToClient(bool ignore_idle = false);
}; };
#endif #endif
+61 -67
View File
@@ -1,6 +1,8 @@
#include "bot.h" #include "bot.h"
#include "client.h" #include "client.h"
#define NO_BOT_LIMIT -1;
bool Client::GetBotOption(BotOwnerOption boo) const { bool Client::GetBotOption(BotOwnerOption boo) const {
if (boo < _booCount) { if (boo < _booCount) {
return bot_owner_options[boo]; return bot_owner_options[boo];
@@ -18,25 +20,25 @@ void Client::SetBotOption(BotOwnerOption boo, bool flag) {
uint32 Client::GetBotCreationLimit(uint8 class_id) { uint32 Client::GetBotCreationLimit(uint8 class_id) {
uint32 bot_creation_limit = RuleI(Bots, CreationLimit); uint32 bot_creation_limit = RuleI(Bots, CreationLimit);
if (Admin() >= RuleI(Bots, MinStatusToBypassCreateLimit)) { if (GetGM() && Admin() >= RuleI(Bots, MinStatusToBypassCreateLimit)) {
return RuleI(Bots, MinStatusBypassCreateLimit); return RuleI(Bots, MinStatusBypassCreateLimit);
} }
const auto bucket_name = fmt::format( const auto bucket_name = fmt::format(
"bot_creation_limit{}", "bot_creation_limit{}",
( class_id && IsPlayerClass(class_id) ?
class_id && IsPlayerClass(class_id) ? fmt::format("_{}", Strings::ToLower(GetClassIDName(class_id))) :
fmt::format(
"_{}",
Strings::ToLower(GetClassIDName(class_id))
) :
"" ""
)
); );
auto bucket_value = GetBucket(bucket_name); auto bucket_value = GetBucket(bucket_name);
if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) { if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) {
bot_creation_limit = Strings::ToUnsignedInt(bucket_value); bot_creation_limit = Strings::ToInt(bucket_value);
}
if (class_id && bucket_value.empty()) {
bot_creation_limit = NO_BOT_LIMIT;
} }
return bot_creation_limit; return bot_creation_limit;
@@ -45,63 +47,55 @@ uint32 Client::GetBotCreationLimit(uint8 class_id) {
int Client::GetBotRequiredLevel(uint8 class_id) { int Client::GetBotRequiredLevel(uint8 class_id) {
int bot_character_level = RuleI(Bots, BotCharacterLevel); int bot_character_level = RuleI(Bots, BotCharacterLevel);
if (GetGM() && Admin() >= RuleI(Bots, MinStatusToBypassBotLevelRequirement)) {
return 0;
}
const auto bucket_name = fmt::format( const auto bucket_name = fmt::format(
"bot_required_level{}", "bot_required_level{}",
( class_id && IsPlayerClass(class_id) ?
class_id && IsPlayerClass(class_id) ? fmt::format("_{}", Strings::ToLower(GetClassIDName(class_id))) :
fmt::format(
"_{}",
Strings::ToLower(GetClassIDName(class_id))
) :
"" ""
)
); );
auto bucket_value = GetBucket(bucket_name); auto bucket_value = GetBucket(bucket_name);
if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) { if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) {
bot_character_level = Strings::ToInt(bucket_value); bot_character_level = Strings::ToInt(bucket_value);
} }
if (class_id && bucket_value.empty()) {
bot_character_level = NO_BOT_LIMIT;
}
return bot_character_level; return bot_character_level;
} }
int Client::GetBotSpawnLimit(uint8 class_id) { int Client::GetBotSpawnLimit(uint8 class_id)
{
int bot_spawn_limit = RuleI(Bots, SpawnLimit); int bot_spawn_limit = RuleI(Bots, SpawnLimit);
if (Admin() >= RuleI(Bots, MinStatusToBypassSpawnLimit)) { if (GetGM() && Admin() >= RuleI(Bots, MinStatusToBypassSpawnLimit)) {
return RuleI(Bots, MinStatusBypassSpawnLimit); return RuleI(Bots, MinStatusBypassSpawnLimit);
} }
const auto bucket_name = fmt::format( const auto bucket_name = fmt::format(
"bot_spawn_limit{}", "bot_spawn_limit{}",
( class_id && IsPlayerClass(class_id) ?
class_id && IsPlayerClass(class_id) ? fmt::format("_{}", Strings::ToLower(GetClassIDName(class_id))) :
fmt::format(
"_{}",
Strings::ToLower(GetClassIDName(class_id))
) :
"" ""
)
); );
auto bucket_value = GetBucket(bucket_name); auto bucket_value = GetBucket(bucket_name);
if (class_id && !bot_spawn_limit && bucket_value.empty()) {
const auto new_bucket_name = "bot_spawn_limit";
bucket_value = GetBucket(new_bucket_name);
if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) {
bot_spawn_limit = Strings::ToInt(bucket_value);
return bot_spawn_limit;
}
}
if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) { if (!bucket_value.empty() && Strings::IsNumber(bucket_value)) {
bot_spawn_limit = Strings::ToInt(bucket_value); bot_spawn_limit = Strings::ToInt(bucket_value);
} }
if (class_id && bucket_value.empty()) {
return NO_BOT_LIMIT;
}
if (RuleB(Bots, QuestableSpawnLimit)) { if (RuleB(Bots, QuestableSpawnLimit)) {
const auto query = fmt::format( const auto query = fmt::format(
"SELECT `value` FROM `quest_globals` WHERE `name` = '{}' AND `charid` = {} LIMIT 1", "SELECT `value` FROM `quest_globals` WHERE `name` = '{}' AND `charid` = {} LIMIT 1",
@@ -111,53 +105,53 @@ int Client::GetBotSpawnLimit(uint8 class_id) {
auto results = database.QueryDatabase(query); // use 'database' for non-bot table calls auto results = database.QueryDatabase(query); // use 'database' for non-bot table calls
if (!results.Success() || !results.RowCount()) { if (results.Success() && results.RowCount()) {
return bot_spawn_limit; auto row = results.begin();
bot_spawn_limit = Strings::ToInt(row[0]);
} }
auto row = results.begin();
bot_spawn_limit = Strings::ToInt(row[0]);
} }
const auto& zones_list = Strings::Split(RuleS(Bots, ZonesWithSpawnLimits), ","); if (!class_id) {
const auto &zones_list = Strings::Split(RuleS(Bots, ZonesWithSpawnLimits), ",");
if (!zones_list.empty()) { if (!zones_list.empty()) {
auto it = std::find(zones_list.begin(), zones_list.end(), std::to_string(zone->GetZoneID())); auto it = std::find(zones_list.begin(), zones_list.end(), std::to_string(zone->GetZoneID()));
if (it != zones_list.end()) { if (it != zones_list.end()) {
const auto& zones_limits_list = Strings::Split(RuleS(Bots, ZoneSpawnLimits), ","); const auto &zones_limits_list = Strings::Split(RuleS(Bots, ZoneSpawnLimits), ",");
if (zones_list.size() == zones_limits_list.size()) { if (zones_list.size() == zones_limits_list.size()) {
try { try {
auto new_limit = std::stoul(zones_limits_list[std::distance(zones_list.begin(), it)]); auto new_limit = std::stoul(zones_limits_list[std::distance(zones_list.begin(), it)]);
if (new_limit < bot_spawn_limit) { if (new_limit < bot_spawn_limit) {
bot_spawn_limit = new_limit; bot_spawn_limit = new_limit;
}
} catch (const std::exception &e) {
LogInfo("Invalid entry in Rule Bots:ZoneSpawnLimits: [{}]", e.what());
} }
} catch (const std::exception& e) {
LogInfo("Invalid entry in Rule Bots:ZoneSpawnLimits: [{}]", e.what());
} }
} }
} }
}
const auto& zones_forced_list = Strings::Split(RuleS(Bots, ZonesWithForcedSpawnLimits), ","); const auto &zones_forced_list = Strings::Split(RuleS(Bots, ZonesWithForcedSpawnLimits), ",");
if (!zones_forced_list.empty()) { if (!zones_forced_list.empty()) {
auto it = std::find(zones_forced_list.begin(), zones_forced_list.end(), std::to_string(zone->GetZoneID())); auto it = std::find(zones_forced_list.begin(), zones_forced_list.end(), std::to_string(zone->GetZoneID()));
if (it != zones_forced_list.end()) { if (it != zones_forced_list.end()) {
const auto& zones_forced_limits_list = Strings::Split(RuleS(Bots, ZoneForcedSpawnLimits), ","); const auto &zones_forced_limits_list = Strings::Split(RuleS(Bots, ZoneForcedSpawnLimits), ",");
if (zones_forced_list.size() == zones_forced_limits_list.size()) { if (zones_forced_list.size() == zones_forced_limits_list.size()) {
try { try {
auto new_limit = std::stoul(zones_forced_limits_list[std::distance(zones_forced_list.begin(), it)]); auto new_limit = std::stoul(zones_forced_limits_list[std::distance(zones_forced_list.begin(), it)]);
if (new_limit != bot_spawn_limit) { if (new_limit != bot_spawn_limit) {
bot_spawn_limit = new_limit; bot_spawn_limit = new_limit;
}
} catch (const std::exception &e) {
LogInfo("Invalid entry in Rule Bots:ZoneForcedSpawnLimits: [{}]", e.what());
} }
} catch (const std::exception& e) {
LogInfo("Invalid entry in Rule Bots:ZoneForcedSpawnLimits: [{}]", e.what());
} }
} }
} }
+23 -6
View File
@@ -92,6 +92,16 @@ void Client::ProcessEvolvingItem(const uint64 exp, const Mob *mob)
continue; continue;
} }
if (!evolving_items_manager.GetEvolvingItemsCache().contains(inst->GetID())) {
LogEvolveItem(
"Character ID {} has an evolving item that is not found in the db. Please check your "
"items_evolving_details table for item id {}",
CharacterID(),
inst->GetID()
);
continue;
}
auto const type = evolving_items_manager.GetEvolvingItemsCache().at(inst->GetID()).type; auto const type = evolving_items_manager.GetEvolvingItemsCache().at(inst->GetID()).type;
auto const sub_type = evolving_items_manager.GetEvolvingItemsCache().at(inst->GetID()).sub_type; auto const sub_type = evolving_items_manager.GetEvolvingItemsCache().at(inst->GetID()).sub_type;
@@ -283,24 +293,31 @@ void Client::DoEvolveItemDisplayFinalResult(const EQApplicationPacket *app)
} }
std::unique_ptr<EQ::ItemInstance> const inst(database.CreateItem(item_id)); std::unique_ptr<EQ::ItemInstance> const inst(database.CreateItem(item_id));
if (!inst) {
return;
}
LogEvolveItemDetail( LogEvolveItemDetail(
"Character ID <green>[{}] requested to view final evolve item id <yellow>[{}] for evolve item id <yellow>[{}]", "Character ID <green>[{}] requested to view final evolve item id <yellow>[{}] for evolve item id <yellow>[{}]",
CharacterID(), CharacterID(),
item_id, item_id,
evolving_items_manager.GetFirstItemInLoreGroupByItemID(item_id)); evolving_items_manager.GetFirstItemInLoreGroupByItemID(item_id)
);
inst->SetEvolveProgression(100); inst->SetEvolveProgression(100);
if (inst) { LogEvolveItemDetail(
LogEvolveItemDetail( "Sending final result for item id <yellow>[{}] to Character ID <green>[{}]", item_id, CharacterID()
"Sending final result for item id <yellow>[{}] to Character ID <green>[{}]", item_id, CharacterID()); );
SendItemPacket(0, inst.get(), ItemPacketViewLink); SendItemPacket(0, inst.get(), ItemPacketViewLink);
}
} }
bool Client::DoEvolveCheckProgression(EQ::ItemInstance &inst) bool Client::DoEvolveCheckProgression(EQ::ItemInstance &inst)
{ {
if (!inst) {
return false;
}
if (inst.GetEvolveProgression() < 100 || inst.GetEvolveLvl() == inst.GetMaxEvolveLvl()) { if (inst.GetEvolveProgression() < 100 || inst.GetEvolveLvl() == inst.GetMaxEvolveLvl()) {
return false; return false;
} }
+243 -21
View File
@@ -807,22 +807,42 @@ void Client::CompleteConnect()
m_last_position_before_bulk_update = 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. */ /* This sub event is for if a player logs in for the first time since entering world. */
if (firstlogon == 1) { if (ingame) {
auto e = CharacterDataRepository::FindOne(
database,
CharacterID()
);
bool is_first_login = e.first_login == 0;
RecordPlayerEventLog(PlayerEvent::WENT_ONLINE, PlayerEvent::EmptyEvent{}); RecordPlayerEventLog(PlayerEvent::WENT_ONLINE, PlayerEvent::EmptyEvent{});
if (parse->PlayerHasQuestSub(EVENT_CONNECT)) { if (parse->PlayerHasQuestSub(EVENT_CONNECT)) {
parse->EventPlayer(EVENT_CONNECT, this, "", 0); const std::string& export_string = fmt::format(
"{} {} {}",
e.last_login,
time(nullptr) - e.last_login,
is_first_login ? 1 : 0
);
parse->EventPlayer(EVENT_CONNECT, this, export_string, 0);
} }
/** if (is_first_login) {
* Update last login since this doesn't get updated until a late save later so we can update online status e.first_login = time(nullptr);
*/ TraderRepository::DeleteWhere(database, fmt::format("`char_id` = '{}'", CharacterID()));
database.QueryDatabase( BuyerRepository::DeleteBuyer(database, CharacterID());
StringFormat( LogTradingDetail(
"UPDATE `character_data` SET `last_login` = UNIX_TIMESTAMP() WHERE id = %u", "Removed trader abd buyer entries for Character ID {} on first logon to ensure table consistency.",
CharacterID() CharacterID()
) );
); }
e.last_login = time(nullptr);
const int updated = CharacterDataRepository::UpdateOne(database, e);
if (!updated) {
LogError("Failed to update login time for character_id [{}]", CharacterID());
}
if (IsPetNameChangeAllowed() && !RuleB(Pets, AlwaysAllowPetRename)) { if (IsPetNameChangeAllowed() && !RuleB(Pets, AlwaysAllowPetRename)) {
InvokeChangePetName(false); InvokeChangePetName(false);
@@ -871,7 +891,7 @@ void Client::CompleteConnect()
entity_list.SendFindableNPCList(this); entity_list.SendFindableNPCList(this);
if (IsInAGuild()) { if (IsInAGuild()) {
if (firstlogon == 1) { if (ingame) {
guild_mgr.UpdateDbMemberOnline(CharacterID(), true); guild_mgr.UpdateDbMemberOnline(CharacterID(), true);
SendGuildMembersList(); SendGuildMembersList();
} }
@@ -1307,14 +1327,14 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
/* Load Character Data */ /* Load Character Data */
query = fmt::format( query = fmt::format(
"SELECT `lfp`, `lfg`, `xtargets`, `firstlogon`, `guild_id`, `rank`, `exp_enabled`, `tribute_enable`, `extra_haste`, `illusion_block` FROM `character_data` LEFT JOIN `guild_members` ON `id` = `char_id` WHERE `id` = {}", "SELECT `lfp`, `lfg`, `xtargets`, `first_login`, `guild_id`, `rank`, `exp_enabled`, `tribute_enable`, `extra_haste`, `illusion_block`, `ingame` FROM `character_data` LEFT JOIN `guild_members` ON `id` = `char_id` WHERE `id` = {}",
cid cid
); );
auto results = database.QueryDatabase(query); auto results = database.QueryDatabase(query);
for (auto row : results) { for (auto row : results) {
if (row[4] && Strings::ToInt(row[4]) > 0) { if (row[4] && Strings::ToInt(row[4]) > 0) {
guild_id = Strings::ToInt(row[4]); guild_id = Strings::ToInt(row[4]);
guildrank = row[5] ? Strings::ToInt(row[5]) : GUILD_RANK_NONE; guildrank = row[5] ? Strings::ToInt(row[5]) : GUILD_RANK_NONE;
guild_tribute_opt_in = row[7] ? Strings::ToBool(row[7]) : 0; guild_tribute_opt_in = row[7] ? Strings::ToBool(row[7]) : 0;
} }
@@ -1322,10 +1342,21 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
SetExtraHaste(Strings::ToInt(row[8]), false); SetExtraHaste(Strings::ToInt(row[8]), false);
SetIllusionBlock(Strings::ToBool(row[9])); SetIllusionBlock(Strings::ToBool(row[9]));
if (LFP) { LFP = Strings::ToInt(row[0]); } if (LFP) {
if (LFG) { LFG = Strings::ToInt(row[1]); } LFP = Strings::ToInt(row[0]);
if (row[3]) }
firstlogon = Strings::ToInt(row[3]);
if (LFG) {
LFG = Strings::ToInt(row[1]);
}
if (row[3]) {
first_login = Strings::ToUnsignedInt(row[3]);
}
if (row[10]) {
ingame = Strings::ToBool(row[10]);
}
} }
if (RuleB(Character, SharedBankPlat)) if (RuleB(Character, SharedBankPlat))
@@ -1350,6 +1381,7 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
database.LoadCharacterLeadershipAbilities(cid, &m_pp); /* Load Character Leadership AA's */ database.LoadCharacterLeadershipAbilities(cid, &m_pp); /* Load Character Leadership AA's */
database.LoadCharacterTribute(this); /* Load CharacterTribute */ database.LoadCharacterTribute(this); /* Load CharacterTribute */
database.LoadCharacterEXPModifier(this); /* Load Character EXP Modifier */ database.LoadCharacterEXPModifier(this); /* Load Character EXP Modifier */
database.LoadCharacterTitleSets(this); /* Load Character Title Sets */
// this pattern is strange // this pattern is strange
// this is remnants of the old way of doing things // this is remnants of the old way of doing things
@@ -1713,7 +1745,7 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app)
// Taunt persists when zoning on newer clients, overwrite default. // Taunt persists when zoning on newer clients, overwrite default.
if (m_ClientVersionBit & EQ::versions::maskUFAndLater) { if (m_ClientVersionBit & EQ::versions::maskUFAndLater) {
if (!firstlogon) { if (!ingame) {
pet->SetTaunting(m_petinfo.taunting); pet->SetTaunting(m_petinfo.taunting);
} }
} }
@@ -4351,6 +4383,14 @@ void Client::Handle_OP_CastSpell(const EQApplicationPacket *app)
m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos); m_TargetRing = glm::vec3(castspell->x_pos, castspell->y_pos, castspell->z_pos);
if (castspell->spell_id && IsValidSpell(castspell->spell_id)) {
bool is_non_combat_zone = !zone->CanDoCombat() || zone->BuffTimersSuspended();
bool is_excluded_reset = is_non_combat_zone && IsBardSong(castspell->spell_id);
if (!is_excluded_reset) {
ResetAFKTimer();
}
}
LogSpells("OP CastSpell: slot [{}] spell [{}] target [{}] inv [{}]", castspell->slot, castspell->spell_id, castspell->target_id, (unsigned long)castspell->inventoryslot); LogSpells("OP CastSpell: slot [{}] spell [{}] target [{}] inv [{}]", castspell->slot, castspell->spell_id, castspell->target_id, (unsigned long)castspell->inventoryslot);
CastingSlot slot = static_cast<CastingSlot>(castspell->slot); CastingSlot slot = static_cast<CastingSlot>(castspell->slot);
@@ -4534,6 +4574,12 @@ void Client::Handle_OP_ChannelMessage(const EQApplicationPacket *app)
return; return;
} }
// reject automatic AFK messages from resetting /afk
std::string message = cm->message;
if (!Strings::Contains(message, "Sorry, I am A.F.K.")) {
ResetAFKTimer();
}
if (IsAIControlled() && !GetGM()) { if (IsAIControlled() && !GetGM()) {
Message(Chat::Red, "You try to speak but can't move your mouth!"); Message(Chat::Red, "You try to speak but can't move your mouth!");
return; return;
@@ -4960,6 +5006,10 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) {
SetMoving(!(cy == m_Position.y && cx == m_Position.x)); SetMoving(!(cy == m_Position.y && cx == m_Position.x));
if (RuleB(Character, EnableAutoAFK)) {
CheckAutoIdleAFK(ppu);
}
CheckClientToNpcAggroTimer(); CheckClientToNpcAggroTimer();
if (m_mob_check_moving_timer.Check()) { if (m_mob_check_moving_timer.Check()) {
@@ -10760,6 +10810,8 @@ void Client::Handle_OP_MoveItem(const EQApplicationPacket *app)
return; return;
} }
ResetAFKTimer();
BenchTimer bench; BenchTimer bench;
MoveItem_Struct* mi = (MoveItem_Struct*) app->pBuffer; MoveItem_Struct* mi = (MoveItem_Struct*) app->pBuffer;
@@ -14700,7 +14752,9 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app)
} }
else if (sa->type == AppearanceType::AFK) { else if (sa->type == AppearanceType::AFK) {
if (afk_toggle_timer.Check()) { if (afk_toggle_timer.Check()) {
AFK = (sa->parameter == 1); m_is_afk = (sa->parameter == 1);
m_is_manual_afk = (sa->parameter == 1);
ResetAFKTimer();
entity_list.QueueClients(this, app, true); entity_list.QueueClients(this, app, true);
} }
} }
@@ -15519,6 +15573,14 @@ void Client::Handle_OP_TradeRequest(const EQApplicationPacket *app)
// Pass trade request on to recipient // Pass trade request on to recipient
if (tradee && tradee->IsClient()) { if (tradee && tradee->IsClient()) {
// if we are idling we need to sync client positions otherwise clients will not be aware of each other
if (m_is_idle) {
SyncWorldPositionsToClient(true);
}
if (tradee->CastToClient()->IsIdle()) {
tradee->CastToClient()->SyncWorldPositionsToClient(true);
}
tradee->CastToClient()->QueuePacket(app); tradee->CastToClient()->QueuePacket(app);
} }
else if (tradee && (tradee->IsNPC() || tradee->IsBot())) { else if (tradee && (tradee->IsNPC() || tradee->IsBot())) {
@@ -15548,6 +15610,14 @@ void Client::Handle_OP_TradeRequestAck(const EQApplicationPacket *app)
Mob* tradee = entity_list.GetMob(msg->to_mob_id); Mob* tradee = entity_list.GetMob(msg->to_mob_id);
if (tradee && tradee->IsClient()) { if (tradee && tradee->IsClient()) {
// if we are idling we need to sync client positions otherwise clients will not be aware of each other
if (m_is_idle) {
SyncWorldPositionsToClient(true);
}
if (tradee->CastToClient()->IsIdle()) {
tradee->CastToClient()->SyncWorldPositionsToClient(true);
}
trade->Start(msg->to_mob_id); trade->Start(msg->to_mob_id);
tradee->CastToClient()->QueuePacket(app); tradee->CastToClient()->QueuePacket(app);
} }
@@ -15567,7 +15637,9 @@ void Client::Handle_OP_TraderShop(const EQApplicationPacket *app)
switch (in->Code) { switch (in->Code) {
case ClickTrader: { case ClickTrader: {
LogTrading("Handle_OP_TraderShop case ClickTrader [{}]", in->Code); LogTrading("Handle_OP_TraderShop case ClickTrader [{}]", in->Code);
auto outapp = std::make_unique<EQApplicationPacket>(OP_TraderShop, sizeof(TraderClick_Struct)); auto outapp =
std::make_unique<EQApplicationPacket>(OP_TraderShop, static_cast<uint32>(sizeof(TraderClick_Struct))
);
auto data = (TraderClick_Struct *) outapp->pBuffer; auto data = (TraderClick_Struct *) outapp->pBuffer;
auto trader_client = entity_list.GetClientByID(in->TraderID); auto trader_client = entity_list.GetClientByID(in->TraderID);
@@ -17132,3 +17204,153 @@ void Client::Handle_OP_EvolveItem(const EQApplicationPacket *app)
} }
} }
} }
bool Client::IsFilteredAFKPacket(const EQApplicationPacket *p)
{
if (p->GetOpcode() == OP_ClientUpdate) {
return true;
}
return false;
}
void Client::CheckAutoIdleAFK(PlayerPositionUpdateClient_Struct *p)
{
if (!RuleB(Character, EnableAutoAFK)) {
return;
}
bool is_non_combat_zone = !zone->CanDoCombat() || zone->BuffTimersSuspended();
int seconds_before_afk =
is_non_combat_zone ?
RuleI(Character, SecondsBeforeAFKNonCombatZone) :
RuleI(Character, SecondsBeforeAFKCombatZone);
int seconds_before_idle =
is_non_combat_zone ?
RuleI(Character, SecondsBeforeIdleNonCombatZone) :
RuleI(Character, SecondsBeforeIdleCombatZone);
// seconds_before_idle can't be greater than seconds_before_afk
if (seconds_before_idle > seconds_before_afk) {
seconds_before_idle = seconds_before_afk;
}
bool has_moved =
m_Position.x != p->x_pos ||
m_Position.y != p->y_pos ||
m_Position.z != p->z_pos ||
m_Position.w != EQ12toFloat(p->heading);
bool triggered_reset = m_afk_reset;
bool was_idle = m_is_idle;
bool is_idle_or_afk = m_is_idle || m_is_afk;
if (!has_moved && (!m_is_idle || !m_is_afk)) {
auto now = std::chrono::steady_clock::now();
auto since_last_moved = now - m_last_moved;
if (!m_is_manual_afk && !m_is_afk && since_last_moved > std::chrono::seconds(seconds_before_afk)) {
bool is_client_excluded_from_afk = (IsBuyer() || IsTrader() || GetGM());
if (is_client_excluded_from_afk) {
return;
}
LogInfo(
"Client [{}] has been AFK for [{}] seconds",
GetCleanName(),
std::chrono::duration_cast<std::chrono::seconds>(since_last_moved).count()
);
SetAFK(true);
return;
}
else if (!m_is_idle && since_last_moved > std::chrono::seconds(seconds_before_idle)) {
bool is_client_excluded_from_idle = GetGM() && !is_non_combat_zone;
if (is_client_excluded_from_idle) {
return;
}
LogInfo(
"Client [{}] has been idle for [{}] seconds",
GetCleanName(),
std::chrono::duration_cast<std::chrono::seconds>(since_last_moved).count()
);
m_is_idle = true;
Message(Chat::Yellow, "You are now idle. Updates will be sent to you less frequently.");
return;
}
}
// if we triggered a reset, but didn't move, we are still idling but not AFK
if (triggered_reset && was_idle) {
m_is_idle = true;
}
// if we moved or triggered reset through other actions, we are no longer AFK.
// we could trigger resetting AFK status through actions like message, cast, attack etc but still by idle until we move
if (!m_is_manual_afk && (has_moved || triggered_reset) && m_is_afk) {
LogInfo("AFK [{}] is no longer idle, syncing positions", GetCleanName());
SetAFK(false);
ResetAFKTimer();
}
// we could be not AFK and idle at the same time
if (has_moved && m_is_idle) {
LogInfo("Idle [{}] is no longer idle, syncing positions", GetCleanName());
m_is_idle = false;
Message(Chat::Yellow, "You are no longer idle.");
SyncWorldPositionsToClient();
ResetAFKTimer();
}
m_afk_reset = false;
}
void Client::SyncWorldPositionsToClient(bool ignore_idle)
{
// if we are idle currently, we need to force updates (which bypasses idle status) and reset idle status
bool reset_idle = false;
if (ignore_idle && m_is_idle) {
m_is_idle = false;
reset_idle = true;
}
LogInfo("Syncing positions for client [{}]", GetCleanName());
CheckSendBulkNpcPositions(true);
static EQApplicationPacket cu(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct));
for (auto &e: entity_list.GetClientList()) {
auto c = e.second;
// skip if not in range
if (Distance(c->GetPosition(), GetPosition()) > RuleI(Range, ClientPositionUpdates)) {
continue;
}
// skip self
if (c == this) {
continue;
}
auto *spu = (PlayerPositionUpdateServer_Struct *) cu.pBuffer;
memset(spu, 0x00, sizeof(PlayerPositionUpdateServer_Struct));
spu->spawn_id = c->GetID();
spu->x_pos = FloatToEQ19(c->GetX());
spu->y_pos = FloatToEQ19(c->GetY());
spu->z_pos = FloatToEQ19(c->GetZ());
spu->heading = FloatToEQ12(c->GetHeading());
spu->delta_x = FloatToEQ13(0);
spu->delta_y = FloatToEQ13(0);
spu->delta_z = FloatToEQ13(0);
spu->delta_heading = FloatToEQ10(0);
spu->animation = 0;
QueuePacket(&cu);
}
if (ignore_idle && reset_idle) {
m_is_idle = false;
}
}
+7 -1
View File
@@ -217,6 +217,8 @@ bool Client::Process() {
GetMerc()->Depop(); GetMerc()->Depop();
} }
instalog = true; instalog = true;
camp_timer.Disable();
} }
if (IsStunned() && stunned_timer.Check()) if (IsStunned() && stunned_timer.Check())
@@ -534,6 +536,10 @@ bool Client::Process() {
DoEnduranceRegen(); DoEnduranceRegen();
BuffProcess(); BuffProcess();
if (auto_attack) {
ResetAFKTimer();
}
if (tribute_timer.Check()) { if (tribute_timer.Check()) {
ToggleTribute(true); //re-activate the tribute. ToggleTribute(true); //re-activate the tribute.
} }
@@ -725,7 +731,7 @@ void Client::OnDisconnect(bool hard_disconnect) {
o->trade->Reset(); o->trade->Reset();
} }
database.SetFirstLogon(CharacterID(), 0); //We change firstlogon status regardless of if a player logs out to zone or not, because we only want to trigger it on their first login from world. database.SetIngame(CharacterID(), 0); //We change ingame status regardless of if a player logs out to zone or not, because we only want to trigger it on their first login from world.
/* Remove from all proximities */ /* Remove from all proximities */
ClearAllProximities(); ClearAllProximities();
+11 -1
View File
@@ -246,6 +246,7 @@ int command_init(void)
command_add("zoneinstance", "[Instance ID] [X] [Y] [Z] - Teleport to specified Instance by ID (coordinates are optional)", AccountStatus::Guide, command_zone_instance) || command_add("zoneinstance", "[Instance ID] [X] [Y] [Z] - Teleport to specified Instance by ID (coordinates are optional)", AccountStatus::Guide, command_zone_instance) ||
command_add("zoneshard", "[zone] [instance_id] - Teleport explicitly to a zone shard", AccountStatus::Player, command_zone_shard) || command_add("zoneshard", "[zone] [instance_id] - Teleport explicitly to a zone shard", AccountStatus::Player, command_zone_shard) ||
command_add("zoneshutdown", "[instance|zone] [Instance ID|Zone ID|Zone Short Name] - Shut down a zone server by Instance ID, Zone ID, or Zone Short Name", AccountStatus::GMLeadAdmin, command_zoneshutdown) || command_add("zoneshutdown", "[instance|zone] [Instance ID|Zone ID|Zone Short Name] - Shut down a zone server by Instance ID, Zone ID, or Zone Short Name", AccountStatus::GMLeadAdmin, command_zoneshutdown) ||
command_add("zonevariable", "[clear|delete|set|view] - Modify zone variables for your current zone", AccountStatus::GMAdmin, command_zonevariable) ||
command_add("zsave", " Saves zheader to the database", AccountStatus::QuestTroupe, command_zsave) command_add("zsave", " Saves zheader to the database", AccountStatus::QuestTroupe, command_zsave)
) { ) {
command_deinit(); command_deinit();
@@ -513,7 +514,15 @@ int command_realdispatch(Client *c, std::string message, bool ignore_status)
parse->EventPlayer(EVENT_GM_COMMAND, c, message, 0); parse->EventPlayer(EVENT_GM_COMMAND, c, message, 0);
} }
if (player_event_logs.IsEventEnabled(PlayerEvent::GM_COMMAND) && message != "#help") { bool log_command = true;
for (auto &cmd: Strings::Split(RuleS(Logging, PlayerEventsIgnoreGMCommands), ",")) {
if (Strings::Contains(command, Strings::ToLower(cmd))) {
log_command = false;
break;
}
}
if (player_event_logs.IsEventEnabled(PlayerEvent::GM_COMMAND) && log_command) {
auto e = PlayerEvent::GMCommandEvent{ auto e = PlayerEvent::GMCommandEvent{
.message = message, .message = message,
.target = c->GetTarget() ? c->GetTarget()->GetName() : "NONE" .target = c->GetTarget() ? c->GetTarget()->GetName() : "NONE"
@@ -928,6 +937,7 @@ void command_bot(Client *c, const Seperator *sep)
#include "gm_commands/zone.cpp" #include "gm_commands/zone.cpp"
#include "gm_commands/zonebootup.cpp" #include "gm_commands/zonebootup.cpp"
#include "gm_commands/zoneshutdown.cpp" #include "gm_commands/zoneshutdown.cpp"
#include "gm_commands/zonevariable.cpp"
#include "gm_commands/zone_instance.cpp" #include "gm_commands/zone_instance.cpp"
#include "gm_commands/zone_shard.cpp" #include "gm_commands/zone_shard.cpp"
#include "gm_commands/zsave.cpp" #include "gm_commands/zsave.cpp"
+1
View File
@@ -198,6 +198,7 @@ void command_zone_instance(Client *c, const Seperator *sep);
void command_zone_shard(Client *c, const Seperator *sep); void command_zone_shard(Client *c, const Seperator *sep);
void command_zonebootup(Client *c, const Seperator *sep); void command_zonebootup(Client *c, const Seperator *sep);
void command_zoneshutdown(Client *c, const Seperator *sep); void command_zoneshutdown(Client *c, const Seperator *sep);
void command_zonevariable(Client *c, const Seperator *sep);
void command_zsave(Client *c, const Seperator *sep); void command_zsave(Client *c, const Seperator *sep);
#include "bot.h" #include "bot.h"
+3 -1
View File
@@ -1,6 +1,7 @@
#include "data_bucket.h" #include "data_bucket.h"
#include "zonedb.h" #include "zonedb.h"
#include "mob.h" #include "mob.h"
#include "client.h"
#include "worldserver.h" #include "worldserver.h"
#include <ctime> #include <ctime>
#include <cctype> #include <cctype>
@@ -359,7 +360,8 @@ bool DataBucket::GetDataBuckets(Mob *mob)
BulkLoadEntitiesToCache(DataBucketLoadType::Bot, {id}); BulkLoadEntitiesToCache(DataBucketLoadType::Bot, {id});
} }
else if (mob->IsClient()) { else if (mob->IsClient()) {
BulkLoadEntitiesToCache(DataBucketLoadType::Account, {id}); uint32 account_id = mob->CastToClient()->AccountID();
BulkLoadEntitiesToCache(DataBucketLoadType::Account, {account_id});
BulkLoadEntitiesToCache(DataBucketLoadType::Client, {id}); BulkLoadEntitiesToCache(DataBucketLoadType::Client, {id});
} }
+3 -1
View File
@@ -852,11 +852,13 @@ void Doors::CreateDatabaseEntry()
const auto& l = DoorsRepository::GetWhere( const auto& l = DoorsRepository::GetWhere(
content_db, content_db,
fmt::format( fmt::format(
"zone = '{}' AND doorid = {}", "zone = '{}' AND (version = {} OR version = -1) AND doorid = {}",
zone->GetShortName(), zone->GetShortName(),
zone->GetInstanceVersion(),
GetDoorID() GetDoorID()
) )
); );
if (!l.empty()) { if (!l.empty()) {
auto e = l[0]; auto e = l[0];
+2 -2
View File
@@ -1125,8 +1125,8 @@ void EntityList::AESpell(
RuleI(Range, MobCloseScanDistance), RuleI(Range, MobCloseScanDistance),
distance distance
); );
auto list = caster_mob->GetCloseMobList(distance);
for (auto& it: caster_mob->GetCloseMobList(distance)) { for (auto& it: list) {
current_mob = it.second; current_mob = it.second;
if (!current_mob) { if (!current_mob) {
continue; continue;
+8
View File
@@ -2543,6 +2543,14 @@ void PerlembParser::ExportEventVariables(
break; break;
} }
case EVENT_CONNECT: {
Seperator sep(data);
ExportVar(package_name.c_str(), "last_login", sep.arg[0]);
ExportVar(package_name.c_str(), "seconds_since_last_login", sep.arg[1]);
ExportVar(package_name.c_str(), "is_first_login", sep.arg[2]);
break;
}
default: { default: {
break; break;
} }
+22 -19
View File
@@ -137,25 +137,28 @@ void Embperl::DoInit()
catch (std::string& e) { catch (std::string& e) {
LogQuests("Warning [{}]: [{}]", Config->PluginPlFile, e); LogQuests("Warning [{}]: [{}]", Config->PluginPlFile, e);
} }
try {
//should probably read the directory in c, instead, so that for (auto & dir : path.GetPluginPaths()) {
//I can echo filenames as I do it, but c'mon... I'm lazy and this 1 line reads in all the plugins try {
const std::string& perl_command = ( //should probably read the directory in c, instead, so that
"if(opendir(D,'" + //I can echo filenames as I do it, but c'mon... I'm lazy and this 1 line reads in all the plugins
path.GetPluginsPath() + const std::string& perl_command = (
"')) { " "if(opendir(D,'" +
" my @d = readdir(D);" dir +
" closedir(D);" "')) { "
" foreach(@d){ " " my @d = readdir(D);"
" main::eval_file('plugin','" + " closedir(D);"
path.GetPluginsPath() + " foreach(@d){ "
"/'.$_)if/\\.pl$/;" " main::eval_file('plugin','" +
" }" dir +
"}"); "/'.$_)if/\\.pl$/;"
eval_pv(perl_command.c_str(), FALSE); " }"
} "}");
catch (std::string& e) { eval_pv(perl_command.c_str(), FALSE);
LogQuests("Warning [{}]", e); }
catch (std::string& e) {
LogQuests("Warning [{}]", e);
}
} }
#endif //EMBPERL_PLUGIN #endif //EMBPERL_PLUGIN
} }
+30 -1
View File
@@ -505,6 +505,9 @@ void EntityList::MobProcess()
zone->GetSecondsBeforeIdle(), zone->GetSecondsBeforeIdle(),
zone->GetSecondsBeforeIdle() != 1 ? "s" : "" zone->GetSecondsBeforeIdle() != 1 ? "s" : ""
); );
CheckToClearTraderAndBuyerTables();
mob_settle_timer->Disable(); mob_settle_timer->Disable();
} }
@@ -2335,7 +2338,7 @@ void EntityList::QueueClientsGuild(const EQApplicationPacket *app, uint32 guild_
void EntityList::QueueClientsGuildBankItemUpdate(GuildBankItemUpdate_Struct *gbius, uint32 guild_id) void EntityList::QueueClientsGuildBankItemUpdate(GuildBankItemUpdate_Struct *gbius, uint32 guild_id)
{ {
auto outapp = std::make_unique<EQApplicationPacket>(OP_GuildBank, sizeof(GuildBankItemUpdate_Struct)); auto outapp = std::make_unique<EQApplicationPacket>(OP_GuildBank, sizeof(GuildBankItemUpdate_Struct));
auto data = reinterpret_cast<GuildBankItemUpdate_Struct *>(outapp->pBuffer); auto data = reinterpret_cast<GuildBankItemUpdate_Struct *>(outapp->pBuffer);
memcpy(data, gbius, sizeof(GuildBankItemUpdate_Struct)); memcpy(data, gbius, sizeof(GuildBankItemUpdate_Struct));
@@ -3159,6 +3162,13 @@ void EntityList::Depop(bool StartSpawnTimer)
UpdateFindableNPCState(pnpc, true); UpdateFindableNPCState(pnpc, true);
} }
// Depop below will eventually remove this npc from the entity list
// but that can happen AFTER we've already tried to spawn its replacement.
// So go ahead and remove it from the limits so it doesn't count.
if (npc_limit_list.count(pnpc->GetID())) {
npc_limit_list.erase(pnpc->GetID());
}
pnpc->WipeHateList(); pnpc->WipeHateList();
pnpc->Depop(StartSpawnTimer); pnpc->Depop(StartSpawnTimer);
} }
@@ -6002,3 +6012,22 @@ void EntityList::RestoreCorpse(NPC *npc, uint32_t decay_time)
c->SetDecayTimer(decay_time); c->SetDecayTimer(decay_time);
} }
} }
void EntityList::CheckToClearTraderAndBuyerTables()
{
if (zone->GetZoneID() == Zones::BAZAAR) {
TraderRepository::DeleteWhere(
database,
fmt::format(
"`char_zone_id` = {} AND `char_zone_instance_id` = {}", zone->GetZoneID(), zone->GetInstanceID()
)
);
BuyerRepository::DeleteBuyers(database, zone->GetZoneID(), zone->GetInstanceID());
LogTradingDetail(
"Removed trader and buyer entries for Zone ID [{}] and Instance ID [{}]",
zone->GetZoneID(),
zone->GetInstanceID()
);
}
}
+1
View File
@@ -581,6 +581,7 @@ public:
void SendMerchantEnd(Mob* merchant); void SendMerchantEnd(Mob* merchant);
void SendMerchantInventory(Mob* m, int32 slot_id = -1, bool is_delete = false); void SendMerchantInventory(Mob* m, int32 slot_id = -1, bool is_delete = false);
void RestoreCorpse(NPC* npc, uint32_t decay_time); void RestoreCorpse(NPC* npc, uint32_t decay_time);
void CheckToClearTraderAndBuyerTables();
protected: protected:
friend class Zone; friend class Zone;
+18
View File
@@ -1693,6 +1693,24 @@ void command_npcedit(Client *c, const Seperator *sep)
c->Message(Chat::White, "Usage: #npcedit set_grid [Grid ID] - Sets an NPC's Grid ID"); c->Message(Chat::White, "Usage: #npcedit set_grid [Grid ID] - Sets an NPC's Grid ID");
return; return;
} }
} else if (!strcasecmp(sep->arg[1], "npc_tint_id")) {
if (sep->IsNumber(2)) {
const uint32 npc_tint_id = (Strings::ToUnsignedInt(sep->arg[2]));
n.npc_tint_id = npc_tint_id;
d = fmt::format(
"Set NPCTintID {} for {}",
npc_tint_id,
npc_id_string
);
} else {
c->Message(
Chat::White,
"Usage: #npcedit npc_tint_id [id] - Sets an NPC's NPCTintID [0 - 78 for RoF2]"
);
return;
}
} else { } else {
SendNPCEditSubCommands(c); SendNPCEditSubCommands(c);
return; return;
+5
View File
@@ -8,6 +8,11 @@ extern WorldServer worldserver;
void command_task(Client *c, const Seperator *sep) void command_task(Client *c, const Seperator *sep)
{ {
if (!RuleB(TaskSystem, EnableTaskSystem)) {
c->Message(Chat::White, "This command cannot be used while the Task system is disabled.");
return;
}
const int arguments = sep->argnum; const int arguments = sep->argnum;
if (!arguments) { if (!arguments) {
c->Message(Chat::White, "Syntax: #task [subcommand]"); c->Message(Chat::White, "Syntax: #task [subcommand]");
+100
View File
@@ -0,0 +1,100 @@
#include "../client.h"
void command_zonevariable(Client *c, const Seperator *sep)
{
const uint16 arguments = sep->argnum;
if (!arguments) {
c->Message(Chat::White, "Usage: #zonevariable clear - Clear all zone variables");
c->Message(Chat::White, "Usage: #zonevariable delete [Variable Name] - Delete a zone variable");
c->Message(Chat::White, "Usage: #zonevariable set [Variable Name] [Variable Value] - Set a zone variable");
c->Message(Chat::White, "Usage: #zonevariable view [Variable Name] - View a zone variable");
c->Message(Chat::White, "Note: You can have spaces in variable names and values by wrapping in double quotes like this");
c->Message(Chat::White, "Example: #zonevariable set \"Test Variable\" \"Test Value\"");
c->Message(Chat::White, "Note: Variable Value is optional and can be set to empty by not providing a value");
return;
}
const char* action = arguments >= 1 ? sep->arg[1] : "";
const bool is_clear = !strcasecmp(action, "clear");
const bool is_delete = !strcasecmp(action, "delete");
const bool is_set = !strcasecmp(action, "set");
const bool is_view = !strcasecmp(action, "view");
if (!is_clear && !is_delete && !is_set && !is_view) {
c->Message(Chat::White, "Usage: #zonevariable clear - Clear all zone variables");
c->Message(Chat::White, "Usage: #zonevariable delete [Variable Name] - Delete a zone variable");
c->Message(Chat::White, "Usage: #zonevariable set [Variable Name] [Variable Value] - Set a zone variable");
c->Message(Chat::White, "Usage: #zonevariable view [Variable Name] - View a zone variable");
c->Message(Chat::White, "Note: You can have spaces in variable names and values by wrapping in double quotes like this");
c->Message(Chat::White, "Example: #zonevariable set \"Test Variable\" \"Test Value\"");
c->Message(Chat::White, "Note: Variable Value is optional and can be set to empty by not providing a value");
return;
}
if (is_clear) {
const bool cleared = zone->ClearVariables();
c->Message(Chat::White, cleared ? "Cleared all zone variables." : "There are no zone variables to clear.");
return;
}
if (is_delete) {
const std::string variable_name = arguments >= 2 ? sep->argplus[2] : "";
if (variable_name.empty() || !zone->VariableExists(variable_name)) {
c->Message(Chat::White, fmt::format("A zone variable named '{}' does not exist.", variable_name).c_str());
return;
}
zone->DeleteVariable(variable_name);
c->Message(Chat::White, fmt::format("Deleted a zone variable named '{}'.", variable_name).c_str());
return;
}
if (is_set) {
const std::string variable_name = arguments >= 2 ? sep->arg[2] : "";
const std::string variable_value = arguments >= 3 ? sep->arg[3] : "";
zone->SetVariable(variable_name, variable_value);
c->Message(Chat::White, fmt::format("Set a zone variable named '{}' to a value of '{}'.", variable_name, variable_value).c_str());
return;
}
if (is_view) {
const auto& l = zone->GetVariables();
const std::string search_criteria = arguments >= 2 ? sep->argplus[2] : "";
uint32 variable_count = 0;
uint32 variable_number = 1;
for (const auto& e : l) {
if (search_criteria.empty() || Strings::Contains(Strings::ToLower(e), Strings::ToLower(search_criteria))) {
c->Message(Chat::White, fmt::format(
"Variable {} | Name: {} Value: {} | {}",
variable_number,
e,
zone->GetVariable(e),
Saylink::Silent(fmt::format("#zonevariable delete {}", e), "Delete")
).c_str());
variable_count++;
variable_number++;
}
}
if (!variable_count) {
c->Message(Chat::White, fmt::format(
"There are no zone variables{}.",
(!search_criteria.empty() ? fmt::format(" matching '{}'", search_criteria) : "")
).c_str());
return;
}
c->Message(Chat::White, fmt::format(
"There {} {} zone variable{}{}, would you like to {} zone variables?",
variable_count != 1 ? "are" : "is",
variable_count,
variable_count != 1 ? "s" : "",
(!search_criteria.empty() ? fmt::format(" matching '{}'", search_criteria) : ""),
Saylink::Silent("#zonevariable clear", "clear")
).c_str());
}
}

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