//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 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(( 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(( 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( msgIdSer >> 24 ); nextMsgId[1] = static_cast( msgIdSer >> 16 ); nextMsgId[2] = static_cast( msgIdSer >> 8 ); nextMsgId[3] = static_cast( 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( msgBoardPost.size >> 8 ); wBuffer[1] = static_cast( 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( msgBoardPost.parentSerial >> 24 ); wBuffer[1] = static_cast( msgBoardPost.parentSerial >> 16 ); wBuffer[2] = static_cast( msgBoardPost.parentSerial >> 8 ); wBuffer[3] = static_cast( 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( 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( msgBoardPost.serial >> 24 ); wBuffer[1] = static_cast( msgBoardPost.serial >> 16 ); wBuffer[2] = static_cast( msgBoardPost.serial >> 8 ); wBuffer[3] = static_cast( 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( time.size() ); const UI08 posterSize = static_cast( msgBoardPost.posterLen ); const UI08 subjSize = static_cast( msgBoardPost.subjectLen ); auto totalSize = static_cast( 15 + posterSize + subjSize + timeSize ); std::for_each( msgBoardPost.msgBoardLine.begin(), msgBoardPost.msgBoardLine.end(), [&totalSize]( const std::string &entry ) { totalSize += 1 + static_cast( 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( 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 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( 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::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 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( 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( msgBoardPost.msgBoardLine.size() ); msgBoardPost.subjectLen = static_cast( tmpSubject.size() + 1 ); strncopy( msgBoardPost.subject, 256, tmpSubject.c_str(), tmpSubject.size() ); msgBoardPost.posterLen = static_cast( 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 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( 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 mbMessages; std::vector::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() )); } } } } }