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

Return-To-The-Roots / s25client / 25624270592

10 May 2026 08:40AM UTC coverage: 50.308% (+0.06%) from 50.245%
25624270592

Pull #1925

github

web-flow
Merge b69a11486 into 9d512408c
Pull Request #1925: Ignore isolated fish resources for fisheries

37 of 64 new or added lines in 3 files covered. (57.81%)

10 existing lines in 4 files now uncovered.

23130 of 45977 relevant lines covered (50.31%)

43642.01 hits per line

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

76.03
/libs/s25main/world/MapLoader.cpp
1
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "world/MapLoader.h"
6
#include "Game.h"
7
#include "GamePlayer.h"
8
#include "GameWorldBase.h"
9
#include "GlobalGameSettings.h"
10
#include "PointOutput.h"
11
#include "RttrForeachPt.h"
12
#include "addons/const_addons.h"
13
#include "buildings/nobHQ.h"
14
#include "factories/BuildingFactory.h"
15
#include "helpers/IdRange.h"
16
#include "helpers/Range.h"
17
#include "helpers/containerUtils.h"
18
#include "helpers/mathFuncs.h"
19
#include "lua/GameDataLoader.h"
20
#include "pathfinding/PathConditionShip.h"
21
#include "random/Random.h"
22
#include "world/World.h"
23
#include "nodeObjs/noAnimal.h"
24
#include "nodeObjs/noEnvObject.h"
25
#include "nodeObjs/noGranite.h"
26
#include "nodeObjs/noStaticObject.h"
27
#include "nodeObjs/noTree.h"
28
#include "gameTypes/ShipDirection.h"
29
#include "gameData/MaxPlayers.h"
30
#include "gameData/TerrainDesc.h"
31
#include "libsiedler2/Archiv.h"
32
#include "libsiedler2/ArchivItem_Map.h"
33
#include "libsiedler2/ArchivItem_Map_Header.h"
34
#include "libsiedler2/prototypen.h"
35
#include "s25util/Log.h"
36
#include <boost/filesystem/operations.hpp>
37
#include <algorithm>
38
#include <map>
39
#include <queue>
40

41
class noBase;
42

43
MapLoader::MapLoader(GameWorldBase& world) : world_(world) {}
7✔
44

45
bool MapLoader::Load(const libsiedler2::ArchivItem_Map& map, Exploration exploration)
5✔
46
{
47
    GameDataLoader gdLoader(world_.GetDescriptionWriteable());
10✔
48
    if(!gdLoader.Load())
5✔
49
        return false;
×
50

51
    uint8_t gfxSet = map.getHeader().getGfxSet();
5✔
52
    DescIdx<LandscapeDesc> lt =
53
      world_.GetDescription().landscapes.find([gfxSet](const LandscapeDesc& l) { return l.s2Id == gfxSet; });
10✔
54
    world_.Init(MapExtent(map.getHeader().getWidth(), map.getHeader().getHeight()), lt); //-V807
5✔
55

56
    if(!InitNodes(map, exploration))
5✔
57
        return false;
×
58
    PlaceObjects(map);
5✔
59
    PlaceAnimals(map);
5✔
60
    if(!InitSeasAndHarbors(world_))
5✔
61
        return false;
×
62

63
    /// Schatten
64
    InitShadows(world_);
5✔
65

66
    // If we have explored FoW, create the FoW objects
67
    if(exploration == Exploration::FogOfWarExplored)
5✔
68
        SetMapExplored(world_);
×
69

70
    return true;
5✔
71
}
72

73
bool MapLoader::Load(const boost::filesystem::path& mapFilePath)
3✔
74
{
75
    // Map laden
76
    libsiedler2::Archiv mapArchiv;
6✔
77

78
    // Karteninformationen laden
79
    if(libsiedler2::loader::LoadMAP(mapFilePath, mapArchiv) != 0)
3✔
80
        return false;
×
81

82
    const libsiedler2::ArchivItem_Map& map = *static_cast<libsiedler2::ArchivItem_Map*>(mapArchiv[0]);
3✔
83

84
    if(!Load(map, world_.GetGGS().exploration))
3✔
85
        return false;
×
86
    if(!PlaceHQs())
3✔
87
        return false;
×
88

89
    world_.CreateTradeGraphs();
3✔
90

91
    return true;
3✔
92
}
93

94
bool MapLoader::LoadLuaScript(Game& game, ILocalGameState& localgameState, const boost::filesystem::path& luaFilePath)
3✔
95
{
96
    if(!bfs::exists(luaFilePath))
3✔
97
        return false;
×
98
    auto lua = std::make_unique<LuaInterfaceGame>(game, localgameState);
6✔
99
    if(!lua->loadScript(luaFilePath) || !lua->CheckScriptVersion())
3✔
100
        return false;
1✔
101
    game.SetLua(std::move(lua));
2✔
102
    return true;
2✔
103
}
104

105
bool MapLoader::PlaceHQs(bool addStartWares)
3✔
106
{
107
    std::vector<MapPoint> hqPositions = hqPositions_;
6✔
108
    if(world_.GetGGS().randomStartPosition)
3✔
109
        RANDOM_SHUFFLE2(hqPositions, 0);
1✔
110
    return PlaceHQs(world_, hqPositions, addStartWares);
6✔
111
}
112

113
void MapLoader::SetupResources(GameWorldBase& world)
3✔
114
{
115
    ResourceType target;
116
    switch(world.GetGGS().getSelection(AddonId::CHANGE_GOLD_DEPOSITS))
3✔
117
    {
118
        case 0:
3✔
119
        default: target = ResourceType::Gold; break;
3✔
NEW
120
        case 1: target = ResourceType::Nothing; break;
×
NEW
121
        case 2: target = ResourceType::Iron; break;
×
NEW
122
        case 3: target = ResourceType::Coal; break;
×
NEW
123
        case 4: target = ResourceType::Granite; break;
×
124
    }
125
    ConvertMineResourceTypes(world, ResourceType::Gold, target);
3✔
126
    PlaceAndFixWater(world);
3✔
127
    RemoveUnusableFishResources(world);
3✔
128
}
3✔
129

130
void MapLoader::ConvertMineResourceTypes(GameWorldBase& world, ResourceType from, ResourceType to)
3✔
131
{
132
    // LOG.write(("Convert map resources from %i to %i\n", from, to);
133
    if(from == to)
3✔
134
        return;
3✔
135

NEW
136
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
×
137
    {
NEW
138
        Resource resources = world.GetNode(pt).resources;
×
139
        // Gibt es Ressourcen dieses Typs?
140
        // Wenn ja, dann umwandeln bzw löschen
NEW
141
        if(resources.getType() == from)
×
142
        {
NEW
143
            resources.setType(to);
×
NEW
144
            world.SetResource(pt, resources);
×
145
        }
146
    }
147
}
148

149
void MapLoader::PlaceAndFixWater(GameWorldBase& world)
3✔
150
{
151
    const bool waterEverywhere = world.GetGGS().getSelection(AddonId::EXHAUSTIBLE_WATER) == 1;
3✔
152

153
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
1,383✔
154
    {
155
        Resource curNodeResource = world.GetNode(pt).resources;
1,320✔
156

157
        if(curNodeResource.getType() == ResourceType::Nothing)
1,320✔
158
        {
159
            if(!waterEverywhere)
1,313✔
160
                continue;
1,320✔
161
        } else if(curNodeResource.getType() != ResourceType::Water)
7✔
162
            continue; // do not override maps resource.
7✔
163

NEW
164
        uint8_t minHumidity = 100;
×
NEW
165
        for(const DescIdx<TerrainDesc> tIdx : world.GetTerrainsAround(pt))
×
166
        {
NEW
167
            const uint8_t curHumidity = world.GetDescription().get(tIdx).humidity;
×
NEW
168
            if(curHumidity < minHumidity)
×
169
            {
NEW
170
                minHumidity = curHumidity;
×
NEW
171
                if(minHumidity == 0)
×
NEW
172
                    break;
×
173
            }
174
        }
NEW
175
        if(minHumidity)
×
176
        {
NEW
177
            curNodeResource =
×
NEW
178
              Resource(ResourceType::Water, waterEverywhere ? 7 : helpers::iround<uint8_t>(minHumidity * 7. / 100.));
×
179
        } else
NEW
180
            curNodeResource = Resource(ResourceType::Nothing, 0);
×
181

NEW
182
        world.SetResource(pt, curNodeResource);
×
183
    }
184
}
3✔
185

186
void MapLoader::RemoveUnusableFishResources(GameWorldBase& world)
3✔
187
{
188
    const auto isWaterPoint = [&world](const MapPoint nb) { return world.IsWaterPoint(nb); };
28✔
189
    for(const MapCoord y : helpers::range(world.GetHeight()))
132✔
190
    {
191
        // Optimization: When there was fish on the previous node (in the same row)
192
        // we do not need to check for isolated water points, as there is at least that water point
193
        bool previousHasFish = false;
60✔
194
        for(const MapCoord x : helpers::range(world.GetWidth()))
2,880✔
195
        {
196
            const MapPoint pt(x, y);
1,320✔
197
            bool hasFish = false;
1,320✔
198

199
            if(world.GetNode(pt).resources.has(ResourceType::Fish))
1,320✔
200
            {
201
                if(!isWaterPoint(pt))
7✔
NEW
202
                    world.SetResource(pt, Resource(ResourceType::Nothing, 0));
×
203
                else if(previousHasFish) // Previous node with fish -> neighbor check can be skipped
7✔
204
                    hasFish = true;
2✔
205
                else if(helpers::contains_if(world.GetNeighbours(pt), isWaterPoint))
5✔
206
                    hasFish = true;
3✔
207
                else
208
                    world.SetResource(pt, Resource(ResourceType::Nothing, 0));
2✔
209
            }
210
            previousHasFish = hasFish;
1,320✔
211
        }
212
    }
213
}
3✔
214

215
void MapLoader::InitShadows(World& world)
5✔
216
{
217
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
70,805✔
218
        world.RecalcShadow(pt);
70,400✔
219
}
5✔
220

221
void MapLoader::SetMapExplored(World& world)
×
222
{
223
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
×
224
    {
225
        // For every player
NEW
226
        for(const auto i : helpers::range(MAX_PLAYERS))
×
227
        {
228
            // If we have FoW here, save it
229
            if(world.GetNode(pt).fow[i].visibility == Visibility::FogOfWar)
×
230
                world.SaveFOWNode(pt, i, 0);
×
231
        }
232
    }
233
}
×
234

235
DescIdx<TerrainDesc> MapLoader::getTerrainFromS2(uint8_t s2Id) const
140,800✔
236
{
237
    return world_.GetDescription().terrain.find([s2Id, landscape = world_.GetLandscapeType()](const TerrainDesc& t) {
140,800✔
238
        return t.s2Id == s2Id && t.landscape == landscape;
1,442,160✔
239
    });
140,800✔
240
}
241

242
bool MapLoader::InitNodes(const libsiedler2::ArchivItem_Map& map, Exploration exploration)
5✔
243
{
244
    using libsiedler2::MapLayer;
245
    // Init node data (everything except the objects, figures and BQ)
246
    RTTR_FOREACH_PT(MapPoint, world_.GetSize())
70,805✔
247
    {
248
        MapNode& node = world_.GetNodeInt(pt);
70,400✔
249

250
        std::fill(node.roads.begin(), node.roads.end(), PointRoad::None);
70,400✔
251
        node.altitude = map.getMapDataAt(MapLayer::Altitude, pt.x, pt.y);
70,400✔
252
        unsigned char t1 = map.getMapDataAt(MapLayer::Terrain1, pt.x, pt.y),
70,400✔
253
                      t2 = map.getMapDataAt(MapLayer::Terrain2, pt.x, pt.y);
70,400✔
254

255
        // Hafenplatz?
256
        if((t1 & libsiedler2::HARBOR_MASK) != 0)
70,400✔
257
            world_.harborData.push_back(HarborPos(pt));
×
258

259
        // Will be set later
260
        node.harborId.reset();
70,400✔
261

262
        node.t1 = getTerrainFromS2(t1 & 0x3F); // Only lower 6 bits
70,400✔
263
        node.t2 = getTerrainFromS2(t2 & 0x3F); // Only lower 6 bits
70,400✔
264
        if(!node.t1 || !node.t2)
70,400✔
265
            return false;
×
266

267
        unsigned char mapResource = map.getMapDataAt(MapLayer::Resources, pt.x, pt.y);
70,400✔
268
        Resource resource;
70,400✔
269
        // Wasser?
270
        if(mapResource == 0x20 || mapResource == 0x21)
70,400✔
271
            resource = Resource(ResourceType::Water, 7);
36,625✔
272
        else if(mapResource > 0x40 && mapResource < 0x48)
33,775✔
273
            resource = Resource(ResourceType::Coal, mapResource - 0x40);
9,815✔
274
        else if(mapResource > 0x48 && mapResource < 0x50)
23,960✔
275
            resource = Resource(ResourceType::Iron, mapResource - 0x48);
2,525✔
276
        else if(mapResource > 0x50 && mapResource < 0x58)
21,435✔
277
            resource = Resource(ResourceType::Gold, mapResource - 0x50);
1,660✔
278
        else if(mapResource > 0x58 && mapResource < 0x60)
19,775✔
279
            resource = Resource(ResourceType::Granite, mapResource - 0x58);
550✔
280
        else if(mapResource > 0x80 && mapResource < 0x90) // fish
19,225✔
281
            resource = Resource(ResourceType::Fish, 4);   // Use 4 fish
2,420✔
282
        node.resources = resource;
70,400✔
283

284
        node.reserved = false;
70,400✔
285
        node.owner = 0;
70,400✔
286
        std::fill(node.boundary_stones.begin(), node.boundary_stones.end(), 0);
70,400✔
287
        node.seaId.reset();
70,400✔
288

289
        Visibility fowVisibility;
290
        switch(exploration)
70,400✔
291
        {
292
            case Exploration::Disabled: fowVisibility = Visibility::Visible; break;
×
293
            case Exploration::Classic:
70,400✔
294
            case Exploration::FogOfWar: fowVisibility = Visibility::Invisible; break;
70,400✔
295
            case Exploration::FogOfWarExplored: fowVisibility = Visibility::FogOfWar; break;
×
296
            default: throw std::invalid_argument("Visibility for FoW");
×
297
        }
298

299
        // FOW-Zeug initialisieren
300
        for(auto& fow : node.fow)
633,600✔
301
        {
302
            fow = FoWNode();
563,200✔
303
            fow.visibility = fowVisibility;
563,200✔
304
        }
305

306
        RTTR_Assert(node.figures.empty());
70,400✔
307
    }
308
    return true;
5✔
309
}
310

311
void MapLoader::PlaceObjects(const libsiedler2::ArchivItem_Map& map)
5✔
312
{
313
    hqPositions_.clear();
5✔
314

315
    RTTR_FOREACH_PT(MapPoint, world_.GetSize())
70,805✔
316
    {
317
        using libsiedler2::MapLayer;
318
        unsigned char lc = map.getMapDataAt(MapLayer::ObjectIndex, pt.x, pt.y);
70,400✔
319
        noBase* obj = nullptr;
70,400✔
320

321
        switch(map.getMapDataAt(MapLayer::ObjectType, pt.x, pt.y))
70,400✔
322
        {
323
            // Player Startpos (provisorisch)
324
            case 0x80:
20✔
325
            {
326
                if(lc < MAX_PLAYERS)
20✔
327
                {
328
                    while(hqPositions_.size() <= lc)
40✔
329
                        hqPositions_.push_back(MapPoint::Invalid());
20✔
330
                    hqPositions_[lc] = pt;
20✔
331
                }
332
            }
333
            break;
20✔
334

335
            // Baum 1-4
336
            case 0xC4:
17,640✔
337
            {
338
                if(lc >= 0x30 && lc <= 0x3D)
17,640✔
339
                    obj = new noTree(pt, 0, 3);
5,610✔
340
                else if(lc >= 0x70 && lc <= 0x7D)
12,030✔
341
                    obj = new noTree(pt, 1, 3);
5,860✔
342
                else if(lc >= 0xB0 && lc <= 0xBD)
6,170✔
343
                    obj = new noTree(pt, 2, 3);
5,555✔
344
                else if(lc >= 0xF0 && lc <= 0xFD)
615✔
345
                    obj = new noTree(pt, 3, 3);
615✔
346
                else
347
                    LOG.write(_("Unknown tree1-4 at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
348
            }
349
            break;
17,640✔
350

351
            // Baum 5-8
352
            case 0xC5:
×
353
            {
354
                if(lc >= 0x30 && lc <= 0x3D)
×
355
                    obj = new noTree(pt, 4, 3);
×
356
                else if(lc >= 0x70 && lc <= 0x7D)
×
357
                    obj = new noTree(pt, 5, 3);
×
358
                else if(lc >= 0xB0 && lc <= 0xBD)
×
359
                    obj = new noTree(pt, 6, 3);
×
360
                else if(lc >= 0xF0 && lc <= 0xFD)
×
361
                    obj = new noTree(pt, 7, 3);
×
362
                else
363
                    LOG.write(_("Unknown tree5-8 at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
364
            }
365
            break;
×
366

367
            // Baum 9
368
            case 0xC6:
×
369
            {
370
                if(lc >= 0x30 && lc <= 0x3D)
×
371
                    obj = new noTree(pt, 8, 3);
×
372
                else
373
                    LOG.write(_("Unknown tree9 at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
374
            }
375
            break;
×
376

377
            // Sonstiges Naturzeug ohne Funktion, nur zur Dekoration
378
            case 0xC8:
3,740✔
379
            case 0xC9: // Note: 0xC9 is actually a bug and should be 0xC8. But the random map generator produced that...
380
            {
381
                // "wasserstein" aus der map_?_z.lst
382
                if(lc == 0x0B)
3,740✔
383
                    obj = new noStaticObject(pt, 500 + lc);
×
384
                // Objekte aus der map_?_z.lst
385
                else if(lc <= 0x0F)
3,740✔
386
                    obj = new noEnvObject(pt, 500 + lc);
1,615✔
387
                // Objekte aus der map.lst
388
                else if(lc <= 0x14)
2,125✔
389
                    obj = new noEnvObject(pt, 542 + lc - 0x10);
1,580✔
390
                // exists in mis0bobs-mis5bobs -> take stranded ship
391
                else if(lc == 0x15)
545✔
392
                    obj = new noStaticObject(pt, 0, 0);
×
393
                // gate
394
                else if(lc == 0x16)
545✔
395
                    obj = new noStaticObject(pt, 560);
×
396
                // open gate
397
                else if(lc == 0x17)
545✔
398
                    obj = new noStaticObject(pt, 561);
×
399
                // Stalagmiten (mis1bobs)
400
                else if(lc <= 0x1E)
545✔
401
                    obj = new noStaticObject(pt, (lc - 0x18) * 2, 1);
×
402
                // toter Baum (mis1bobs)
403
                else if(lc <= 0x20)
545✔
404
                    obj = new noStaticObject(pt, 20 + (lc - 0x1F) * 2, 1);
×
405
                // Gerippe (mis1bobs)
406
                else if(lc == 0x21)
545✔
407
                    obj = new noStaticObject(pt, 30, 1);
×
408
                // Objekte aus der map.lst
409
                else if(lc <= 0x2B)
545✔
410
                    obj = new noEnvObject(pt, 550 + lc - 0x22);
545✔
411
                // tent, ruin of guardhouse, tower ruin, cross
412
                else if(lc <= 0x2E || lc == 0x30)
×
413
                    obj = new noStaticObject(pt, (lc - 0x2C) * 2, 2);
×
414
                // castle ruin
415
                else if(lc == 0x2F)
×
416
                    obj = new noStaticObject(pt, (lc - 0x2C) * 2, 2, 2);
×
417
                // small wiking with boat
418
                else if(lc == 0x31)
×
419
                    obj = new noStaticObject(pt, 0, 3);
×
420
                // Pile of wood
421
                else if(lc == 0x32)
×
422
                    obj = new noStaticObject(pt, 0, 4);
×
423
                // whale skeleton (head right)
424
                else if(lc == 0x33)
×
425
                    obj = new noStaticObject(pt, 0, 5);
×
426
                // The next 2 are non standard and only for access in RTTR (replaced in original by
427
                // whale skeleton (head left)
428
                else if(lc == 0x34)
×
429
                    obj = new noStaticObject(pt, 2, 5);
×
430
                // Cave
431
                else if(lc == 0x35)
×
432
                    obj = new noStaticObject(pt, 4, 5);
×
433
                else
434
                    LOG.write(_("Unknown nature object at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
435
            }
436
            break;
3,740✔
437

438
            // Granit Typ 1
439
            case 0xCC:
1,370✔
440
            {
441
                if(lc >= 0x01 && lc <= 0x06)
1,370✔
442
                    obj = new noGranite(GraniteType::One, lc - 1);
1,370✔
443
                else
444
                    LOG.write(_("Unknown granite type2 at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
445
            }
446
            break;
1,370✔
447

448
            // Granit Typ 2
449
            case 0xCD:
1,395✔
450
            {
451
                if(lc >= 0x01 && lc <= 0x06)
1,395✔
452
                    obj = new noGranite(GraniteType::Two, lc - 1);
1,395✔
453
                else
454
                    LOG.write(_("Unknown granite type2 at %1%: (0x%2$x)\n")) % pt % unsigned(lc);
×
455
            }
456
            break;
1,395✔
457

458
            // Nichts
459
            case 0: break;
46,235✔
460

461
            default:
×
462
#ifndef NDEBUG
463
                unsigned unknownObj = map.getMapDataAt(MapLayer::ObjectType, pt.x, pt.y);
×
464
                LOG.write(_("Unknown object at %1%: (0x%2$x: 0x%3$x)\n")) % pt % unknownObj % unsigned(lc);
×
465
#endif // !NDEBUG
466
                break;
×
467
        }
468

469
        world_.GetNodeInt(pt).obj = obj;
70,400✔
470
    }
471
}
5✔
472

473
void MapLoader::PlaceAnimals(const libsiedler2::ArchivItem_Map& map)
5✔
474
{
475
    // Tiere auslesen
476
    RTTR_FOREACH_PT(MapPoint, world_.GetSize())
70,805✔
477
    {
478
        Species species;
479
        using libsiedler2::MapLayer;
480
        switch(map.getMapDataAt(MapLayer::Animals, pt.x, pt.y))
70,400✔
481
        {
482
            // TODO: Which id is the polar bear?
483
            case 1:
175✔
484
                species = (RANDOM.Rand(RANDOM_CONTEXT2(0), 2) == 0) ? Species::RabbitWhite : Species::RabbitGrey;
175✔
485
                break; // Random rabbit
175✔
486
            case 2: species = Species::Fox; break;
140✔
487
            case 3: species = Species::Stag; break;
×
488
            case 4: species = Species::Deer; break;
140✔
489
            case 5: species = Species::Duck; break;
×
490
            case 6: species = Species::Sheep; break;
70✔
491
            case 0:
69,875✔
492
            case 0xFF: // 0xFF is for (really) old S2 maps
493
                continue;
69,875✔
494
            default:
×
495
#ifndef NDEBUG
496
                unsigned unknownAnimal = map.getMapDataAt(MapLayer::Animals, pt.x, pt.y);
×
497
                LOG.write(_("Unknown animal species at %1%: (0x%2$x)\n")) % pt % unknownAnimal;
×
498
#endif // !NDEBUG
499
                continue;
69,875✔
500
        }
501

502
        world_.AddFigure(pt, std::make_unique<noAnimal>(species, pt)).StartLiving();
525✔
503
    }
504
}
5✔
505

506
bool MapLoader::PlaceHQs(GameWorldBase& world, const std::vector<MapPoint>& hqPositions, const bool addStartWares)
177✔
507
{
508
    for(unsigned i = 0; i < world.GetNumPlayers(); ++i)
553✔
509
    {
510
        // Skip unused slots
511
        if(!world.GetPlayer(i).isUsed())
376✔
512
            continue;
13✔
513

514
        // Does the HQ have a position?
515
        if(i >= hqPositions.size() || !hqPositions[i].isValid())
363✔
516
        {
517
            LOG.write(_("Player %u does not have a valid start position!")) % i;
×
518
            return false;
×
519
        }
520

521
        auto* hq = checkedCast<nobHQ*>(BuildingFactory::CreateBuilding(world, BuildingType::Headquarters,
363✔
522
                                                                       hqPositions[i], i, world.GetPlayer(i).nation));
363✔
523
        if(addStartWares)
363✔
524
            hq->addStartWares();
8✔
525
    }
526
    return true;
177✔
527
}
528

529
bool MapLoader::InitSeasAndHarbors(World& world, const std::vector<MapPoint>& additionalHarbors)
27✔
530
{
531
    for(MapPoint pt : additionalHarbors)
185✔
532
        world.harborData.push_back(HarborPos(pt));
158✔
533
    // Clear current harbors and seas
534
    RTTR_FOREACH_PT(MapPoint, world.GetSize()) //-V807
192,697✔
535
    {
536
        MapNode& node = world.GetNodeInt(pt);
190,284✔
537
        node.seaId.reset();
190,284✔
538
        node.harborId.reset();
190,284✔
539
    }
540

541
    /// Determine all seas
542
    world.seas.clear();
27✔
543
    SeaId curSeaId(1);
27✔
544
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
192,697✔
545
    {
546
        // Point is not yet assigned a sea but should be
547
        if(!world.GetNode(pt).seaId && world.IsSeaPoint(pt))
190,284✔
548
        {
549
            const auto seaSize = MeasureSea(world, pt, curSeaId);
46✔
550
            world.seas.push_back(World::Sea(seaSize));
46✔
551
            curSeaId = curSeaId.next();
46✔
552
        }
553
    }
554

555
    /// Determine seas adjacent to the harbor places
556
    HarborId curHarborId(1);
27✔
557
    for(auto it = world.harborData.begin(); it != world.harborData.end();)
201✔
558
    {
559
        helpers::StrongIdVector<bool, SeaId> hasCoastAtSea(world.seas.size(), false);
348✔
560
        bool foundCoast = false;
174✔
561
        for(const auto dir : helpers::EnumRange<Direction>{})
2,784✔
562
        {
563
            SeaId seaId;
1,044✔
564
            // Skip point at NW as often there is no path from it if the harbor is north of an island
565
            if(dir != Direction::NorthWest)
1,044✔
566
            {
567
                seaId = world.GetSeaFromCoastalPoint(world.GetNeighbour(it->pos, dir));
870✔
568
                if(seaId)
870✔
569
                {
570
                    foundCoast = true;
226✔
571
                    // Only 1 coastal point per sea
572
                    if(hasCoastAtSea[seaId])
226✔
573
                        seaId.reset();
52✔
574
                    else
575
                        hasCoastAtSea[seaId] = true;
174✔
576
                }
577
            }
578
            it->seaIds[dir] = seaId;
1,044✔
579
        }
580
        if(!foundCoast)
174✔
581
        {
582
            LOG.write("Map Bug: Found harbor without coast at %1%. Removing!\n") % it->pos;
×
583
            it = world.harborData.erase(it);
×
584
        } else
585
        {
586
            world.GetNodeInt(it->pos).harborId = curHarborId;
174✔
587
            curHarborId = curHarborId.next();
174✔
588
            ++it;
174✔
589
        }
590
    }
591

592
    // Calculate the neighbors and distances
593
    CalcHarborPosNeighbors(world);
27✔
594

595
    // Validate
596
    for(const auto startHbId : helpers::idRange<HarborId>(world.harborData.size()))
201✔
597
    {
598
        const HarborPos& startHbPos = world.harborData[startHbId];
174✔
599
        for(const std::vector<HarborPos::Neighbor>& neighbors : startHbPos.neighbors)
1,218✔
600
        {
601
            for(const HarborPos::Neighbor& neighbor : neighbors)
1,664✔
602
            {
603
                if(world.CalcHarborDistance(neighbor.id, startHbId) != neighbor.distance)
620✔
604
                {
605
                    LOG.write("Bug: Harbor distance mismatch for harbors %1%->%2%: %3% != %4%\n") % startHbId
×
606
                      % neighbor.id % world.CalcHarborDistance(neighbor.id, startHbId) % neighbor.distance;
×
607
                    return false;
×
608
                }
609
            }
610
        }
611
    }
612
    return true;
27✔
613
}
614

615
// class for finding harbor neighbors
616
struct CalcHarborPosNeighborsNode
617
{
618
    CalcHarborPosNeighborsNode() = default; //-V730
619
    CalcHarborPosNeighborsNode(const MapPoint pt, unsigned distance) : pos(pt), distance(distance) {}
232,032✔
620

621
    MapPoint pos;
622
    unsigned distance;
623
};
624

625
/// Calculate the distance from each harbor to the others
626
void MapLoader::CalcHarborPosNeighbors(World& world)
27✔
627
{
628
    for(HarborPos& harbor : world.harborData)
201✔
629
    {
630
        for(const auto dir : helpers::EnumRange<ShipDirection>{})
2,784✔
631
            harbor.neighbors[dir].clear();
1,044✔
632
    }
633
    PathConditionShip shipPathChecker(world);
27✔
634

635
    // pre-calculate sea-points, as IsSeaPoint is rather expensive
636
    std::vector<int8_t> ptIsSeaPt(world.nodes.size()); //-V656
54✔
637

638
    RTTR_FOREACH_PT(MapPoint, world.GetSize())
192,697✔
639
    {
640
        if(shipPathChecker.IsNodeOk(pt))
190,284✔
641
            ptIsSeaPt[world.GetIdx(pt)] = -1;
64,106✔
642
    }
643

644
    // FIFO queue used for a BFS
645
    std::queue<CalcHarborPosNeighborsNode> todo_list;
54✔
646

647
    for(const auto startHbId : helpers::idRange<HarborId>(world.harborData.size()))
201✔
648
    {
649
        RTTR_Assert(todo_list.empty());
174✔
650

651
        // Copy sea points to working flags. Possible values are
652
        // -1 - sea point, not already visited
653
        // 0 - visited or no sea point
654
        // 1 - Coast to a harbor
655
        std::vector<int8_t> ptToVisitOrHb(ptIsSeaPt);
348✔
656

657
        helpers::StrongIdVector<bool, HarborId> hbFound(world.harborData.size(), false);
348✔
658
        // For each sea, store the coastal point indices and their harbor
659
        helpers::StrongIdVector<std::multimap<unsigned, HarborId>, SeaId> coastToHarborPerSea(world.seas.size());
348✔
660
        std::vector<MapPoint> ownCoastalPoints;
348✔
661

662
        // mark coastal points around harbors
663
        for(const auto otherHbId : helpers::idRange<HarborId>(world.harborData.size()))
1,600✔
664
        {
665
            for(const auto dir : helpers::EnumRange<Direction>{})
22,816✔
666
            {
667
                SeaId seaId = world.GetSeaId(otherHbId, dir);
8,556✔
668
                // No sea? -> Next
669
                if(!seaId)
8,556✔
670
                    continue;
7,130✔
671
                const MapPoint coastPt = world.GetNeighbour(world.GetHarborPoint(otherHbId), dir);
1,426✔
672
                // This should not be marked for visit
673
                unsigned idx = world.GetIdx(coastPt);
1,426✔
674
                RTTR_Assert(ptToVisitOrHb[idx] != -1);
1,426✔
675
                if(otherHbId == startHbId)
1,426✔
676
                {
677
                    // This is our start harbor. Add the coast points around it to our todo list.
678
                    ownCoastalPoints.push_back(coastPt);
174✔
679
                } else
680
                {
681
                    ptToVisitOrHb[idx] = 1;
1,252✔
682
                    coastToHarborPerSea[seaId].insert(std::make_pair(idx, otherHbId));
1,252✔
683
                }
684
            }
685
        }
686

687
        for(const MapPoint& ownCoastPt : ownCoastalPoints)
348✔
688
        {
689
            // Special case: Get all harbors that share the coast point with us
690
            SeaId seaId = world.GetSeaFromCoastalPoint(ownCoastPt);
174✔
691
            auto const coastToHbs = coastToHarborPerSea[seaId].equal_range(world.GetIdx(ownCoastPt));
174✔
692
            for(auto it = coastToHbs.first; it != coastToHbs.second; ++it)
180✔
693
            {
694
                ShipDirection shipDir = world.GetShipDir(ownCoastPt, ownCoastPt);
6✔
695
                world.harborData[startHbId].neighbors[shipDir].push_back(HarborPos::Neighbor(it->second, 0));
6✔
696
                hbFound[it->second] = true;
6✔
697
            }
698
            todo_list.push(CalcHarborPosNeighborsNode(ownCoastPt, 0));
174✔
699
        }
700

701
        while(!todo_list.empty()) // as long as there are sea points on our todo list...
232,206✔
702
        {
703
            CalcHarborPosNeighborsNode curNode = todo_list.front();
232,032✔
704
            todo_list.pop();
232,032✔
705

706
            for(const auto dir : helpers::EnumRange<Direction>{})
3,712,512✔
707
            {
708
                MapPoint curPt = world.GetNeighbour(curNode.pos, dir);
1,392,192✔
709
                unsigned idx = world.GetIdx(curPt);
1,392,192✔
710

711
                const int8_t ptValue = ptToVisitOrHb[idx];
1,392,192✔
712
                // Already visited
713
                if(ptValue == 0)
1,392,192✔
714
                    continue;
1,160,334✔
715
                // Not reachable
716
                if(!shipPathChecker.IsEdgeOk(curNode.pos, dir))
231,874✔
717
                    continue;
16✔
718

719
                if(ptValue > 0) // found harbor(s)
231,858✔
720
                {
721
                    ShipDirection shipDir = world.GetShipDir(world.harborData[startHbId].pos, curPt);
597✔
722
                    SeaId seaId = world.GetSeaFromCoastalPoint(curPt);
597✔
723
                    auto const coastToHbs = coastToHarborPerSea[seaId].equal_range(idx);
597✔
724
                    for(auto it = coastToHbs.first; it != coastToHbs.second; ++it)
1,217✔
725
                    {
726
                        const HarborId otherHbId = it->second;
620✔
727
                        if(hbFound[otherHbId])
620✔
728
                            continue;
6✔
729

730
                        hbFound[otherHbId] = true;
614✔
731
                        world.harborData[startHbId].neighbors[shipDir].push_back(
1,228✔
732
                          HarborPos::Neighbor(otherHbId, curNode.distance + 1));
614✔
733

734
                        // Make this the only coastal point of this harbor for this sea
735
                        HarborPos& otherHb = world.harborData[otherHbId];
614✔
736
                        RTTR_Assert(seaId);
614✔
737
                        for(const auto hbDir : helpers::EnumRange<Direction>{})
9,824✔
738
                        {
739
                            if(otherHb.seaIds[hbDir] == seaId && world.GetNeighbour(otherHb.pos, hbDir) != curPt)
3,684✔
740
                                otherHb.seaIds[hbDir].reset();
×
741
                        }
742
                    }
743
                }
744
                todo_list.push(CalcHarborPosNeighborsNode(curPt, curNode.distance + 1));
231,858✔
745
                ptToVisitOrHb[idx] = 0; // mark as visited, so we do not go here again
231,858✔
746
            }
747
        }
748
    }
749
}
27✔
750

751
/// Vermisst ein neues Weltmeer von einem Punkt aus, indem es alle mit diesem Punkt verbundenen
752
/// Wasserpunkte mit der gleichen ID belegt und die Anzahl zur�ckgibt
753
unsigned MapLoader::MeasureSea(World& world, const MapPoint start, SeaId seaId)
46✔
754
{
755
    // Breitensuche von diesem Punkt aus durchf�hren
756
    std::vector<bool> visited(world.GetWidth() * world.GetHeight(), false);
92✔
757
    std::queue<MapPoint> todo;
46✔
758

759
    todo.push(start);
46✔
760
    visited[world.GetIdx(start)] = true;
46✔
761

762
    // Count of nodes (including start node)
763
    unsigned count = 0;
46✔
764

765
    while(!todo.empty())
64,152✔
766
    {
767
        MapPoint p = todo.front();
64,106✔
768
        todo.pop();
64,106✔
769

770
        RTTR_Assert(visited[world.GetIdx(p)]);
64,106✔
771
        world.GetNodeInt(p).seaId = seaId;
64,106✔
772

773
        for(const MapPoint neighbourPt : world.GetNeighbours(p))
448,742✔
774
        {
775
            if(visited[world.GetIdx(neighbourPt)])
384,636✔
776
                continue;
310,153✔
777
            visited[world.GetIdx(neighbourPt)] = true;
74,483✔
778

779
            // Ist das dort auch ein Meerespunkt?
780
            if(world.IsSeaPoint(neighbourPt))
74,483✔
781
                todo.push(neighbourPt);
64,060✔
782
        }
783

784
        ++count;
64,106✔
785
    }
786

787
    return count;
92✔
788
}
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