mirror of
https://github.com/UOX3DevTeam/UOX3
synced 2026-08-13 12:27:04 -04:00
Increased world load speed by almost 20% by reducing how often loading progress is written to the console Increased world save speed by almost 50% through the use of binary mode and buffer tweaks for ofstream combined with a cached newline char and std::to_string instead of static_cast Fixed a server crash involving killing NPCs of a specific spawn region using the 'spawnkill # command Fixed a server crash involving moving items from decaying corpses to the ground Fixed a server crash involving carving up human corpses Fixed a server crash related to AreaCharacterFunction JS function, which could trigger for any script that potentially kills any NPC this function iterates over (like explosion potions) Fixed a server crash related to GMs attempting to add items from add-menu while having no backpack Fixed a client crash issue caused by attempting to sell house deeds back to architect NPCs Fixed an issue where players would get incorrect amounts of gold for selling house deeds back to architect NPCs Fixed an issue with characters dying onboard boats which prevented players from packing up the boats later Added new item property in code (via bools bitset) that can be used to enable/disable maker's mark on crafted items. This property can be set to true for items crafted at exceptional quality by GM craftsmen Exposed the new item property to JS engine: .isMarkedByMaker // If true, maker's mark will be displayed Updated default item tooltip to display " of exceptional quality" for items crafted with exceptional quality Updated default item tooltip to display "[Crafted] by [Crafter's Name]" for exceptional quality items crafted by GM craftsmen. The [Crafted] text comes from the MADEWORD defined in dfndata/skills/skills.dfn for the primary skill used to craft the item Updated old-school single-click names for items (when AoS bit is disabled in client/server features) to display exceptional status and/or maker's mark for appropriate items Fixed an issue with the display of titles for NPCs that used dictionary lookups for their title Added new UOX.INI settings under the [settings] section: DISPLAYMAKERSMARK=1/0 // Controls if maker's marks on crafted items are shown on a global level SHOWNPCTITLESOVERHEAD=1/0 // Controls whether NPC titles are shown over their heads SHOWINVULNERABLETAGOVERHEAD=0/1 // Controls whether invulnerable tags are shown overhead GLOBALRESTOCKMULTIPLIER=1.0 // Global multiplier applied to RESTOCK property of items when loaded from DFNs Added new UOX.INI settings under the [combat] section: PETCOMBATTRAINING=1/0 // Controls whether pets can gain skills/stats from combat HIRELINGCOMBATTRAINING=1/0 // Controls whether hirelings can gain skills/stats from combat NPCCOMBATTRAINING=0/1 // Controls whether NPCs in general can gain skills/stats from combat Fixed an issue where a player meeting the exact minimum requirements for crafting an item would always fail Fixed a bug with teleporting off boats to valid nearby locations when double-clicking boat planks while standing on it Updated behaviour of planks on boats: Boat key can unlock plank, but won't automatically open it Boat key used on an open, unlocked plank will lock and automatically close the plank Boat key used on an open, locked plank will unlock the plank and leave it open Player using an unlocked plank now opens it Player using an open plank will disembark (if onboard) or embark (if not) the boat Player can always open a locked plank if they're on the boat, even with no key, to allow disembarking the boat. Planks that are open, but locked cannot be accessed from outside the boat, and will close automatically after five of seconds Updated behaviour of Magic Lock/Unlock spells: Magic Lock cannot be used on objects inside a multi Magic Lock applies difficulty to lock based on caster's Magery skill Magic Lock effect will last between 7 to 50 seconds (depending on caster's Magery skill) Any object that can be locked via Magic Lock spell can also be unlocked via Magic Unlock spell, if caster has high enough Magery skill Fixed a bug where certain JS Functions, Methods and Properties would occasionally pass incorrect values to scripts that called upon them. Scripts that rely on the GetCurrentClock() Function, the .GetTimer() Method, .decayTime, .logTime, .oreTime and .fishTime Properties might need to be updated to account for the fix, which causes larger values to be passed in than before. Fixed a bug related to GetCurrentClock() that caused decay and respawning of dungeon chests from working properly Fixed a bug related to GetCurrentClock() and .logTime/.fishTime that prevented consumption and regeneration of log/fish resources from working properly Added a new JS Function: GetStartTime() // Returns a timestamp for when server started up Fixed a bug related to GetCurrentClock() where server uptime stats in help menu was showing up incorrectly Fixed a bug related to GetCurrentClock() and purging of registrations of visitors in public houses so their visits can be counted again later Fixed a bug related to GetCurrentClock() that caused monster speech to not work properly Fixed a Z-related issue that sometimes prevented players from fishing when standing on the shoreline next to the sea Fixed an issue with Animal Lore skill that would always return a pet's loyalty level as "Wild" Increased max range pets can be from their owner in order to still be included in moongate/teleporter travel from 12 to 24 tiles Fixed an issue where the alchemy bonus damage for explosion potions was off by an order of magnitude Fixed an issue where food IDs from foodlists referenced by other foodlists was not detected as valid food for tamed pets or NPCs Fixed an issue where Alchemy crafting skill did not properly check for the existence of the crafting tool before proceeding with the crafting process Fixed an issue that prevented the Teleport spell from working as intended Fixed an issue where players would still get 1 gold even if STARTGOLD in uox.ini was set to 0 Fixed an issue with mount restrictions script that prevented players from mounting Unicorns, Ki-rins and Cu Sidhes Fixed an issue where aggressive creatures wouldn't stop attacking player after being tamed (js/skill/taming.js) Added a system message to inform the player when they've dropped out of stealth due to taking too many steps Fixed an issue where hunger level of NPCs instantly dropped upon spawning because hunger timer was not active yet Fixed a bug where character corpses would appear with a [unidentified] tag attached Fixed an issue where the [unidentified] tag did not show up for unidentified magic items Added ANIMAL tag to a small bunch of creature definitions (dfndata/creatures/creatures.dfn) Fixed an issue with Bowcraft skill where player could not gain enough skill from crafting bows to reach min requirement for crossbows, and similiarly could not reach min requirement for heavy crossbows from crossbows Updated default value of MURDERDECAYTIMER INI setting from 60 seconds to 28800 seconds (8 hours), to match the amount of time it would take for one (short term) murder count to decay on LBR-era OSI shards.
1948 lines
68 KiB
C++
1948 lines
68 KiB
C++
#include "uox3.h"
|
|
#include "msgboard.h"
|
|
#include "movement.h"
|
|
#include "cServerDefinitions.h"
|
|
#include "cSpawnRegion.h"
|
|
#include "skills.h"
|
|
#include "ssection.h"
|
|
#include "CJSMapping.h"
|
|
#include "cScript.h"
|
|
#include "teffect.h"
|
|
#include "CPacketSend.h"
|
|
#include "cEffects.h"
|
|
#include "classes.h"
|
|
#include "regions.h"
|
|
#include "cRaces.h"
|
|
#include "townregion.h"
|
|
#include "Dictionary.h"
|
|
|
|
#include "ObjectFactory.h"
|
|
|
|
using namespace std::string_literals;
|
|
|
|
cCharStuff *Npcs = nullptr;
|
|
GenericList< CChar * > alwaysAwakeNPCs;
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - GenericList< CChar * > * GetAlwaysAwakeNPCs( void )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Returns list of NPCs that are always awake
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
GenericList< CChar * > * cCharStuff::GetAlwaysAwakeNPCs( void )
|
|
{
|
|
return &alwaysAwakeNPCs;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CItem *addRandomLoot( CItem *s, std::string lootlist )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Add loot to monsters packs
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
auto cCharStuff::addRandomLoot( CItem *s, const std::string& lootlist, bool shouldSave ) ->CItem *{
|
|
|
|
CItem *retItem = nullptr;
|
|
std::string sect = "LOOTLIST "s + lootlist;
|
|
auto LootList = FileLookup->FindEntry( sect, items_def );
|
|
if(LootList){
|
|
auto i = LootList->NumEntries();
|
|
if( i > 0 ) {
|
|
i = RandomNum( static_cast< size_t >(0), i - 1 );
|
|
std::string tag = LootList->MoveTo( i );
|
|
std::string tagData = LootList->GrabData();
|
|
auto csecs = oldstrutil::sections( oldstrutil::trim( oldstrutil::removeTrailing( tagData, "//" )), "," );
|
|
auto tcsecs = oldstrutil::sections( oldstrutil::trim( oldstrutil::removeTrailing( tag, "//" )), "," );
|
|
|
|
if( !tag.empty() ){
|
|
UI16 iAmount = 0;
|
|
|
|
if( oldstrutil::upper( tag ) == "LOOTLIST" ) {
|
|
if( csecs.size() > 1 ){ // Amount specified behind lootlist entry?
|
|
iAmount = static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( csecs[1], "//" )), nullptr, 0));
|
|
CItem *retItemNested = nullptr;
|
|
for( UI16 iCount = 0; iCount < iAmount; ++iCount ) {
|
|
retItemNested = addRandomLoot( s, oldstrutil::trim( oldstrutil::removeTrailing( csecs[0], "//" )), shouldSave );
|
|
}
|
|
}
|
|
else {
|
|
retItem = addRandomLoot( s, LootList->GrabData(), shouldSave );
|
|
}
|
|
}
|
|
else {
|
|
if( tcsecs.size() > 1 ){ // Amount specified behind lootlist entry?
|
|
iAmount = static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( tcsecs[1], "//" )), nullptr, 0));
|
|
retItem = Items->CreateBaseScriptItem( s, oldstrutil::trim( oldstrutil::removeTrailing( tcsecs[0], "//" )), s->WorldNumber(), iAmount, s->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if(retItem) {
|
|
retItem->SetCont( s );
|
|
retItem->PlaceInPack();
|
|
}
|
|
}
|
|
else {
|
|
retItem = Items->CreateBaseScriptItem( s, tag, s->WorldNumber(), 1, s->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if( retItem ) {
|
|
retItem->SetCont( s );
|
|
retItem->PlaceInPack();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return retItem;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CChar *CreateBaseNPC( std::string ourNPC )
|
|
//| Date - 10/12/2003
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates a basic npc from the scripts
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
CChar *cCharStuff::CreateBaseNPC( std::string ourNPC, bool shouldSave )
|
|
{
|
|
ourNPC = oldstrutil::trim( oldstrutil::removeTrailing( ourNPC, "//" ));
|
|
ScriptSection *npcCreate = FileLookup->FindEntry( ourNPC, npc_def );
|
|
if( npcCreate == nullptr )
|
|
{
|
|
Console.error(oldstrutil::format( "CreateBaseNPC(): Bad script npc %s (NPC Not Found).", ourNPC.c_str()) );
|
|
return nullptr;
|
|
}
|
|
|
|
CChar *cCreated = nullptr;
|
|
if( npcCreate->NpcListExist() )
|
|
cCreated = CreateRandomNPC( npcCreate->NpcListData() );
|
|
else
|
|
{
|
|
cCreated = static_cast< CChar * >(ObjectFactory::getSingleton().CreateObject( OT_CHAR ));
|
|
if( cCreated == nullptr )
|
|
return nullptr;
|
|
if( !shouldSave )
|
|
cCreated->ShouldSave( false );
|
|
|
|
cCreated->SetSkillTitles( true );
|
|
cCreated->SetNpc( true );
|
|
cCreated->SetLoDamage( 1 );
|
|
cCreated->SetHiDamage( 1 );
|
|
cCreated->SetResist( 1, PHYSICAL );
|
|
cCreated->SetSpawn( INVALIDSERIAL );
|
|
|
|
if( !ApplyNpcSection( cCreated, npcCreate, ourNPC ) )
|
|
Console.error( "Trying to apply an npc section failed" );
|
|
|
|
std::vector<UI16> scriptTriggers = cCreated->GetScriptTriggers();
|
|
for( auto scriptTrig : scriptTriggers )
|
|
{
|
|
cScript *toExecute = JSMapping->GetScript( scriptTrig );
|
|
if( toExecute != nullptr )
|
|
{
|
|
toExecute->OnCreate( cCreated, true );
|
|
}
|
|
}
|
|
}
|
|
return cCreated;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CChar *CreateRandomNPC( const std::string& npcList )
|
|
//| Date - 10/12/2003
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates a random npc from an npclist in specified dfn file
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
auto cCharStuff::CreateRandomNPC( const std::string& npcList )->CChar *{
|
|
CChar *cCreated = nullptr;
|
|
auto sect = "NPCLIST "s + npcList;
|
|
sect = oldstrutil::trim( oldstrutil::removeTrailing( sect, "//" ));
|
|
auto NPCList = FileLookup->FindEntry( sect, npc_def );
|
|
if(NPCList) {
|
|
auto i = NPCList->NumEntries();
|
|
if( i > 0 ) {
|
|
std::string k = NPCList->MoveTo( RandomNum( static_cast<size_t>(0), i - 1 ));
|
|
if( !k.empty() ) {
|
|
if( oldstrutil::upper( k ) == "NPCLIST" ) {
|
|
cCreated = CreateRandomNPC( NPCList->GrabData() );
|
|
}
|
|
else {
|
|
cCreated = CreateBaseNPC( k );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return cCreated;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CChar *CreateNPC( CSpawnItem *iSpawner, const std::string &npc )
|
|
//| Date - 10/12/2003
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates an npc spawned from an item spawner
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
CChar *cCharStuff::CreateNPC( CSpawnItem *iSpawner, const std::string &npc )
|
|
{
|
|
const ItemTypes iType = iSpawner->GetType();
|
|
// If the spawner type is 125 and escort quests are not active then abort
|
|
if( iType == IT_ESCORTNPCSPAWNER && !cwmWorldState->ServerData()->EscortsEnabled() )
|
|
return nullptr;
|
|
|
|
CChar *cCreated = nullptr;
|
|
if( iSpawner->IsSectionAList() )
|
|
cCreated = CreateRandomNPC( npc );
|
|
else
|
|
cCreated = CreateBaseNPC( npc );
|
|
if( cCreated == nullptr )
|
|
return nullptr;
|
|
|
|
cCreated->SetSpawn( iSpawner->GetSerial() );
|
|
SI16 awayX = 0, awayY = 0;
|
|
if(( iType == IT_AREASPAWNER || iType == IT_ESCORTNPCSPAWNER ) && iSpawner->GetCont() == nullptr )
|
|
{
|
|
awayX = iSpawner->GetTempVar( CITV_MORE, 3 );
|
|
awayY = iSpawner->GetTempVar( CITV_MORE, 4 );
|
|
}
|
|
|
|
FindSpotForNPC( cCreated, iSpawner->GetX(), iSpawner->GetY(), awayX, awayY, iSpawner->GetZ(), iSpawner->WorldNumber(), iSpawner->GetInstanceID() );
|
|
PostSpawnUpdate( cCreated );
|
|
|
|
if( iType == IT_ESCORTNPCSPAWNER )
|
|
MsgBoardQuestEscortCreate( cCreated );
|
|
|
|
cCreated->SetWipeable( true );
|
|
|
|
return cCreated;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CChar *CreateNPCxyz( const std::string &npc, SI16 x, SI16 y, SI08 z, UI08 worldNumber, UI16 instanceID, bool useNpcList )
|
|
//| Date - 10/12/2003
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates an npc at location xyz
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
CChar *cCharStuff::CreateNPCxyz( const std::string &npc, SI16 x, SI16 y, SI08 z, UI08 worldNumber, UI16 instanceID, bool useNpcList )
|
|
{
|
|
CChar *cCreated = nullptr;
|
|
if( useNpcList )
|
|
cCreated = CreateRandomNPC( npc );
|
|
else
|
|
cCreated = CreateBaseNPC( npc );
|
|
if( cCreated == nullptr )
|
|
return nullptr;
|
|
|
|
cCreated->SetLocation( x, y, z, worldNumber, instanceID );
|
|
// Update "old location" for new NPCs straight away
|
|
cCreated->SetOldLocation( x, y, z );
|
|
PostSpawnUpdate( cCreated );
|
|
return cCreated;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void PostSpawnUpdate( CChar *cCreated )
|
|
//| Date - 10/12/2003
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Updates created npc
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void cCharStuff::PostSpawnUpdate( CChar *cCreated )
|
|
{
|
|
CTownRegion *tReg = calcRegionFromXY( cCreated->GetX(), cCreated->GetY(), cCreated->WorldNumber(), cCreated->GetInstanceID(), cCreated );
|
|
cCreated->SetRegion( tReg->GetRegionNum() );
|
|
|
|
for( UI08 z = 0; z < ALLSKILLS; ++z )
|
|
{
|
|
Skills->updateSkillLevel( cCreated, z );
|
|
}
|
|
|
|
// Set hunger timer so NPC's hunger level doesn't instantly drop after spawning
|
|
auto hungerRate = Races->GetHungerRate( cCreated->GetRace() );
|
|
cCreated->SetTimer( tCHAR_HUNGER, BuildTimeValue( static_cast<R32>( hungerRate )));
|
|
|
|
UpdateFlag( cCreated );
|
|
cCreated->Update();
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - SI16 getRadius( CChar *c )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Generate a sensible radius given the values from the NPC DFNs
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
SI16 getRadius( CChar *c )
|
|
{
|
|
// see if they supplied a 'radius'
|
|
if( c->GetFx( 1 ) > 0 )
|
|
{
|
|
// if they were supplying a bounding area, use the radius from that
|
|
if( c->GetFx( 0 ) > 0 )
|
|
c->SetFx( abs(c->GetFx( 0 ) - c->GetFx( 1 )), 1 );
|
|
}
|
|
// ensure its not something bogus
|
|
if( c->GetFx( 1 ) <= 0 || c->GetFx( 1 ) > 100)
|
|
c->SetFx( 10, 1 );
|
|
return c->GetFx( 1 );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool checkBoundingBox( const SI16 xPos, const SI16 yPos, const SI16 fx1, const SI16 fy1,
|
|
//| const SI08 fz1, const SI16 fx2, const SI16 fy2, const UI08 worldNumber, const UI16 instanceID )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Check bounding box
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool checkBoundingBox( const SI16 xPos, const SI16 yPos, const SI16 fx1, const SI16 fy1, const SI08 fz1,
|
|
const SI16 fx2, const SI16 fy2, const UI08 worldNumber, const UI16 instanceID )
|
|
{
|
|
if( xPos >= ( ( fx1 < fx2 ) ? fx1 : fx2 ) && xPos <= ( ( fx1 < fx2 ) ? fx2 : fx1 ) )
|
|
{
|
|
if( yPos >= ( ( fy1 < fy2 ) ? fy1 : fy2 ) && yPos <= ( ( fy1 < fy2 ) ? fy2 : fy1 ) )
|
|
{
|
|
if( fz1 == -1 || abs( fz1 - Map->Height( xPos, yPos, fz1, worldNumber, instanceID ) ) <= 5 )
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool checkBoundingCircle( const SI16 xPos, const SI16 yPos, const SI16 fx1, const SI16 fy1,
|
|
//| const SI08 fz1, const SI16 radius, const UI08 worldNumber, const UI16 instanceID )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Check bounding circle
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool checkBoundingCircle( const SI16 xPos, const SI16 yPos, const SI16 fx1, const SI16 fy1, const SI08 fz1,
|
|
const SI16 radius, const UI08 worldNumber, const UI16 instanceID )
|
|
{
|
|
if( ( xPos - fx1 ) * ( xPos - fx1 ) + ( yPos - fy1 ) * ( yPos - fy1 ) <= radius * radius )
|
|
{
|
|
if( fz1 == -1 || abs( fz1 - Map->Height( xPos, yPos, fz1, worldNumber, instanceID ) ) <= 5 )
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void InitializeWanderArea( CChar *c, SI16 xAway, SI16 yAway )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Setup the wander area if the npcwander is rect or circle
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void InitializeWanderArea( CChar *c, SI16 xAway, SI16 yAway )
|
|
{
|
|
// compute the rectangular bounding area
|
|
if( WT_BOX == c->GetNpcWander() )
|
|
{
|
|
// if they provided a legal rectangle and
|
|
// ensure the bounding rect contains the current location
|
|
// if it doesn't the monster will never move!
|
|
if( c->GetFx( 0 ) >= 0 && c->GetFy( 0 ) >= 0 && c->GetFy( 1 ) >= 0 && c->GetFx( 1 ) >= 0 &&
|
|
checkBoundingBox( c->GetX(), c->GetY(), c->GetFx( 0 ), c->GetFy( 0 ), c->GetFz(), c->GetFx( 1 ), c->GetFy( 1 ), c->WorldNumber(), c->GetInstanceID() ))
|
|
{
|
|
return;// don't do anything to use what they specified in the NPC DFNs
|
|
}
|
|
else
|
|
{
|
|
// if they provided a 'radius' in the NPC DFNs, use that
|
|
if( c->GetFx( 1 ) > 0 )
|
|
xAway = yAway = getRadius( c );
|
|
// setup info for rectangular areas
|
|
c->SetFx( c->GetX() - xAway, 0 );
|
|
c->SetFy( c->GetY() - yAway, 0 );
|
|
c->SetFx( c->GetX() + xAway, 1 );
|
|
c->SetFy( c->GetY() + yAway, 1 );
|
|
}
|
|
}
|
|
else if( WT_CIRCLE == c->GetNpcWander() )
|
|
{
|
|
// if they provided a legal circle and
|
|
// ensure the bounding circle contains the current location
|
|
// if it doesn't the monster will never move!
|
|
if( c->GetFx( 0 ) >= 0 && c->GetFy( 0 ) >= 0 && c->GetFx( 1 ) >= 0 &&
|
|
checkBoundingCircle( c->GetX(), c->GetY(), c->GetFx( 0 ), c->GetFy( 0 ), c->GetFz(), c->GetFx( 1 ), c->WorldNumber(), c->GetInstanceID() ))
|
|
{
|
|
return;// don't do anything to use what they specified in NPC DFNs
|
|
}
|
|
else
|
|
{
|
|
// if they provided a 'radius' in the NPC DFNs use that
|
|
if( c->GetFx( 1 ) > 0 )
|
|
xAway = yAway = getRadius( c );
|
|
// setup info for circular areas
|
|
c->SetFx( c->GetX(), 0 );
|
|
c->SetFy( c->GetY(), 0 );
|
|
c->SetFx( xAway, 1 );
|
|
c->SetFy( -1, 1 );
|
|
}
|
|
}
|
|
// setting fz1 actually makes it check against the height and slows the system down a lot
|
|
// does anyone really need to constrain the height at which a monster can move??
|
|
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void cCharStuff::FindSpotForNPC( CChar *cCreated, const SI16 originX, const SI16 originY,
|
|
//| const SI16 xAway, const SI16 yAway, const SI08 z, const UI08 worldNumber, const UI16 instanceID )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Find a valid spot to drop an NPC near the spawners location
|
|
//|
|
|
//| Changes - 06/26/2020 - Added support for instanceID
|
|
//|
|
|
//| Changes - 04/20/2002 - (Type 69) xos and yos (X OffSet, Y OffSet)
|
|
//| are used to find a random number that is then added to the spawner's
|
|
//| x and y (Using the spawner's z) and then place the NPC anywhere in
|
|
//| a square around the spawner. This square is random anywhere from -10
|
|
//| to +10 from the spawner's location (for x and y) If the place chosen
|
|
//| is not a valid position (the NPC can't walk there) then a new place
|
|
//| will be chosen, if a valid place cannot be found in a certain # of
|
|
//| tries (50),the NPC will be placed directly on the spawner and the
|
|
//| server op will be warned.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void cCharStuff::FindSpotForNPC( CChar *cCreated, const SI16 originX, const SI16 originY, const SI16 xAway,
|
|
const SI16 yAway, const SI08 z, const UI08 worldNumber, const UI16 instanceID )
|
|
{
|
|
|
|
#ifdef DEBUG_SPAWN
|
|
Console.print( oldstrutil::format( "Going to spawn at (%d,%d) within %d by %d\n", originX, originY, xAway, yAway ));
|
|
#endif
|
|
|
|
if( !ValidateObject( cCreated ) )
|
|
return;
|
|
SI32 k = xAway * yAway / 2;
|
|
SI16 xos = 0, yos = 0;
|
|
SI08 targZ = 0;
|
|
bool foundSpot = false;
|
|
if( k > 50 )
|
|
k = 50;
|
|
|
|
while( !foundSpot )
|
|
{
|
|
targZ = z;
|
|
if( --k < 0 ) //this CAN be a bit laggy. adjust as nessicary
|
|
{
|
|
if( xAway > 0 && yAway > 0 )
|
|
Console << "Problem area spawner found, NPC placed at default location" << myendl;
|
|
xos = originX;
|
|
yos = originY;
|
|
foundSpot = true;
|
|
break;
|
|
}
|
|
|
|
xos = originX + RandomNum( static_cast< SI16 >(-xAway), xAway );
|
|
yos = originY + RandomNum( static_cast< SI16 >(-yAway), yAway );
|
|
|
|
if( xos >= 1 && yos >= 1 )
|
|
{
|
|
targZ = Map->Height( xos, yos, z, worldNumber, instanceID );
|
|
if( !cwmWorldState->creatures[cCreated->GetID()].IsWater() )
|
|
foundSpot = Map->ValidSpawnLocation( xos, yos, targZ, worldNumber, instanceID );
|
|
else if( cwmWorldState->creatures[cCreated->GetID()].IsWater() || ( !foundSpot && cwmWorldState->creatures[cCreated->GetID()].IsAmphibian() ) )
|
|
foundSpot = Map->ValidSpawnLocation( xos, yos, targZ, worldNumber, instanceID, false );
|
|
}
|
|
}
|
|
|
|
cCreated->SetLocation( xos, yos, targZ, worldNumber, instanceID );
|
|
InitializeWanderArea( cCreated, xAway, yAway );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void LoadShopList( const std::string& list, CChar *c )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Loads the shopping list pointed to by data in ITEM DFNs
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
auto cCharStuff::LoadShopList( const std::string& list, CChar *c )->void {
|
|
auto buyLayer = c->GetItemAtLayer( IL_BUYCONTAINER ); //Contains items the NPC is willing to buy
|
|
auto boughtLayer = c->GetItemAtLayer( IL_BOUGHTCONTAINER ); //Contains items the NPC has already bought
|
|
auto sellLayer = c->GetItemAtLayer( IL_SELLCONTAINER ); //Contains items the NPC will sell
|
|
|
|
auto sect = "SHOPLIST "s + list;
|
|
sect = oldstrutil::trim( oldstrutil::removeTrailing( sect, "//" ));
|
|
auto ShoppingList = FileLookup->FindEntry( sect, items_def );
|
|
if(ShoppingList){
|
|
|
|
bool shouldSave = c->ShouldSave();
|
|
std::string cdata;
|
|
SI32 ndata = -1, odata = -1;
|
|
CItem *retItem = nullptr;
|
|
for (const auto &sec : ShoppingList->collection2()){
|
|
auto tag = sec->tag ;
|
|
cdata = sec->cdata ;
|
|
ndata = sec->ndata ;
|
|
odata = sec->odata ;
|
|
switch( tag ) {
|
|
case DFNTAG_RSHOPITEM:
|
|
if( ValidateObject(buyLayer)) {
|
|
retItem = Items->CreateBaseScriptItem( nullptr, cdata, c->WorldNumber(), 1, c->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if(retItem){
|
|
retItem->SetCont( buyLayer );
|
|
retItem->PlaceInPack();
|
|
if( !retItem->GetName2().empty() && ( retItem->GetName2()!="#" )){
|
|
retItem->SetName( retItem->GetName2() ); // Item identified!
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
Console << "Warning: Bad Shopping List " << list << " with no Vendor Buy Pack for NPC " << c << " (serial: " << c->GetSerial() << myendl;
|
|
}
|
|
break;
|
|
case DFNTAG_SELLITEM:
|
|
if( ValidateObject( sellLayer ) ) {
|
|
retItem = Items->CreateBaseScriptItem( nullptr, cdata, c->WorldNumber(), 1, c->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if(retItem) {
|
|
retItem->SetCont( sellLayer );
|
|
//retitem->SetSellValue( retitem->GetBuyValue() / 2 );
|
|
retItem->PlaceInPack();
|
|
if( !retItem->GetName2().empty() && ( retItem->GetName2()!="#" )){
|
|
retItem->SetName( retItem->GetName2() );
|
|
}
|
|
|
|
// Let's start out with the shops fully restocked after spawning
|
|
retItem->IncAmount( retItem->GetRestock() );
|
|
retItem->SetRestock( 0 );
|
|
}
|
|
}
|
|
else{
|
|
Console << "Warning: Bad Shopping List " << list << " with no Vendor Sell Pack for NPC " << c << " (serial: " << c->GetSerial() << myendl;
|
|
}
|
|
break;
|
|
case DFNTAG_SHOPITEM:
|
|
if( ValidateObject(boughtLayer)) {
|
|
retItem = Items->CreateBaseScriptItem( nullptr, cdata, c->WorldNumber(), 1, c->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if( retItem){
|
|
retItem->SetCont( boughtLayer );
|
|
retItem->PlaceInPack();
|
|
if( !retItem->GetName2().empty() && ( retItem->GetName2() != "#" )){
|
|
retItem->SetName( retItem->GetName2() );
|
|
}
|
|
}
|
|
}
|
|
else{
|
|
Console << "Warning: Bad Shopping List " << list << " with no Vendor Bought Pack for NPC " << c << " (serial: " << c->GetSerial() << myendl;
|
|
}
|
|
break;
|
|
case DFNTAG_VALUE:
|
|
if( retItem ){
|
|
if(!cdata.empty()) {
|
|
auto ssecs = oldstrutil::sections( cdata, " " );
|
|
if( ssecs.size() > 1 ) {
|
|
retItem->SetBuyValue( static_cast<UI32>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssecs[0], "//" )), nullptr, 0)) );
|
|
retItem->SetSellValue( static_cast<UI32>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssecs[1], "//" )), nullptr, 0)) );
|
|
break;
|
|
}
|
|
}
|
|
retItem->SetBuyValue( ndata );
|
|
retItem->SetSellValue( (ndata / 2) );
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void setRandomName( CChar *s, const std::string& namelist )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Sets a character with a random name from NPC namelists in namelists.dfn
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void setRandomName( CChar *s, const std::string& namelist )
|
|
{
|
|
std::string sect = std::string("RANDOMNAME ") + namelist;
|
|
sect = oldstrutil::trim( oldstrutil::removeTrailing( sect, "//" ));
|
|
std::string tempName;
|
|
|
|
ScriptSection *RandomName = FileLookup->FindEntry( sect, npc_def );
|
|
if( RandomName == nullptr )
|
|
{
|
|
tempName = std::string("Error Namelist ") + namelist+std::string(" Not Found");
|
|
}
|
|
else
|
|
{
|
|
size_t i = RandomName->NumEntries();
|
|
if( i > 0 )
|
|
{
|
|
i = RandomNum( static_cast<size_t>(0), i - 1 );
|
|
tempName = RandomName->MoveTo( static_cast<SI16>(i) );
|
|
}
|
|
}
|
|
s->SetName( tempName );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - UI16 addRandomColor( const std::string& colorlist )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Randomly colors character
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
UI16 addRandomColor( const std::string& colorlist )
|
|
{
|
|
std::string sect = std::string("RANDOMCOLOR ") + colorlist;
|
|
sect = oldstrutil::trim( oldstrutil::removeTrailing( sect, "//" ));
|
|
ScriptSection *RandomColours = FileLookup->FindEntry( sect, colors_def );
|
|
if( RandomColours == nullptr )
|
|
{
|
|
Console.warning( oldstrutil::format("Error Colorlist %s Not Found", colorlist.c_str() ));
|
|
return 0;
|
|
}
|
|
size_t i = RandomColours->NumEntries();
|
|
if( i > 0 )
|
|
{
|
|
i = RandomNum( static_cast<size_t>(0), i - 1 );
|
|
std::string tag = RandomColours->MoveTo( static_cast<SI16>(i) );
|
|
return static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( tag, "//" )), nullptr, 0));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MakeShop( CChar *c )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Turn an NPC into a shopkeeper
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MakeShop( CChar *c )
|
|
{
|
|
if( !ValidateObject( c ) )
|
|
return;
|
|
c->SetShop( true );
|
|
CItem *tPack = nullptr;
|
|
for( UI08 i = IL_SELLCONTAINER; i <= IL_BUYCONTAINER; ++i )
|
|
{
|
|
tPack = c->GetItemAtLayer( static_cast<ItemLayers>(i) );
|
|
if( !ValidateObject( tPack ) )
|
|
{
|
|
bool shouldSave = c->ShouldSave();
|
|
tPack = Items->CreateItem( nullptr, c, 0x2AF8, 1, 0, OT_ITEM, false, shouldSave );
|
|
if( ValidateObject( tPack ) )
|
|
{
|
|
tPack->SetDecayable( false );
|
|
tPack->SetLayer( static_cast<ItemLayers>(i) );
|
|
if( !tPack->SetCont( c ) )
|
|
tPack->Delete();
|
|
else
|
|
{
|
|
tPack->SetType( IT_CONTAINER );
|
|
tPack->SetNewbie( true );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
c->Update();
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool cCharStuff::ApplyNpcSection( CChar *applyTo, ScriptSection *NpcCreation, bool isGate )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Apply NPC DFN sections to an NPC
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
auto cCharStuff::ApplyNpcSection( CChar *applyTo, ScriptSection *NpcCreation, std::string sectionID, bool isGate ) ->bool {
|
|
if( NpcCreation && ValidateObject( applyTo ) ){
|
|
|
|
UI16 haircolor = INVALIDCOLOUR;
|
|
CItem *buyPack = nullptr, *boughtPack = nullptr, *sellPack = nullptr;
|
|
CItem *retitem = nullptr, *mypack = nullptr;
|
|
|
|
std::string cdata;
|
|
SI32 ndata = -1, odata = -1;
|
|
UI08 skillToSet = 0xFF;
|
|
|
|
TAGMAPOBJECT customTag;
|
|
std::string customTagName;
|
|
std::string customTagStringValue;
|
|
for (const auto &sec : NpcCreation->collection2()){
|
|
DFNTAGS tag = sec->tag ;
|
|
cdata = sec->cdata ;
|
|
ndata = sec->ndata ;
|
|
odata = sec->odata ;
|
|
cdata = oldstrutil::trim( oldstrutil::removeTrailing( cdata, "//" ));
|
|
auto ssects = oldstrutil::sections( cdata, " " );
|
|
auto csects = oldstrutil::sections( cdata, "," );
|
|
switch( tag )
|
|
{
|
|
case DFNTAG_ALCHEMY: skillToSet = ALCHEMY; break;
|
|
case DFNTAG_ANATOMY: skillToSet = ANATOMY; break;
|
|
case DFNTAG_ANIMALLORE: skillToSet = ANIMALLORE; break;
|
|
case DFNTAG_ARMSLORE: skillToSet = ARMSLORE; break;
|
|
case DFNTAG_ARCHERY: skillToSet = ARCHERY; break;
|
|
case DFNTAG_DAMAGE:
|
|
case DFNTAG_ATT:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetLoDamage( static_cast<SI16>( ndata ) );
|
|
applyTo->SetHiDamage( static_cast<SI16>( odata ) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetLoDamage( static_cast<SI16>( ndata ) );
|
|
applyTo->SetHiDamage( static_cast<SI16>( ndata ) );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in ATT/DAMAGE tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_AWAKE:
|
|
if( !isGate )
|
|
applyTo->SetAwake(ndata != 0);
|
|
break;
|
|
case DFNTAG_BACKPACK:
|
|
if( !isGate )
|
|
{
|
|
if( mypack == nullptr )
|
|
mypack = applyTo->GetPackItem();
|
|
if( mypack == nullptr )
|
|
{
|
|
bool shouldSave = applyTo->ShouldSave();
|
|
mypack = Items->CreateItem( nullptr, applyTo, 0x0E75, 1, 0, OT_ITEM, false, shouldSave );
|
|
if( ValidateObject( mypack ) )
|
|
{
|
|
mypack->SetDecayable( false );
|
|
applyTo->SetPackItem( mypack );
|
|
mypack->SetName( "Backpack" );
|
|
mypack->SetLayer( IL_PACKITEM );
|
|
if( !mypack->SetCont( applyTo ) )
|
|
mypack = nullptr;
|
|
else
|
|
{
|
|
mypack->SetX( 0 );
|
|
mypack->SetY( 0 );
|
|
mypack->SetZ( 0 );
|
|
mypack->SetType( IT_CONTAINER );
|
|
mypack->SetDye( true );
|
|
mypack->SetMaxItems( cwmWorldState->ServerData()->MaxPlayerPackItems() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DFNTAG_BEGGING: skillToSet = BEGGING; break;
|
|
case DFNTAG_BLACKSMITHING: skillToSet = BLACKSMITHING; break;
|
|
case DFNTAG_BOWCRAFT: skillToSet = BOWCRAFT; break;
|
|
case DFNTAG_BUSHIDO: skillToSet = BUSHIDO; break;
|
|
case DFNTAG_CAMPING: skillToSet = CAMPING; break;
|
|
case DFNTAG_CARPENTRY: skillToSet = CARPENTRY; break;
|
|
case DFNTAG_CARTOGRAPHY: skillToSet = CARTOGRAPHY; break;
|
|
case DFNTAG_CARVE:
|
|
if( !isGate )
|
|
applyTo->SetCarve( static_cast<SI16>(ndata) );
|
|
break;
|
|
case DFNTAG_CHIVALRY: skillToSet = CHIVALRY; break;
|
|
case DFNTAG_COOKING: skillToSet = COOKING; break;
|
|
case DFNTAG_COLOUR:
|
|
if( retitem != nullptr )
|
|
retitem->SetColour( static_cast<UI16>(ndata) );
|
|
break;
|
|
case DFNTAG_COLOURLIST:
|
|
if( retitem != nullptr )
|
|
retitem->SetColour( addRandomColor( cdata ) );
|
|
break;
|
|
case DFNTAG_COLOURMATCHHAIR:
|
|
if( retitem != nullptr )
|
|
retitem->SetColour( static_cast<UI16>(haircolor) );
|
|
break;
|
|
case DFNTAG_CONTROLSLOTS:
|
|
if( !isGate )
|
|
applyTo->SetControlSlots( static_cast<UI16>( ndata ) );
|
|
break;
|
|
case DFNTAG_DEX:
|
|
if( ndata > 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetDexterity( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetDexterity( ndata );
|
|
}
|
|
applyTo->SetStamina( applyTo->GetMaxStam() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in DEX tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_DETECTINGHIDDEN: skillToSet = DETECTINGHIDDEN; break;
|
|
case DFNTAG_DEF:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(RandomNum( ndata, odata )), PHYSICAL );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(ndata), PHYSICAL );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in DEF tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_DIR:
|
|
if( !isGate )
|
|
{
|
|
std::string cupper = oldstrutil::upper( cdata );
|
|
if( cupper == "NE" ) {
|
|
applyTo->SetDir( NORTHEAST );
|
|
}
|
|
else if( cupper == "E" ) {
|
|
applyTo->SetDir( EAST );
|
|
}
|
|
else if( cupper == "SE" ) {
|
|
applyTo->SetDir( SOUTHEAST );
|
|
}
|
|
else if( cupper == "S" ) {
|
|
applyTo->SetDir( SOUTH );
|
|
}
|
|
else if( cupper == "SW" ) {
|
|
applyTo->SetDir( SOUTHWEST );
|
|
}
|
|
else if( cupper == "W" ) {
|
|
applyTo->SetDir( WEST );
|
|
}
|
|
else if( cupper == "NW" ) {
|
|
applyTo->SetDir( NORTHWEST );
|
|
}
|
|
else if( cupper == "N" ){
|
|
applyTo->SetDir( NORTH );
|
|
}
|
|
else if( cupper == "RND" )
|
|
{
|
|
UI08 rndDir = RandomNum( 0, 7 );
|
|
switch( rndDir )
|
|
{
|
|
case 0:
|
|
applyTo->SetDir( NORTHEAST );
|
|
break;
|
|
case 1:
|
|
applyTo->SetDir( EAST );
|
|
break;
|
|
case 2:
|
|
applyTo->SetDir( SOUTHEAST );
|
|
break;
|
|
case 3:
|
|
applyTo->SetDir( SOUTH );
|
|
break;
|
|
case 4:
|
|
applyTo->SetDir( SOUTHWEST );
|
|
break;
|
|
case 5:
|
|
applyTo->SetDir( WEST );
|
|
break;
|
|
case 6:
|
|
applyTo->SetDir( NORTHWEST );
|
|
break;
|
|
case 7:
|
|
applyTo->SetDir( NORTH );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DFNTAG_ELEMENTRESIST:
|
|
if( ssects.size() >= 4 )
|
|
{
|
|
applyTo->SetResist( ( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[0], "//" )), nullptr, 0)) ), HEAT );
|
|
applyTo->SetResist( ( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[1], "//" )), nullptr, 0)) ), COLD );
|
|
applyTo->SetResist( ( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[2], "//" )), nullptr, 0)) ), LIGHTNING );
|
|
applyTo->SetResist( ( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[3], "//" )), nullptr, 0)) ), POISON );
|
|
}
|
|
break;
|
|
case DFNTAG_EMOTECOLOUR:
|
|
if( !isGate )
|
|
applyTo->SetEmoteColour( static_cast<UI16>(ndata) );
|
|
break;
|
|
case DFNTAG_ENTICEMENT: skillToSet = ENTICEMENT; break;
|
|
case DFNTAG_EQUIPITEM:
|
|
if( !isGate )
|
|
{
|
|
bool shouldSave = applyTo->ShouldSave();
|
|
retitem = Items->CreateBaseScriptItem( nullptr, cdata, applyTo->WorldNumber(), 1, applyTo->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if( retitem != nullptr )
|
|
{
|
|
if( retitem->GetLayer() == IL_NONE )
|
|
Console << "Warning: Bad NPC Script ([" << sectionID.c_str() << "]) with problem item " << cdata << " executed!" << myendl;
|
|
else if( !retitem->SetCont( applyTo ) )
|
|
{
|
|
if( !retitem->SetCont( applyTo->GetPackItem() ) )
|
|
retitem = nullptr;
|
|
}
|
|
|
|
if( retitem->GetLayer() == 25 )
|
|
{
|
|
// Item equipped on mount layer. Set NPC as mounted!
|
|
applyTo->SetMounted( true );
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DFNTAG_EVALUATINGINTEL: skillToSet = EVALUATINGINTEL; break;
|
|
case DFNTAG_FAME: applyTo->SetFame( static_cast<SI16>(ndata) ); break;
|
|
case DFNTAG_FENCING: skillToSet = FENCING; break;
|
|
case DFNTAG_FISHING: skillToSet = FISHING; break;
|
|
case DFNTAG_FLEEAT:
|
|
if( !isGate )
|
|
applyTo->SetFleeAt( static_cast<SI16>(ndata) );
|
|
break;
|
|
case DFNTAG_FLEEINGSPEED:
|
|
applyTo->SetFleeingSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_FLEEINGSPEEDMOUNTED:
|
|
applyTo->SetMountedFleeingSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_FLAG:
|
|
if( !isGate )
|
|
{
|
|
if( !cdata.empty() )
|
|
{
|
|
std::string upperData = oldstrutil::upper( cdata );
|
|
if( upperData == "NEUTRAL" )
|
|
{
|
|
applyTo->SetNPCFlag( fNPC_NEUTRAL );
|
|
}
|
|
else if( upperData == "INNOCENT" )
|
|
{
|
|
applyTo->SetNPCFlag( fNPC_INNOCENT );
|
|
}
|
|
else if( upperData == "EVIL" )
|
|
{
|
|
applyTo->SetNPCFlag( fNPC_EVIL );
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case DFNTAG_FORENSICS: skillToSet = FORENSICS; break;
|
|
case DFNTAG_FOCUS: skillToSet = FOCUS; break;
|
|
case DFNTAG_FX1:
|
|
if( !isGate )
|
|
applyTo->SetFx( static_cast<SI16>(ndata), 0 );
|
|
break;
|
|
case DFNTAG_FX2:
|
|
if( !isGate )
|
|
applyTo->SetFx( static_cast<SI16>(ndata), 1 );
|
|
break;
|
|
case DFNTAG_FY1:
|
|
if( !isGate )
|
|
applyTo->SetFy( static_cast<SI16>(ndata), 0 );
|
|
break;
|
|
case DFNTAG_FY2:
|
|
if( !isGate )
|
|
applyTo->SetFy( static_cast<SI16>(ndata), 1 );
|
|
break;
|
|
case DFNTAG_FZ1:
|
|
if( !isGate )
|
|
applyTo->SetFz( static_cast<SI08>(ndata) );
|
|
break;
|
|
case DFNTAG_FOOD: applyTo->SetFood( cdata ); break;
|
|
case DFNTAG_GET:
|
|
{
|
|
std::string scriptEntry = "";
|
|
if( ssects.size() == 1 )
|
|
{
|
|
scriptEntry = cdata;
|
|
}
|
|
else
|
|
{
|
|
SI32 rndEntry = RandomNum( 0, static_cast<SI32>(ssects.size()-1));
|
|
scriptEntry = oldstrutil::trim( oldstrutil::removeTrailing( ssects[rndEntry], "//" ));
|
|
}
|
|
|
|
ScriptSection *toFind = FileLookup->FindEntry( scriptEntry, npc_def );
|
|
if( toFind == nullptr )
|
|
Console.warning( oldstrutil::format( "Invalid script entry ([%s]) called with GET tag, NPC serial 0x%X", scriptEntry.c_str(), applyTo->GetSerial() ));
|
|
else if( toFind == NpcCreation )
|
|
Console.warning( oldstrutil::format( "Infinite loop avoided with GET tag inside NPC script [%s]", scriptEntry.c_str() ));
|
|
else
|
|
ApplyNpcSection( applyTo, toFind, scriptEntry, isGate );
|
|
break;
|
|
}
|
|
break;
|
|
case DFNTAG_GOLD:
|
|
if( !isGate )
|
|
{
|
|
if( !ValidateObject( mypack ) )
|
|
mypack = applyTo->GetPackItem();
|
|
if( ValidateObject( mypack ) )
|
|
{
|
|
if( ndata >= 0 )
|
|
{
|
|
bool shouldSave = applyTo->ShouldSave();
|
|
if( odata && odata > ndata )
|
|
{
|
|
retitem = Items->CreateScriptItem( nullptr, applyTo, "0x0EED", static_cast<UI16>(RandomNum( ndata, odata )), OT_ITEM, true, 0xFFFF, shouldSave );
|
|
}
|
|
else
|
|
{
|
|
retitem = Items->CreateScriptItem( nullptr, applyTo, "0x0EED", ndata, OT_ITEM, true, 0xFFFF, shouldSave );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in GOLD tag inside NPC script [%s]", sectionID.c_str() ));
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Bad NPC Script ([%s]) with problem no backpack for gold", sectionID.c_str() ));
|
|
}
|
|
break;
|
|
case DFNTAG_HAIRCOLOUR:
|
|
haircolor = addRandomColor( cdata );
|
|
if( retitem != nullptr )
|
|
retitem->SetColour( haircolor );
|
|
break;
|
|
case DFNTAG_HEALING: skillToSet = HEALING; break;
|
|
case DFNTAG_HERDING: skillToSet = HERDING; break;
|
|
case DFNTAG_HIDING: skillToSet = HIDING; break;
|
|
case DFNTAG_HIDAMAGE: applyTo->SetHiDamage( static_cast<SI16>(ndata) ); break;
|
|
case DFNTAG_HIRELING:
|
|
if( !isGate )
|
|
applyTo->SetCanHire( true );
|
|
break;
|
|
case DFNTAG_HP:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetHP( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetHP( ndata );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in HP tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_HPMAX:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetFixedMaxHP( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetFixedMaxHP( ndata );
|
|
}
|
|
|
|
// Update current HP
|
|
applyTo->SetHP( applyTo->GetMaxHP() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in HPMAX tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_ID:
|
|
/*applyTo->SetID( static_cast<UI16>(ndata) );
|
|
applyTo->SetOrgID( static_cast<UI16>(ndata) );
|
|
break;*/
|
|
|
|
if( ssects.size() == 1 )
|
|
{
|
|
applyTo->SetID( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[0], "//" )), nullptr, 0)) );
|
|
applyTo->SetOrgID( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[0], "//" )), nullptr, 0)) );
|
|
}
|
|
else
|
|
{
|
|
SI32 rndEntry = RandomNum( 0, static_cast<SI32>(ssects.size()-1));
|
|
applyTo->SetID( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[rndEntry], "//" )), nullptr, 0)) );
|
|
applyTo->SetOrgID( static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( ssects[rndEntry], "//" )), nullptr, 0)) );
|
|
}
|
|
break;
|
|
case DFNTAG_IMBUING: skillToSet = IMBUING; break;
|
|
case DFNTAG_INSCRIPTION: skillToSet = INSCRIPTION; break;
|
|
case DFNTAG_INTELLIGENCE:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetIntelligence( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetIntelligence( ndata );
|
|
}
|
|
applyTo->SetMana( applyTo->GetMaxMana() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in INTELLIGENCE tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_ITEMID: skillToSet = ITEMID; break;
|
|
case DFNTAG_KARMA: applyTo->SetKarma( static_cast<SI16>(ndata) ); break;
|
|
case DFNTAG_PACKITEM:
|
|
[[fallthrough]];
|
|
case DFNTAG_LOOT:
|
|
if( !isGate )
|
|
{
|
|
if( !ValidateObject( mypack ) )
|
|
mypack = applyTo->GetPackItem();
|
|
if( ValidateObject( mypack ) )
|
|
{
|
|
if( !cdata.empty() )
|
|
{
|
|
auto csecs = oldstrutil::sections( oldstrutil::trim( oldstrutil::removeTrailing( cdata, "//" )), "," );
|
|
if( csecs.size() > 1 )
|
|
{
|
|
UI16 iAmount = 0;
|
|
std::string amountData = oldstrutil::trim( oldstrutil::removeTrailing( csecs[1], "//" ));
|
|
auto tsects = oldstrutil::sections( amountData, " " );
|
|
if( tsects.size() > 1 ) // check if the second part of the tag-data contains two sections separated by a space
|
|
{
|
|
auto first = static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( tsects[0], "//" )), nullptr, 0));
|
|
auto second = static_cast<UI16>(std::stoul(oldstrutil::trim( oldstrutil::removeTrailing( tsects[1], "//" )), nullptr, 0));
|
|
|
|
// Tag contained a minimum and maximum value for amount! Let's randomize!
|
|
iAmount = static_cast<UI16>(RandomNum( first, second ));
|
|
}
|
|
else
|
|
{
|
|
iAmount = static_cast<UI16>(std::stoul(amountData, nullptr, 0));
|
|
}
|
|
auto tdata = oldstrutil::trim( oldstrutil::removeTrailing( csecs[0], "//" ));
|
|
|
|
if( tag == DFNTAG_LOOT )
|
|
Items->AddRespawnItem( mypack, tdata, true, true, iAmount, true );
|
|
else
|
|
Items->AddRespawnItem( mypack, tdata, true, false, iAmount, false );
|
|
}
|
|
else
|
|
{
|
|
if( tag == DFNTAG_LOOT )
|
|
Items->AddRespawnItem( mypack, cdata, true, true, 1, true );
|
|
else
|
|
Items->AddRespawnItem( mypack, cdata, true, false, 1, false );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Bad NPC Script ([%s]) with problem no backpack for loot", sectionID.c_str() ) );
|
|
}
|
|
break;
|
|
case DFNTAG_LOCKPICKING: skillToSet = LOCKPICKING; break;
|
|
case DFNTAG_LODAMAGE: applyTo->SetLoDamage( static_cast<SI16>(ndata) ); break;
|
|
case DFNTAG_LUMBERJACKING: skillToSet = LUMBERJACKING; break;
|
|
case DFNTAG_LOYALTY:
|
|
applyTo->SetLoyalty( static_cast<UI16>( ndata ) );
|
|
break;
|
|
case DFNTAG_MACEFIGHTING: skillToSet = MACEFIGHTING; break;
|
|
case DFNTAG_MAGERY: skillToSet = MAGERY; break;
|
|
case DFNTAG_MAGICRESISTANCE: skillToSet = MAGICRESISTANCE; break;
|
|
case DFNTAG_MANA:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetMana( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetMana( ndata );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in MANA tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_MANAMAX:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetFixedMaxMana( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetFixedMaxMana( ndata );
|
|
}
|
|
|
|
// Update current Mana
|
|
applyTo->SetMana( applyTo->GetMaxMana() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in MANAMAX tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_MAXLOYALTY:
|
|
applyTo->SetMaxLoyalty( static_cast<UI16>( ndata ) );
|
|
break;
|
|
case DFNTAG_MEDITATION: skillToSet = MEDITATION; break;
|
|
case DFNTAG_MINING: skillToSet = MINING; break;
|
|
case DFNTAG_MUSICIANSHIP: skillToSet = MUSICIANSHIP; break;
|
|
case DFNTAG_MYSTICISM: skillToSet = MYSTICISM; break;
|
|
case DFNTAG_NAME: applyTo->SetName( cdata ); break;
|
|
case DFNTAG_NAMELIST: setRandomName( applyTo, cdata ); break;
|
|
case DFNTAG_NECROMANCY: skillToSet = NECROMANCY; break;
|
|
case DFNTAG_NINJITSU: skillToSet = NINJITSU; break;
|
|
case DFNTAG_NPCWANDER:
|
|
if( !isGate )
|
|
applyTo->SetNpcWander( static_cast<SI08>(ndata) );
|
|
break;
|
|
case DFNTAG_NPCAI:
|
|
if( !isGate )
|
|
applyTo->SetNPCAiType( static_cast<SI16>(ndata) );
|
|
break;
|
|
case DFNTAG_NOHIRELING:
|
|
if( !isGate )
|
|
applyTo->SetCanHire( false );
|
|
break;
|
|
case DFNTAG_NOTRAIN:
|
|
if( !isGate )
|
|
applyTo->SetCanTrain( false );
|
|
break;
|
|
case DFNTAG_POISONSTRENGTH: applyTo->SetPoisonStrength( static_cast<UI08>(ndata) ); break;
|
|
case DFNTAG_PRIV:
|
|
if( !isGate )
|
|
applyTo->SetPriv( static_cast<UI16>(ndata) );
|
|
break;
|
|
case DFNTAG_PARRYING: skillToSet = PARRYING; break;
|
|
case DFNTAG_PEACEMAKING: skillToSet = PEACEMAKING; break;
|
|
case DFNTAG_PROVOCATION: skillToSet = PROVOCATION; break;
|
|
case DFNTAG_POISONING: skillToSet = POISONING; break;
|
|
case DFNTAG_RESISTFIRE:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(RandomNum( ndata, odata )), HEAT );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetResist( ndata, HEAT );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in RESISTFIRE tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_RESISTCOLD:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(RandomNum( ndata, odata )), COLD );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetResist( ndata, COLD );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in RESISTCOLD tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_RESISTLIGHTNING:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(RandomNum( ndata, odata )), LIGHTNING );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetResist( ndata, LIGHTNING );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in RESISTLIGHTNING tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_RESISTPOISON:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetResist( static_cast<UI16>(RandomNum( ndata, odata )), POISON );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetResist( ndata, POISON );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in RESISTPOISON tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_RSHOPITEM:
|
|
if( !isGate )
|
|
{
|
|
if( !ValidateObject( buyPack ) )
|
|
buyPack = applyTo->GetItemAtLayer( IL_BUYCONTAINER );
|
|
if( ValidateObject( buyPack) ) {
|
|
retitem = Items->CreateBaseScriptItem( nullptr, cdata, applyTo->WorldNumber(), 1 );
|
|
if(retitem) {
|
|
retitem->SetCont( buyPack );
|
|
retitem->PlaceInPack();
|
|
if( !retitem->GetName2().empty() && ( retitem->GetName2()!="#" )){
|
|
retitem->SetName( retitem->GetName2() ); // Item identified!
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Console.warning( oldstrutil::format("Bad NPC Script ([%s]) with no Vendor Buy Pack for item", sectionID.c_str() ) );
|
|
}
|
|
}
|
|
break;
|
|
case DFNTAG_REATTACKAT:
|
|
if( !isGate ){
|
|
applyTo->SetReattackAt( static_cast<SI16>(ndata) );
|
|
}
|
|
break;
|
|
case DFNTAG_REMOVETRAP: skillToSet = REMOVETRAP; break;
|
|
case DFNTAG_RACE: applyTo->SetRace( static_cast<UI16>(ndata)); break;
|
|
case DFNTAG_RUNS:
|
|
if( !isGate )
|
|
applyTo->SetRun( true );
|
|
break;
|
|
case DFNTAG_RUNNINGSPEED:
|
|
applyTo->SetRunningSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_RUNNINGSPEEDMOUNTED:
|
|
applyTo->SetMountedRunningSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_SKIN: applyTo->SetSkin( static_cast<UI16>(ndata) ); break;
|
|
case DFNTAG_SHOPKEEPER:
|
|
if( !isGate )
|
|
MakeShop( applyTo );
|
|
break;
|
|
case DFNTAG_SHOPLIST:
|
|
if( !isGate )
|
|
LoadShopList( cdata, applyTo );
|
|
break;
|
|
case DFNTAG_SELLITEM:
|
|
if( !isGate )
|
|
{
|
|
if( !ValidateObject( sellPack ) )
|
|
sellPack = applyTo->GetItemAtLayer( IL_SELLCONTAINER );
|
|
if( ValidateObject( sellPack ) )
|
|
{
|
|
bool shouldSave = applyTo->ShouldSave();
|
|
retitem = Items->CreateBaseScriptItem( nullptr, cdata, applyTo->WorldNumber(), 1, applyTo->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if( retitem != nullptr )
|
|
{
|
|
retitem->SetCont( sellPack );
|
|
retitem->PlaceInPack();
|
|
if( !retitem->GetName2().empty() && ( retitem->GetName2()!="#" )){
|
|
retitem->SetName( retitem->GetName2() );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Bad NPC Script ([%s]) with no Vendor SellPack for item", sectionID.c_str() ));
|
|
}
|
|
break;
|
|
case DFNTAG_SHOPITEM:
|
|
if( !isGate )
|
|
{
|
|
if( !ValidateObject( boughtPack ) )
|
|
boughtPack = applyTo->GetItemAtLayer( IL_BOUGHTCONTAINER );
|
|
if( ValidateObject( boughtPack ) )
|
|
{
|
|
bool shouldSave = applyTo->ShouldSave();
|
|
retitem = Items->CreateBaseScriptItem( nullptr, cdata, applyTo->WorldNumber(), 1, applyTo->GetInstanceID(), OT_ITEM, 0xFFFF, shouldSave );
|
|
if( retitem != nullptr )
|
|
{
|
|
retitem->SetCont( boughtPack );
|
|
retitem->PlaceInPack();
|
|
if( !retitem->GetName2().empty() && ( retitem->GetName2()!= "#" ) )
|
|
retitem->SetName( retitem->GetName2() );
|
|
}
|
|
}
|
|
else
|
|
Console.warning(oldstrutil::format( "Bad NPC Script ([%s]) with no Vendor Bought Pack for item", sectionID.c_str() ) );
|
|
}
|
|
break;
|
|
case DFNTAG_SAYCOLOUR:
|
|
if( !isGate )
|
|
applyTo->SetSayColour( static_cast<UI16>(ndata) );
|
|
break;
|
|
case DFNTAG_SPATTACK:
|
|
if( !isGate )
|
|
applyTo->SetSpAttack( static_cast<SI16>(ndata) );
|
|
break;
|
|
case DFNTAG_SPADELAY:
|
|
if( !isGate )
|
|
applyTo->SetSpDelay( static_cast<SI08>(ndata) );
|
|
break;
|
|
case DFNTAG_SPELLWEAVING: skillToSet = SPELLWEAVING; break;
|
|
case DFNTAG_SPLIT:
|
|
if( !isGate )
|
|
applyTo->SetSplit( static_cast<UI08>(ndata) );
|
|
break;
|
|
case DFNTAG_SPLITCHANCE:
|
|
if( !isGate )
|
|
applyTo->SetSplitChance( static_cast<UI08>(ndata) );
|
|
break;
|
|
case DFNTAG_SNOOPING: skillToSet = SNOOPING; break;
|
|
case DFNTAG_SPIRITSPEAK: skillToSet = SPIRITSPEAK; break;
|
|
case DFNTAG_STEALING: skillToSet = STEALING; break;
|
|
case DFNTAG_STRENGTH:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetStrength( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetStrength( ndata );
|
|
}
|
|
applyTo->SetHP( applyTo->GetMaxHP() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in STRENGTH tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_SWORDSMANSHIP: skillToSet = SWORDSMANSHIP; break;
|
|
case DFNTAG_STAMINA:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetStamina( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetStamina( ndata );
|
|
}
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in STAMINA tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_STAMINAMAX:
|
|
if( ndata >= 0 )
|
|
{
|
|
if( odata && odata > ndata )
|
|
{
|
|
applyTo->SetFixedMaxStam( static_cast<SI16>(RandomNum( ndata, odata )) );
|
|
}
|
|
else
|
|
{
|
|
applyTo->SetFixedMaxStam( ndata );
|
|
}
|
|
|
|
// Update current Stamina
|
|
applyTo->SetStamina( applyTo->GetMaxStam() );
|
|
}
|
|
else
|
|
Console.warning( oldstrutil::format("Invalid data found in STAMINA tag inside NPC script [%s]", sectionID.c_str() ));
|
|
break;
|
|
case DFNTAG_STEALTH: skillToSet = STEALTH; break;
|
|
case DFNTAG_SKINLIST: applyTo->SetSkin( addRandomColor( cdata ) ); break;
|
|
case DFNTAG_SKILL: applyTo->SetBaseSkill( static_cast<UI16>(odata), static_cast<UI08>(ndata) ); break;
|
|
case DFNTAG_SCRIPT:
|
|
if( !isGate )
|
|
{
|
|
applyTo->AddScriptTrigger( static_cast<UI16>(ndata) );
|
|
}
|
|
break;
|
|
case DFNTAG_THROWING: skillToSet = THROWING; break;
|
|
case DFNTAG_TITLE: applyTo->SetTitle( cdata ); break;
|
|
case DFNTAG_TOTAME:
|
|
if( !isGate )
|
|
{
|
|
applyTo->SetTaming( static_cast<SI16>(ndata) );
|
|
applyTo->SetOrneriness( static_cast<UI16>(ndata) ); // Set default difficulty to control equal to skill required to tame
|
|
}
|
|
break;
|
|
case DFNTAG_TOPROV:
|
|
if( !isGate )
|
|
applyTo->SetProvoing( static_cast<SI16>(ndata) );
|
|
break;
|
|
case DFNTAG_TOPEACE:
|
|
if( !isGate )
|
|
applyTo->SetPeaceing( static_cast<SI16>(ndata) );
|
|
applyTo->SetBrkPeaceChanceGain( static_cast<SI16>(odata) );
|
|
break;
|
|
case DFNTAG_TAMEDHUNGER:
|
|
if( !isGate )
|
|
{
|
|
applyTo->SetTamedHungerRate( static_cast<UI16>(ndata) );
|
|
applyTo->SetTamedHungerWildChance( static_cast<UI08>(odata) );
|
|
}
|
|
break;
|
|
case DFNTAG_TAMEDTHIRST:
|
|
if( !isGate )
|
|
{
|
|
applyTo->SetTamedThirstRate( static_cast<UI16>( ndata ) );
|
|
applyTo->SetTamedThirstWildChance( static_cast<UI08>( odata ) );
|
|
}
|
|
break;
|
|
case DFNTAG_WILLHUNGER:
|
|
if( !isGate )
|
|
{
|
|
if( ndata > 0 )
|
|
applyTo->SetHungerStatus( true );
|
|
else
|
|
applyTo->SetHungerStatus( false );
|
|
}
|
|
break;
|
|
case DFNTAG_WALKINGSPEED:
|
|
applyTo->SetWalkingSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_WALKINGSPEEDMOUNTED:
|
|
applyTo->SetMountedWalkingSpeed( std::stof(cdata) );
|
|
break;
|
|
case DFNTAG_TACTICS: skillToSet = TACTICS; break;
|
|
case DFNTAG_TAILORING: skillToSet = TAILORING; break;
|
|
case DFNTAG_TAMING: skillToSet = TAMING; break;
|
|
case DFNTAG_TASTEID: skillToSet = TASTEID; break;
|
|
case DFNTAG_TINKERING: skillToSet = TINKERING; break;
|
|
case DFNTAG_TRACKING: skillToSet = TRACKING; break;
|
|
case DFNTAG_VETERINARY: skillToSet = VETERINARY; break;
|
|
case DFNTAG_WRESTLING: skillToSet = WRESTLING; break;
|
|
case DFNTAG_CUSTOMSTRINGTAG:
|
|
{
|
|
auto count = 0;
|
|
std::string result;
|
|
for( auto &sec : ssects )
|
|
{
|
|
if( count > 0 )
|
|
{
|
|
if( count == 1 )
|
|
{
|
|
result = oldstrutil::trim( oldstrutil::removeTrailing( sec, "//" ));
|
|
}
|
|
else
|
|
{
|
|
result = result + " " + oldstrutil::trim( oldstrutil::removeTrailing( sec, "//" ));
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
customTagName = oldstrutil::trim( oldstrutil::removeTrailing( ssects[0], "//" ));
|
|
customTagStringValue = result;
|
|
|
|
if( !customTagName.empty() && !customTagStringValue.empty() )
|
|
{
|
|
customTag.m_Destroy = false;
|
|
customTag.m_StringValue = customTagStringValue;
|
|
customTag.m_IntValue = static_cast<SI32>(customTag.m_StringValue.size());
|
|
customTag.m_ObjectType = TAGMAP_TYPE_STRING;
|
|
applyTo->SetTag( customTagName, customTag);
|
|
}
|
|
else
|
|
{
|
|
Console.warning( oldstrutil::format( "Invalid data found in CUSTOMSTRINGTAG tag inside NPC script [%s] - Supported data format: <tagName> <text>", sectionID.c_str() ) );
|
|
}
|
|
break;
|
|
}
|
|
case DFNTAG_CUSTOMINTTAG:
|
|
{
|
|
auto count = 0;
|
|
std::string result;
|
|
for( auto &sec : ssects )
|
|
{
|
|
if( count > 0 )
|
|
{
|
|
if( count == 1 )
|
|
{
|
|
result = oldstrutil::trim( oldstrutil::removeTrailing( sec, "//" ));
|
|
}
|
|
}
|
|
count++;
|
|
}
|
|
customTagName = oldstrutil::trim( oldstrutil::removeTrailing( ssects[0], "//" ));
|
|
customTagStringValue = result;
|
|
if( !customTagName.empty() && !customTagStringValue.empty() )
|
|
{
|
|
customTag.m_Destroy = false;
|
|
customTag.m_IntValue = std::stoi( customTagStringValue );
|
|
customTag.m_ObjectType = TAGMAP_TYPE_INT;
|
|
customTag.m_StringValue = "";
|
|
applyTo->SetTag( customTagName, customTag );
|
|
if( count > 2 )
|
|
{
|
|
Console.warning( oldstrutil::format( "Multiple values detected for CUSTOMINTTAG in NPC script [%s] - only first value will be used! Supported data format: <tagName> <value>", sectionID.c_str() ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.warning( oldstrutil::format( "Invalid data found in CUSTOMINTTAG tag in NPC script [%s] - Supported data format: <tagName> <value>", sectionID.c_str() ) );
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
Console << "Unknown tag in ApplyNpcSection(): " << (SI32)tag << myendl; break;
|
|
}
|
|
if( skillToSet != 0xFF )
|
|
{
|
|
applyTo->SetBaseSkill( static_cast<UI16>(RandomNum( ndata, odata )), skillToSet );
|
|
skillToSet = 0xFF;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool cCharStuff::canControlPet( CChar *mChar, CChar *Npc, bool isRestricted, bool checkDifficulty, bool ignoreOwnerCheck, bool ignoreLoyaltyChanges )
|
|
{
|
|
if( ValidateObject( Npc->GetOwnerObj() ) && Npc->GetNPCAiType() != AI_PLAYERVENDOR && Npc->GetQuestType() == 0 )
|
|
{
|
|
if( !ignoreOwnerCheck && Npc->GetOwnerObj() != mChar && ( isRestricted || !Npcs->checkPetFriend( mChar, Npc ) ) )
|
|
return false;
|
|
|
|
if( checkDifficulty && Npc->IsTamed() && cwmWorldState->ServerData()->CheckPetControlDifficulty() )
|
|
{
|
|
// Let's base this on how difficult it is to control the pet, as well
|
|
UI16 chanceToControl = Skills->CalculatePetControlChance( mChar, Npc );
|
|
UI16 loyaltyGainOnSuccess = cwmWorldState->ServerData()->GetPetLoyaltyGainOnSuccess();
|
|
UI16 loyaltyLossOnFailure = cwmWorldState->ServerData()->GetPetLoyaltyLossOnFailure();
|
|
|
|
// Easy to control pets/summoned creatures don't gain/lose loyalty from commands
|
|
if( chanceToControl == 1000 )
|
|
return true;
|
|
|
|
if( chanceToControl >= RandomNum( 0, 1000 ) )
|
|
{
|
|
// Succeeded in controlling pet
|
|
if( !ignoreLoyaltyChanges )
|
|
{
|
|
Npc->SetLoyalty( std::min( 100, Npc->GetLoyalty() + loyaltyGainOnSuccess ) );
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Failed to control pet
|
|
if( !ignoreLoyaltyChanges )
|
|
{
|
|
Npc->SetLoyalty( std::max( 0, Npc->GetLoyalty() - loyaltyLossOnFailure ) );
|
|
UI16 soundToPlay = cwmWorldState->creatures[Npc->GetID()].GetSound( SND_STARTATTACK );
|
|
Effects->PlaySound( Npc, soundToPlay );
|
|
|
|
if( Npc->GetLoyalty() == 0 )
|
|
{
|
|
// Reduce player's control slot usage by the amount of control slots taken up by the pet
|
|
mChar->SetControlSlotsUsed( std::max( 0, mChar->GetControlSlotsUsed() - Npc->GetControlSlots() ) );
|
|
|
|
// Pet goes wild for lack of loyalty
|
|
Npcs->releasePet( Npc );
|
|
return false;
|
|
}
|
|
|
|
CSocket *mSock = mChar->GetSocket();
|
|
if( mSock != nullptr )
|
|
{
|
|
std::string npcName = getNpcDictName( Npc, mSock );
|
|
mSock->sysmessage( 2412, npcName.c_str() ); // %s disobeys your command
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// No difficulty checking needed, just allow it
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void finalizeTransfer( CChar *npc )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Finalize transfer of pet to new owner
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void cCharStuff::finalizeTransfer( CChar *petChar, CChar *srcChar, CChar *targChar )
|
|
{
|
|
// If the pet is guarding something, stop guarding that something
|
|
Npcs->stopPetGuarding( petChar );
|
|
|
|
// Clear pet's existing friend-list
|
|
petChar->ClearFriendList();
|
|
|
|
std::string petName = getNpcDictName( petChar );
|
|
petChar->TextMessage( nullptr, 1074, TALK, 0, petName.c_str(), targChar->GetName().c_str() ); // * %s will now take %s as his master *
|
|
|
|
// Transfer ownership
|
|
petChar->SetOwner( targChar );
|
|
|
|
// Make pet follow their new owner
|
|
petChar->SetFTarg( static_cast<CChar*>( targChar ) );
|
|
petChar->FlushPath();
|
|
petChar->SetNpcWander( WT_FOLLOW );
|
|
|
|
// Update control slots used for both old and new owners
|
|
srcChar->SetControlSlotsUsed( std::max(0, srcChar->GetControlSlotsUsed() - petChar->GetControlSlots()) );
|
|
targChar->SetControlSlotsUsed( std::clamp(targChar->GetControlSlotsUsed() + petChar->GetControlSlots(), 0, 255 ));
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void releasePet( CChar *npc )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Release the pet from its owner
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void cCharStuff::releasePet( CChar *pet )
|
|
{
|
|
// Stop guarding
|
|
Npcs->stopPetGuarding( pet );
|
|
|
|
// Stop following
|
|
pet->SetFTarg( nullptr );
|
|
pet->SetNpcWander( WT_FREE );
|
|
|
|
// Remove owner
|
|
pet->SetOwner( nullptr );
|
|
|
|
std::string petName = getNpcDictName( pet );
|
|
pet->TextMessage( nullptr, 1325, TALK, 0, petName.c_str() ); // *%s appears to have decided that it is better off without a master *
|
|
|
|
// If a summoned creature, unsummon it
|
|
if( pet->GetTimer( tNPC_SUMMONTIME ) )
|
|
{
|
|
Effects->PlaySound( pet, 0x01FE );
|
|
pet->Delete();
|
|
}
|
|
|
|
// Reset hunger
|
|
pet->SetHunger( 6 );
|
|
|
|
// Increase pet orneriness, to make it more difficult to control for its next owner
|
|
auto ownerCount = pet->GetOwnerCount();
|
|
switch( ownerCount )
|
|
{
|
|
case 1:
|
|
pet->SetOrneriness( pet->GetTaming() + 48 );
|
|
break;
|
|
case 2:
|
|
pet->SetOrneriness( pet->GetTaming() + 192 );
|
|
break;
|
|
case 3:
|
|
pet->SetOrneriness( pet->GetTaming() + 432 );
|
|
break;
|
|
case 4:
|
|
pet->SetOrneriness( pet->GetTaming() + 768 );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - CChar * getGuardingPet( CChar *mChar, CBaseObject *guarded )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Get the pet guarding an item / character
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
auto cCharStuff::getGuardingPet( CChar *mChar, CBaseObject *guarded ) ->CChar * {
|
|
if( ValidateObject( mChar ) && ValidateObject( guarded ) ){
|
|
|
|
auto myPets = mChar->GetPetList();
|
|
for (const auto &pet : myPets->collection()){
|
|
if( ValidateObject( pet ) ) {
|
|
//if( !pet->GetMounted() && pet->GetNPCAiType() == AI_PET_GUARD &&
|
|
if( !pet->GetMounted() &&
|
|
pet->GetGuarding() == guarded && pet->GetOwnerObj() == mChar ){
|
|
return pet;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool checkPetFriend( CChar *mChar, CChar *pet )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Search a pets friends list to check if a character is a friend
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool cCharStuff::checkPetFriend( CChar *mChar, CChar *pet )
|
|
{
|
|
auto petFriends = pet->GetFriendList();
|
|
if( petFriends != nullptr )
|
|
{
|
|
for (auto &getFriend : *petFriends){
|
|
if( ValidateObject( getFriend ) && getFriend == mChar )
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void stopPetGuarding( CChar *pet )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Find the item/char pet is guarding and set it to not guarded
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void cCharStuff::stopPetGuarding( CChar *pet )
|
|
{
|
|
CBaseObject *petGuarding = pet->GetGuarding();
|
|
if( !ValidateObject( petGuarding ) )
|
|
return;
|
|
|
|
if( petGuarding->GetObjType() == OT_ITEM )
|
|
{
|
|
CItem *itemGuard = static_cast<CItem *>(petGuarding);
|
|
if( ValidateObject( itemGuard ) )
|
|
itemGuard->SetGuarded( false );
|
|
}
|
|
else
|
|
{
|
|
CChar *charGuard = static_cast<CChar *>(petGuarding);
|
|
if( ValidateObject( charGuard ) )
|
|
charGuard->SetGuarded( false );
|
|
}
|
|
pet->SetGuarding( nullptr );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MonsterGate( CChar *s, const std::string& scriptEntry )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handle monster gates (polymorphs players into monster bodies)
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MonsterGate( CChar *s, const std::string& scriptEntry )
|
|
{
|
|
CItem *mypack = nullptr;
|
|
if( s->IsNpc() )
|
|
return;
|
|
|
|
auto entry = scriptEntry;
|
|
entry = oldstrutil::trim( oldstrutil::removeTrailing( entry, "//" ));
|
|
ScriptSection *Monster = FileLookup->FindEntry( entry, npc_def );
|
|
if( Monster == nullptr )
|
|
return;
|
|
|
|
s->SetTitle( "\0" );
|
|
|
|
CItem *n;
|
|
for( CItem *z = s->FirstItem(); !s->FinishedItems(); z = s->NextItem() )
|
|
{
|
|
if( ValidateObject( z ) )
|
|
{
|
|
if( z->isFree() )
|
|
continue;
|
|
if( z->GetLayer() == IL_HAIR || z->GetLayer() == IL_FACIALHAIR )
|
|
z->Delete();
|
|
else if( z->GetLayer() != IL_PACKITEM && z->GetLayer() != IL_BANKBOX )
|
|
{
|
|
if( mypack == nullptr )
|
|
mypack = s->GetPackItem();
|
|
if( mypack == nullptr )
|
|
{
|
|
n = Items->CreateItem( nullptr, s, 0x0E75, 1, 0, OT_ITEM );
|
|
if( !ValidateObject( n ) )
|
|
return;
|
|
s->SetPackItem( n );
|
|
n->SetDecayable( false );
|
|
n->SetLayer( IL_PACKITEM );
|
|
if( n->SetCont( s ) )
|
|
{
|
|
n->SetType( IT_CONTAINER );
|
|
n->SetDye( true );
|
|
mypack = n;
|
|
}
|
|
}
|
|
z->SetCont( mypack );
|
|
z->PlaceInPack();
|
|
}
|
|
}
|
|
}
|
|
|
|
Npcs->ApplyNpcSection( s, Monster, scriptEntry, true );
|
|
//Now find real 'skill' based on 'baseskill' (stat modifiers)
|
|
for( UI08 j = 0; j < ALLSKILLS; ++j )
|
|
Skills->updateSkillLevel( s, j );
|
|
s->Update();
|
|
Effects->PlayStaticAnimation( s, 0x373A, 0, 15 );
|
|
Effects->PlaySound( s, 0x01E9 );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void Karma( CChar *nCharID, CChar *nKilledID, const SI16 nKarma )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handle karma addition/subtraction when character kills
|
|
//| another Character / NPC
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void Karma( CChar *nCharID, CChar *nKilledID, const SI16 nKarma )
|
|
{
|
|
SI16 nChange = 0;
|
|
bool nEffect = false;
|
|
SI16 nCurKarma = nCharID->GetKarma();
|
|
if( nCurKarma > 10000 )
|
|
{
|
|
nCharID->SetKarma( 10000 );
|
|
nCurKarma = 10000;
|
|
}
|
|
else if( nCurKarma < -10000 )
|
|
{
|
|
nCharID->SetKarma( -10000 );
|
|
nCurKarma = -10000;
|
|
}
|
|
|
|
if( nCurKarma < nKarma && nKarma > 0 )
|
|
{
|
|
nChange = ( ( nKarma - nCurKarma ) / 75 );
|
|
nCharID->SetKarma( (SI16)( nCurKarma + nChange ) );
|
|
nEffect = true;
|
|
}
|
|
if( nCurKarma > nKarma && ( !ValidateObject( nKilledID ) || nKilledID->GetKarma() > 0 ) )
|
|
{
|
|
nChange = ( ( nCurKarma - nKarma ) / 50 );
|
|
nCharID->SetKarma( (SI16)( nCurKarma - nChange ) );
|
|
nEffect = false;
|
|
}
|
|
if( nChange == 0 ) // NPCs CAN gain/lose karma
|
|
return;
|
|
|
|
CSocket *mSock = nCharID->GetSocket();
|
|
if( nCharID->IsNpc() || mSock == nullptr )
|
|
return;
|
|
if( nChange <= 25 )
|
|
{
|
|
if( nEffect )
|
|
mSock->sysmessage( 1367 );
|
|
else
|
|
mSock->sysmessage( 1368 );
|
|
return;
|
|
}
|
|
if( nChange <= 50 )
|
|
{
|
|
if( nEffect )
|
|
mSock->sysmessage( 1369 );
|
|
else
|
|
mSock->sysmessage( 1370 );
|
|
return;
|
|
}
|
|
if( nEffect )
|
|
mSock->sysmessage( 1371 );
|
|
else
|
|
mSock->sysmessage( 1372 );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void Fame( CChar *nCharID, const SI16 nFame )
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handle fame addition when character kills another Character / NPC
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void Fame( CChar *nCharID, const SI16 nFame )
|
|
{
|
|
SI16 nChange = 0;
|
|
bool nEffect = false;
|
|
const SI16 nCurFame = nCharID->GetFame();
|
|
if( nCharID->IsDead() )
|
|
{
|
|
if( nCurFame <= 0 )
|
|
nCharID->SetFame( 0 );
|
|
else
|
|
{
|
|
nChange = ( nCurFame - 0 ) / 25;
|
|
nCharID->SetFame( (SI16)( nCurFame - nChange ) );
|
|
}
|
|
nCharID->SetDeaths( (UI16)( nCharID->GetDeaths() + 1 ) );
|
|
nEffect = false;
|
|
}
|
|
else if( nCurFame <= nFame )
|
|
{
|
|
nChange = ( nFame - nCurFame ) / 75;
|
|
nCharID->SetFame( (SI16)( nCurFame + nChange ) );
|
|
nEffect = true;
|
|
if( nCharID->GetFame() > 10000 )
|
|
nCharID->SetFame( 10000 );
|
|
}
|
|
else
|
|
return; // current fame is greater than target fame, and we're not dead
|
|
if( nChange == 0 )
|
|
return;
|
|
CSocket *mSock = nCharID->GetSocket();
|
|
if( mSock == nullptr || nCharID->IsNpc() )
|
|
return;
|
|
if( nChange <= 25 )
|
|
{
|
|
if( nEffect )
|
|
mSock->sysmessage( 1373 );
|
|
else
|
|
mSock->sysmessage( 1374 );
|
|
}
|
|
else if( nChange <= 50 )
|
|
{
|
|
if( nEffect )
|
|
mSock->sysmessage( 1375 );
|
|
else
|
|
mSock->sysmessage( 1376 );
|
|
}
|
|
else
|
|
{
|
|
if( nEffect )
|
|
mSock->sysmessage( 1377 );
|
|
else
|
|
mSock->sysmessage( 1378 );
|
|
}
|
|
}
|
|
|
|
|