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

polserver / polserver / 29720407616

20 Jul 2026 05:59AM UTC coverage: 61.394% (-0.03%) from 61.419%
29720407616

push

github

web-flow
Improve uoconvert correctness and speed (#897)

* fix old Tokuno map issue

* quick fixes

* restructure mapwriter to store files in memory until a write is needed

* add dirty flag to only flush files which changed

* change uoconvert use of mapwriter to a stack variable

* added detailed timers to uoconvert map

* precalculate lowest adjacent z so we dont need to calculate again for each tile

* syntax formatting

* read landtiles directly instead of using safe_getmapinfo

* encapsulate the solid block processing result for a single block

* make processing parallel - 5 sec to 2,7 sec for britannia

* add changelog entry

* reduce redundant per-tile work in the solid-block loop

* bucket each static block once instead of rescanning it per tile

readstatics_block() presorts a block's statics by cell in a single pass
(storage order preserved); each tile's bucket then serves directly as the
per-tile scratch, dropping the 64x whole-block rescan and the per-tile copy.
Outputs byte-identical (SHA256 gate, map+maptile, both realms).

* extract merge_shapes and flatten the water-type lookup

- the shape-consolidation loop moves verbatim into merge_shapes(), with
  its invariants and comments; ComputeSolidBlock now reads as
  collect -> filter -> sort -> merge -> emit
- DiscardedWaterTypes is snapshotted into a flat per-graphic table after
  config load; the hot per-static probe drops the tree lookup
Outputs byte-identical (SHA256 gate, map+maptile, both realms).

* name the conversion's magic numbers

MAX_LANDTILE_ID joins LANDTILE_COUNT in plib; wall height, water-discard
window and sand-over-water gap become named constexprs. Landtile classifiers become constexpr in
terrainplane.h with their id lists sourced. Outputs byte-identical.

* unify the block-index math into one helper per layout

realm_block_index (8x8 row-major: base.dat/solidx1/statics.dat),
maptile_index (64x64, rounded-up stride), and staticblock_from_coords
(client mul column-major, reused ... (continued)

513 of 641 new or added lines in 22 files covered. (80.03%)

47 existing lines in 8 files now uncovered.

45116 of 73486 relevant lines covered (61.39%)

516637.68 hits per line

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

69.92
/pol-core/uoconvert/UoConvertMain.cpp
1
#include "UoConvertMain.h"
2

3
#include <algorithm>
4
#include <chrono>
5
#include <cstring>
6
#include <stdexcept>
7
#include <stdio.h>
8
#include <string.h>
9
#include <string>
10
#include <vector>
11

12
#include "clib/Program/ProgramMain.h"
13
#include "clib/cfgelem.h"
14
#include "clib/cfgfile.h"
15
#include "clib/fileutil.h"
16
#include "clib/logfacility.h"
17
#include "clib/passert.h"
18
#include "clib/rawtypes.h"
19
#include "clib/stlutil.h"
20
#include "clib/timer.h"
21
#include "plib/clidata.h"
22
#include "plib/mapcell.h"
23
#include "plib/mapfunc.h"
24
#include "plib/mapshape.h"
25
#include "plib/mapsolid.h"
26
#include "plib/maptile.h"
27
#include "plib/mapwriter.h"
28
#include "plib/mul/map.h"
29
#include "plib/objtype.h"
30
#include "plib/polfile.h"
31
#include "plib/realmdescriptor.h"
32
#include "plib/systemstate.h"
33
#include "plib/udatfile.h"
34
#include "plib/uofile.h"
35
#include "plib/uofilei.h"
36
#include "plib/uoinstallfinder.h"
37
#include "plib/uopreader/uop.h"
38
#include "plib/uopreader/uophash.h"
39
#include "plib/ustruct.h"
40
#include "pol/landtile.h"
41

42
#include "parallel.h"
43
#include "terrainplane.h"
44

45

46
namespace Pol::UoConvert
47
{
48
using namespace std;
49
using namespace Pol::Core;
50
using namespace Pol::Plib;
51

52
namespace
53
{
54
// RAII owner for the C FILE* handles used by the .cfg writers below, so an early
55
// return or a throw closes the file instead of leaking it. Implicitly converts
56
// to FILE*, so the fprintf-based writer code reads exactly as before.
57
class UniqueFile
58
{
59
public:
60
  explicit UniqueFile( FILE* fp ) : fp_( fp ) {}
5✔
61
  ~UniqueFile()
5✔
62
  {
63
    if ( fp_ )
5✔
64
      fclose( fp_ );
5✔
65
  }
5✔
66
  UniqueFile( const UniqueFile& ) = delete;
67
  UniqueFile& operator=( const UniqueFile& ) = delete;
68

69
  operator FILE*() const { return fp_; }
1,087✔
70

71
private:
72
  FILE* fp_;
73
};
74

75
// Open a text file for writing, throwing with the path on failure (the old code
76
// left the FILE* unchecked and would crash on the first fprintf).
77
UniqueFile open_out_text( const std::string& path )
3✔
78
{
79
  FILE* fp = fopen( path.c_str(), "wt" );
3✔
80
  if ( !fp )
3✔
NEW
81
    throw std::runtime_error( "Unable to open output file for writing: " + path );
×
82
  return UniqueFile( fp );
3✔
83
}
84
}  // namespace
85

86
///////////////////////////////////////////////////////////////////////////////
87

88

89
///////////////////////////////////////////////////////////////////////////////
90

91
UoConvertMain::UoConvertMain()
11✔
92
    : Pol::Clib::ProgramMain(), cfg_use_no_shoot( false ), cfg_LOS_through_windows( false )
11✔
93
{
94
}
11✔
95
UoConvertMain::~UoConvertMain() = default;
×
96
///////////////////////////////////////////////////////////////////////////////
97

98
void UoConvertMain::showHelp()
×
99
{
100
  ERROR_PRINTLN( std::string{
×
101
      "Usage:\n"
102
      "    \n"
103
      "  UOCONVERT command [options ...]\n"
104
      "    \n"
105
      "  Commands: \n"
106
      "    map {uodata=Dir} {realm=realmname} {width=Width}        {height=Height} {mapid=0} "
107
      "{readuop=1} {x=X} {y=Y} {profile=0} {threads=0}\n"
108
      "    statics {uodata=Dir} {realm=realmname}\n"
109
      "    maptile {uodata=Dir} {realm=realmname}\n"
110
      "    multis {uodata=Dir} {outdir=dir}\n"
111
      "    tiles {uodata=Dir} {outdir=dir}\n"
112
      "    landtiles {uodata=Dir} {outdir=dir}" } );
113
}
×
114

115
using namespace Core;
116
using namespace Plib;
117

118
// A WALL-flagged landtile is treated as a solid of this height, unless a static
119
// sitting on it caps it lower (see the wall override in ComputeSolidBlock).
120
constexpr short WALL_LANDTILE_HEIGHT = 20;
121

122
// A discarded-water static replaces the map base only when it lies at or above
123
// the terrain top by at most this many z (statics further up are unrelated water).
124
constexpr short WATER_DISCARD_Z_WINDOW = 10;
125

126
// "Sand over water": a shape starting at most this far above a water shape is
127
// extended downward to meet it (see merge_shapes).
128
constexpr short SAND_OVER_WATER_MAX_GAP = 4;
129

UNCOV
130
void UoConvertMain::display_flags()
×
131
{
132
  for ( unsigned blocking = 0; blocking <= 1; ++blocking )
×
133
  {
134
    for ( unsigned platform = 0; platform <= 1; ++platform )
×
135
    {
136
      for ( unsigned walk = 0; walk <= 1; ++walk )
×
137
      {
138
        for ( unsigned wall = 0; wall <= 1; ++wall )
×
139
        {
140
          for ( unsigned half = 0; half <= 1; ++half )
×
141
          {
142
            for ( unsigned floor = 0; floor <= 1; ++floor )
×
143
            {
144
              unsigned flags = 0;
×
145
              if ( blocking )
×
146
                flags |= USTRUCT_TILE::FLAG_BLOCKING;
×
147
              if ( platform )
×
148
                flags |= USTRUCT_TILE::FLAG_PLATFORM;
×
149
              if ( walk )
×
150
                flags |= USTRUCT_TILE::FLAG__WALK;
×
151
              if ( wall )
×
152
                flags |= USTRUCT_TILE::FLAG_WALL;
×
153
              if ( half )
×
154
                flags |= USTRUCT_TILE::FLAG_HALF_HEIGHT;
×
155
              if ( floor )
×
156
                flags |= USTRUCT_TILE::FLAG_FLOOR;
×
157

158
              unsigned int polflags = Plib::polflags_from_tileflags(
×
159
                  0x4000, flags, cfg_use_no_shoot, cfg_LOS_through_windows );
×
160
              unsigned moveland = ( polflags & Plib::FLAG::MOVELAND ) ? 1 : 0;
×
161
              INFO_PRINTLN( "{} {} {} {} {} {}: {}", blocking, platform, walk, wall, half, floor,
×
162
                            moveland );
163
            }
164
          }
165
        }
166
      }
167
    }
168
  }
169
}
×
170

171
void UoConvertMain::create_maptile( const std::string& realmname )
2✔
172
{
173
  Plib::RealmDescriptor descriptor = Plib::RealmDescriptor::Load( realmname );
2✔
174
  uo_map_height = static_cast<unsigned short>( descriptor.height );
2✔
175
  uo_map_width = static_cast<unsigned short>( descriptor.width );
2✔
176

177
  INFO_PRINTLN(
2✔
178
      "Creating maptile file.\n"
179
      "  Realm: {}\n"
180
      "  Map ID: {}\n"
181
      "  Use Dif files: {}\n"
182
      "  Size: {}x{}",
183
      realmname, descriptor.uomapid, ( descriptor.uodif ? "Yes" : "No" ), uo_map_width,
2✔
184
      uo_map_height );
185

186
  Plib::MapWriter writer;
2✔
187
  writer.OpenExistingFiles( realmname );
2✔
188

189
  // Same precomputed terrain as create_map; maptile only needs the landtile id and the
190
  // effective (liquid-overridden) z per tile, so skip the lowest-adjacent-z pass.
191
  rawmapfullread();
2✔
192
  TerrainPlane plane;
2✔
193
  plane.build( uo_map_width, uo_map_height, /*need_low_z=*/false, cfg_threads );
2✔
194

195
  // Plain row-major sweep; the old 64x64 blocked iteration only existed to suit the
196
  // former one-block writer cache, which got replaced with in-memory buffers.
197
  for ( unsigned short y = 0; y < uo_map_height; ++y )
1,794✔
198
  {
199
    if ( y % Plib::MAPTILE_CHUNK == 0 )
1,792✔
200
      INFO_PRINT( "\rConverting: {}%", y * 100 / uo_map_height );
28✔
201
    for ( unsigned short x = 0; x < uo_map_width; ++x )
2,598,656✔
202
    {
203
      const std::size_t plane_idx = plane.index( x, y );
2,596,864✔
204
      const u16 landtile = plane.landtile[plane_idx];
2,596,864✔
205
      const s8 z = plane.eff_z[plane_idx];  // effective z (liquid override applied)
2,596,864✔
206

207
      if ( landtile > Plib::MAX_LANDTILE_ID )
2,596,864✔
NEW
208
        INFO_PRINTLN( "Tile {:#x} at ({},{},{}) is an invalid ID!", landtile, x, y, z );
×
209

210
      Plib::MAPTILE_CELL cell;
211
      cell.landtile = landtile;
2,596,864✔
212
      cell.z = z;
2,596,864✔
213
      writer.SetMapTile( x, y, cell );
2,596,864✔
214
    }
215
  }
216
  writer.Flush();
2✔
217
  INFO_PRINTLN( "\rConversion complete." );
2✔
218
}
2✔
219

220
class StaticsByZ
221
{
222
public:
223
  bool operator()( const StaticRec& a, const StaticRec& b ) const
496✔
224
  {
225
    return ( a.z < b.z ) || ( ( a.z == b.z && a.height < b.height ) );
496✔
226
  }
227
};
228

229

230
constexpr bool flags_match( unsigned int f1, unsigned int f2, unsigned char bits_compare )
48✔
231
{
232
  return ( f1 & bits_compare ) == ( f2 & bits_compare );
48✔
233
}
234

UNCOV
235
void UoConvertMain::update_map( const std::string& realm, unsigned short x, unsigned short y )
×
236
{
NEW
237
  MapWriter mapwriter;
×
NEW
238
  mapwriter.OpenExistingFiles( realm );
×
239
  rawmapfullread();
×
240
  rawstaticfullread();
×
241
  unsigned short x_base = x / SOLIDX_X_SIZE * SOLIDX_X_SIZE;
×
242
  unsigned short y_base = y / SOLIDX_Y_SIZE * SOLIDX_Y_SIZE;
×
243

244
  // ProcessSolidBlock reads the smoothed-terrain plane; build the full plane. This is a
245
  // rarely-used single-block debug path (x=/y= args), so the full build is acceptable.
NEW
246
  TerrainPlane plane;
×
NEW
247
  plane.build( uo_map_width, uo_map_height, /*need_low_z=*/true, cfg_threads );
×
248

NEW
249
  BlockResult result;
×
NEW
250
  ComputeSolidBlock( x_base, y_base, plane, result );
×
NEW
251
  StitchBlock( mapwriter, x_base, y_base, result );
×
252
  INFO_PRINTLN( "empty={}, nonempty={}\nwith more_solids: {}\ntotal statics={}", empty, nonempty,
×
253
                with_more_solids, total_statics );
×
254
}
×
255

256
void UoConvertMain::create_map( const std::string& realm, unsigned short width,
4✔
257
                                unsigned short height )
258
{
259
  MapWriter mapwriter;
4✔
260
  INFO_PRINT(
4✔
261
      "Creating map base and solids files.\n"
262
      "  Realm: {}\n"
263
      "  Map ID: {}\n"
264
      "  Reading UOP file: {}\n"
265
      "  Use Dif files: {}\n"
266
      "  Size: {}x{}\n"
267
      "Initializing files: ",
268
      realm, uo_mapid, ( uo_readuop ? "Yes" : "No" ), ( uo_usedif ? "Yes" : "No" ), uo_map_width,
4✔
269
      uo_map_height );
270
  // Reset the opt-in per-tile profiling accumulators for this run.
271
  prof_mapinfo_ns = prof_statics_ns = prof_shape_ns = prof_writer_ns = 0;
4✔
272

273
  // Coarse, always-on phase timers so a plain run shows where the wall-clock goes
274
  // (file init vs map read vs static read vs the hot loop vs the final flush).
275
  Tools::Timer<> init_timer;
4✔
276
  mapwriter.CreateNewFiles( realm, width, height );
4✔
277
  init_timer.stop();
4✔
278
  INFO_PRINTLN( "Done." );
4✔
279

280
  Tools::Timer<> mapread_timer;
4✔
281
  rawmapfullread();
4✔
282
  mapread_timer.stop();
4✔
283

284
  Tools::Timer<> staticread_timer;
4✔
285
  rawstaticfullread();
4✔
286
  staticread_timer.stop();
4✔
287

288
  // Precompute the smoothed-terrain plane once (safe_getmapinfo + lowest-adjacent-z for
289
  // every tile) so the block loop below is flat array reads instead of ~36 raw cell
290
  // fetches per tile. The plane is immutable during the loop.
291
  Tools::Timer<> plane_timer;
4✔
292
  TerrainPlane plane;
4✔
293
  plane.build( width, height, /*need_low_z=*/true, cfg_threads );
4✔
294
  plane_timer.stop();
4✔
295

296
  // ComputeSolidBlock runs concurrently below and calls getstaticblock / the raw-map
297
  // readers, whose lazy first-touch init (rawstaticfullread / rawmapfullread) would race if
298
  // it fired inside the parallel region. Both were already forced above; assert it so a
299
  // future reorder trips here instead of racing.
300
  passert_always( Plib::rawmap_loaded() );
4✔
301
  passert_always( Plib::rawstatics_loaded() );
4✔
302

303
  // Phase A/B: compute every block in parallel over contiguous block-row bands (each
304
  // ComputeSolidBlock is pure and writes only its own BlockResult -- no shared state), in a
305
  // single parallel region (threads spawned once). Then stitch all blocks into the writer
306
  // serially in the fixed y-outer/x-inner order so the solids/solidx2 append offsets, and thus
307
  // every output byte, stay identical to a serial run. Profiling forces the compute serial:
308
  // per-tile timings are meaningless summed across overlapping threads. threads=0 =>
309
  // hardware_concurrency.
310
  const unsigned block_threads = cfg_profile ? 1u : cfg_threads;
4✔
311
  const std::size_t blocks_per_row =
4✔
312
      ( static_cast<std::size_t>( width ) + SOLIDX_X_SIZE - 1 ) / SOLIDX_X_SIZE;
4✔
313
  const std::size_t num_block_rows =
4✔
314
      ( static_cast<std::size_t>( height ) + SOLIDX_Y_SIZE - 1 ) / SOLIDX_Y_SIZE;
4✔
315
  std::vector<BlockResult> results( blocks_per_row * num_block_rows );
4✔
316

317
  Tools::Timer<> loop_timer;
4✔
318
  parallel_for(
4✔
319
      num_block_rows,
320
      [&]( std::size_t yr )
4✔
321
      {
322
        const unsigned short y_base = static_cast<unsigned short>( yr * SOLIDX_Y_SIZE );
1,248✔
323
        for ( std::size_t xi = 0; xi < blocks_per_row; ++xi )
131,852✔
324
        {
325
          const unsigned short x_base = static_cast<unsigned short>( xi * SOLIDX_X_SIZE );
130,604✔
326
          ComputeSolidBlock( x_base, y_base, plane, results[yr * blocks_per_row + xi] );
130,604✔
327
        }
328
      },
1,248✔
329
      block_threads );
330

331
  for ( std::size_t yr = 0; yr < num_block_rows; ++yr )
1,252✔
332
  {
333
    const unsigned short y_base = static_cast<unsigned short>( yr * SOLIDX_Y_SIZE );
1,248✔
334
    for ( std::size_t xi = 0; xi < blocks_per_row; ++xi )
131,936✔
335
    {
336
      const unsigned short x_base = static_cast<unsigned short>( xi * SOLIDX_X_SIZE );
130,688✔
337
      StitchBlock( mapwriter, x_base, y_base, results[yr * blocks_per_row + xi] );
130,688✔
338
    }
339
    INFO_PRINT( "\rConverting: {}%", static_cast<unsigned>( yr ) * 100 / num_block_rows );
1,248✔
340
  }
341
  loop_timer.stop();
4✔
342

343
  Tools::Timer<> flush_timer;
4✔
344
  mapwriter.WriteConfigFile();
4✔
345
  mapwriter.Flush();  // surface any write errors before reporting success
4✔
346
  flush_timer.stop();
4✔
347

348
  long long total_ms = init_timer.ellapsed() + mapread_timer.ellapsed() +
4✔
349
                       staticread_timer.ellapsed() + plane_timer.ellapsed() +
4✔
350
                       loop_timer.ellapsed() + flush_timer.ellapsed();
4✔
351

352
  INFO_PRINTLN(
4✔
353
      "\rConversion complete.\n"
354
      "Conversion details:\n"
355
      "  Total blocks: {}\n"
356
      "  Blocks with solids: {} ({}%)\n"
357
      "  Blocks without solids: {} ({}%)\n"
358
      "  Locations with solids: {}\n"
359
      "  Total number of solids: {}\n"
360
      "Timing:\n"
361
      "  Init files:   {} ms\n"
362
      "  Map read:     {} ms\n"
363
      "  Static read:  {} ms\n"
364
      "  Terrain plane:{} ms\n"
365
      "  Process loop: {} ms\n"
366
      "  Config+flush: {} ms\n"
367
      "  Total:        {} ms",
UNCOV
368
      empty + nonempty, nonempty, ( nonempty * 100 / ( empty + nonempty ) ), empty,
×
NEW
369
      ( empty * 100 / ( empty + nonempty ) ), with_more_solids, total_statics,
×
370
      init_timer.ellapsed(), mapread_timer.ellapsed(), staticread_timer.ellapsed(),
4✔
371
      plane_timer.ellapsed(), loop_timer.ellapsed(), flush_timer.ellapsed(), total_ms );
4✔
372

373
  if ( cfg_profile )
4✔
374
  {
375
    // Sub-totals of the process loop (see prof_* members). Their sum is slightly below the
376
    // loop time; the remainder is block bookkeeping plus the chrono-read overhead itself.
NEW
377
    auto to_ms = []( long long ns ) { return ns / 1'000'000; };
×
NEW
378
    INFO_PRINTLN(
×
379
        "Process loop breakdown (profile=1):\n"
380
        "    Map-info:    {} ms\n"
381
        "    Statics:     {} ms\n"
382
        "    Shape-build: {} ms\n"
383
        "    Writer:      {} ms\n"
384
        "    Sum:         {} ms",
NEW
385
        to_ms( prof_mapinfo_ns ), to_ms( prof_statics_ns ), to_ms( prof_shape_ns ),
×
NEW
386
        to_ms( prof_writer_ns ),
×
NEW
387
        to_ms( prof_mapinfo_ns + prof_statics_ns + prof_shape_ns + prof_writer_ns ) );
×
388
  }
389
}
4✔
390

391
// Consolidate one tile's statics (sorted descending by z/height, consumed via
392
// pop_back) into the tile's final shape stack: shapes[0] becomes the map base,
393
// the rest the cell's solid runs, bottom-up. See the declaration for the contract;
394
// every rule below is reflected byte-for-byte in solids.dat, so treat any change
395
// here as an output-format change.
396
void UoConvertMain::merge_shapes( StaticList& statics, std::vector<MapShape>& shapes ) const
7,828,226✔
397
{
398
  shapes.clear();
7,828,226✔
399

400
  // try to consolidate like shapes, and discard ones we don't care about.
401
  while ( !statics.empty() )
15,778,712✔
402
  {
403
    StaticRec srec = statics.back();
7,996,418✔
404
    statics.pop_back();
7,933,391✔
405

406
    unsigned int polflags = polflags_from_tileflags( srec.graphic, srec.flags, cfg_use_no_shoot,
15,916,050✔
407
                                                     cfg_LOS_through_windows );
7,897,562✔
408
    if ( ( ~polflags & FLAG::MOVELAND ) && ( ~polflags & FLAG::MOVESEA ) &&
8,018,488✔
409
         ( ~polflags & FLAG::BLOCKSIGHT ) && ( ~polflags & FLAG::BLOCKING ) &&
196✔
NEW
410
         ( ~polflags & FLAG::OVERFLIGHT ) )
×
411
    {
412
      // Invariant from the caller's filter: every element still in `statics` has
413
      // at least one of these bits set, so this can never fire.
NEW
414
      passert_always( 0 );
×
415
      continue;
7,884,258✔
416
    }
417

418
    if ( shapes.empty() )
8,018,488✔
419
    {
420
      // this, whatever it is, is the map base.
421
      // TODO: look for water statics and use THOSE as the map.
422
      // these will be converted below to make the map "solid"; the lowest
423
      // level is always gradual no matter what.
424
      // emplace_back( z, height, flags ) throughout this function: constructing the
425
      // MapShape in place lets the compiler keep the working shape in registers
426
      // (push_back's const& parameter forces it into memory for the copy).
427
      shapes.emplace_back( srec.z, short{ 0 }, ( polflags & 0xFFu ) | FLAG::GRADUAL );
7,938,458✔
428

429
      // for wall flag - map tile always height 0, at bottom. if map tile has height, add it as
430
      // a static
431
      if ( srec.height != 0 )
7,943,250✔
432
        shapes.emplace_back( srec.z, srec.height, polflags );
63,554✔
433
      continue;
7,883,914✔
434
    }
435

NEW
436
    MapShape& prev = shapes.back();
×
437
    // we're adding it.
438
    MapShape shape{ .z = srec.z, .height = srec.height, .flags = polflags };
352✔
439

440
    // always add the map shape seperately
441
    if ( shapes.size() == 1 )
352✔
442
    {
443
      shapes.emplace_back( shape.z, shape.height, shape.flags );
208✔
444
      continue;
208✔
445
    }
446

447
    if ( shape.z < prev.z + prev.height )
144✔
448
    {
449
      // things can't exist in the same place.
450
      // shrink the bottom part of this shape.
451
      // if that would give it negative height, then skip it.
NEW
452
      short height_remove = prev.z + prev.height - shape.z;
×
NEW
453
      if ( height_remove <= shape.height )
×
454
      {
NEW
455
        shape.z += height_remove;
×
NEW
456
        shape.height -= height_remove;
×
457
      }
458
      else
459
      {  // example: 5530, 14
NEW
460
        continue;
×
461
      }
462
    }
463

464
    // sometimes water has "sand" a couple z-coords above it.
465
    // We'll try to detect this (really, anything that is up to
466
    // SAND_OVER_WATER_MAX_GAP dist from water) and extend the thing above downward.
467
    if ( ( prev.flags & FLAG::MOVESEA ) && ( shape.z > prev.z + prev.height ) &&
144✔
NEW
468
         ( shape.z <= prev.z + prev.height + SAND_OVER_WATER_MAX_GAP ) )
×
469
    {
NEW
470
      short height_add = shape.z - prev.z - prev.height;
×
NEW
471
      shape.z -= height_add;
×
NEW
472
      shape.height += height_add;
×
473
    }
474
    if ( ( prev.flags & FLAG::MOVESEA ) && ( prev.z + prev.height == -5 ) &&
144✔
NEW
475
         ( shape.flags & FLAG::MOVESEA ) && ( shape.z == 25 ) )
×
476
    {
477
      // The client's statics0.mul really does contain stray water statics (0x1796)
478
      // floating at z=25 directly above ordinary ocean whose surface tops out at -5;
479
      // on real Britannia (map1) e.g. around (1344,573) and (1703,450). Dropping
480
      // them keeps the ocean a single walkable-sea shape instead of stacking a
481
      // phantom water slab 30z above it. The -5 and 25 literals are those exact
482
      // client-data values, not tunables.
NEW
483
      continue;
×
484
    }
485

486
    if ( shape.z > prev.z + prev.height )
144✔
487
    {
488
      //
489
      // elevated above what's below, must include separately
490
      //
491

492
      shapes.emplace_back( shape.z, shape.height, shape.flags );
96✔
493
      continue;
96✔
494
    }
495

496
    passert_always( shape.z == prev.z + prev.height );
48✔
497

498
    if ( shape.z == prev.z + prev.height )
48✔
499
    {
500
      //
501
      // sitting right on top of the previous solid
502
      //
503

504
      // standable atop non-standable: standable
505
      // nonstandable atop standable: nonstandable
506
      // etc
507
      bool can_combine = flags_match( prev.flags, shape.flags, FLAG::BLOCKSIGHT | FLAG::BLOCKING );
48✔
508
      if ( prev.flags & FLAG::MOVELAND && ~shape.flags & FLAG::BLOCKING &&
48✔
NEW
509
           ~shape.flags & FLAG::MOVELAND )
×
510
      {
NEW
511
        can_combine = false;
×
512
      }
513

514
      if ( can_combine )
48✔
515
      {
516
        prev.flags = shape.flags;
8✔
517
        prev.height += shape.height;
8✔
518
      }
519
      else  // if one blocks LOS, but not the other, they can't be combined this way.
520
      {
521
        shapes.emplace_back( shape.z, shape.height, shape.flags );
40✔
522
        continue;
40✔
523
      }
524
    }
525
  }
526
}
7,546,014✔
527

528
// Phase A (pure compute): produce this block's cells, solids, and block-local index
529
// data from immutable inputs only. Deliberately touches no MapWriter and no
530
// UoConvertMain counters -- stats and warnings accumulate into `result` so this can
531
// run concurrently across blocks. StitchBlock() folds the result into the MapWriter
532
// in block order.
533
void UoConvertMain::ComputeSolidBlock( unsigned short x_base, unsigned short y_base,
130,534✔
534
                                       const TerrainPlane& plane, BlockResult& result ) const
535
{
536
  // Raw UO tile flags fetched for every tile's statics below. Any static reaching
537
  // `statics` is guaranteed to have at least one of these bits set, so nothing
538
  // downstream needs to re-check srec.flags against this same mask.
539
  static constexpr unsigned int kSolidStaticFlags =
540
      USTRUCT_TILE::FLAG_BLOCKING | USTRUCT_TILE::FLAG_PLATFORM | USTRUCT_TILE::FLAG_HALF_HEIGHT |
541
      USTRUCT_TILE::FLAG_LIQUID | USTRUCT_TILE::FLAG_HOVEROVER;
542

543
  // Reset the caller's buffer, retaining vector capacity so reusing one BlockResult across
544
  // blocks avoids per-block heap allocation. addindex must be re-zeroed (the stitch copies
545
  // it wholesale and run-less cells must read 0); cells are fully rewritten for the clamped
546
  // region and never read outside it, so they need no reset.
547
  result.solids.clear();
130,534✔
548
  result.warnings.clear();
130,615✔
549
  result.has_solids = false;
130,632✔
550
  result.nonempty_locations = 0;
130,632✔
551
  result.total_statics = 0;
130,632✔
552
  result.prof_mapinfo_ns = result.prof_statics_ns = result.prof_shape_ns = 0;
130,632✔
553
  std::memset( result.addindex, 0, sizeof( result.addindex ) );
130,632✔
554

555
  // Block-local, 0-based count of solid elements appended so far in this block. Replaces
556
  // the old `NextSolidIndex() - baseindex` math (which was already block-local); the
557
  // stitch rebases it against the live solids buffer.
558
  unsigned int local_elems = 0;
130,632✔
559

560
  unsigned short x_add_max = SOLIDX_X_SIZE, y_add_max = SOLIDX_Y_SIZE;
130,632✔
561
  if ( x_base + x_add_max > uo_map_width )
130,632✔
562
    x_add_max = uo_map_width - x_base;
×
563
  if ( y_base + y_add_max > uo_map_height )
130,632✔
564
    y_add_max = uo_map_height - y_base;
×
565
  result.x_add_max = x_add_max;
130,632✔
566
  result.y_add_max = y_add_max;
130,632✔
567

568
  // Reused across tiles (cleared, not reconstructed) to avoid a per-tile
569
  // allocation across the ~25M tiles a full map conversion visits. The reserve
570
  // covers the typical per-tile count, so most tiles never reallocate at all.
571
  std::vector<MapShape> shapes;
130,632✔
572
  shapes.reserve( 8 );
130,632✔
573

574
  // Opt-in per-tile profiling: `t` is a rolling cursor and lap() folds the elapsed time
575
  // since the last cursor into an accumulator, advancing the cursor. Guarded by cfg_profile
576
  // at every call site so a normal run does no chrono reads at all.
577
  using ProfClock = std::chrono::high_resolution_clock;
578
  ProfClock::time_point t;
130,580✔
NEW
579
  auto lap = [&]( long long& acc )
×
580
  {
NEW
581
    auto now = ProfClock::now();
×
NEW
582
    acc += std::chrono::duration_cast<std::chrono::nanoseconds>( now - t ).count();
×
NEW
583
    t = now;
×
NEW
584
  };
×
585

586
  // All of this block's statics, bucketed per cell in a single pass over the raw
587
  // block, instead of one whole-block readstatics() scan per tile (64x the work).
588
  // thread_local so each parallel worker reuses one scratch -- with its vectors'
589
  // capacity -- across every block of its band; BlockResult itself is per-block in
590
  // create_map, so the scratch must not live there.
591
  thread_local StaticBuckets cell_statics;
130,580✔
592
  if ( cfg_profile )
130,580✔
NEW
593
    t = ProfClock::now();
×
594
  readstatics_block( cell_statics, x_base, y_base, kSolidStaticFlags );
130,580✔
595
  if ( cfg_profile )
130,588✔
NEW
596
    lap( result.prof_statics_ns );
×
597

598
  for ( unsigned short x_add = 0; x_add < x_add_max; ++x_add )
1,107,431✔
599
  {
600
    for ( unsigned short y_add = 0; y_add < y_add_max; ++y_add )
9,099,781✔
601
    {
602
      unsigned short x = x_base + x_add;
8,122,989✔
603
      unsigned short y = y_base + y_add;
8,122,989✔
604

605
      if ( cfg_profile )
8,122,989✔
NEW
606
        t = ProfClock::now();
×
607

608
      // read the precomputed smoothed terrain, and treat it like a static.
609
      const std::size_t plane_idx = plane.index( x, y );
8,122,989✔
610
      const u16 landtile = plane.landtile[plane_idx];
8,031,770✔
611
      short z = plane.eff_z[plane_idx];  // effective z: the liquid override is already folded in
8,008,438✔
612

613
      if ( landtile > MAX_LANDTILE_ID )
8,030,609✔
NEW
614
        result.warnings.push_back(
×
NEW
615
            fmt::format( "Tile {:#x} at ({},{},{}) is an invalid ID!", landtile, x, y, z ) );
×
616

617
      short low_z = plane.low_z[plane_idx];
8,030,609✔
618

619
      short lt_height = z - low_z;
8,023,288✔
620
      z = low_z;
8,023,288✔
621

622
      if ( landtile > MAX_LANDTILE_ID )
8,023,288✔
NEW
623
        result.warnings.push_back(
×
NEW
624
            fmt::format( "Tile {:#x} at ({},{},{}) is an invalid ID!", landtile, x, y, z ) );
×
625

626
      unsigned int lt_flags = Plib::landtile_uoflags_read( landtile );
8,023,288✔
627
      if ( ~lt_flags & USTRUCT_TILE::FLAG_BLOCKING )
8,004,942✔
628
      {  // this seems to be the default.
629
        lt_flags |= USTRUCT_TILE::FLAG_PLATFORM;
7,359,981✔
630
      }
631
      lt_flags |=
8,004,942✔
632
          USTRUCT_TILE::FLAG_NO_SHOOT;  // added to make sure people using noshoot will have shapes
633
      // generated by this tile in future block LOS, shouldn't
634
      // affect people using old LOS method one way or another.
635
      lt_flags |= USTRUCT_TILE::FLAG_FLOOR;
8,004,942✔
636
      lt_flags |= USTRUCT_TILE::FLAG_HALF_HEIGHT;  // the entire map is this way
8,004,942✔
637

638
      if ( lt_flags & USTRUCT_TILE::FLAG_WALL )
8,004,942✔
NEW
639
        lt_height = WALL_LANDTILE_HEIGHT;
×
640

641
      if ( cfg_profile )
8,004,942✔
NEW
642
        lap( result.prof_mapinfo_ns );
×
643

644
      // This tile's bucket doubles as the mutable per-tile scratch: the merge loop
645
      // below consumes it via pop_back, and readstatics_block re-clears it for the
646
      // next block, so no copy into a separate vector is needed.
647
      StaticList& statics = cell_statics[x_add * STATICBLOCK_CHUNK + y_add];
8,004,942✔
648

649
      std::erase_if( statics,
7,930,488✔
650
                     [this]( const StaticRec& srec )
352✔
651
                     {
652
                       unsigned int polflags = polflags_from_tileflags(
704✔
653
                           srec.graphic, srec.flags, cfg_use_no_shoot, cfg_LOS_through_windows );
352✔
654
                       return ( ~polflags & FLAG::MOVELAND ) && ( ~polflags & FLAG::MOVESEA ) &&
196✔
655
                              ( ~polflags & FLAG::BLOCKSIGHT ) && ( ~polflags & FLAG::BLOCKING ) &&
548✔
656
                              ( ~polflags & FLAG::OVERFLIGHT );
352✔
657
                     } );
658

659
      if ( cfg_profile )
7,938,190✔
NEW
660
        lap( result.prof_statics_ns );
×
661

662
      bool addMap = true;
7,938,190✔
663

664
      for ( const auto& srec : statics )
7,938,542✔
665
      {
666
        // Look for water tiles. If there are any, discard the map (which is usually at -15 anyway)
667
        if ( z + lt_height <= srec.z &&
1,056✔
668
             // only where the map is below or same Z as the static
669
             ( ( srec.z - ( z + lt_height ) ) <= WATER_DISCARD_Z_WINDOW ) &&
692✔
670
             is_discarded_water[srec.graphic] )
340✔
671
        {
672
          // arr, there be water here
673
          addMap = false;
×
674
        }
675

676
        // if there's a static on top of one of these "wall" landtiles, make it override.
677
        if ( ( lt_flags & USTRUCT_TILE::FLAG_WALL ) &&  // wall?
352✔
678
             z <= srec.z && srec.z - z <= lt_height )
×
679
        {
680
          lt_height = srec.z - z;
×
681
        }
682
      }
683
      // shadows above caves (check the cheap emptiness test first; most tiles
684
      // have no statics at all)
685
      if ( !statics.empty() && is_cave_shadow( landtile ) )
7,949,945✔
686
      {
687
        addMap = false;
×
688
      }
689

690

691
      // If the map is a NODRAW tile, and there are statics, discard the map tile
692
      if ( landtile == 2 && !statics.empty() )
7,906,231✔
693
        addMap = false;
×
694

695
      if ( addMap )
7,906,231✔
696
        statics.emplace_back( 0, static_cast<signed char>( z ), lt_flags,
×
697
                              static_cast<char>( lt_height ) );
7,962,504✔
698

699
      if ( statics.size() > 1 )
7,821,210✔
700
      {
701
        sort( statics.begin(), statics.end(), StaticsByZ() );
208✔
702
        reverse( statics.begin(), statics.end() );
208✔
703
      }
704

705
      merge_shapes( statics, shapes );
7,916,150✔
706

707
      // the first StaticShape is the map base; the rest are this cell's solid runs
708
      // (left in place -- no need to pay an O(n) front erase per tile).
709
      const MapShape& base = shapes[0];
7,946,484✔
710
      const size_t num_runs = shapes.size() - 1;
7,920,641✔
711
      MAPCELL cell;
712
      passert_always( base.height == 0 );
7,966,867✔
713
      cell.z = static_cast<signed char>(
7,966,867✔
714
          base.z );  // assume now map has height=1. a static was already added if it was >0
7,966,867✔
715
      cell.flags = static_cast<u8>( base.flags );
7,966,867✔
716
      if ( num_runs != 0 )
7,966,867✔
717
        cell.flags |= FLAG::MORE_SOLIDS;
63,751✔
718

719
      if ( cfg_profile )
7,966,867✔
NEW
720
        lap( result.prof_shape_ns );
×
721

722
      result.cells.cell[x_add][y_add] = cell;
8,060,632✔
723

724
      if ( num_runs != 0 )
8,060,632✔
725
      {
726
        ++result.nonempty_locations;
63,776✔
727
        result.total_statics += static_cast<unsigned int>( num_runs );
63,776✔
728
        result.has_solids = true;
63,776✔
729

730
        // Block-local element offset of this cell's first run. Captured before the
731
        // appends below, so it equals the count of runs emitted by earlier cells of
732
        // this block -- identical to the old `NextSolidIndex() - baseindex`.
733
        unsigned int addindex = local_elems;
63,776✔
734
        if ( addindex > std::numeric_limits<unsigned short>::max() )
63,776✔
735
          throw std::runtime_error( "addoffset overflow" );
8✔
736
        result.addindex[x_add][y_add] = static_cast<unsigned short>( addindex );
63,760✔
737
        for ( size_t j = 1; j < shapes.size(); ++j )
127,650✔
738
        {
739
          const MapShape& shape = shapes[j];
63,893✔
740
          char _z, height, flags;
741
          _z = static_cast<char>( shape.z );
63,890✔
742
          height = static_cast<char>( shape.height );
63,890✔
743
          flags = static_cast<u8>( shape.flags );
63,890✔
744
          if ( !height )  // make 0 height solid
63,890✔
745
          {
746
            --_z;
144✔
747
            ++height;
144✔
748
          }
749

750
          if ( j != shapes.size() - 1 )
63,890✔
751
            flags |= FLAG::MORE_SOLIDS;
136✔
752
          result.solids.emplace_back( _z, height, flags );
63,897✔
753
          ++local_elems;
63,890✔
754
        }
755
      }
756
    }
757
  }
758
}
68,252✔
759

760
// Phase B (stitch): fold one block's compute result into the MapWriter. Runs in the
761
// exact block order create_map used to run ProcessSolidBlock, so every solids/solidx2
762
// append offset -- and thus every byte of solidx1/solidx2 -- is identical to the old
763
// serial path.
764
void UoConvertMain::StitchBlock( MapWriter& mapwriter, unsigned short x_base, unsigned short y_base,
130,688✔
765
                                 const BlockResult& result )
766
{
767
  // Reduce the per-block stat sums into the run-wide counters.
768
  with_more_solids += result.nonempty_locations;
130,688✔
769
  total_statics += result.total_statics;
130,688✔
770
  if ( cfg_profile )
130,688✔
771
  {
NEW
772
    prof_mapinfo_ns += result.prof_mapinfo_ns;
×
NEW
773
    prof_statics_ns += result.prof_statics_ns;
×
NEW
774
    prof_shape_ns += result.prof_shape_ns;
×
775
  }
776

777
  // Replay invalid-ID warnings in the order they were produced.
778
  for ( const auto& w : result.warnings )
130,688✔
NEW
779
    INFO_PRINTLN( "{}", w );
×
780

781
  // Time the MapWriter mutations as the "writer" slice of the loop breakdown (the
782
  // per-tile writer laps moved out of ComputeSolidBlock, which no longer writes).
783
  using ProfClock = std::chrono::high_resolution_clock;
784
  ProfClock::time_point writer_start;
130,688✔
785
  if ( cfg_profile )
130,688✔
NEW
786
    writer_start = ProfClock::now();
×
787

788
  // Cell writes are disjoint per block. A full block (the only case in practice --
789
  // map dimensions are enforced divisible by 8) is one aligned block assignment;
790
  // the per-cell fallback covers a hypothetical edge-clamped partial block.
791
  if ( result.x_add_max == SOLIDX_X_SIZE && result.y_add_max == SOLIDX_Y_SIZE )
130,688✔
792
  {
793
    mapwriter.SetMapBlock( x_base, y_base, result.cells );
130,688✔
794
  }
795
  else
796
  {
NEW
797
    for ( unsigned short x_add = 0; x_add < result.x_add_max; ++x_add )
×
NEW
798
      for ( unsigned short y_add = 0; y_add < result.y_add_max; ++y_add )
×
NEW
799
        mapwriter.SetMapCell( x_base + x_add, y_base + y_add, result.cells.cell[x_add][y_add] );
×
800
  }
801

802
  if ( !result.has_solids )
130,688✔
803
  {
804
    ++empty;
127,488✔
805
    mapwriter.SetSolidx2Offset( x_base, y_base, 0 );
127,488✔
806
  }
807
  else
808
  {
809
    ++nonempty;
3,200✔
810
    SOLIDX2_ELEM idx2_elem{};
3,200✔
811
    idx2_elem.baseindex = mapwriter.NextSolidIndex();
3,200✔
812
    // addindex is already block-local and zero for run-less cells -- emit as-is.
813
    std::memcpy( idx2_elem.addindex, result.addindex, sizeof( idx2_elem.addindex ) );
3,200✔
814

815
    // Byte offset this block's solidx2 elem lands at (before appending it).
816
    unsigned int idx2_offset = mapwriter.NextSolidx2Offset();
3,200✔
817
    mapwriter.AppendSolidx2Elem( idx2_elem );
3,200✔
818
    for ( const auto& solid : result.solids )
67,120✔
819
      mapwriter.AppendSolid( solid );
63,920✔
820
    mapwriter.SetSolidx2Offset( x_base, y_base, idx2_offset );
3,200✔
821
  }
822

823
  if ( cfg_profile )
130,688✔
824
  {
NEW
825
    auto now = ProfClock::now();
×
NEW
826
    prof_writer_ns +=
×
NEW
827
        std::chrono::duration_cast<std::chrono::nanoseconds>( now - writer_start ).count();
×
828
  }
829
}
130,688✔
830

831
std::string UoConvertMain::resolve_type_from_id( unsigned id ) const
20✔
832
{
833
  if ( BoatTypes.contains( id ) )
20✔
834
    return "Boat";
12✔
835
  return "Multi";
8✔
836
}
837

838
void UoConvertMain::write_multi_element( FILE* multis_cfg, const USTRUCT_MULTI_ELEMENT& elem,
820✔
839
                                         const std::string& mytype, bool& first )
840
{
841
  if ( elem.graphic == GRAPHIC_NODRAW )
820✔
842
    return;
×
843

844
  std::string type = elem.flags ? "static" : "dynamic";
820✔
845

846
  if ( mytype == "Boat" && first && elem.graphic != 1 )
820✔
847
    type = "static";
12✔
848

849
  std::string comment;
820✔
850
  if ( cfg_use_new_hsa_format )
820✔
851
  {
852
    USTRUCT_TILE_HSA tile;
853
    readtile( elem.graphic, &tile );
×
854
    comment.assign( tile.name, sizeof( tile.name ) );
×
855
  }
856
  else
857
  {
858
    USTRUCT_TILE tile;
859
    readtile( elem.graphic, &tile );
820✔
860
    comment.assign( tile.name, sizeof( tile.name ) );
820✔
861
  }
862

863
  fprintf( multis_cfg, "    %-7s 0x%04x %4d %4d %4d   // %s\n", type.c_str(), elem.graphic, elem.x,
1,640✔
864
           elem.y, elem.z, comment.c_str() );
820✔
865

866
  first = false;
820✔
867
}
820✔
868

869
void UoConvertMain::write_multi( FILE* multis_cfg, unsigned id,
×
870
                                 std::vector<Plib::USTRUCT_MULTI_ELEMENT>& multi_elems )
871
{
872
  std::string mytype = resolve_type_from_id( id );
×
873

874
  fprintf( multis_cfg, "%s 0x%x\n", mytype.c_str(), id );
×
875
  fprintf( multis_cfg, "{\n" );
×
876

877
  bool first = true;
×
878
  for ( const auto& elem : multi_elems )
×
879
  {
880
    write_multi_element( multis_cfg, elem, mytype, first );
×
881
  }
882

883
  fprintf( multis_cfg, "}\n\n" );
×
884
}
×
885

886
void UoConvertMain::write_multi( FILE* multis_cfg, unsigned id, FILE* multi_mul,
20✔
887
                                 unsigned int offset, unsigned int length )
888
{
889
  USTRUCT_MULTI_ELEMENT elem;
890
  unsigned int count =
20✔
891
      cfg_use_new_hsa_format ? length / sizeof( USTRUCT_MULTI_ELEMENT_HSA ) : length / sizeof elem;
892

893
  std::string mytype = resolve_type_from_id( id );
20✔
894

895
  fprintf( multis_cfg, "%s 0x%x\n", mytype.c_str(), id );
20✔
896
  fprintf( multis_cfg, "{\n" );
20✔
897

898
  if ( fseek( multi_mul, offset, SEEK_SET ) != 0 )
20✔
899
  {
900
    throw std::runtime_error( "write_multi(): fseek() failed" );
×
901
  }
902

903

904
  bool first = true;
20✔
905
  while ( count-- )
840✔
906
  {
907
    if ( fread( &elem, sizeof elem, 1, multi_mul ) != 1 )
820✔
908
    {
909
      throw std::runtime_error( "write_multi(): fread() failed" );
×
910
    }
911

912
    if ( cfg_use_new_hsa_format )
820✔
913
    {
914
      if ( fseek( multi_mul, 4, SEEK_CUR ) != 0 )
×
915
        throw std::runtime_error( "write_multi(): fseek() failed" );
×
916
    }
917

918
    write_multi_element( multis_cfg, elem, mytype, first );
820✔
919
  }
920

921
  fprintf( multis_cfg, "}\n\n" );
20✔
922
}
20✔
923

924
void UoConvertMain::create_multis_cfg( FILE* multi_idx, FILE* multi_mul, FILE* multis_cfg )
1✔
925
{
926
  if ( fseek( multi_idx, 0, SEEK_SET ) != 0 )
1✔
927
    throw std::runtime_error( "create_multis_cfg: fseek failed" );
×
928
  unsigned count = 0;
1✔
929
  USTRUCT_IDX idxrec;
930
  for ( int i = 0; fread( &idxrec, sizeof idxrec, 1, multi_idx ) == 1; ++i )
16,384✔
931
  {
932
    const USTRUCT_VERSION* vrec = nullptr;
16,383✔
933

934
    if ( check_verdata( VERFILE_MULTI_MUL, i, vrec ) )
16,383✔
935
    {
936
      write_multi( multis_cfg, i, verfile, vrec->filepos, vrec->length );
×
937
      ++count;
×
938
    }
939
    else
940
    {
941
      if ( idxrec.offset == 0xFFffFFffLu )
16,383✔
942
        continue;
16,363✔
943

944
      write_multi( multis_cfg, i, multi_mul, idxrec.offset, idxrec.length );
20✔
945
      ++count;
20✔
946
    }
947
  }
948
  INFO_PRINTLN( "{} multi definitions written to multis.cfg", count );
1✔
949
}
1✔
950

951
void UoConvertMain::create_multis_cfg()
1✔
952
{
953
  std::map<unsigned int, std::vector<USTRUCT_MULTI_ELEMENT>> multi_map;
1✔
954

955
  std::string outdir = programArgsFindEquals( "outdir=", "." );
1✔
956
  UniqueFile multis_cfg = open_out_text( outdir + "/multis.cfg" );
1✔
957

958
  if ( open_uopmulti_file( multi_map ) )
1✔
959
  {
960
    for ( auto& [id, elems] : multi_map )
×
961
    {
962
      write_multi( multis_cfg, id, elems );
×
963
    }
964

965
    INFO_PRINTLN( "{} multi definitions written to multis.cfg", multi_map.size() );
×
966

967
    return;
×
968
  }
969

970
  UniqueFile multi_idx( open_uo_file( "multi.idx" ) );
1✔
971
  UniqueFile multi_mul( open_uo_file( "multi.mul" ) );
1✔
972

973
  create_multis_cfg( multi_idx, multi_mul, multis_cfg );
1✔
974
}
1✔
975
void UoConvertMain::write_flags( FILE* fp, unsigned int flags )
117✔
976
{
977
  if ( flags & FLAG::MOVELAND )
117✔
978
    fprintf( fp, "    MoveLand 1\n" );
27✔
979
  if ( flags & FLAG::MOVESEA )
117✔
980
    fprintf( fp, "    MoveSea 1\n" );
1✔
981
  if ( flags & FLAG::BLOCKSIGHT )
117✔
982
    fprintf( fp, "    BlockSight 1\n" );
92✔
983
  if ( ~flags & FLAG::OVERFLIGHT )
117✔
984
    fprintf( fp, "    OverFlight 0\n" );
117✔
985
  if ( flags & FLAG::ALLOWDROPON )
117✔
986
    fprintf( fp, "    AllowDropOn 1\n" );
27✔
987
  if ( flags & FLAG::GRADUAL )
117✔
988
    fprintf( fp, "    Gradual 1\n" );
4✔
989
  if ( flags & FLAG::STACKABLE )
117✔
990
    fprintf( fp, "    Stackable 1\n" );
2✔
991
  if ( flags & FLAG::BLOCKING )
117✔
992
    fprintf( fp, "    Blocking 1\n" );
67✔
993
  if ( flags & FLAG::MOVABLE )
117✔
994
    fprintf( fp, "    Movable 1\n" );
48✔
995
  if ( flags & FLAG::EQUIPPABLE )
117✔
996
    fprintf( fp, "    Equippable 1\n" );
4✔
997
  if ( flags & FLAG::DESC_PREPEND_A )
117✔
998
    fprintf( fp, "    DescPrependA 1\n" );
2✔
999
  if ( flags & FLAG::DESC_PREPEND_AN )
117✔
1000
    fprintf( fp, "    DescPrependAn 1\n" );
×
1001
}
117✔
1002

1003
void UoConvertMain::create_tiles_cfg()
1✔
1004
{
1005
  std::string outdir = programArgsFindEquals( "outdir=", "." );
1✔
1006
  UniqueFile fp = open_out_text( outdir + "/tiles.cfg" );
1✔
1007
  char name[21];
1008

1009
  unsigned count = 0;
1✔
1010
  for ( unsigned int graphic_i = 0; graphic_i <= Plib::systemstate.config.max_tile_id; ++graphic_i )
16,385✔
1011
  {
1012
    u16 graphic = static_cast<u16>( graphic_i );
16,384✔
1013
    USTRUCT_TILE tile;
1014
    if ( cfg_use_new_hsa_format )
16,384✔
1015
    {
1016
      USTRUCT_TILE_HSA newtile;
1017
      read_objinfo( graphic, newtile );
×
1018
      tile.anim = newtile.anim;
×
1019
      tile.flags = newtile.flags;
×
1020
      tile.height = newtile.height;
×
1021
      tile.layer = newtile.layer;
×
1022
      memcpy( tile.name, newtile.name, sizeof tile.name );
×
1023
      tile.unk14 = newtile.unk14;
×
1024
      tile.unk15 = newtile.unk15;
×
1025
      tile.unk6 = newtile.unk6;
×
1026
      tile.unk7 = newtile.unk7;
×
1027
      tile.unk8 = newtile.unk8;
×
1028
      tile.unk9 = newtile.unk9;
×
1029
      tile.weight = newtile.weight;
×
1030
    }
1031
    else
1032
      read_objinfo( graphic, tile );
16,384✔
1033
    const bool is_mount = MountTypes.contains( graphic );
16,384✔
1034

1035
    if ( tile.name[0] == '\0' && tile.flags == 0 && tile.layer == 0 && tile.height == 0 &&
16,384✔
1036
         !is_mount )
16,269✔
1037
    {
1038
      continue;
16,269✔
1039
    }
1040
    unsigned int flags =
1041
        polflags_from_tileflags( graphic, tile.flags, cfg_use_no_shoot, cfg_LOS_through_windows );
115✔
1042
    if ( is_mount )
115✔
1043
    {
1044
      tile.layer = 25;
×
1045
      flags |= FLAG::EQUIPPABLE;
×
1046
    }
1047

1048
    memset( name, 0, sizeof name );
115✔
1049
    memcpy( name, tile.name, sizeof tile.name );
115✔
1050

1051
    fprintf( fp, "tile 0x%x\n", graphic );
115✔
1052
    fprintf( fp, "{\n" );
115✔
1053
    fprintf( fp, "    Desc %s\n", name );
115✔
1054
    fprintf( fp, "    UoFlags 0x%08lx\n", static_cast<unsigned long>( tile.flags ) );
115✔
1055
    if ( tile.layer )
115✔
1056
      fprintf( fp, "    Layer %u\n", tile.layer );
31✔
1057
    if ( flags & FLAG::EQUIPPABLE )
115✔
1058
      fprintf( fp, "    AnimID %u\n", tile.anim );
4✔
1059
    if ( static_cast<unsigned long>( tile.flags ) & USTRUCT_TILE::FLAG_PARTIAL_HUE )
115✔
1060
      fprintf( fp, "    PartialHue 1\n" );
×
1061
    fprintf( fp, "    Height %u\n", tile.height );
115✔
1062
    fprintf( fp, "    Weight %u\n", tile.weight );
115✔
1063
    write_flags( fp, flags );
115✔
1064
    fprintf( fp, "}\n" );
115✔
1065
    fprintf( fp, "\n" );
115✔
1066
    ++count;
115✔
1067
  }
1068

1069
  INFO_PRINTLN( "{} tile definitions written to tiles.cfg", count );
1✔
1070
}
1✔
1071

1072
void UoConvertMain::create_landtiles_cfg()
1✔
1073
{
1074
  std::string outdir = programArgsFindEquals( "outdir=", "." );
1✔
1075
  UniqueFile fp = open_out_text( outdir + "/landtiles.cfg" );
1✔
1076
  unsigned count = 0;
1✔
1077

1078
  for ( u16 i = 0; i <= MAX_LANDTILE_ID; ++i )
16,385✔
1079
  {
1080
    USTRUCT_LAND_TILE landtile;
1081
    if ( cfg_use_new_hsa_format )
16,384✔
1082
    {
1083
      USTRUCT_LAND_TILE_HSA newlandtile;
1084
      readlandtile( i, &newlandtile );
×
1085
      landtile.flags = newlandtile.flags;
×
1086
      landtile.unk = newlandtile.unk;
×
1087
      memcpy( landtile.name, newlandtile.name, sizeof landtile.name );
×
1088
    }
1089
    else
1090
      readlandtile( i, &landtile );
16,384✔
1091

1092
    if ( landtile.name[0] || landtile.flags )
16,384✔
1093
    {
1094
      fprintf( fp, "landtile 0x%x\n", i );
2✔
1095
      fprintf( fp, "{\n" );
2✔
1096
      fprintf( fp, "    Name %s\n", landtile.name );
2✔
1097
      fprintf( fp, "    UoFlags 0x%08lx\n", static_cast<unsigned long>( landtile.flags ) );
2✔
1098

1099
      unsigned int flags = polflags_from_landtileflags( i, landtile.flags );
2✔
1100
      flags &= ~FLAG::MOVABLE;  // movable makes no sense for landtiles
2✔
1101
      write_flags( fp, flags );
2✔
1102
      fprintf( fp, "}\n" );
2✔
1103
      fprintf( fp, "\n" );
2✔
1104
      ++count;
2✔
1105
    }
1106
  }
1107

1108
  INFO_PRINTLN( "{} landtile definitions written to landtiles.cfg", count );
1✔
1109
}
1✔
1110

1111
int UoConvertMain::main()
11✔
1112
{
1113
  const std::vector<std::string>& binArgs = programArgs();
11✔
1114

1115
  /**********************************************
1116
   * show help
1117
   **********************************************/
1118
  if ( binArgs.size() == 1 )
11✔
1119
  {
1120
    showHelp();
×
1121
    return 0;  // return "okay"
×
1122
  }
1123

1124
  // Setups uoconvert by finding the path of uo files and max tiles from pol.cfg or command
1125
  // line arguments. Also loads parameters from uoconvert.cfg.
1126
  setup_uoconvert();
11✔
1127

1128
  std::string command = binArgs[1];
11✔
1129
  if ( command == "uoptomul" )
11✔
1130
  {
1131
    if ( !convert_uop_to_mul() )
×
1132
      return 1;
×
1133
  }
1134
  else if ( command == "map" )
11✔
1135
  {
1136
    UoConvert::uo_mapid = programArgsFindEquals( "mapid=", 0, false );
4✔
1137
    UoConvert::uo_usedif = programArgsFindEquals( "usedif=", 1, false );
4✔
1138
    UoConvert::uo_readuop = (bool)programArgsFindEquals( "readuop=", 1, false );
4✔
1139
    cfg_profile = (bool)programArgsFindEquals( "profile=", 0, false );
4✔
1140
    cfg_threads = static_cast<unsigned>( programArgsFindEquals( "threads=", 0, false ) );
4✔
1141

1142
    std::string realm = programArgsFindEquals( "realm=", "britannia" );
4✔
1143

1144
    UoConvert::open_uo_data_files();
4✔
1145
    UoConvert::read_uo_data();
4✔
1146

1147
    // Auto-detects defaults for mapid=0 or 1 based on the map size. All other sizes are fixed based
1148
    // on the mapid.
1149
    Pol::Plib::MUL::MapInfo mapinfo( UoConvert::uo_mapid, UoConvert::uo_map_size );
4✔
1150
    int default_width = mapinfo.width();
4✔
1151
    int default_height = mapinfo.height();
4✔
1152

1153
    if ( mapinfo.guessed() )
4✔
1154
      INFO_PRINTLN( "Auto-detected map dimensions: {}x{}", default_width, default_height );
2✔
1155

1156
    uo_map_width =
4✔
1157
        static_cast<unsigned short>( programArgsFindEquals( "width=", default_width, false ) );
4✔
1158
    uo_map_height =
4✔
1159
        static_cast<unsigned short>( programArgsFindEquals( "height=", default_height, false ) );
4✔
1160

1161
    check_for_errors_in_map_parameters();
4✔
1162

1163
    int x = programArgsFindEquals( "x=", -1, false );
4✔
1164
    int y = programArgsFindEquals( "y=", -1, false );
4✔
1165

1166
    // britannia: realm=main mapid=0 width=6144 height=4096
1167
    // ilshenar: realm=ilshenar mapid=2 width=2304 height=1600
1168
    // malas: realm=malas mapid=3 width=2560 height=2048
1169
    // tokuno: realm=tokuno mapid=4 width=1448 height=1448
1170
    // termur: realm=termur mapid=5 width=1280 height=4096
1171

1172
    if ( x >= 0 && y >= 0 )
4✔
1173
    {
1174
      UoConvertMain::update_map( realm, static_cast<u16>( x ), static_cast<u16>( y ) );
×
1175
    }
1176
    else
1177
    {
1178
      UoConvertMain::create_map( realm, static_cast<unsigned short>( uo_map_width ),
4✔
1179
                                 static_cast<unsigned short>( uo_map_height ) );
1180
    }
1181
  }
4✔
1182
  else if ( command == "statics" )
7✔
1183
  {
1184
    std::string realm = programArgsFindEquals( "realm=", "britannia" );
2✔
1185
    Plib::RealmDescriptor descriptor = Plib::RealmDescriptor::Load( realm );
2✔
1186

1187
    UoConvert::uo_mapid = descriptor.uomapid;
2✔
1188
    UoConvert::uo_usedif = descriptor.uodif;
2✔
1189
    UoConvert::uo_map_width = static_cast<unsigned short>( descriptor.width );
2✔
1190
    UoConvert::uo_map_height = static_cast<unsigned short>( descriptor.height );
2✔
1191

1192
    UoConvert::open_uo_data_files();
2✔
1193
    UoConvert::read_uo_data();
2✔
1194

1195
    write_pol_static_files( realm );
2✔
1196
  }
2✔
1197
  else if ( command == "multis" )
5✔
1198
  {
1199
    UoConvert::open_uo_data_files();
1✔
1200
    UoConvert::read_uo_data();
1✔
1201
    UoConvertMain::create_multis_cfg();
1✔
1202
  }
1203
  else if ( command == "tiles" )
4✔
1204
  {
1205
    UoConvert::open_uo_data_files();
1✔
1206
    UoConvert::read_uo_data();
1✔
1207
    UoConvertMain::create_tiles_cfg();
1✔
1208
  }
1209
  else if ( command == "landtiles" )
3✔
1210
  {
1211
    UoConvert::open_uo_data_files();
1✔
1212
    UoConvert::read_uo_data();
1✔
1213
    UoConvertMain::create_landtiles_cfg();
1✔
1214
  }
1215
  else if ( command == "maptile" )
2✔
1216
  {
1217
    cfg_threads = static_cast<unsigned>( programArgsFindEquals( "threads=", 0, false ) );
2✔
1218
    std::string realm = programArgsFindEquals( "realm=", "britannia" );
2✔
1219
    Plib::RealmDescriptor descriptor = Plib::RealmDescriptor::Load( realm );
2✔
1220

1221
    UoConvert::uo_mapid = descriptor.uomapid;
2✔
1222
    UoConvert::uo_usedif = descriptor.uodif;
2✔
1223
    UoConvert::uo_map_width = static_cast<unsigned short>( descriptor.width );
2✔
1224
    UoConvert::uo_map_height = static_cast<unsigned short>( descriptor.height );
2✔
1225

1226
    UoConvert::open_uo_data_files();
2✔
1227
    UoConvert::read_uo_data();
2✔
1228

1229
    UoConvertMain::create_maptile( realm );
2✔
1230
  }
2✔
1231
  else if ( command == "flags" )
×
1232
  {
1233
    UoConvertMain::display_flags();
×
1234
  }
1235
  else  // unknown option
1236
  {
1237
    showHelp();
×
1238
    return 1;
×
1239
  }
1240
  UoConvert::clear_tiledata();
11✔
1241
  return 0;
11✔
1242
}
11✔
1243
void UoConvertMain::check_for_errors_in_map_parameters()
4✔
1244
{
1245
  if ( !MUL::Map::valid_size( UoConvert::uo_map_size, uo_map_width, uo_map_height ) )
4✔
1246
  {
1247
    size_t expected_size =
1248
        MUL::Map::blockSize * MUL::Map::expected_blocks( uo_map_width, uo_map_height );
×
1249

1250
    INFO_PRINTLN( "\nWarning: Width and height do not match the map size ({} bytes, expected {})",
×
1251
                  UoConvert::uo_map_size, expected_size );
1252

1253
    if ( uo_map_width == 0 || uo_map_height == 0 )
×
1254
      throw std::runtime_error(
×
1255
          "Width and height were not identified automatically. Please specify them manually." );
×
1256

1257

1258
    if ( ( uo_map_width % MUL::Map::blockWidth != 0 ) ||
×
1259
         ( uo_map_height % MUL::Map::blockHeight != 0 ) )
×
1260
      throw std::runtime_error( "Width and height must be divisible by 8" );
×
1261

1262
    if ( uo_map_size < expected_size )
×
1263
      throw std::runtime_error( "Map size is smaller than the given width and height" );
×
1264
  }
1265
}
4✔
1266
bool UoConvertMain::convert_uop_to_mul()
×
1267
{
1268
  // this is kludgy and doesn't take into account the UODataPath. Mostly a proof of concept now.
1269
  UoConvert::uo_mapid = programArgsFindEquals( "mapid=", 0, false );
×
1270

1271
  std::string mul_mapfile = "map" + to_string( uo_mapid ) + ".mul";
×
1272
  std::string uop_mapfile = "map" + to_string( uo_mapid ) + "LegacyMUL.uop";
×
1273

1274
  auto maphash = []( int mapid, size_t chunkidx )
×
1275
  { return HashLittle2( fmt::format( "build/map{}legacymul/{:08d}.dat", mapid, chunkidx ) ); };
×
1276

1277
  std::ifstream ifs( uop_mapfile, std::ifstream::binary );
×
1278
  if ( !ifs )
×
1279
  {
1280
    ERROR_PRINTLN( "Error when opening mapfile: {}", uop_mapfile );
×
1281
    return false;
×
1282
  }
1283

1284
  kaitai::kstream ks( &ifs );
×
1285
  uop_t uopfile( &ks );
×
1286

1287
  // TODO: read all blocks
1288
  std::map<uint64_t, uop_t::file_t*> filemap;
×
1289
  uop_t::block_addr_t* currentblock = uopfile.header()->firstblock();
×
1290
  for ( auto file : *currentblock->block_body()->files() )
×
1291
  {
1292
    if ( file == nullptr )
×
1293
      continue;
×
1294
    if ( file->decompressed_size() == 0 )
×
1295
      continue;
×
1296
    filemap[file->filehash()] = file;
×
1297
  }
1298

1299
  if ( uopfile.header()->nfiles() != filemap.size() )
×
1300
    INFO_PRINTLN( "Warning: not all chunks read ({}/{})", filemap.size(),
×
1301
                  uopfile.header()->nfiles() );
×
1302

1303
  std::ofstream ofs( mul_mapfile, std::ofstream::binary );
×
1304
  for ( size_t i = 0; i < filemap.size(); i++ )
×
1305
  {
1306
    auto fileitr = filemap.find( maphash( uo_mapid, i ) );
×
1307
    if ( fileitr == filemap.end() )
×
1308
    {
1309
      INFO_PRINTLN( "Couldn't find file hash: {}", maphash( uo_mapid, i ) );
×
1310
      continue;
×
1311
    }
1312

1313
    auto file = fileitr->second;
×
1314
    ofs << file->data()->filebytes();
×
1315
    INFO_PRINTLN( "Wrote: {}/{}", i + 1, filemap.size() );
×
1316
  }
1317
  INFO_PRINTLN( "Done converting." );
×
1318

1319
  return true;
×
1320
}
×
1321
void UoConvertMain::setup_uoconvert()
11✔
1322
{
1323
  std::string uodata_root = programArgsFindEquals( "uodata=", "" );
11✔
1324
  unsigned short max_tile =
1325
      static_cast<unsigned short>( programArgsFindEquals( "maxtileid=", 0x0, true ) );
11✔
1326

1327
  if ( max_tile )
11✔
1328
  {
1329
    INFO_PRINTLN( "Warning: maxtileid will be ignored and detected from tiledata.mul instead." );
×
1330
  }
1331

1332
  // read required parameters from pol.cfg
1333
  if ( uodata_root.empty() )
11✔
1334
  {
1335
    INFO_PRINTLN( "Reading pol.cfg." );
×
1336
    Clib::ConfigFile cf( "pol.cfg" );
×
1337
    Clib::ConfigElem elem;
×
1338

1339
    cf.readraw( elem );
×
1340

1341
    if ( uodata_root.empty() )
×
1342
      uodata_root = Plib::UOInstallFinder::remove_elem( elem );
×
1343
  }
×
1344

1345
  // Save the parameters into this ugly global state we have
1346
  Plib::systemstate.config.uo_datafile_root = Clib::normalized_dir_form( uodata_root );
11✔
1347

1348
  // Load parameters from uoconvert.cfg (multi types, mounts, etc)
1349
  load_uoconvert_cfg();
11✔
1350
}
11✔
1351

1352
void parse_graphics_properties( Clib::ConfigElem& elem, const std::string& prop_name,
16✔
1353
                                std::set<unsigned int>& dest )
1354
{
1355
  std::string prop_value;
16✔
1356
  std::string graphicnum;
16✔
1357

1358
  if ( !elem.has_prop( prop_name.c_str() ) )
16✔
1359
  {
1360
    elem.throw_prop_not_found( prop_name );
×
1361
  }
1362

1363
  while ( elem.remove_prop( prop_name.c_str(), &prop_value ) )
32✔
1364
  {
1365
    ISTRINGSTREAM is( prop_value );
16✔
1366
    while ( is >> graphicnum )
120✔
1367
    {
1368
      dest.insert( strtoul( graphicnum.c_str(), nullptr, 0 ) );
104✔
1369
    }
1370
  }
16✔
1371
}
16✔
1372

1373
void notice_deprecated( Clib::ConfigElem& elem, const std::string& prop_name )
16✔
1374
{
1375
  if ( elem.has_prop( prop_name.c_str() ) )
16✔
1376
  {
1377
    INFO_PRINTLN( "Note: specifying {} in MultiTypes is no longer needed.", prop_name );
16✔
1378
  }
1379
}
16✔
1380

1381
void UoConvertMain::load_uoconvert_cfg()
11✔
1382
{
1383
  std::string main_cfg = "uoconvert.cfg";
11✔
1384
  if ( Clib::FileExists( main_cfg ) )
11✔
1385
  {
1386
    Clib::ConfigElem elem;
8✔
1387
    INFO_PRINTLN( "Reading uoconvert.cfg." );
8✔
1388
    Clib::ConfigFile cf_main( main_cfg );
8✔
1389
    while ( cf_main.read( elem ) )
48✔
1390
    {
1391
      if ( elem.type_is( "MultiTypes" ) )
40✔
1392
      {
1393
        parse_graphics_properties( elem, "Boats", BoatTypes );
8✔
1394
        notice_deprecated( elem, "Houses" );
8✔
1395
        notice_deprecated( elem, "Stairs" );
8✔
1396
      }
1397
      else if ( elem.type_is( "LOSOptions" ) )
32✔
1398
      {
1399
        if ( elem.has_prop( "UseNoShoot" ) )
8✔
1400
          UoConvertMain::cfg_use_no_shoot = elem.remove_bool( "UseNoShoot" );
8✔
1401

1402
        if ( elem.has_prop( "LOSThroughWindows" ) )
8✔
1403
          UoConvertMain::cfg_LOS_through_windows = elem.remove_bool( "LOSThroughWindows" );
8✔
1404
      }
1405
      else if ( elem.type_is( "Mounts" ) )
24✔
1406
      {
1407
        parse_graphics_properties( elem, "Tiles", MountTypes );
8✔
1408
      }
1409
      else if ( elem.type_is( "StaticOptions" ) )
16✔
1410
      {
1411
        if ( elem.has_prop( "MaxStaticsPerBlock" ) )
8✔
1412
        {
1413
          UoConvert::cfg_max_statics_per_block = elem.remove_int( "MaxStaticsPerBlock" );
8✔
1414

1415
          if ( UoConvert::cfg_max_statics_per_block > MAX_STATICS_PER_BLOCK )
8✔
1416
          {
1417
            UoConvert::cfg_max_statics_per_block = MAX_STATICS_PER_BLOCK;
×
1418
            INFO_PRINTLN( "max. Statics per Block limited to {} Items",
×
1419
                          UoConvert::cfg_max_statics_per_block );
1420
          }
1421
          else if ( UoConvert::cfg_max_statics_per_block < 0 )
8✔
1422
            UoConvert::cfg_max_statics_per_block = 1000;
×
1423
        }
1424

1425
        if ( elem.has_prop( "WarningStaticsPerBlock" ) )
8✔
1426
        {
1427
          UoConvert::cfg_warning_statics_per_block = elem.remove_int( "WarningStaticsPerBlock" );
8✔
1428

1429
          if ( UoConvert::cfg_warning_statics_per_block > MAX_STATICS_PER_BLOCK )
8✔
1430
          {
1431
            UoConvert::cfg_warning_statics_per_block = MAX_STATICS_PER_BLOCK;
×
1432
            INFO_PRINTLN( "max. Statics per Block for Warning limited to {} Items",
×
1433
                          UoConvert::cfg_warning_statics_per_block );
1434
          }
1435
          else if ( UoConvert::cfg_warning_statics_per_block < 0 )
8✔
1436
            UoConvert::cfg_warning_statics_per_block = 1000;
×
1437
        }
1438

1439
        if ( elem.has_prop( "DiscardedWaterTiles" ) )
8✔
1440
        {
1441
          parse_graphics_properties( elem, "DiscardedWaterTiles", DiscardedWaterTypes );
×
1442
        }
1443
        else
1444
          for ( int i = 0x1796; i <= 0x17B2; ++i )
240✔
1445
            DiscardedWaterTypes.insert( i );
232✔
1446

1447
        if ( elem.has_prop( "ShowIllegalGraphicWarning" ) )
8✔
1448
          UoConvert::cfg_show_illegal_graphic_warning =
8✔
1449
              elem.remove_bool( "ShowIllegalGraphicWarning" );
8✔
1450
      }
1451
      else if ( elem.type_is( "TileOptions" ) )
8✔
1452
      {
1453
        if ( elem.has_prop( "ShowRoofAndPlatformWarning" ) )
8✔
1454
          cfg_show_roof_and_platform_warning = elem.remove_bool( "ShowRoofAndPlatformWarning" );
8✔
1455
      }
1456
      else if ( elem.type_is( "ClientOptions" ) )
×
1457
      {
1458
        if ( elem.has_prop( "UseNewHSAFormat" ) )
×
1459
          INFO_PRINTLN( "Warning: UseNewHSAFormat in uoconvert.cfg is no longer needed." );
×
1460
      }
1461
    }
1462
  }
8✔
1463

1464
  // Snapshot the water-type set into the flat lookup the conversion loop probes.
1465
  for ( unsigned int graphic : DiscardedWaterTypes )
243✔
1466
    if ( graphic < is_discarded_water.size() )
232✔
1467
      is_discarded_water[graphic] = true;
232✔
1468
}
11✔
1469
}  // namespace Pol::UoConvert
1470

1471

1472
///////////////////////////////////////////////////////////////////////////////
1473
///////////////////////////////////////////////////////////////////////////////
1474
///////////////////////////////////////////////////////////////////////////////
1475

1476
int main( int argc, char* argv[] )
11✔
1477
{
1478
  Pol::UoConvert::UoConvertMain program;
11✔
1479
  program.start( argc, argv );
11✔
UNCOV
1480
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc