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

Return-To-The-Roots / s25client / 5111912776

pending completion
5111912776

Pull #1592

github

web-flow
Merge aa25149c0 into 2c095fc86
Pull Request #1592: Update translations

13 of 13 new or added lines in 4 files covered. (100.0%)

21671 of 42980 relevant lines covered (50.42%)

30486.29 hits per line

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

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

5
#include "Settings.h"
6
#include "DrawPoint.h"
7
#include "RTTR_Version.h"
8
#include "RttrConfig.h"
9
#include "drivers/AudioDriverWrapper.h"
10
#include "drivers/VideoDriverWrapper.h"
11
#include "files.h"
12
#include "helpers/strUtils.h"
13
#include "languages.h"
14
#include "gameData/const_gui_ids.h"
15
#include "libsiedler2/ArchivItem_Ini.h"
16
#include "libsiedler2/ArchivItem_Text.h"
17
#include "libsiedler2/libsiedler2.h"
18
#include "s25util/StringConversion.h"
19
#include "s25util/System.h"
20
#include "s25util/error.h"
21
#include <boost/filesystem/operations.hpp>
22

23
const int Settings::VERSION = 13;
24
const std::array<std::string, 10> Settings::SECTION_NAMES = {
25
  {"global", "video", "language", "driver", "sound", "lobby", "server", "proxy", "interface", "addons"}};
26

27
const std::array<short, 13> Settings::SCREEN_REFRESH_RATES = {
28
  {-1, 25, 30, 50, 60, 75, 80, 100, 120, 150, 180, 200, 240}};
29

30
const std::map<GUI_ID, std::string> persistentWindows = {{CGI_CHAT, "wnd_chat"},
31
                                                         {CGI_POSTOFFICE, "wnd_postoffice"},
32
                                                         {CGI_DISTRIBUTION, "wnd_distribution"},
33
                                                         {CGI_BUILDORDER, "wnd_buildorder"},
34
                                                         {CGI_TRANSPORT, "wnd_transport"},
35
                                                         {CGI_MILITARY, "wnd_military"},
36
                                                         {CGI_TOOLS, "wnd_tools"},
37
                                                         {CGI_INVENTORY, "wnd_inventory"},
38
                                                         {CGI_MINIMAP, "wnd_minimap"},
39
                                                         {CGI_BUILDINGS, "wnd_buildings"},
40
                                                         {CGI_BUILDINGSPRODUCTIVITY, "wnd_buildingsproductivity"},
41
                                                         {CGI_MUSICPLAYER, "wnd_musicplayer"},
42
                                                         {CGI_STATISTICS, "wnd_statistics"},
43
                                                         {CGI_ECONOMICPROGRESS, "wnd_economicprogress"},
44
                                                         {CGI_DIPLOMACY, "wnd_diplomacy"},
45
                                                         {CGI_SHIP, "wnd_ship"},
46
                                                         {CGI_MERCHANDISE_STATISTICS, "wnd_merchandise_statistics"}};
47

48
namespace validate {
49
boost::optional<uint16_t> checkPort(const std::string& port)
9✔
50
{
51
    int32_t iPort;
52
    if((helpers::tryFromString(port, iPort) || s25util::tryFromStringClassic(port, iPort)) && checkPort(iPort))
9✔
53
        return static_cast<uint16_t>(iPort);
3✔
54
    else
55
        return boost::none;
6✔
56
}
57
bool checkPort(int port)
7✔
58
{
59
    // Disallow port 0 as it may cause problems
60
    return port > 0 && port <= 65535;
7✔
61
}
62
} // namespace validate
63

64
Settings::Settings() //-V730
5✔
65
{
66
    LoadDefaults();
5✔
67
}
5✔
68

69
void Settings::LoadDefaults()
5✔
70
{
71
    // global
72
    // {
73
    // 0 = ask user at start, 1 = enabled, 2 = always ask
74
    global.submit_debug_data = 0;
5✔
75
    global.use_upnp = false;
5✔
76
    global.smartCursor = true;
5✔
77
    global.debugMode = false;
5✔
78
    global.showGFInfo = false;
5✔
79
    // }
80

81
    // video
82
    // {
83
    if(VIDEODRIVER.IsLoaded())
5✔
84
    {
85
        video.fullscreenSize = VIDEODRIVER.GetWindowSize();
3✔
86
        video.windowedSize = VIDEODRIVER.IsFullscreen() ? VideoMode(800, 600) : video.fullscreenSize;
3✔
87
        video.fullscreen = VIDEODRIVER.IsFullscreen();
3✔
88
    } else
89
    {
90
        video.windowedSize = video.fullscreenSize = VideoMode(800, 600);
2✔
91
        video.fullscreen = false;
2✔
92
    }
93
    video.framerate = 0; // Special value for HW vsync
5✔
94
    video.vbo = true;
5✔
95
    video.shared_textures = true;
5✔
96
    // }
97

98
    // language
99
    // {
100
    language.language.clear();
5✔
101
    // }
102

103
    LANGUAGES.setLanguage(language.language);
5✔
104

105
    // driver
106
    // {
107
    driver.audio = AUDIODRIVER.GetName();
5✔
108
    driver.video = VIDEODRIVER.GetName();
5✔
109
    // }
110

111
    // sound
112
    // {
113
    sound.musicEnabled = false;
5✔
114
    sound.musicVolume = 30;
5✔
115
    sound.effectsEnabled = true;
5✔
116
    sound.effectsVolume = 75;
5✔
117
    sound.playlist = s25::files::defaultPlaylist;
5✔
118
    // }
119

120
    // lobby
121
    // {
122

123
    lobby.name = System::getUserName();
5✔
124
    lobby.password.clear();
5✔
125
    lobby.save_password = false;
5✔
126
    // }
127

128
    // server
129
    // {
130
    server.last_ip.clear();
5✔
131
    server.localPort = 3665;
5✔
132
    server.ipv6 = false;
5✔
133
    // }
134

135
    proxy = ProxySettings();
5✔
136
    proxy.port = 1080;
5✔
137

138
    // interface
139
    // {
140
    interface.autosave_interval = 0;
5✔
141
    interface.revert_mouse = false;
5✔
142
    // }
143

144
    // addons
145
    // {
146
    addons.configuration.clear();
5✔
147
    // }
148

149
    LoadIngameDefaults();
5✔
150
}
5✔
151

152
void Settings::LoadIngameDefaults()
5✔
153
{
154
    // ingame
155
    // {
156
    ingame.scale_statistics = false;
5✔
157
    ingame.showBQ = false;
5✔
158
    ingame.showNames = false;
5✔
159
    ingame.showProductivity = false;
5✔
160
    ingame.minimapExtended = false;
5✔
161
    // }
162

163
    // windows
164
    // {
165
    for(const auto& window : persistentWindows)
90✔
166
        windows.persistentSettings[window.first] = PersistentWindowSettings();
85✔
167
    // }
168
}
5✔
169

170
///////////////////////////////////////////////////////////////////////////////
171
// Routine zum Laden der Konfiguration
172
void Settings::Load()
×
173
{
174
    libsiedler2::Archiv settings;
×
175
    const auto settingsPath = RTTRCONFIG.ExpandPath(s25::resources::config);
×
176
    try
177
    {
178
        if(libsiedler2::Load(settingsPath, settings) != 0 || settings.size() < SECTION_NAMES.size())
×
179
            throw std::runtime_error("File missing or invalid");
×
180

181
        const libsiedler2::ArchivItem_Ini* iniGlobal =
182
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("global"));
×
183
        const libsiedler2::ArchivItem_Ini* iniVideo = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("video"));
×
184
        const libsiedler2::ArchivItem_Ini* iniLanguage =
185
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("language"));
×
186
        const libsiedler2::ArchivItem_Ini* iniDriver =
187
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("driver"));
×
188
        const libsiedler2::ArchivItem_Ini* iniSound = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("sound"));
×
189
        const libsiedler2::ArchivItem_Ini* iniLobby = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("lobby"));
×
190
        const libsiedler2::ArchivItem_Ini* iniServer =
191
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("server"));
×
192
        const libsiedler2::ArchivItem_Ini* iniProxy = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("proxy"));
×
193
        const libsiedler2::ArchivItem_Ini* iniInterface =
194
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("interface"));
×
195
        const libsiedler2::ArchivItem_Ini* iniAddons =
196
          static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("addons"));
×
197

198
        // ist eine der Kategorien nicht vorhanden?
199
        if(!iniGlobal || !iniVideo || !iniLanguage || !iniDriver || !iniSound || !iniLobby || !iniServer || !iniProxy
×
200
           || !iniInterface || !iniAddons)
×
201
        {
202
            throw std::runtime_error("Missing section");
×
203
        }
204
        // stimmt die Settingsversion?
205
        if(iniGlobal->getValue("version", 0) != VERSION)
×
206
            throw std::runtime_error("Wrong version");
×
207

208
        // global
209
        // {
210
        if(iniGlobal->getValue("gameversion") != rttr::version::GetRevision())
×
211
            s25util::warning("Your application version has changed - please recheck your settings!");
×
212

213
        global.submit_debug_data = iniGlobal->getIntValue("submit_debug_data");
×
214
        global.use_upnp = iniGlobal->getBoolValue("use_upnp");
×
215
        global.smartCursor = iniGlobal->getValue("smartCursor", true);
×
216
        global.debugMode = iniGlobal->getValue("debugMode", false);
×
217
        global.showGFInfo = iniGlobal->getValue("showGFInfo", false);
×
218
        // };
219

220
        // video
221
        // {
222
        video.windowedSize.width = iniVideo->getIntValue("windowed_width");
×
223
        video.windowedSize.height = iniVideo->getIntValue("windowed_height");
×
224
        video.fullscreenSize.width = iniVideo->getIntValue("fullscreen_width");
×
225
        video.fullscreenSize.height = iniVideo->getIntValue("fullscreen_height");
×
226
        video.fullscreen = iniVideo->getBoolValue("fullscreen");
×
227
        video.framerate = iniVideo->getValue("framerate", 0);
×
228
        video.vbo = iniVideo->getBoolValue("vbo");
×
229
        video.shared_textures = iniVideo->getBoolValue("shared_textures");
×
230
        // };
231

232
        if(video.fullscreenSize.width == 0 || video.fullscreenSize.height == 0 || video.windowedSize.width == 0
×
233
           || video.windowedSize.height == 0)
×
234
            throw std::runtime_error("Invalid video settings");
×
235

236
        // language
237
        // {
238
        language.language = iniLanguage->getValue("language");
×
239
        // }
240

241
        LANGUAGES.setLanguage(language.language);
×
242

243
        // driver
244
        // {
245
        driver.video = iniDriver->getValue("video");
×
246
        driver.audio = iniDriver->getValue("audio");
×
247
        // }
248

249
        // sound
250
        // {
251
        sound.musicEnabled = iniSound->getBoolValue("musik");
×
252
        sound.musicVolume = iniSound->getIntValue("musik_volume");
×
253
        sound.effectsEnabled = iniSound->getBoolValue("effekte");
×
254
        sound.effectsVolume = iniSound->getIntValue("effekte_volume");
×
255
        sound.playlist = iniSound->getValue("playlist");
×
256
        // }
257

258
        // lobby
259
        // {
260
        lobby.name = iniLobby->getValue("name");
×
261
        lobby.password = iniLobby->getValue("password");
×
262
        lobby.save_password = iniLobby->getBoolValue("save_password");
×
263
        // }
264

265
        if(lobby.name.empty())
×
266
            lobby.name = System::getUserName();
×
267

268
        // server
269
        // {
270
        server.last_ip = iniServer->getValue("last_ip");
×
271
        boost::optional<uint16_t> port = validate::checkPort(iniServer->getValue("local_port"));
×
272
        server.localPort = port.value_or(3665);
×
273
        server.ipv6 = iniServer->getBoolValue("ipv6");
×
274
        // }
275

276
        // proxy
277
        // {
278
        proxy.hostname = iniProxy->getValue("proxy");
×
279
        port = validate::checkPort(iniProxy->getValue("port"));
×
280
        proxy.port = port.value_or(1080);
×
281
        proxy.type = ProxyType(iniProxy->getIntValue("typ"));
×
282
        // }
283

284
        // leere proxyadresse deaktiviert proxy komplett
285
        // deaktivierter proxy entfernt proxyadresse
286
        if(proxy.hostname.empty() || (proxy.type != ProxyType::Socks4 && proxy.type != ProxyType::Socks5))
×
287
        {
288
            proxy.type = ProxyType::None;
×
289
            proxy.hostname.clear();
×
290
        }
291
        // aktivierter Socks v4 deaktiviert ipv6
292
        else if(proxy.type == ProxyType::Socks4 && server.ipv6)
×
293
            server.ipv6 = false;
×
294

295
        // interface
296
        // {
297
        interface.autosave_interval = iniInterface->getIntValue("autosave_interval");
×
298
        interface.revert_mouse = iniInterface->getBoolValue("revert_mouse");
×
299
        // }
300

301
        // addons
302
        // {
303
        for(unsigned addon = 0; addon < iniAddons->size(); ++addon)
×
304
        {
305
            const auto* item = dynamic_cast<const libsiedler2::ArchivItem_Text*>(iniAddons->get(addon));
×
306

307
            if(item)
×
308
                addons.configuration.insert(std::make_pair(s25util::fromStringClassic<unsigned>(item->getName()),
×
309
                                                           s25util::fromStringClassic<unsigned>(item->getText())));
×
310
        }
311

312
        LoadIngame();
×
313
        // }
314
    } catch(std::runtime_error& e)
×
315
    {
316
        s25util::warning(std::string("Could not use settings from \"") + settingsPath.string()
×
317
                         + "\", using default values. Reason: " + e.what());
×
318
        LoadDefaults();
×
319
        Save();
×
320
    }
321
}
×
322

323
void Settings::LoadIngame()
×
324
{
325
    libsiedler2::Archiv settingsIngame;
×
326
    const auto settingsPathIngame = RTTRCONFIG.ExpandPath(s25::resources::ingameOptions);
×
327
    try
328
    {
329
        if(libsiedler2::Load(settingsPathIngame, settingsIngame) != 0)
×
330
            throw std::runtime_error("File missing");
×
331

332
        const libsiedler2::ArchivItem_Ini* iniIngame =
333
          static_cast<libsiedler2::ArchivItem_Ini*>(settingsIngame.find("ingame"));
×
334
        if(!iniIngame)
×
335
            throw std::runtime_error("Missing section");
×
336
        // ingame
337
        // {
338
        ingame.scale_statistics = iniIngame->getBoolValue("scale_statistics");
×
339
        ingame.showBQ = iniIngame->getBoolValue("show_building_quality");
×
340
        ingame.showNames = iniIngame->getBoolValue("show_names");
×
341
        ingame.showProductivity = iniIngame->getBoolValue("show_productivity");
×
342
        ingame.minimapExtended = iniIngame->getBoolValue("minimap_extended");
×
343
        // }
344
        // ingame windows
345
        for(const auto& window : persistentWindows)
×
346
        {
347
            const auto* iniWindow = static_cast<const libsiedler2::ArchivItem_Ini*>(settingsIngame.find(window.second));
×
348
            if(!iniWindow)
×
349
                continue;
×
350
            windows.persistentSettings[window.first].lastPos.x = iniWindow->getIntValue("pos_x");
×
351
            windows.persistentSettings[window.first].lastPos.y = iniWindow->getIntValue("pos_y");
×
352
            windows.persistentSettings[window.first].isOpen = iniWindow->getIntValue("is_open");
×
353
        }
354
    } catch(std::runtime_error& e)
×
355
    {
356
        s25util::warning(std::string("Could not use ingame settings from \"") + settingsPathIngame.string()
×
357
                         + "\", using default values. Reason: " + e.what());
×
358
        LoadIngameDefaults();
×
359
        SaveIngame();
×
360
    }
361
}
×
362

363
///////////////////////////////////////////////////////////////////////////////
364
// Routine zum Speichern der Konfiguration
365
void Settings::Save()
×
366
{
367
    libsiedler2::Archiv settings;
×
368
    settings.alloc(SECTION_NAMES.size());
×
369
    for(unsigned i = 0; i < SECTION_NAMES.size(); ++i)
×
370
        settings.set(i, std::make_unique<libsiedler2::ArchivItem_Ini>(SECTION_NAMES[i]));
×
371

372
    libsiedler2::ArchivItem_Ini* iniGlobal = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("global"));
×
373
    libsiedler2::ArchivItem_Ini* iniVideo = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("video"));
×
374
    libsiedler2::ArchivItem_Ini* iniLanguage = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("language"));
×
375
    libsiedler2::ArchivItem_Ini* iniDriver = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("driver"));
×
376
    libsiedler2::ArchivItem_Ini* iniSound = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("sound"));
×
377
    libsiedler2::ArchivItem_Ini* iniLobby = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("lobby"));
×
378
    libsiedler2::ArchivItem_Ini* iniServer = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("server"));
×
379
    libsiedler2::ArchivItem_Ini* iniProxy = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("proxy"));
×
380
    libsiedler2::ArchivItem_Ini* iniInterface = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("interface"));
×
381
    libsiedler2::ArchivItem_Ini* iniAddons = static_cast<libsiedler2::ArchivItem_Ini*>(settings.find("addons"));
×
382

383
    // ist eine der Kategorien nicht vorhanden?
384
    RTTR_Assert(iniGlobal && iniVideo && iniLanguage && iniDriver && iniSound && iniLobby && iniServer && iniProxy
×
385
                && iniInterface && iniAddons);
386

387
    // global
388
    // {
389
    iniGlobal->setValue("version", VERSION);
×
390
    iniGlobal->setValue("gameversion", rttr::version::GetRevision());
×
391
    iniGlobal->setValue("submit_debug_data", global.submit_debug_data);
×
392
    iniGlobal->setValue("use_upnp", global.use_upnp);
×
393
    iniGlobal->setValue("smartCursor", global.smartCursor);
×
394
    iniGlobal->setValue("debugMode", global.debugMode);
×
395
    iniGlobal->setValue("showGFInfo", global.showGFInfo);
×
396
    // };
397

398
    // video
399
    // {
400
    iniVideo->setValue("fullscreen_width", video.fullscreenSize.width);
×
401
    iniVideo->setValue("fullscreen_height", video.fullscreenSize.height);
×
402
    iniVideo->setValue("windowed_width", video.windowedSize.width);
×
403
    iniVideo->setValue("windowed_height", video.windowedSize.height);
×
404
    iniVideo->setValue("fullscreen", video.fullscreen);
×
405
    iniVideo->setValue("framerate", video.framerate);
×
406
    iniVideo->setValue("vbo", video.vbo);
×
407
    iniVideo->setValue("shared_textures", video.shared_textures);
×
408
    // };
409

410
    // language
411
    // {
412
    iniLanguage->setValue("language", language.language);
×
413
    // }
414

415
    // driver
416
    // {
417
    iniDriver->setValue("video", driver.video);
×
418
    iniDriver->setValue("audio", driver.audio);
×
419
    // }
420

421
    // sound
422
    // {
423
    iniSound->setValue("musik", sound.musicEnabled);
×
424
    iniSound->setValue("musik_volume", sound.musicVolume);
×
425
    iniSound->setValue("effekte", sound.effectsEnabled);
×
426
    iniSound->setValue("effekte_volume", sound.effectsVolume);
×
427
    iniSound->setValue("playlist", sound.playlist);
×
428
    // }
429

430
    // lobby
431
    // {
432
    iniLobby->setValue("name", lobby.name);
×
433
    iniLobby->setValue("password", lobby.password);
×
434
    iniLobby->setValue("save_password", lobby.save_password);
×
435
    // }
436

437
    // server
438
    // {
439
    iniServer->setValue("last_ip", server.last_ip);
×
440
    iniServer->setValue("local_port", server.localPort);
×
441
    iniServer->setValue("ipv6", server.ipv6);
×
442
    // }
443

444
    // proxy
445
    // {
446
    iniProxy->setValue("proxy", proxy.hostname);
×
447
    iniProxy->setValue("port", proxy.port);
×
448
    iniProxy->setValue("typ", static_cast<int>(proxy.type));
×
449
    // }
450

451
    // interface
452
    // {
453
    iniInterface->setValue("autosave_interval", interface.autosave_interval);
×
454
    iniInterface->setValue("revert_mouse", interface.revert_mouse);
×
455
    // }
456

457
    // addons
458
    // {
459
    iniAddons->clear();
×
460
    for(const auto& it : addons.configuration)
×
461
        iniAddons->setValue(s25util::toStringClassic(it.first), s25util::toStringClassic(it.second));
×
462
    // }
463

464
    bfs::path settingsPath = RTTRCONFIG.ExpandPath(s25::resources::config);
×
465
    if(libsiedler2::Write(settingsPath, settings) == 0)
×
466
        bfs::permissions(settingsPath, bfs::owner_read | bfs::owner_write);
×
467

468
    SaveIngame();
×
469
}
×
470

471
void Settings::SaveIngame()
×
472
{
473
    libsiedler2::Archiv settingsIngame;
×
474
    settingsIngame.alloc(1 + persistentWindows.size());
×
475
    settingsIngame.set(0, std::make_unique<libsiedler2::ArchivItem_Ini>("ingame"));
×
476
    unsigned i = 1;
×
477
    for(const auto& window : persistentWindows)
×
478
    {
479
        settingsIngame.set(i, std::make_unique<libsiedler2::ArchivItem_Ini>(window.second));
×
480
        i++;
×
481
    }
482

483
    auto* iniIngame = static_cast<libsiedler2::ArchivItem_Ini*>(settingsIngame.find("ingame"));
×
484

485
    RTTR_Assert(iniIngame);
×
486

487
    // ingame
488
    // {
489
    iniIngame->setValue("scale_statistics", ingame.scale_statistics);
×
490
    iniIngame->setValue("show_building_quality", ingame.showBQ);
×
491
    iniIngame->setValue("show_names", ingame.showNames);
×
492
    iniIngame->setValue("show_productivity", ingame.showProductivity);
×
493
    iniIngame->setValue("minimap_extended", ingame.minimapExtended);
×
494
    // }
495

496
    // ingame windows
497
    for(const auto& window : persistentWindows)
×
498
    {
499
        auto* iniWindow = static_cast<libsiedler2::ArchivItem_Ini*>(settingsIngame.find(window.second));
×
500
        if(!iniWindow)
×
501
            continue;
×
502
        iniWindow->setValue("pos_x", windows.persistentSettings[window.first].lastPos.x);
×
503
        iniWindow->setValue("pos_y", windows.persistentSettings[window.first].lastPos.y);
×
504
        iniWindow->setValue("is_open", windows.persistentSettings[window.first].isOpen);
×
505
    }
506

507
    bfs::path settingsPathIngame = RTTRCONFIG.ExpandPath(s25::resources::ingameOptions);
×
508
    if(libsiedler2::Write(settingsPathIngame, settingsIngame) == 0)
×
509
        bfs::permissions(settingsPathIngame, bfs::owner_read | bfs::owner_write);
×
510
}
×
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