uox3/source/msgboard.cpp
charles e2cfe011c6 Stream replacement
This changes all routine calls that wanted a specific type of stream ifstream/ofstream to the generic istream/ostream.  So you can use stringstream, or other derived types.
2023-08-27 21:42:47 -04:00

1251 lines
42 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 "osunique.hpp"
#include <filesystem>
using namespace std::string_literals;
//o------------------------------------------------------------------------------------------------o
//| Function - GetMsgBoardFile()
//| 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" ) + oldstrutil::number( mbRegion->GetRegionNum() ) + std::string( ".bbf" );
break;
case PT_LOCAL:
fileName = oldstrutil::number( msgBoardSer, 16 ) + std::string( ".bbf" );
break;
default:
Console.Error( "GetMsgBoardFile() Invalid post type, aborting" );
break;
}
return fileName;
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardOpen()
//| 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;
[[maybe_unused]] 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 - MsgBoardList()
//| 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( oldstrutil::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( oldstrutil::format( "Failed to list all posts for MessageBoard ID: 0x%X", mSock->GetDWord( 4 )));
}
}
//o------------------------------------------------------------------------------------------------o
//| Function - GetMaxSerial()
//| 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( oldstrutil::format( "Maximum Posts reached for board %s, no further posts can be created", fileName.c_str() ));
return false;
}
return true;
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardWritePost()
//| Date - 8/10/2005
//o------------------------------------------------------------------------------------------------o
//| Purpose - Writes a new post to the .bbf file
//o------------------------------------------------------------------------------------------------o
void MsgBoardWritePost( std::ostream& 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 );
std::for_each( msgBoardPost.msgBoardLine.begin(), msgBoardPost.msgBoardLine.end(), [&mFile]( const std::string &entry )
{
// Write length of line as next byte, and
// then write the line as next X bytes
auto lineSize = static_cast<UI08>( entry.size() );
mFile.write(( const char * )&lineSize, 1 );
mFile.write( entry.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 - MsgBoardWritePost()
//| 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() );
struct tm timeOfPost;
lcltime( timet, timeOfPost );
lcltime( timet, timeOfPost );
auto time = oldstrutil::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 );
auto totalSize = static_cast<UI16>( 15 + posterSize + subjSize + timeSize );
std::for_each( msgBoardPost.msgBoardLine.begin(), msgBoardPost.msgBoardLine.end(), [&totalSize]( const std::string &entry )
{
totalSize += 1 + static_cast<UI16>( entry.size() );
});
msgBoardPost.size = totalSize;
msgBoardPost.dateLen = timeSize;
strncopy( msgBoardPost.date, 256, 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 - MsgBoardPost()
//| 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 ); // You can not reply to global or regional posts.
return;
}
std::string fileName = GetMsgBoardFile( tSock->GetDWord( 4 ), msgType );
if( fileName.empty() )
{
tSock->SysMessage( 725 ); // Invalid post type.
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;
strncopy( msgBoardPost.poster, 128, tChar->GetName().c_str(), msgBoardPost.posterLen );
msgBoardPost.subjectLen = internalBuffer[++offset];
strncopy( msgBoardPost.subject, 256, ( 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 - MsgBoardReadPost()
//| Date - 8/10/2005
//o------------------------------------------------------------------------------------------------o
//| Purpose - Reads in a post from its specified file.
//o------------------------------------------------------------------------------------------------o
bool MsgBoardReadPost( std::istream& 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 std::vector<std::string> 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 - MsgBoardOpenPost()
//| 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 ); // Post not valid, please notify GM!
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( oldstrutil::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( oldstrutil::format( "Failed to find MessageBoard file for MessageID: 0x%X", msgSerial ));
}
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardRemovePost()
//| 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( oldstrutil::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 ); // Post removed.
}
}
// 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( oldstrutil::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 - CPIMsgBoardEvent::Handle()
//| 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 - MsgBoardPostQuest()
//| Date - 7/23/2005
//o------------------------------------------------------------------------------------------------o
//| Purpose - Creates an escort quest post on regional messageboards
//o------------------------------------------------------------------------------------------------o
auto MsgBoardPostQuest( CChar *mNPC, const QuestTypes questType ) -> bool
{
MsgBoardPost_st msgBoardPost;
std::string sect, tag, data;
auto fileName = "region"s + oldstrutil::number( mNPC->GetQuestOrigRegion() ) + ".bbf"s;
CScriptSection *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: Needed!
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 "s + Escort->GrabData();
EscortData = FileLookup->FindEntry( sect, msgboard_def );
if( EscortData == nullptr )
{
Console.Error( oldstrutil::format( "MsgBoardPostQuest() Couldn't find entry %s", sect.c_str() ));
return false;
}
for( const auto &sec : EscortData->collection() )
{
tag = sec->tag;
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, oldstrutil::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 ); // Bounty: Reward!
msgBoardPost.toggle = QT_BOUNTYQUEST;
break;
case QT_ITEMQUEST:
tmpSubject = Dictionary->GetEntry( 737 ); // Lost valuable item.
msgBoardPost.toggle = QT_ITEMQUEST;
break;
default:
Console.Error( oldstrutil::format( "MsgBoardPostQuest() Invalid questType %d", questType ));
return false;
}
msgBoardPost.lines = static_cast<UI08>( msgBoardPost.msgBoardLine.size() );
msgBoardPost.subjectLen = static_cast<UI08>( tmpSubject.size() + 1 );
strncopy( msgBoardPost.subject, 256, tmpSubject.c_str(), tmpSubject.size() );
msgBoardPost.posterLen = static_cast<UI08>( mNPC->GetName().size() + 1 );
strncopy( msgBoardPost.poster, 128, 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 - MsgBoardQuestEscortCreate()
//| 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();
// Loop through all escort regions in the server and pick out all the ones in the same world as NPC
std::vector<UI16> regionCandidates;
for( const auto escortRegion : cwmWorldState->escortRegions )
{
if( escortRegion != npcRegion )
{
CTownRegion *tempRegion = cwmWorldState->townRegions[escortRegion];
if( tempRegion->WorldNumber() == mNPC->WorldNumber() )
{
regionCandidates.push_back( escortRegion );
}
}
}
// Abort out if there are no valid escort region candidates
if( regionCandidates.empty() )
{
Console.Error( oldstrutil::format( "MsgBoardQuestEscortCreate() No valid regions defined for escort quests" ) );
mNPC->Delete();
return;
}
// Choose an escort region at random based on non-empty collection of candidates
mNPC->SetQuestDestRegion( regionCandidates[RandomNum( static_cast<size_t>( 0 ), ( regionCandidates.size() - 1 ))] );
// 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( oldstrutil::format( "MsgBoardQuestEscortCreate() Failed to add quest post for %s", mNPC->GetName().c_str() ));
mNPC->Delete();
}
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardQuestEscortArrive()
//| 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 )
{
// Thank you %s for thy service. We have made it safely to %s. Alas, I seem to be a little short on gold. I have nothing to pay you with.
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 );
// Thank you %s for thy service. We have made it safely to %s. Here is thy pay as promised.
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 );
// Mark NPC as always awake, to ensure they'll still get checked and removed by server when timer is up
mNPC->SetAwake( true );
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardQuestEscortRemovePost()
//| 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" ) + oldstrutil::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( oldstrutil::format( "Attempting to seek past end of file in %s", fileName.c_str() ));
break;
}
}
}
file.close();
}
else
{
Console.Error( oldstrutil::format( "Could not open file %s for reading", fileName.c_str() ));
}
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardRemoveFile()
//| 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 += oldstrutil::number( msgBoardSer, 16 ) + std::string( ".bbf" );
[[maybe_unused]] int removeResult = remove( fileName.c_str() );
Console.Print( oldstrutil::format( "Deleted MessageBoard file for Board Serial 0x%X\n", msgBoardSer ));
}
//o------------------------------------------------------------------------------------------------o
//| Function - MsgBoardMaintenance()
//| 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( oldstrutil::format( "Failed to open MessageBoard file for reading: %s", entry.path().string().c_str() ));
}
}
}
}
}