• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

polserver / polserver / 12834126339

17 Jan 2025 05:43PM UTC coverage: 57.354% (+0.04%) from 57.311%
12834126339

push

github

web-flow
Suspend worldsave script (#745)

* SaveWorldState(): if possible the script will be suspended until the
async part is finished, added to the returning struct
`ElapsedMillisecondsTotal` for the complete time it takes.

* readded removed header

* extended test by calling save without beeing able to suspend

* fixed function name

* do not suspend script during worldsave when its critical
since testscripts are fast make sure via sleep that the gameclock
changes

* docs

47 of 56 new or added lines in 4 files covered. (83.93%)

1 existing line in 1 file now uncovered.

41124 of 71702 relevant lines covered (57.35%)

384088.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.75
/pol-core/pol/savedata.cpp
1
/** @file
2
 *
3
 * @par History
4
 * - 2007/06/17 Shinigami: added config.world_data_path
5
 */
6

7

8
#include "savedata.h"
9

10
#include <cerrno>
11
#include <exception>
12
#include <filesystem>
13
#include <fstream>
14

15
#include "../clib/Debugging/ExceptionParser.h"
16
#include "../clib/Program/ProgramConfig.h"
17
#include "../clib/clib_endian.h"
18
#include "../clib/esignal.h"
19
#include "../clib/fileutil.h"
20
#include "../clib/iohelp.h"
21
#include "../clib/logfacility.h"
22
#include "../clib/rawtypes.h"
23
#include "../clib/streamsaver.h"
24
#include "../clib/strutil.h"
25
#include "../clib/timer.h"
26
#include "../plib/systemstate.h"
27
#include "accounts/accounts.h"
28
#include "globals/object_storage.h"
29
#include "globals/uvars.h"
30
#include "item/item.h"
31
#include "item/itemdesc.h"
32
#include "mobile/charactr.h"
33
#include "mobile/npc.h"
34
#include "multi/house.h"
35
#include "multi/multi.h"
36
#include "objecthash.h"
37
#include "polsem.h"
38
#include "realms/realm.h"
39
#include "regions/resource.h"
40
#include "storage.h"
41
#include "ufunc.h"
42
#include "uobject.h"
43
#include "uworld.h"
44

45
namespace fs = std::filesystem;
46

47
namespace Pol
48
{
49
namespace Module
50
{
51
void commit_datastore();
52
void write_datastore( Clib::StreamWriter& sw );
53
}  // namespace Module
54
namespace Core
55
{
56
void write_party( Clib::StreamWriter& sw );
57
void write_guilds( Clib::StreamWriter& sw );
58

59
std::shared_future<void> SaveContext::finished;
60
std::atomic<gameclock_t> SaveContext::last_worldsave_success = 0;
61

62
SaveContext::SaveContext()
4✔
63
    : _pol(),
4✔
64
      _objects(),
4✔
65
      _pcs(),
4✔
66
      _pcequip(),
4✔
67
      _npcs(),
4✔
68
      _npcequip(),
4✔
69
      _items(),
4✔
70
      _multis(),
4✔
71
      _storage(),
4✔
72
      _resource(),
4✔
73
      _guilds(),
4✔
74
      _datastore(),
4✔
75
      _party(),
4✔
76
      pol( &_pol ),
4✔
77
      objects( &_objects ),
4✔
78
      pcs( &_pcs ),
4✔
79
      pcequip( &_pcequip ),
4✔
80
      npcs( &_npcs ),
4✔
81
      npcequip( &_npcequip ),
4✔
82
      items( &_items ),
4✔
83
      multis( &_multis ),
4✔
84
      storage( &_storage ),
4✔
85
      resource( &_resource ),
4✔
86
      guilds( &_guilds ),
4✔
87
      datastore( &_datastore ),
4✔
88
      party( &_party )
4✔
89
{
90
  pol.init( Plib::systemstate.config.world_data_path + "pol.ndt" );
4✔
91
  objects.init( Plib::systemstate.config.world_data_path + "objects.ndt" );
4✔
92
  pcs.init( Plib::systemstate.config.world_data_path + "pcs.ndt" );
4✔
93
  pcequip.init( Plib::systemstate.config.world_data_path + "pcequip.ndt" );
4✔
94
  npcs.init( Plib::systemstate.config.world_data_path + "npcs.ndt" );
4✔
95
  npcequip.init( Plib::systemstate.config.world_data_path + "npcequip.ndt" );
4✔
96
  items.init( Plib::systemstate.config.world_data_path + "items.ndt" );
4✔
97
  multis.init( Plib::systemstate.config.world_data_path + "multis.ndt" );
4✔
98
  storage.init( Plib::systemstate.config.world_data_path + "storage.ndt" );
4✔
99
  resource.init( Plib::systemstate.config.world_data_path + "resource.ndt" );
4✔
100
  guilds.init( Plib::systemstate.config.world_data_path + "guilds.ndt" );
4✔
101
  datastore.init( Plib::systemstate.config.world_data_path + "datastore.ndt" );
4✔
102
  party.init( Plib::systemstate.config.world_data_path + "parties.ndt" );
4✔
103

104
  pcs.comment( "" );
4✔
105
  pcs.comment( " PCS.TXT: Player-Character Data" );
4✔
106
  pcs.comment( "" );
4✔
107
  pcs.comment( " In addition to PC data, this also contains hair, beards, death shrouds," );
4✔
108
  pcs.comment( " and backpacks, but not the contents of each backpack." );
4✔
109
  pcs.comment( "\n" );
4✔
110

111
  pcequip.comment( "" );
4✔
112
  pcequip.comment( " PCEQUIP.TXT: Player-Character Equipment Data" );
4✔
113
  pcequip.comment( "" );
4✔
114
  pcequip.comment( " This file can be deleted to wipe all items held/equipped by characters" );
4✔
115
  pcequip.comment( " Note that hair, beards, empty backpacks, and death shrouds are in PCS.TXT." );
4✔
116
  pcequip.comment( "\n" );
4✔
117

118
  npcs.comment( "" );
4✔
119
  npcs.comment( " NPCS.TXT: Nonplayer-Character Data" );
4✔
120
  npcs.comment( "" );
4✔
121
  npcs.comment( " If you delete this file to perform an NPC wipe," );
4✔
122
  npcs.comment( " be sure to also delete NPCEQUIP.TXT" );
4✔
123
  npcs.comment( "\n" );
4✔
124

125
  npcequip.comment( "" );
4✔
126
  npcequip.comment( " NPCEQUIP.TXT: Nonplayer-Character Equipment Data" );
4✔
127
  npcequip.comment( "" );
4✔
128
  npcequip.comment( " Delete this file along with NPCS.TXT to perform an NPC wipe" );
4✔
129

130
  npcequip.comment( "\n" );
4✔
131

132
  items.comment( "" );
4✔
133
  items.comment( " ITEMS.TXT: Item data" );
4✔
134
  items.comment( "" );
4✔
135
  items.comment( " This file also contains ship and house components (doors, planks etc)" );
4✔
136
  items.comment( "\n" );
4✔
137

138
  multis.comment( "" );
4✔
139
  multis.comment( " MULTIS.TXT: Ship and House data" );
4✔
140
  multis.comment( "" );
4✔
141
  multis.comment( " Deleting this file will not properly wipe houses and ships," );
4✔
142
  multis.comment( " because doors, planks, and tillermen will be left in the world." );
4✔
143
  multis.comment( "\n" );
4✔
144

145
  storage.comment( "" );
4✔
146
  storage.comment( " STORAGE.TXT: Contains bank boxes, vendor inventories, and other data." );
4✔
147
  storage.comment( "" );
4✔
148
  storage.comment( " This file can safely be deleted to wipe bank boxes and vendor inventories." );
4✔
149
  storage.comment( " Note that scripts may use this for other types of storage as well" );
4✔
150
  storage.comment( "\n" );
4✔
151

152
  resource.comment( "" );
4✔
153
  resource.comment( " RESOURCE.TXT: Resource System Data" );
4✔
154
  resource.comment( "\n" );
4✔
155

156
  guilds.comment( "" );
4✔
157
  guilds.comment( " GUILDS.TXT: Guild Data" );
4✔
158
  guilds.comment( "\n" );
4✔
159

160
  datastore.comment( "" );
4✔
161
  datastore.comment( " DATASTORE.TXT: DataStore Data" );
4✔
162
  datastore.comment( "\n" );
4✔
163

164
  party.comment( "" );
4✔
165
  party.comment( " PARTIES.TXT: Party Data" );
4✔
166
  party.comment( "\n" );
4✔
167
}
4✔
168

169
SaveContext::~SaveContext() noexcept( false )
4✔
170
{
171
  auto stack_unwinding = std::uncaught_exceptions();
4✔
172
  try
173
  {
174
    pol.flush_file();
4✔
175
    objects.flush_file();
4✔
176
    pcs.flush_file();
4✔
177
    pcequip.flush_file();
4✔
178
    npcs.flush_file();
4✔
179
    npcequip.flush_file();
4✔
180
    items.flush_file();
4✔
181
    multis.flush_file();
4✔
182
    storage.flush_file();
4✔
183
    resource.flush_file();
4✔
184
    guilds.flush_file();
4✔
185
    datastore.flush_file();
4✔
186
    party.flush_file();
4✔
187
  }
188
  catch ( ... )
×
189
  {
190
    // during stack unwinding an exception would terminate
191
    if ( !stack_unwinding )
×
192
      throw;
×
193
  }
×
194
}
4✔
195

196
/// blocks till possible last commit finishes
197
void SaveContext::ready()
6✔
198
{
199
  if ( SaveContext::finished.valid() )
6✔
200
  {
201
    // Tools::Timer<Tools::DebugT> t("future");
202
    SaveContext::finished.wait();
4✔
203
  }
204
}
6✔
205

206
void write_system_data( Clib::StreamWriter& sw )
4✔
207
{
208
  sw.begin( "System" );
4✔
209
  sw.add( "CoreVersion", POL_VERSION_STR );
4✔
210
  sw.add( "CoreVersionString", POL_VERSION_STR );
4✔
211
  sw.add( "CompileDateTime", Clib::ProgramConfig::build_datetime() );
4✔
212
  sw.add( "LastItemSerialNumber", GetCurrentItemSerialNumber() );
4✔
213
  sw.add( "LastCharSerialNumber", GetCurrentCharSerialNumber() );
4✔
214
  sw.end();
4✔
215
}
4✔
216

217
void write_global_properties( Clib::StreamWriter& sw )
4✔
218
{
219
  sw.begin( "GlobalProperties" );
4✔
220
  gamestate.global_properties->printProperties( sw );
4✔
221
  sw.end();
4✔
222
}
4✔
223

224
void write_realms( Clib::StreamWriter& sw )
4✔
225
{
226
  for ( const auto& realm : gamestate.Realms )
14✔
227
  {
228
    sw.begin( "Realm" );
10✔
229
    if ( !realm->is_shadowrealm )
10✔
230
    {
231
      sw.add( "Name", realm->name() );
8✔
232
    }
233
    else
234
    {
235
      sw.add( "Name", realm->shadowname );
2✔
236
      sw.add( "BaseRealm", realm->baserealm->name() );
2✔
237
    }
238
    sw.add( "HasDecay", realm->has_decay );
10✔
239
    sw.end();
10✔
240
  }
241
}
4✔
242

243
// Austin (Oct. 17, 2006)
244
// Added to handle gotten item saving.
245
void WriteGottenItem( Mobile::Character* chr, Items::Item* item, Clib::StreamWriter& sw )
×
246
{
247
  if ( item == nullptr || item->orphan() )
×
248
    return;
×
249
  // For now, it just saves the item in items.txt
250
  item->setposition( chr->pos() );
×
251

252
  item->printOn( sw );
×
253

254
  item->setposition(
×
255
      Pos4d( 0, 0, 0,
256
             item->realm() ) );  // TODO POS position should have no meaning remove this completely
257
}
258

259
void write_characters( Core::SaveContext& sc )
4✔
260
{
261
  for ( const auto& objitr : objStorageManager.objecthash )
292✔
262
  {
263
    UObject* obj = objitr.second.get();
288✔
264
    if ( obj->ismobile() && !obj->orphan() )
288✔
265
    {
266
      Mobile::Character* chr = static_cast<Mobile::Character*>( obj );
23✔
267
      if ( !chr->isa( UOBJ_CLASS::CLASS_NPC ) )
23✔
268
      {
269
        chr->printOn( sc.pcs );
18✔
270
        chr->clear_dirty();
18✔
271
        chr->printWornItems( sc.pcs, sc.pcequip );
18✔
272
      }
273
    }
274
  }
275
}
4✔
276

277
void write_npcs( Core::SaveContext& sc )
4✔
278
{
279
  for ( const auto& objitr : objStorageManager.objecthash )
292✔
280
  {
281
    UObject* obj = objitr.second.get();
288✔
282
    if ( obj->ismobile() && !obj->orphan() )
288✔
283
    {
284
      Mobile::Character* chr = static_cast<Mobile::Character*>( obj );
23✔
285
      if ( chr->isa( UOBJ_CLASS::CLASS_NPC ) )
23✔
286
      {
287
        if ( chr->saveonexit() )
5✔
288
        {
289
          chr->printOn( sc.npcs );
4✔
290
          chr->clear_dirty();
4✔
291
          chr->printWornItems( sc.npcs, sc.npcequip );
4✔
292
        }
293
      }
294
    }
295
  }
296
}
4✔
297

298
void write_items( Clib::StreamWriter& sw_items )
4✔
299
{
300
  for ( const auto& realm : gamestate.Realms )
14✔
301
  {
302
    for ( const auto& p : realm->gridarea() )
7,582✔
303
    {
304
      for ( const auto& item : realm->getzone_grid( p ).items )
3,837✔
305
      {
306
        if ( item->itemdesc().save_on_exit && item->saveonexit() )
51✔
307
        {
308
          item->printOn( sw_items );
50✔
309
          item->clear_dirty();
50✔
310
        }
311
      }
312
    }
313
  }
314

315
  for ( const auto& objitr : objStorageManager.objecthash )
292✔
316
  {
317
    UObject* obj = objitr.second.get();
288✔
318
    if ( obj->ismobile() && !obj->orphan() )
288✔
319
    {
320
      Mobile::Character* chr = static_cast<Mobile::Character*>( obj );
23✔
321
      if ( !chr->isa( UOBJ_CLASS::CLASS_NPC ) )
23✔
322
      {
323
        // Figure out where to save the 'gotten item' - Austin (Oct. 17, 2006)
324
        if ( chr->has_gotten_item() )
18✔
325
          WriteGottenItem( chr, chr->gotten_item().item(), sw_items );
×
326
      }
327
    }
328
  }
329
}
4✔
330

331
void write_multis( Clib::StreamWriter& ofs )
4✔
332
{
333
  for ( const auto& realm : gamestate.Realms )
14✔
334
  {
335
    for ( const auto& p : realm->gridarea() )
7,582✔
336
    {
337
      for ( auto& multi : realm->getzone_grid( p ).multis )
3,792✔
338
      {
339
        if ( Clib::exit_signalled )  // drop waiting commit on shutdown
6✔
340
        {
341
          Multi::UHouse* house = multi->as_house();
6✔
342
          if ( house != nullptr )
6✔
343
          {
344
            if ( house->IsCustom() )
4✔
345
            {
346
              if ( house->IsWaitingForAccept() )
2✔
347
                house->AcceptHouseCommit( nullptr, false );
×
348
            }
349
          }
350
        }
351
        multi->printOn( ofs );
6✔
352
        multi->clear_dirty();
6✔
353
      }
354
    }
355
  }
356
}
4✔
357
bool should_write_data()
6✔
358
{
359
  if ( Plib::systemstate.config.inhibit_saves )
6✔
360
    return false;
×
361
  if ( Clib::passert_shutdown_due_to_assertion && Clib::passert_nosave )
6✔
362
    return false;
×
363

364
  return true;
6✔
365
}
366

367
bool commit( const std::string& basename )
52✔
368
{
369
  auto bakfile = fs::path( Plib::systemstate.config.world_data_path ) / ( basename + ".bak" );
52✔
370
  auto datfile = fs::path( Plib::systemstate.config.world_data_path ) / ( basename + ".txt" );
52✔
371
  auto ndtfile = fs::path( Plib::systemstate.config.world_data_path ) / ( basename + ".ndt" );
52✔
372

373
  try
374
  {
375
    fs::remove( bakfile );  // does not throw if not existing
52✔
376
    if ( fs::exists( datfile ) )
52✔
377
      fs::rename( datfile, bakfile );
40✔
378
    if ( fs::exists( ndtfile ) )
52✔
379
      fs::rename( ndtfile, datfile );
52✔
380
  }
381
  catch ( const fs::filesystem_error& error )
×
382
  {
383
    POLLOG_ERRORLN( "Unable to commit worldsave: {}\n{}", error.what(),
×
384
                    Clib::ExceptionParser::getTrace() );
×
385
    return false;
×
386
  }
×
387
  return true;
52✔
388
}
52✔
389

390
std::optional<bool> write_data( std::function<void( bool, u32, u32, s64 )> callback,
4✔
391
                                u32* dirty_writes, u32* clean_writes, s64* elapsed_ms )
392
{
393
  SaveContext::ready();  // allow only one active
4✔
394
  if ( !should_write_data() )
4✔
UNCOV
395
    return {};
×
396

397
  UObject::dirty_writes = 0;
4✔
398
  UObject::clean_writes = 0;
4✔
399

400
  Tools::Timer<> timer;
4✔
401
  // launch complete save as seperate thread
402
  // but wait till the first critical part is finished
403
  // which means all objects got written into a format object
404
  // the remaining operations are only pure buffered i/o
405
  auto critical_promise = std::promise<bool>();
4✔
406
  auto critical_future = critical_promise.get_future();
4✔
407
  auto set_promise = []( auto& promise, bool result )
4✔
408
  {
409
    try  // guard to be able to try to set it twice (exceptions)
410
    {
411
      promise.set_value( result );
4✔
412
    }
413
    catch ( ... )
×
414
    {
415
    }
416
  };
4✔
417
  SaveContext::finished = std::async(
4✔
418
      std::launch::async,
419
      [&, critical_promise = std::move( critical_promise ),
12✔
420
       callback = std::move( callback )]() mutable
4✔
421
      {
422
        Tools::Timer<> blocking_timer;
4✔
423
        std::atomic<bool> result( true );
4✔
424
        try
425
        {
426
          SaveContext sc;
4✔
427
          std::vector<std::future<bool>> critical_parts;
4✔
428
          auto save = [&]( auto func, std::string name )
44✔
429
          {
430
            critical_parts.push_back( gamestate.task_thread_pool.checked_push(
44✔
431
                [&, name, func = std::move( func )]() mutable
132✔
432
                {
433
                  try
434
                  {
435
                    func();
44✔
436
                  }
437
                  catch ( ... )
×
438
                  {
439
                    POLLOG_ERRORLN( "failed to store {} datafile!\n{}", name,
×
440
                                    Clib::ExceptionParser::getTrace() );
441
                    result = false;
44✔
442
                  }
443
                } ) );
444
          };
48✔
445

446
          save(
4✔
447
              [&]()
4✔
448
              {
449
                sc.pol.comment( "" );
4✔
450
                sc.pol.comment( " Created by Version: {}", POL_VERSION_ID );
4✔
451
                sc.pol.comment( " Mobiles: {}", get_mobile_count() );
4✔
452
                sc.pol.comment( " Top-level Items: {}", get_toplevel_item_count() );
4✔
453
                sc.pol.comment( "\n" );
4✔
454

455
                write_system_data( sc.pol );
4✔
456
                write_global_properties( sc.pol );
4✔
457
                write_realms( sc.pol );
4✔
458
              },
4✔
459
              "pol" );
460
          save( [&]() { write_items( sc.items ); }, "items" );
8✔
461
          save( [&]() { write_characters( sc ); }, "character" );
8✔
462
          save( [&]() { write_npcs( sc ); }, "npcs" );
8✔
463
          save( [&]() { write_multis( sc.multis ); }, "multis" );
8✔
464
          save( [&]() { gamestate.storage.print( sc.storage ); }, "storage" );
8✔
465
          save( [&]() { write_resources_dat( sc.resource ); }, "resource" );
8✔
466
          save( [&]() { write_guilds( sc.guilds ); }, "guilds" );
8✔
467
          save(
4✔
468
              [&]()
4✔
469
              {
470
                Module::write_datastore( sc.datastore );
4✔
471
                // Atomically (hopefully) perform the switch.
472
                Module::commit_datastore();
4✔
473
              },
4✔
474
              "datastore" );
475
          save( [&]() { write_party( sc.party ); }, "party" );
8✔
476
          save(
4✔
477
              [&]()
4✔
478
              {
479
                if ( Plib::systemstate.accounts_txt_dirty )
4✔
480
                  Accounts::write_account_data();
×
481
              },
4✔
482
              "accounts" );
483

484
          for ( auto& task : critical_parts )
48✔
485
            task.wait();
44✔
486

487
          set_promise( critical_promise, result );  // critical part end
4✔
488
        }  // deconstructor of the SaveContext flushes and joins the queues
4✔
489
        catch ( std::ios_base::failure& e )
×
490
        {
491
          POLLOG_ERRORLN( "failed to save datafiles! {}:{}\n{}", e.what(), std::strerror( errno ),
×
492
                          Clib::ExceptionParser::getTrace() );
×
493

494
          result = false;
×
495
          set_promise( critical_promise, result );
×
496
        }
×
497
        catch ( ... )
×
498
        {
499
          POLLOG_ERRORLN( "failed to save datafiles!\n{}", Clib::ExceptionParser::getTrace() );
×
500
          result = false;
×
501
          set_promise( critical_promise, result );
×
502
        }
×
503
        if ( result )
4✔
504
        {
505
          auto files = { "pol",      "objects",   "pcs",    "pcequip", "npcs",
4✔
506
                         "npcequip", "items",     "multis", "storage", "resource",
507
                         "guilds",   "datastore", "parties" };
4✔
508
          result =
4✔
509
              std::all_of( files.begin(), files.end(), []( auto file ) { return commit( file ); } );
56✔
510
          if ( result )
4✔
511
            SaveContext::last_worldsave_success = read_gameclock();
4✔
512
        }
513
        if ( callback )
4✔
514
          callback( result.load(), UObject::clean_writes, UObject::dirty_writes,
1✔
515
                    blocking_timer.ellapsed() );
1✔
516
      } );
8✔
517
  auto res = critical_future.get();  // wait for end of critical part
4✔
518

519
  objStorageManager.objecthash.ClearDeleted();
4✔
520

521
  if ( clean_writes )
4✔
522
    *clean_writes = UObject::clean_writes;
1✔
523
  if ( dirty_writes )
4✔
524
    *dirty_writes = UObject::dirty_writes;
1✔
525
  if ( elapsed_ms )
4✔
526
    *elapsed_ms = timer.ellapsed();
3✔
527
  return res;
4✔
528
}
4✔
529

530
}  // namespace Core
531
}  // namespace Pol
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc