mirror of
https://github.com/UOX3DevTeam/UOX3
synced 2026-08-13 12:27:04 -04:00
Added JS command to reload dictionaries 'reloaddictionaries Fixed a server crash related to casting of spells Fixed some issues with Line-of-Sight checks related to items under/above ground, LoS checks between different floors of buildings, etc. Updated handling of skill-training keywords to be based on language-independent keyword IDs rather than trying to match player-input text to specific skill-names Updated animal-trainer/stablemasterscript (js/npc/ai/stablemaster.js) with several changes: Fixed an issue where the same pet would be stabled in all available slots if payment was taken from player's bank account instead of their backpack Fixed an issue where stablemaster would not pause walking for a while when a player interacts with them Converted all system messages and text messages in stablemaster script to dictionary entries Added new NPC AI type to source - AI_STABLEMASTER (9) - and assigned this to animal trainers. This is used to identify this type of NPCs for the purpose of displaying relevant context menus Reworked banker AI script (js/npc/ai/banker.js) to use triggerwords from client instead of hard-defined strings, to work better with multiple languages (and work with all relevant triggerwords) Fixed a bug with banker AI script (js/npc/ai/banker.js) that prevented players from using the "withdraw" command Re-added NPC AI 8 to banker NPCs (dfndata/npc/male_vendors.dfn and female_vendors.dfn) so code can check for presence of this AI when handling context menus Removed remaining hard-coded banking functionality - all handled by script anyway Added new context menu option for NPC bankers - Open Bankbox Updated code handling of context menus to only show context menus when relevant: Open Paperdoll - Shows for all characters with a paperdoll Open Backpack - Shows for player's character, pack animals and hirelings Open Bankbox - Shows for banker NPCs (if within 8 tiles) Buy/Sell - Shows for vendor NPCs (if within 8 tiles, and if they have anything for sale/are buying anything) Added new context menu entries for pets, which can be used if player is within 12 tiles of pet: Command: Kill Command: Stop Command: Follow Command: Stay Command: Guard Add Friend Remove Friend Transfer Release Added new context menu entries for stablemasters/animal trainers: Claim All Pets Stable Pet Added new context menu entries for Escort Quest NPCs Ask Destination (if within 3 tiles) Accept Escort (if within 3 tiles) Abandon Escort Added new context menus for NPCs that can teach skills to players (limited to max 10 per NPC) Train [skillName] Added new AI script for NPC hirelings (3204=js/npc/ai/hireling.js), who can be hired to follow the player around for a limited amount of time, and function the same way as pets. In addition to the regular pet commands, these speech commands are supported: hire // The hireling will respond with the cost to hire them dismiss // Replaces the "release" command for pets; will dismiss the hireling patrol // The hireling will patrol between it's current location and a second targeted location nearby move // The hireling will try to shuffle out of the way fetch // The hireling will try to fetch a targeted item drop // The hireling will drop to the ground all loot they're currently carrying report // The hireling will report on the remaining time they're hired for Added new NPC DFN tag used by code to detect hireling NPCs for the purpose of displaying context menus: HIRELING // No additional value/data needed for tag Added new context menu entries for Hireling NPCs, in addition to the ones for regular pets: Hire // Can be used within 3 steps of the hireling, who will respond with cost to hire them Dismiss // Replaces the "release" command for pets, will dismiss the hireling Added HIRELING tag and SCRIPT=3204 to and updated stats of various NPCs that will be hireable: m_fighter, f_fighter, m_beggar, f_beggar, m_peasant, f_peasant, m_sailor, f_sailor, m_pirate, f_pirate Added new hireling NPC: m_paladin, f_paladin Added new UOX ini setting to enable/disable context menus CONTEXTMENUS=1/0 (defaults to 1) Added new Item JS Method to check if an item's ID is on a specified food list .IsOnFoodList( foodList ) // Returns true/false if item's ID is on specified foodList Added new OMNIVORE food list and assigned it to [basehuman] DFN section Added new Character JS Properties .isGuarded // Gets/Sets whether character is guarded by a pet/hireling .guarding // Gets/Sets the object (if any) being guarded by a pet/hireling Fixed multiple issues with health bars, flagging and character highlighting Added some crash protection for invalid data used with CUSTOMINTTAG and CUSTOMSTRINGTAG DFN tags for Items, NPCs and Multis Fixed an issue which prevented hard-coded checks from running when using items Added new global js object (Timer) to make the use of character timers in JS more robust, with properties matching timer names from enum in source. Properties: .TIMEOUT // Time until next attack can be done in combat .INVIS // Time until invisible character becomes visible .HUNGER // Time until character grows more hungry .THIRST // Time until character grows more thirsty .POISONTIME // Time until next tick of poison damage .POISONTEXT // Time until next message about suffering from poison .POISONWEAROFF // Time until poison wears off .SPELLTIME // Time until spell cast is completed. Can be set to 0 to interrupt .ANTISPAM // Time until next speech message can be sent (for anti spam purposes) .CRIMFLAG // Time until criminal flag runs out .MURDERRATE // Time until next murder count decay .PEACETIMER // Time until character can re-enter combat after being affected by peacemaking .FLYINGTOGGLE // Time until next time flying ability can be toggled for gargoyles .MOVETIME // Time until NPC can move again .SPATIMER // Time until next time NPC can cast a spell .SUMMONTIME // Time until a summoned NPC will vanish .EVADETIME // Time until an NPC will exit evade state .LOGOUT // Time it takes for a player char to vanish after logout Examples of use: var hungerTimer = myChar.GetTimer( Timer.HUNGER ) myChar.SetTimer( Timer.HUNGER, 15000 ) Added new Character JS Methods to add, remove and list friends of a pet/hireling: .AddFriend( playerToAdd ) // Adds player to friend list .RemoveFriend( playerToRemove ) // Removes player from friend list .GetFriendList() // Gets list of friends .ClearFriendList() // Clears list of friends Added new Character JS Method to fetch a player character's list of pets/followers .GetPetList() // Gets list of pets/followers Added new persistent NPC property to keep track of a pet's previous owners, whether those tamed the pet or had it transferred to them. Every time a pet is tamed, or is transferred to a new owner, this list is updated: GenericList< CChar * > petOwnerList Added new Character JS Method to check if a player is on a pet's owner list as a previous owner: .HasBeenOwner( mChar ) Previous owners of a pet can re-tame it with guaranteed chance of success Updated JS Method Refresh() to be usable with both items or characters, to send updated state of object to nearby players Added character tooltip [Guarded] for characters being guarded by a pet/hireling Pets/hirelings that are transferred will now immediately start following their new master instead of wandering freely Pets/hirelings can no longer be transferred between players as long as either party is flagged as a criminal Pets/hirelings can no longer be transferred to dead players Pets/hirelings can no longer be transferred to NPCs Summoned creatures can no longer be transferred to other players Summoned creatures can no longer have friends Friend lists of pets/hirelings are now cleared when the pet/hireling is transferred to another player Added new UOX.INI settings to control how many pets players can have active: MAXCONTROLSLOTS=0 // Maximum number of pet control slots available to player. Disabled if 0 MAXFOLLOWERS=5 // Maximum pets/followers a player can have active at the same time. Used if control slots are disabled MAXPETOWNERS=5 // Maximum number of different owners a pet can have over its lifetime before it becomes impossible to retame Added new NPC property and NPC DFN tag that keeps track of how many pet control slots an NPC would take up if owned by a player: UI08 controlSlots // source property CONTROLSLOTS=# // DFN tag Added new Character JS properties: .ownerCount // Get the total number of owners a pet/hireling has had, based on NPC's petOwnerList .controlSlots // Get/Set number of pet control slots an NPC will occupy .controlSlotsUsed // Get/Set the number of control slots used by a player Updated get (js/commands/targeting/get.js) and set (js/commands/targeting/set.js) commands to support getting/setting the following character properties: deaths ownerCount (read only) controlSlots controlSlotsUsed Updated NPC DFNs with default CONTROLSLOT=# tags Increased the max distance from player that onSpeech JS event will trigger from 7 to 12 Updated how "all attack" and "all follow" commands for pets are handled. Now loops through all pets owned by character and executes command for each eligible pet Fixed an issue where NPCs could follow characters in different worlds/instances than them selves Updated default max amount of items that can be sold to NPC vendors from 5 to 250 Guards can no longer train players in skills Updated equipment itemlists (dfndata/items/itemlists/itemlists.dfn) with blank entries to introduce some more variety in the type of clothes NPCs wear in general Added new NPCs to DFNs (dfndata/npc/undead.dfn) and added them to undead npclist: [skeletalmage] // variation of [bonemage] [skeletalknight] // variation of [boneknight] Updated Titles (dfndata/titles/titles.dfn) with up-to-date titles for skills around Pub 15, and added titles for skills added after that. Also added "Elder" and "Legendary" titles for 110 and 120 skillpoints Updated EQUIPITEM tag in newbie.dfn (dfndata/newbie/newbie.dfn) to support an optional hue parameter (EQUIPITEM=id,hue) Updated starting equipment for new characters (dfndata/newbie/newbie.dfn), with commented out practice weapons. Uncomment to use Added item definitions for practice weapons (dfndata/items/gear/weapons/practice_weapons.dfn): practice_skinning_knife practice_hatchet practice_axe practice_mace practice_longsword practice_spear practice_bow practice_club practice_crook practice_gnarled_staff Updated mount statues DFN (dfndata/items/misc/mount-statues.dfn) and JS (js/npc/pets/*.js) files to include pet control slot related stuff Pets now inherit the karma of their owners, but revert to their original karma upon release Pets and hirelings will no longer follow the ghosts of their dead owners, but stay to guard their corpse Added optional 9th parameter for JS Function CreateDFNItem to specify a color for item created. This comes before the other optional parameters - worldNumber and instanceID (which are only used if character is NULL): CreateDFNItem( mSock, mChar, sectionName, inPack, iAmount, itemType, iColor, worldNumber, instanceID ) Added ID of Giant Beetle to various pack animal checks Pack animals will now drop any newbie/blessed items stored in their packs upon death Added new NPC properties to track pet loyalty, and exposed these as Character JS properties, and NPC DFN tags: JS Properties .maxLoyalty // Defaults to 100 .loyalty // Starts at 25 DFN Tags MAXLOYALTY=# // Defaults to 100 LOYALTY=# // Starts at 25 Updated TriggerEvent JS function to support return values (int, bool, string, object) from called script Updated various JS scripts to make use of new TriggerEvent functionality and reduce reliance on custom tags: js/item/archerybutte.js js/item/trainingdummy.js js/server/data/combatanims.js js/server/data/weapontypes.js Updated archerybutte script to use dictionary for system and text messages Updated archerybutte script to support increased range, distance penalty and dex/str bonuses when calculating score Added new UOX.INI settings under [pets and hirelings] section related to pet control and loyalty: CHECKPETCONTROLDIFFICULTY=1 // Enable/Disable pet control difficulty system PETLOYALTYGAINONSUCCESS=1 // Amount of pet loyalty gained on successful pet command use PETLOYALTYLOSSONFAILURE=3 // Amount of pet loyalty lost on failed pet command use PETLOYALTYRATE=900 // Amount of seconds between each time pet loyalty is automatically reduced by 1. Takes 25 hours to deplete completely from max Added new persistent NPC property that keeps track of the difficulty of taming and controlling a pet, and exposed it as a Character JS property: .orneriness Every time a tamed pet is released or goes wild, its "orneriness" increases, making it more difficult for other players to tame, and more difficult for anyone to control. Updated CBasePetResponse::canControlPet() function to use pet control difficulty system if enabled, which checks player's animal taming/animal lore skill vs a pet's "orneriness" to determine chance of pet accepting a given pet command On successful use of pet command, increases pet loyalty by value defined in PETLOYALTYGAINONSUCCESS ini setting On failed use of pet command, decreases pet loyalty by value defined in PETLOYALTYLOSSONFAILURE ini setting Added new Character JS Method to calculate chance of a player successfully controlling the pet: .CalculateControlChance( mChar ) // Returns value between 0 and 1000 indicating chance of success Feeding a pet will now restore its loyalty to maximum Pets will now lose loyalty on hunger checks when at maximum hunger Pets that are maximum hungry will now only have a chance to go wild if loyalty has dropped to zero Added new DFN tag for creatures.dfn - TYPE - which contains a string describing the type of creature Added secure pet trading. Players who trade pets will now see a pet transfer deed appear in a secure trade window with the name and type of creature, and upon completion of the trade the associated pet will be instantly transferred to the other player Updated resource JS scripts (js/server/resource/*) to use dictionaries for all system messages Updated skill JS scripts (js/skill/*) to use dictionaries for all system messages Updated housing JS scripts (js/server/house/*) to use dictionaries for all system messages Updated item JS scripts (js/item/*) to use dictionaries for all system messages Updated magic JS scripts (js/magic/*) to use dictionaries for all system messages Updated NPC AI JS scripts (js/npc/ai/*) to use dictionaries for all system messages Updated command JS scripts (js/commands/*) to use dictionaries for all system messages, gump tooltips, etc Updated code to use dictionaries for all system messages Fixed incorrect spelling for UOX.INI setting HIDEW(H)ILEMOUNTED and updated hiding skill (js/skill/hiding.js) to actually allow/disallow hiding while mounted based on this setting Added new Character JS Method to make it easier to make one NPC initiate combat with another: .InitiateCombat( targetChar ) // Character attempts to initiate combat with target character Updated parrying portion of combat code to be in line with Pub15/pre-AoS parrying mechanics. High AR shields now absorb more damage on a successful parry, while low AR shields have a higher chance of parrying. Shields are also now more effective against archery attacks (full absorption potential) than melee attacks (half absorption potential). Updated combat damage calculations to be in line with Pub15/pre-AoS damage calculations Adjusted default value of COMBATNPCDAMAGERATE ini setting (damage divisor when target is a player) from 2 to 1 to account for these changes. Removed dictionary.UNK, and made dictionary.ZRO the default dictionary used for all unsupported languages Added dictionary support for additional languages: Portuguese, Italian, Czech Updated default dictionaries for the following languages: English, French Added default dictionaries for the following languages: German, Spanish, Portuguese, Italian, Czech (note that translations have been provided by automatic services and will contain inaccuracies!) Added new UOX.INI setting to allow specifying a default dictionary language for server, which if set will force that language for all dictionary messages, regardless of client settings: SERVERLANGUAGE=0 // Set default server dictionary language. Supported languages: 0 - None/language used by each client, 1 - English, 2 - German, 3 - Spanish, 5 - French, 6 - Portuguese, 7 - Italian, 8 - Czech Added new function in combat.cpp - AdjustArmorClassDamage() - which adjusts the damage dealt in combat based on whether armour class (AC DFN tag or .ac JS property) of weapon and armour equipped on hit location of target match up. A weapon with armour class 1 would essentially be doubly effective against armour of armour class 1. Allows setting up things like piercing weapons being better against some armor types than mace weapons, etc. Added new UOX.INI setting under [combat] to enable/disable double damage from armour class bonuses: ARMORCLASSDAMAGEBONUS=0/1 //defaults to 0)
1225 lines
41 KiB
C++
1225 lines
41 KiB
C++
//o-----------------------------------------------------------------------------------------------o
|
|
//| File - msgboard.cpp
|
|
//| Date - 16/04/1999
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Message Board Handling
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Changes - Version History
|
|
//|
|
|
//| 1.0 16th April 1999 - 0.69.03
|
|
//| Initial implimentation
|
|
//|
|
|
//| 1.1 19th January 2000 - 0.70.02
|
|
//| Additional features added to messageboard
|
|
//| Escort quests added / integrated with message board enhancements
|
|
//|
|
|
//| 2.0 10th August 2005 - 0.98-3.0g
|
|
//| Complete rewrite of msgboard.cpp
|
|
//| Modified file i/o to use fstreams for faster file reads/writes.
|
|
//| Completely re-formatted the message board files to remove the need for two files, and reduce waste.
|
|
//| Made use of packet classes to simplify sending & receiving data from the client.
|
|
//| Added a maintenance function that will automatically clean up the message board files.
|
|
//| Added support for message board file deletion upon deleting the associated world object.
|
|
//|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
#include "uox3.h"
|
|
#include "msgboard.h"
|
|
#include "townregion.h"
|
|
#include "cServerDefinitions.h"
|
|
#include "ssection.h"
|
|
#include "cEffects.h"
|
|
#include "CPacketSend.h"
|
|
#include "classes.h"
|
|
#include "Dictionary.h"
|
|
#include "StringUtility.hpp"
|
|
#include <filesystem>
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - std::string GetMsgBoardFile( const SERIAL msgBoardSer, const UI08 msgType )
|
|
//| Date - 8/6/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates the proper MessageBoard filename based on the messageType and board Serial
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
std::string GetMsgBoardFile( const SERIAL msgBoardSer, const UI08 msgType )
|
|
{
|
|
std::string fileName;
|
|
switch( msgType )
|
|
{
|
|
case PT_GLOBAL: fileName = "global.bbf"; break;
|
|
case PT_REGIONAL: CItem *msgBoard;
|
|
CTownRegion *mbRegion;
|
|
msgBoard = calcItemObjFromSer( msgBoardSer );
|
|
mbRegion = calcRegionFromXY( msgBoard->GetX(), msgBoard->GetY(), msgBoard->WorldNumber(), msgBoard->GetInstanceID() );
|
|
fileName = std::string("region") + strutil::number( mbRegion->GetRegionNum() ) + std::string(".bbf");
|
|
break;
|
|
case PT_LOCAL: fileName = strutil::number( msgBoardSer, 16 ) + std::string(".bbf"); break;
|
|
default: Console.error( "GetMsgBoardFile() Invalid post type, aborting" );
|
|
break;
|
|
}
|
|
return fileName;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardOpen( CSocket *mSock )
|
|
//| Date - 7/16/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Opens the Messageboard and Advises the client of the messages
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardOpen( CSocket *mSock )
|
|
{
|
|
const SERIAL boardSer = mSock->GetDWord( 1 );
|
|
|
|
CItem *msgBoard = calcItemObjFromSer( boardSer );
|
|
if( !ValidateObject( msgBoard ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
char buffer[4];
|
|
std::string fileName, dirPath;
|
|
|
|
mSock->PostClear();
|
|
|
|
CPOpenMessageBoard mbPost( mSock );
|
|
mSock->Send( &mbPost );
|
|
|
|
CPSendMsgBoardPosts mbSend;
|
|
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
dirPath = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD );
|
|
}
|
|
|
|
for( UI08 currentFile = 1; currentFile < 4 && mSock->PostCount() < MAXPOSTS; ++currentFile )
|
|
{
|
|
fileName = dirPath + GetMsgBoardFile( boardSer, currentFile );
|
|
|
|
std::ifstream file ( fileName.c_str(), std::ios::in | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
msgBoardPost_st msgBoardPost;
|
|
|
|
file.seekg( 0, std::ios::beg );
|
|
while( file && mSock->PostCount() < MAXPOSTS )
|
|
{
|
|
SERIAL tmpSerial = 0;
|
|
UI08 tmpToggle = 0;
|
|
UI16 tmpSize = 0;
|
|
SERIAL repliedTo = 0;
|
|
|
|
file.read( buffer, 2 );
|
|
tmpSize = static_cast<UI16>((buffer[0]<<8) + buffer[1]);
|
|
|
|
file.read( buffer, 1 );
|
|
tmpToggle = buffer[0];
|
|
|
|
file.read( buffer, 4 );
|
|
repliedTo = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
|
|
file.seekg( tmpSize-11, std::ios::cur );
|
|
|
|
file.read( buffer, 4 );
|
|
tmpSerial = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
|
|
if( !file.fail() )
|
|
{
|
|
if( tmpToggle ) // If it's 0, flagged for deletion
|
|
{
|
|
mbSend.CopyData( mSock, tmpSerial, tmpToggle, boardSer );
|
|
mSock->PostAcked( tmpSerial );
|
|
}
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
if( mSock->PostCount() > 0 )
|
|
{
|
|
mbSend.Finalize();
|
|
mSock->Send( &mbSend );
|
|
mSock->PostAckCount( 0 );
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardList( CSocket *mSock )
|
|
//| Date - 7/16/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Sends the summary of each message to the client
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardList( CSocket *mSock )
|
|
{
|
|
char buffer[4];
|
|
std::string fileName, dirPath;
|
|
|
|
CItem *msgBoard = calcItemObjFromSer( mSock->GetDWord( 4 ) );
|
|
if( !ValidateObject( msgBoard ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
dirPath = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD );
|
|
}
|
|
|
|
for( UI08 currentFile = 1; currentFile < 4 && mSock->PostCount() > 0; ++currentFile )
|
|
{
|
|
fileName = dirPath + GetMsgBoardFile( msgBoard->GetSerial(), currentFile );
|
|
|
|
std::ifstream file ( fileName.c_str(), std::ios::in | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
size_t totalSize = 0;
|
|
msgBoardPost_st msgBoardPost;
|
|
|
|
file.seekg( 0, std::ios::beg );
|
|
while( file )
|
|
{
|
|
file.read( buffer, 2 );
|
|
msgBoardPost.Size = static_cast<UI16>((buffer[0]<<8) + buffer[1]);
|
|
|
|
file.seekg( msgBoardPost.Size-6, std::ios::cur );
|
|
|
|
file.read( buffer, 4 );
|
|
msgBoardPost.Serial = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
for( SERIAL postAck = mSock->FirstPostAck(); !mSock->FinishedPostAck(); )
|
|
{
|
|
if( msgBoardPost.Serial == postAck )
|
|
{
|
|
postAck = mSock->RemovePostAck();
|
|
|
|
file.seekg( totalSize+2, std::ios::beg );
|
|
|
|
file.read( buffer, 1 );
|
|
if( buffer[0] == 0x05 )
|
|
{
|
|
file.read( buffer, 4 );
|
|
msgBoardPost.ParentSerial = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
}
|
|
else
|
|
{
|
|
file.seekg( 4, std::ios::cur );
|
|
}
|
|
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Poster, buffer[0] );
|
|
msgBoardPost.PosterLen = buffer[0];
|
|
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Subject, buffer[0] );
|
|
msgBoardPost.SubjectLen = buffer[0];
|
|
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Date, buffer[0] );
|
|
msgBoardPost.DateLen = buffer[0];
|
|
|
|
if( file.fail() )
|
|
{
|
|
Console.warning( strutil::format("Malformed MessageBoard post, MessageID: 0x%X", msgBoardPost.Serial ));
|
|
file.close();
|
|
}
|
|
else
|
|
{
|
|
CPOpenMsgBoardPost mbPost( mSock, msgBoardPost, false );
|
|
mSock->Send( &mbPost );
|
|
}
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
postAck = mSock->NextPostAck();
|
|
}
|
|
}
|
|
totalSize += msgBoardPost.Size;
|
|
file.seekg( totalSize, std::ios::beg );
|
|
if( totalSize > 3000 || msgBoardPost.Serial == 0 ) //HACCKKKK!!!1one. Without this in place, it will loop for ever :(
|
|
{
|
|
file.close();
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
if( !mSock->FinishedPostAck() )
|
|
{
|
|
mSock->PostClear();
|
|
Console.error( strutil::format("Failed to list all posts for MessageBoard ID: 0x%X", mSock->GetDWord( 4 )) );
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool GetMaxSerial( const std::string& fileName, UI08 *nextMsgID, const PostTypes msgType )
|
|
//| Date - 7/22/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Updates nextMsgID with the next available message serial.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool GetMaxSerial( const std::string& fileName, UI08 *nextMsgID, const PostTypes msgType )
|
|
{
|
|
SERIAL msgIDSer = ( calcserial( nextMsgID[0], nextMsgID[1], nextMsgID[2], nextMsgID[3] ) );
|
|
if( msgIDSer == INVALIDSERIAL )
|
|
{
|
|
switch( msgType )
|
|
{
|
|
case PT_GLOBAL: msgIDSer = BASEGLOBALPOST; break;
|
|
case PT_REGIONAL: msgIDSer = BASEREGIONPOST; break;
|
|
case PT_LOCAL: msgIDSer = BASELOCALPOST; break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++msgIDSer;
|
|
}
|
|
|
|
nextMsgID[0] = static_cast<UI08>(msgIDSer>>24);
|
|
nextMsgID[1] = static_cast<UI08>(msgIDSer>>16);
|
|
nextMsgID[2] = static_cast<UI08>(msgIDSer>>8);
|
|
nextMsgID[3] = static_cast<UI08>(msgIDSer%256);
|
|
|
|
if( nextMsgID[1] == 0xFF && nextMsgID[2] == 0xFF && nextMsgID[3] == 0xFF )
|
|
{
|
|
Console.warning(strutil::format("Maximum Posts reached for board %s, no further posts can be created", fileName.c_str()) );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardWritePost( std::ofstream& mFile, const msgBoardPost_st& msgBoardPost )
|
|
//| Date - 8/10/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Writes a new post to the .bbf file
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardWritePost( std::ofstream& mFile, const msgBoardPost_st& msgBoardPost )
|
|
{
|
|
char wBuffer[4];
|
|
|
|
// Add post size to 2 bytes of buffer, and write buffer to file
|
|
wBuffer[0] = static_cast<SI08>(msgBoardPost.Size>>8);
|
|
wBuffer[1] = static_cast<SI08>(msgBoardPost.Size%256);
|
|
mFile.write( (const char *)&wBuffer, 2 );
|
|
|
|
// Write the value of Toggle as the next byte
|
|
mFile.write( (const char *)&msgBoardPost.Toggle, 1 );
|
|
|
|
// Add parent serial to 4 bytes of buffer, and write buffer to file
|
|
wBuffer[0] = static_cast<SI08>(msgBoardPost.ParentSerial>>24);
|
|
wBuffer[1] = static_cast<SI08>(msgBoardPost.ParentSerial>>16);
|
|
wBuffer[2] = static_cast<SI08>(msgBoardPost.ParentSerial>>8);
|
|
wBuffer[3] = static_cast<SI08>(msgBoardPost.ParentSerial%256);
|
|
mFile.write( (const char *)&wBuffer, 4 );
|
|
|
|
// Write length of poster's name as next byte, and
|
|
// then write poster's name as next X bytes
|
|
mFile.write( (const char *)&msgBoardPost.PosterLen, 1 );
|
|
mFile.write( (const char *)&msgBoardPost.Poster, msgBoardPost.PosterLen );
|
|
|
|
// Write length of post's subject in next byte, and
|
|
// then write post subject as next X bytes
|
|
mFile.write( (const char *)&msgBoardPost.SubjectLen, 1 );
|
|
mFile.write( (const char *)&msgBoardPost.Subject, msgBoardPost.SubjectLen );
|
|
|
|
// Write length of post's date string in next byte, and
|
|
// then write date string as next X bytes
|
|
mFile.write( (const char *)&msgBoardPost.DateLen, 1 );
|
|
mFile.write( (const char *)&msgBoardPost.Date, msgBoardPost.DateLen );
|
|
|
|
// Write number of lines in post as next byte, and
|
|
// then loop through each line
|
|
mFile.write( (const char *)&msgBoardPost.Lines, 1 );
|
|
STRINGLIST_CITERATOR lIter;
|
|
for( lIter = msgBoardPost.msgBoardLine.begin(); lIter != msgBoardPost.msgBoardLine.end(); ++lIter )
|
|
{
|
|
// Write length of line as next byte, and
|
|
// then write the line as next X bytes
|
|
const UI08 lineSize = static_cast<UI08>((*lIter).size());
|
|
mFile.write( (const char *)&lineSize, 1 );
|
|
mFile.write( (*lIter).c_str(), lineSize );
|
|
}
|
|
|
|
// Finally, add actual post serial to 4 bytes of buffer, and write buffer to file
|
|
wBuffer[0] = static_cast<SI08>(msgBoardPost.Serial>>24);
|
|
wBuffer[1] = static_cast<SI08>(msgBoardPost.Serial>>16);
|
|
wBuffer[2] = static_cast<SI08>(msgBoardPost.Serial>>8);
|
|
wBuffer[3] = static_cast<SI08>(msgBoardPost.Serial%256);
|
|
mFile.write( (const char *)&wBuffer, 4 );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - SERIAL MsgBoardWritePost( msgBoardPost_st& msgBoardPost, const std::string& fileName, const PostTypes msgType )
|
|
//| Date - 7/22/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Writes a new post to the .bbf file, returning the messages SERIAL
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
SERIAL MsgBoardWritePost( msgBoardPost_st& msgBoardPost, const std::string& fileName, const PostTypes msgType )
|
|
{
|
|
SERIAL msgID = INVALIDSERIAL;
|
|
|
|
std::string fullFile;
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
fullFile = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ) + fileName;
|
|
}
|
|
|
|
std::ifstream file ( fullFile.c_str(), std::ios::in | std::ios::ate | std::ios::binary );
|
|
|
|
UI08 nextMsgID[4];
|
|
memset( nextMsgID, 0xFF, 4 );
|
|
if( file.is_open() )
|
|
{
|
|
file.seekg( -4, std::ios::cur );
|
|
file.read( (char *)&nextMsgID, 4 );
|
|
file.close();
|
|
}
|
|
|
|
if( !GetMaxSerial( fileName, &nextMsgID[0], msgType ) )
|
|
{
|
|
return msgID;
|
|
}
|
|
else
|
|
{
|
|
msgID = calcserial( nextMsgID[0], nextMsgID[1], nextMsgID[2], nextMsgID[3] );
|
|
}
|
|
|
|
auto timet = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
|
|
auto timeOfPost = *std::localtime(&timet);
|
|
auto time = strutil::format( "Day %i @ %i:%02i\0", (timeOfPost.tm_yday+1), timeOfPost.tm_hour, timeOfPost.tm_min );
|
|
time.resize( time.size()+1 );
|
|
const UI08 timeSize = static_cast<UI08>(time.size());
|
|
const UI08 posterSize = static_cast<UI08>(msgBoardPost.PosterLen);
|
|
const UI08 subjSize = static_cast<UI08>(msgBoardPost.SubjectLen);
|
|
UI16 totalSize = static_cast<UI16>(15 + posterSize + subjSize + timeSize);
|
|
STRINGLIST_CITERATOR lIter;
|
|
for( lIter = msgBoardPost.msgBoardLine.begin(); lIter != msgBoardPost.msgBoardLine.end(); ++lIter )
|
|
{
|
|
totalSize += 1 + static_cast<UI16>( ( *lIter ).size() );
|
|
}
|
|
|
|
msgBoardPost.Size = totalSize;
|
|
msgBoardPost.DateLen = timeSize;
|
|
strncpy( msgBoardPost.Date, time.c_str(), timeSize );
|
|
msgBoardPost.Serial = msgID;
|
|
|
|
std::ofstream mFile ( fullFile.c_str(), std::ios::out | std::ios::app | std::ios::binary );
|
|
|
|
MsgBoardWritePost( mFile, msgBoardPost );
|
|
|
|
mFile.close();
|
|
|
|
return msgID;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardPost( CSocket *tSock )
|
|
//| Date - 7/22/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handles the client requesting to post a new message
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardPost( CSocket *tSock )
|
|
{
|
|
CChar *tChar = tSock->CurrcharObj();
|
|
if( !ValidateObject( tChar ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const PostTypes msgType = static_cast<PostTypes>(tChar->GetPostType());
|
|
|
|
if( msgType != PT_LOCAL && tChar->GetCommandLevel() < CL_GM )
|
|
{
|
|
tSock->sysmessage( 9036 ); // Only GM's may post Global or Regional messages
|
|
return;
|
|
}
|
|
|
|
SERIAL repliedTo = tSock->GetDWord( 8 );
|
|
if( (repliedTo&BASEITEMSERIAL) == BASEITEMSERIAL )
|
|
{
|
|
repliedTo -= BASEITEMSERIAL;
|
|
}
|
|
|
|
if( repliedTo != 0 && repliedTo < BASELOCALPOST )
|
|
{
|
|
tSock->sysmessage( 729 );
|
|
return;
|
|
}
|
|
|
|
std::string fileName = GetMsgBoardFile( tSock->GetDWord( 4 ), msgType );
|
|
if( fileName.empty() )
|
|
{
|
|
tSock->sysmessage( 725 );
|
|
return;
|
|
}
|
|
|
|
std::vector< UI08 > internalBuffer;
|
|
|
|
const UI16 length = tSock->GetWord( 1 );
|
|
internalBuffer.resize( length );
|
|
memcpy( &internalBuffer[0], tSock->Buffer(), length );
|
|
|
|
msgBoardPost_st msgBoardPost;
|
|
|
|
msgBoardPost.Toggle = 0x05;
|
|
|
|
msgBoardPost.ParentSerial = repliedTo;
|
|
|
|
UI08 i = 0;
|
|
UI16 offset = 11;
|
|
|
|
msgBoardPost.PosterLen = static_cast<UI08>(tChar->GetName().size()) + 1;
|
|
strncpy( msgBoardPost.Poster, tChar->GetName().c_str(), msgBoardPost.PosterLen );
|
|
|
|
msgBoardPost.SubjectLen = internalBuffer[++offset];
|
|
strncpy( msgBoardPost.Subject, (const char*)&internalBuffer[++offset], msgBoardPost.SubjectLen );
|
|
|
|
offset += msgBoardPost.SubjectLen;
|
|
|
|
msgBoardPost.msgBoardLine.resize( internalBuffer[offset] );
|
|
msgBoardPost.Lines = internalBuffer[offset];
|
|
std::vector< std::string >::iterator lIter;
|
|
UI08 j = 0;
|
|
for( lIter = msgBoardPost.msgBoardLine.begin(); lIter != msgBoardPost.msgBoardLine.end(); ++lIter )
|
|
{
|
|
(*lIter).resize( internalBuffer[++offset] );
|
|
for( i = 0; i < (*lIter).size(); ++i )
|
|
{
|
|
(*lIter)[i] = internalBuffer[++offset];
|
|
}
|
|
|
|
// Check if line is null-terminated, and null-terminate it if not
|
|
if( msgBoardPost.msgBoardLine[j][msgBoardPost.msgBoardLine[j].size()] > 0 )
|
|
{
|
|
auto checkForNullTerminator = msgBoardPost.msgBoardLine[j][msgBoardPost.msgBoardLine[j].size()-1];
|
|
if( checkForNullTerminator != '\0' )
|
|
{
|
|
msgBoardPost.msgBoardLine[j][msgBoardPost.msgBoardLine[j].size()-1] = '\0';
|
|
}
|
|
}
|
|
j++;
|
|
}
|
|
|
|
const SERIAL msgID = MsgBoardWritePost( msgBoardPost, fileName, msgType );
|
|
if( msgID != INVALIDSERIAL )
|
|
{
|
|
CPAddItemToCont toAdd;
|
|
if( tSock->ClientVerShort() >= CVS_6017 )
|
|
{
|
|
toAdd.UOKRFlag( true );
|
|
}
|
|
toAdd.Serial( (msgID | BASEITEMSERIAL) );
|
|
toAdd.Container( tSock->GetDWord( 4 ) );
|
|
tSock->Send( &toAdd );
|
|
|
|
// Setup buffer to expect to receive an ACK from the client for this posting
|
|
tSock->PostAcked( msgID );
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool MsgBoardReadPost( std::ifstream& file, msgBoardPost_st& msgBoardPost, SERIAL msgSerial = INVALIDSERIAL )
|
|
//| Date - 8/10/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Reads in a post from its specified file.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool MsgBoardReadPost( std::ifstream& file, msgBoardPost_st& msgBoardPost, SERIAL msgSerial = INVALIDSERIAL )
|
|
{
|
|
char buffer[4];
|
|
|
|
// Read two first bytes of post, which contains total size
|
|
file.read( buffer, 2 );
|
|
|
|
if( file.gcount() != 2 )
|
|
{
|
|
// Return early if we failed to read 2 bytes into buffer
|
|
return false;
|
|
}
|
|
|
|
msgBoardPost.Size = ( (buffer[0]<<8) + buffer[1] );
|
|
|
|
// Seek to position of msgboardpost serial stored at end of the post, and read it into buffer
|
|
file.seekg( msgBoardPost.Size-6, std::ios::cur );
|
|
file.read( buffer, 4 );
|
|
msgBoardPost.Serial = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
if( !file.fail() && ( msgBoardPost.Serial == msgSerial || msgSerial == INVALIDSERIAL ) )
|
|
{
|
|
char tmpLine[256];
|
|
// Seek to start of post, right after 2 bytes containing size
|
|
file.seekg( -(msgBoardPost.Size-2), std::ios::cur );
|
|
|
|
// Get toggle value from next byte
|
|
file.read( buffer, 1 );
|
|
msgBoardPost.Toggle = buffer[0];
|
|
|
|
// Get parent serial from next 4 bytes
|
|
file.read( buffer, 4 );
|
|
msgBoardPost.ParentSerial = calcserial( buffer[0], buffer[1], buffer[2], buffer[3] );
|
|
|
|
// Get size of poster's name from next byte, and then get poster's name
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Poster, buffer[0] );
|
|
msgBoardPost.PosterLen = buffer[0];
|
|
|
|
// Get size of post's subject from next byte, and then get subject string
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Subject, buffer[0] );
|
|
msgBoardPost.SubjectLen = buffer[0];
|
|
|
|
// Get size of post's date string from next byte, and then get date string
|
|
file.read( buffer, 1 );
|
|
file.read( msgBoardPost.Date, buffer[0] );
|
|
msgBoardPost.DateLen = buffer[0];
|
|
|
|
// Get number of lines from next byte, then read each line from post
|
|
file.read( buffer, 1 );
|
|
msgBoardPost.Lines = buffer[0];
|
|
for( UI08 i = 0; i < msgBoardPost.Lines; ++i )
|
|
{
|
|
// Get line length from next byte, then read line string into tmpLine
|
|
file.read( buffer, 1 );
|
|
file.read( tmpLine, buffer[0]);
|
|
|
|
// Stuff contents of tmpLine into msgBoardLine STRINGLIST in msgBoardPost struct, then add null terminator
|
|
msgBoardPost.msgBoardLine.push_back( tmpLine );
|
|
msgBoardPost.msgBoardLine[msgBoardPost.msgBoardLine.size()-1] += '\0';
|
|
}
|
|
|
|
file.seekg( 4, std::ios::cur );
|
|
|
|
if( msgBoardPost.Toggle != 0x00 )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If Toggle value has been set to 0x00 for a post, return false so it won't be included when writing posts back to file later
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardOpenPost( CSocket *mSock )
|
|
//| Date - 7/16/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Opens a post on a message board when double clicked.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardOpenPost( CSocket *mSock )
|
|
{
|
|
std::string fileName = GetMsgBoardFile( mSock->GetDWord( 4 ), (mSock->GetByte( 8 ) - 0x40) );
|
|
if( fileName.empty() )
|
|
{
|
|
mSock->sysmessage( 732 );
|
|
return;
|
|
}
|
|
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
fileName = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ) + fileName;
|
|
}
|
|
|
|
msgBoardPost_st msgBoardPost;
|
|
bool foundEntry = false;
|
|
|
|
const SERIAL msgSerial = (mSock->GetDWord( 8 ) - BASEITEMSERIAL );
|
|
std::ifstream file ( fileName.c_str(), std::ios::in | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
file.seekg( 0, std::ios::beg );
|
|
while( file && !foundEntry )
|
|
{
|
|
if( MsgBoardReadPost( file, msgBoardPost, msgSerial ) )
|
|
{
|
|
foundEntry = true;
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
else
|
|
{
|
|
Console.error( strutil::format("Failed to open MessageBoard file for reading MessageID: 0x%X", msgSerial) );
|
|
}
|
|
|
|
if( foundEntry )
|
|
{
|
|
CPOpenMsgBoardPost mbPost( mSock, msgBoardPost, true );
|
|
mSock->Send( &mbPost );
|
|
}
|
|
else
|
|
{
|
|
Console.warning(strutil::format( "Failed to find MessageBoard file for MessageID: 0x%X", msgSerial ));
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardRemovePost( CSocket *mSock )
|
|
//| Date - 7/17/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Removes a post from a message board
|
|
//|
|
|
//| Note: The client should trigger this function, but I have
|
|
//| been unable to find a way through the client to send the
|
|
//| remove post packet.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardRemovePost( CSocket *mSock )
|
|
{
|
|
CChar *mChar = mSock->CurrcharObj();
|
|
if( !ValidateObject( mChar ) || ( ValidateObject( mChar ) && !mChar->IsGM() ))
|
|
{
|
|
mSock->sysmessage( 9037 ); // Only GMs may delete posts!
|
|
return;
|
|
}
|
|
|
|
// If the messageboard is not found, do nothing
|
|
CItem *msgBoard = calcItemObjFromSer( mSock->GetDWord( 4 ) );
|
|
if( !ValidateObject( msgBoard ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the messageboard filename is not returned properly, do nothing
|
|
std::string fileName = GetMsgBoardFile( msgBoard->GetSerial(), (mSock->GetByte( 8 ) - 0x40 ) );
|
|
if( fileName.empty() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the folder storing messageboard files exists, construct a path for the filename
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
fileName = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ) + fileName;
|
|
}
|
|
|
|
bool foundPost = false;
|
|
|
|
// Fetch the serial of the messageboard post in need of removal
|
|
const SERIAL msgSer = (mSock->GetDWord( 8 ) - BASEITEMSERIAL );
|
|
|
|
// Let's grab the size of the file so we can use it later
|
|
std::size_t fileSize = 0;
|
|
try {
|
|
fileSize = std::filesystem::file_size( fileName );
|
|
} catch( ... ) {
|
|
fileSize = 0;
|
|
}
|
|
|
|
// If file size is zero, not much point in continuing!
|
|
if( fileSize == 0 )
|
|
{
|
|
if( ValidateObject( mChar ))
|
|
{
|
|
mSock->sysmessage( 9038 ); // Failed to remove post; file size not found! Check server log for more details.
|
|
}
|
|
Console.error( strutil::format("Could not fetch file size for file %s", fileName.c_str()) );
|
|
return;
|
|
}
|
|
|
|
// Open a filestream in binary mode to read in data from the messageboard file
|
|
std::fstream file ( fileName.c_str(), std::ios::in | std::ios::out | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
SERIAL tmpSerial = 0;
|
|
size_t totalSize = 0;
|
|
UI16 tmpSize = 0;
|
|
char rBuffer[4];
|
|
bool removeReply = true;
|
|
|
|
while( !file.eof() )
|
|
{
|
|
removeReply = false;
|
|
|
|
// Read first 2 characters from filestream into rBuffer, starting from first line in file
|
|
file.read( rBuffer, 2 );
|
|
|
|
// Shift first byte up eight bytes, add to second byte
|
|
tmpSize = ( (rBuffer[0]<<8) + rBuffer[1] );
|
|
|
|
// Move stream's "get" position relative to current position
|
|
file.seekg( 1, std::ios::cur );
|
|
|
|
// Read the next 4 characters from filestream into rBuffer, and use this to attempt calculating post serial
|
|
file.read( rBuffer, 4 );
|
|
tmpSerial = calcserial( rBuffer[0], rBuffer[1], rBuffer[2], rBuffer[3] );
|
|
|
|
// If the serial matches the serial of post to be removed, prepare to check for
|
|
// any replies to the post/thread as well, which will also need to be removed
|
|
if( tmpSerial == msgSer )
|
|
{
|
|
removeReply = true;
|
|
|
|
// Move stream's "set" position to the start of the post in question, and overwrite first 2 bytes with 0x00 to mark it for deletion
|
|
// This will hide it from view, but not actually delete it from the file itself; that's handled by message board maintenance
|
|
file.seekp( totalSize+2, std::ios::beg );
|
|
file.put( 0x00 );
|
|
}
|
|
|
|
// If looking for replies to remove, or we haven't found the post to remove yet
|
|
if( removeReply || !foundPost )
|
|
{
|
|
// Move stream's "get" position relative to current position, to... what? Look for a reply?
|
|
file.seekg( tmpSize-11, std::ios::cur );
|
|
|
|
// Read another serial candidate into the buffer, and try to calculate post serial from buffer
|
|
file.read( rBuffer, 4 );
|
|
tmpSerial = calcserial( rBuffer[0], rBuffer[1], rBuffer[2], rBuffer[3] );
|
|
|
|
if( removeReply )
|
|
{
|
|
// Send packet to remove item (each post is an item in a container - the container being the message board) visually from message board
|
|
CPRemoveItem toRemove;
|
|
toRemove.Serial( (tmpSerial + BASEITEMSERIAL) );
|
|
mSock->Send( &toRemove );
|
|
}
|
|
else if( tmpSerial == msgSer )
|
|
{
|
|
// Mark post as found, but don't break out of the loop, in order to get all replies
|
|
foundPost = true;
|
|
|
|
// Move stream's "set" position to the start of the post in question, and overwrite first 2 bytes with 0x00 to mark it for deletion
|
|
// This will hide it from view, but not actually delete it from the file itself; that's handled by message board maintenance
|
|
file.seekp( totalSize+2, std::ios::beg );
|
|
file.put( 0x00 );
|
|
|
|
mSock->sysmessage( 734 );
|
|
}
|
|
}
|
|
|
|
// Make sure we don't get stuck in an infinite loop... don't allow seeking past the end of the file!
|
|
totalSize += tmpSize;
|
|
if( totalSize < fileSize )
|
|
{
|
|
file.seekg( totalSize, std::ios::beg );
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
else
|
|
{
|
|
Console.error( strutil::format("Could not open file %s for reading", fileName.c_str()) );
|
|
}
|
|
|
|
// If we found the post to remove previously, remove it at this point, since all potential replies to it have been removed already
|
|
if( foundPost )
|
|
{
|
|
CPRemoveItem toRemove;
|
|
toRemove.Serial( mSock->GetDWord( 8 ) );
|
|
mSock->Send( &toRemove );
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool CPIMsgBoardEvent::Handle( void )
|
|
//| Date - 7/22/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handles a mesage board event from the client.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool CPIMsgBoardEvent::Handle( void )
|
|
{
|
|
UI08 msgType = tSock->GetByte( 3 );
|
|
|
|
if( tSock->GetByte( 0 ) == 0x06 )
|
|
msgType = 0;
|
|
|
|
switch( msgType )
|
|
{
|
|
case 0: MsgBoardOpen( tSock ); break;
|
|
case 3: MsgBoardOpenPost( tSock ); break;
|
|
case 4:
|
|
tSock->PostAckCount( tSock->PostAckCount() + 1 );
|
|
if( tSock->PostAckCount() != tSock->PostCount() )
|
|
return true;
|
|
|
|
tSock->PostAckCount( 0 );
|
|
MsgBoardList( tSock );
|
|
break;
|
|
case 5: MsgBoardPost( tSock ); break;
|
|
case 6: MsgBoardRemovePost( tSock ); break;
|
|
default: break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - bool MsgBoardPostQuest( CChar *mNPC, const QuestTypes questType )
|
|
//| Date - 7/23/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates an escort quest post on regional messageboards
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
bool MsgBoardPostQuest( CChar *mNPC, const QuestTypes questType )
|
|
{
|
|
msgBoardPost_st msgBoardPost;
|
|
std::string sect, tag, data;
|
|
std::string fileName = std::string("region") + strutil::number( mNPC->GetQuestOrigRegion() ) + std::string(".bbf");
|
|
ScriptSection *EscortData = nullptr, *Escort = nullptr;
|
|
size_t totalEntries = 0;
|
|
std::string tmpSubject = "";
|
|
|
|
switch( questType )
|
|
{
|
|
case QT_ESCORTQUEST:
|
|
msgBoardPost.Toggle = QT_ESCORTQUEST;
|
|
tmpSubject = Dictionary->GetEntry( 735 );
|
|
Escort = FileLookup->FindEntry( "ESCORTS", msgboard_def );
|
|
if( Escort == nullptr )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
totalEntries = Escort->NumEntries();
|
|
if( totalEntries == 0 )
|
|
{
|
|
Console.error( "MsgBoardPostQuest() No msgboard dfn entries found" );
|
|
return false;
|
|
}
|
|
|
|
Escort->MoveTo( RandomNum( static_cast<size_t>(0), totalEntries-1 ) );
|
|
sect = "ESCORT " + Escort->GrabData();
|
|
EscortData = FileLookup->FindEntry( sect, msgboard_def );
|
|
if( EscortData == nullptr )
|
|
{
|
|
Console.error( strutil::format("MsgBoardPostQuest() Couldn't find entry %s", sect.c_str()) );
|
|
return false;
|
|
}
|
|
for( tag = EscortData->First(); !EscortData->AtEnd(); tag = EscortData->Next() )
|
|
{
|
|
std::string fullLine = tag;
|
|
|
|
size_t position = fullLine.find( "%n" );
|
|
while( position != std::string::npos )
|
|
{
|
|
fullLine.replace( position, 2, mNPC->GetName() );
|
|
position = fullLine.find( "%n" );
|
|
}
|
|
|
|
position = fullLine.find( "%l" );
|
|
while( position != std::string::npos )
|
|
{
|
|
fullLine.replace( position, 2, strutil::format( "%d %d", mNPC->GetX(), mNPC->GetY() ) );
|
|
position = fullLine.find( "%l" );
|
|
}
|
|
|
|
position = fullLine.find( "%t" );
|
|
while( position != std::string::npos )
|
|
{
|
|
std::string mNPCTitle = getNpcDictTitle( mNPC );
|
|
fullLine.replace( position, 2, mNPCTitle );
|
|
position = fullLine.find( "%t" );
|
|
}
|
|
|
|
position = fullLine.find( "%r" );
|
|
while( position != std::string::npos )
|
|
{
|
|
fullLine.replace( position, 2, cwmWorldState->townRegions[mNPC->GetQuestDestRegion()]->GetName() );
|
|
position = fullLine.find( "%r" );
|
|
}
|
|
|
|
position = fullLine.find( "%R" );
|
|
while( position != std::string::npos )
|
|
{
|
|
fullLine.replace( position, 2, mNPC->GetRegion()->GetName() );
|
|
position = fullLine.find( "%R" );
|
|
}
|
|
|
|
fullLine.resize( fullLine.size() + 1 );
|
|
msgBoardPost.msgBoardLine.push_back( fullLine );
|
|
}
|
|
break;
|
|
case QT_BOUNTYQUEST:
|
|
tmpSubject = Dictionary->GetEntry( 736 );
|
|
msgBoardPost.Toggle = QT_BOUNTYQUEST;
|
|
break;
|
|
case QT_ITEMQUEST:
|
|
tmpSubject = Dictionary->GetEntry( 737 );
|
|
msgBoardPost.Toggle = QT_ITEMQUEST;
|
|
break;
|
|
default:
|
|
Console.error( strutil::format("MsgBoardPostQuest() Invalid questType %d", questType) );
|
|
return false;
|
|
}
|
|
|
|
msgBoardPost.Lines = static_cast<UI08>(msgBoardPost.msgBoardLine.size());
|
|
|
|
msgBoardPost.SubjectLen = static_cast<UI08>(tmpSubject.size() + 1);
|
|
strncpy( msgBoardPost.Subject, tmpSubject.c_str(), tmpSubject.size() );
|
|
|
|
msgBoardPost.PosterLen = static_cast<UI08>(mNPC->GetName().size() + 1);
|
|
strncpy( msgBoardPost.Poster, mNPC->GetName().c_str(), mNPC->GetName().size() );
|
|
|
|
msgBoardPost.ParentSerial = mNPC->GetSerial();
|
|
|
|
if( MsgBoardWritePost( msgBoardPost, fileName, PT_REGIONAL ) != INVALIDSERIAL )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardQuestEscortCreate( CChar *mNPC )
|
|
//| Date - 7/23/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Creates an escort quest assigning it a valid escort region
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardQuestEscortCreate( CChar *mNPC )
|
|
{
|
|
const UI16 npcRegion = mNPC->GetRegionNum();
|
|
UI16 destRegion = npcRegion;
|
|
const size_t escortSize = cwmWorldState->escortRegions.size();
|
|
|
|
if( !cwmWorldState->escortRegions.empty() && ( escortSize > 1 || cwmWorldState->escortRegions[0] != npcRegion ) )
|
|
{
|
|
for( UI16 i = 0; i < 50 && npcRegion == destRegion; ++i )
|
|
destRegion = cwmWorldState->escortRegions[RandomNum( static_cast<size_t>(0), (escortSize-1) )];
|
|
}
|
|
|
|
if( destRegion == 0 || destRegion == npcRegion )
|
|
{
|
|
Console.error( strutil::format("MsgBoardQuestEscortCreate() No valid regions defined for escort quests") );
|
|
mNPC->Delete();
|
|
return;
|
|
}
|
|
|
|
mNPC->SetQuestDestRegion( destRegion );
|
|
|
|
// Set quest type to escort
|
|
mNPC->SetQuestType( QT_ESCORTQUEST );
|
|
|
|
// Make sure they don't move until an player accepts the quest
|
|
mNPC->SetNpcWander( WT_NONE );
|
|
mNPC->SetNPCAiType( AI_NONE );
|
|
mNPC->SetQuestOrigRegion( npcRegion );
|
|
|
|
if( cwmWorldState->ServerData()->SystemTimer( tSERVER_ESCORTWAIT ) )
|
|
{
|
|
mNPC->SetTimer( tNPC_SUMMONTIME, cwmWorldState->ServerData()->BuildSystemTimeValue( tSERVER_ESCORTWAIT ) );
|
|
}
|
|
|
|
if( !MsgBoardPostQuest( mNPC, QT_ESCORTQUEST ) )
|
|
{
|
|
Console.error(strutil::format( "MsgBoardQuestEscortCreate() Failed to add quest post for %s", mNPC->GetName().c_str() ));
|
|
mNPC->Delete();
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardQuestEscortArrive( CSocket *mSock, CChar *mNPC )
|
|
//| Date - 7/23/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Handles payment and release upon arrival of the escort
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardQuestEscortArrive( CSocket *mSock, CChar *mNPC )
|
|
{
|
|
CChar *mChar = mSock->CurrcharObj();
|
|
if( !ValidateObject( mChar ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
CTownRegion *destReg = cwmWorldState->townRegions[mNPC->GetQuestDestRegion()];
|
|
|
|
// Calculate payment for services rendered, partly based on escort's fame and partly based on a percentage (25%) of the amount of gold they're carrying
|
|
UI16 questReward = (( mNPC->GetFame() > 0 ? (( mNPC->GetFame() / 100 ) * 50 ) : 0 ) + std::round( GetItemAmount( mNPC, 0x0EED ) * 0.25 ));
|
|
|
|
// If they have no money, well, oops!
|
|
if( questReward == 0 )
|
|
{
|
|
mNPC->TextMessage( mSock, 738, TALK, 0, mChar->GetName().c_str(), destReg->GetName().c_str() );
|
|
}
|
|
else // Otherwise pay the poor sod for his time
|
|
{
|
|
Items->CreateScriptItem( mSock, mChar, "0x0EED", questReward, OT_ITEM, true );
|
|
Effects->goldSound( mSock, questReward );
|
|
mNPC->TextMessage( mSock, 739, TALK, 0, mChar->GetName().c_str(), destReg->GetName().c_str() );
|
|
}
|
|
|
|
// Inform the PC of what he has just been given as payment
|
|
std::string mNPCTitle = getNpcDictTitle( mNPC, mSock );
|
|
mSock->sysmessage( 740, questReward, mNPC->GetName().c_str(), mNPCTitle.c_str() ); // You have just received %d gold coins from %s %s.
|
|
|
|
// Take the NPC out of quest mode
|
|
mNPC->SetNpcWander( WT_FREE ); // Wander freely
|
|
mNPC->SetFTarg( nullptr ); // Reset follow target
|
|
mNPC->SetQuestType( 0 ); // Reset quest type
|
|
mNPC->SetQuestDestRegion( 0 ); // Reset quest destination region
|
|
|
|
// Set a timer to automatically delete the NPC
|
|
mNPC->SetTimer( tNPC_SUMMONTIME, cwmWorldState->ServerData()->BuildSystemTimeValue( tSERVER_ESCORTDONE ) );
|
|
mNPC->SetOwner( nullptr );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardQuestEscortRemovePost( CChar *mNPC )
|
|
//| Date - 7/23/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Removes an escort quest post on regional messageboards
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardQuestEscortRemovePost( CChar *mNPC )
|
|
{
|
|
std::string fileName;
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
fileName = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD );
|
|
}
|
|
|
|
fileName += std::string("region") + strutil::number( mNPC->GetQuestOrigRegion() ) + std::string(".bbf");
|
|
std::size_t fileSize = 0;
|
|
try {
|
|
fileSize = std::filesystem::file_size( fileName );
|
|
} catch( ... ) {
|
|
fileSize = 0;
|
|
}
|
|
|
|
if( fileSize == 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::fstream file;
|
|
file.open( fileName.c_str(), std::ios::in | std::ios::out | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
SERIAL tmpSerial = 0;
|
|
size_t totalSize = 0;
|
|
UI16 tmpSize = 0;
|
|
char rBuffer[4];
|
|
|
|
file.seekg( 0, std::ios::beg );
|
|
while( !file.eof() )
|
|
{
|
|
file.read( rBuffer, 2 );
|
|
tmpSize = ( (rBuffer[0]<<8) + rBuffer[1] );
|
|
|
|
file.seekg( 1, std::ios::cur );
|
|
|
|
file.read( rBuffer, 4 );
|
|
tmpSerial = calcserial( rBuffer[0], rBuffer[1], rBuffer[2], rBuffer[3] );
|
|
if( tmpSerial == mNPC->GetSerial() )
|
|
{
|
|
file.seekp( totalSize + 2, std::ios::beg );
|
|
file.put( 0x00 );
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
totalSize += tmpSize;
|
|
if( totalSize < fileSize )
|
|
{
|
|
file.seekg( totalSize, std::ios::beg );
|
|
}
|
|
else
|
|
{
|
|
Console.error( strutil::format("Attempting to seek past end of file in %s", fileName.c_str()) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
else
|
|
{
|
|
Console.error( strutil::format("Could not open file %s for reading", fileName.c_str()) );
|
|
}
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardRemoveFile( const SERIAL msgBoardSer )
|
|
//| Date - 8/10/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Removes the MessageBoard .bbf file attached to the specified serial
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardRemoveFile( const SERIAL msgBoardSer )
|
|
{
|
|
std::string fileName;
|
|
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
fileName = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD );
|
|
}
|
|
|
|
fileName += strutil::number( msgBoardSer, 16 ) + std::string(".bbf");
|
|
|
|
[[maybe_unused]] int removeResult = remove( fileName.c_str() );
|
|
|
|
Console.print( strutil::format("Deleted MessageBoard file for Board Serial 0x%X\n", msgBoardSer) );
|
|
}
|
|
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Function - void MsgBoardMaintenance( void )
|
|
//| Date - 8/10/2005
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
//| Purpose - Finds all .bbf files and cleans any deleted posts from them
|
|
//| It will also remove any empty .bbf files if necessary.
|
|
//o-----------------------------------------------------------------------------------------------o
|
|
void MsgBoardMaintenance( void )
|
|
{
|
|
std::vector< msgBoardPost_st > mbMessages;
|
|
std::vector< msgBoardPost_st >::const_iterator mbIter;
|
|
|
|
std::string dirName = "./msgboards/";
|
|
if( !cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD ).empty() )
|
|
{
|
|
dirName = cwmWorldState->ServerData()->Directory( CSDDP_MSGBOARD );
|
|
}
|
|
|
|
// Fetch file listing from msgboards directory
|
|
for( auto &entry : std::filesystem::directory_iterator(dirName) )
|
|
{
|
|
if( std::filesystem::is_regular_file(entry.path()) )
|
|
{
|
|
// It is a regular file
|
|
if( entry.path().extension() == ".bbf" )
|
|
{
|
|
// Open file stream for reading in binary mode
|
|
std::ifstream file ( entry.path().string(), std::ios::in | std::ios::binary );
|
|
if( file.is_open() )
|
|
{
|
|
// Seek start of file
|
|
file.seekg( 0, std::ios::beg );
|
|
|
|
// As long as end of file hasn't been reached, continue reading in more posts
|
|
while( !file.eof() )
|
|
{
|
|
msgBoardPost_st toAdd;
|
|
|
|
// If post had Toggle value other than 0x00, add it to the list of posts to write back out to file later
|
|
if( MsgBoardReadPost( file, toAdd ) )
|
|
{
|
|
mbMessages.push_back( toAdd );
|
|
}
|
|
}
|
|
file.close();
|
|
|
|
// If there are no more posts in the file, delete the file
|
|
if( mbMessages.empty() )
|
|
{
|
|
std::filesystem::remove( entry.path() );
|
|
}
|
|
else
|
|
{
|
|
// Open an output file stream for the .bbf file in binary mode
|
|
std::ofstream mFile ( entry.path(), std::ios::out | std::ios::trunc | std::ios::binary );
|
|
|
|
// Write each of the posts we want to keep back into the file
|
|
mFile.seekp( 0, std::ios::beg );
|
|
for( mbIter = mbMessages.begin(); mbIter != mbMessages.end(); ++mbIter )
|
|
{
|
|
MsgBoardWritePost( mFile, (*mbIter) );
|
|
}
|
|
mFile.close();
|
|
}
|
|
|
|
// Clear the message vector to prepare it for next file (if any)
|
|
mbMessages.clear();
|
|
}
|
|
else
|
|
{
|
|
Console.error( strutil::format("Failed to open MessageBoard file for reading: %s", entry.path().string().c_str() ));
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|