Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
PlayerSelectionHistory.cpp
Go to the documentation of this file.
2
3#include <SDL3/SDL.h>
4#include <algorithm>
5#include <charconv>
6#include <fstream>
7#include <limits>
8#include <sstream>
9#ifdef _WIN32
10#define WIN32_LEAN_AND_MEAN
11#define NOMINMAX
12#include <Windows.h>
13#endif
14
16{
17 namespace
18 {
19 std::string narrow(std::wstring_view input)
20 {
21 const std::wstring value(input);
22 char* result=SDL_iconv_string("UTF-8","WCHAR_T",reinterpret_cast<const char*>(value.c_str()),(value.size()+1)*sizeof(wchar_t));
23 if(!result)return {};
24 std::string output(result);SDL_free(result);return output;
25 }
26 std::wstring widen(std::string_view input)
27 {
28 const std::string value(input);
29 char* result=SDL_iconv_string("WCHAR_T","UTF-8",value.c_str(),value.size()+1);
30 if(!result)return {};
31 std::wstring output(reinterpret_cast<const wchar_t*>(result));SDL_free(result);return output;
32 }
33 int number(std::string_view input)
34 {
35 int value{};
36 const auto parsed=std::from_chars(input.data(),input.data()+input.size(),value);
37 return parsed.ec==std::errc{}?value:0;
38 }
39 std::string_view trim(std::string_view value)
40 {
41 const auto begin=value.find_first_not_of(" \t\r");
42 if(begin==std::string_view::npos)return {};
43 return value.substr(begin,value.find_last_not_of(" \t\r")-begin+1);
44 }
45 }
46
47 std::expected<void,std::string> PlayerSelectionHistory::open(std::filesystem::path path)
48 {
49 if(path.empty())return std::unexpected("Player history path is empty");
50 std::array<HistoryEntry,100> entries{};
51 std::string preserved;
52 std::error_code error;
53 const bool exists=std::filesystem::exists(path,error);
54 if(error)return std::unexpected(error.message());
55 if(exists)
56 {
57 if(std::filesystem::file_size(path,error)>4*1024*1024 || error)
58 return std::unexpected("Player history INI exceeds read limit");
59 std::ifstream file(path,std::ios::binary);
60 if(!file)return std::unexpected("Cannot read player history INI");
61 std::string line;int section=-1;
62 while(std::getline(file,line))
63 {
64 auto value=trim(line);
65 if(value.size()>2 && value.front()=='[' && value.back()==']')
66 {
67 const auto name=value.substr(1,value.size()-2);
68 section=-1;
69 if(name.starts_with("Player"))
70 {
71 const auto suffix=name.substr(6);
72 int n{};
73 const auto parsed=std::from_chars(suffix.data(),suffix.data()+suffix.size(),n);
74 if(parsed.ec==std::errc{} && parsed.ptr==suffix.data()+suffix.size() && n>=1 && n<=100)section=n-1;
75 }
76 }
77 if(section<0){preserved+=line;preserved+='\n';continue;}
78 const auto equals=value.find('=');
79 if(equals==std::string_view::npos)continue;
80 const auto key=trim(value.substr(0,equals));
81 const auto content=trim(value.substr(equals+1));
82 auto& entry=entries[static_cast<std::size_t>(section)];
83 if(key=="Name")entry.name=widen(content).substr(0,255);
84 else if(key=="Wins")entry.wins=number(content);
85 else if(key=="GreatestNetWorth")entry.greatestNetWorth=number(content);
86 }
87 if(file.bad())return std::unexpected("Failed reading player history INI");
88 }
89 entries_=std::move(entries);preserved_=std::move(preserved);path_=std::move(path);
90 winnerProcessed_=false;
91 return {};
92 }
93
94 std::expected<void,std::string> PlayerSelectionHistory::save(const std::array<HistoryEntry,100>& entries)
95 {
96 if(!configured())return std::unexpected("Player history is not configured");
97 std::error_code error;
98 if(!path_.parent_path().empty())std::filesystem::create_directories(path_.parent_path(),error);
99 if(error)return std::unexpected(error.message());
100 auto temporary=path_;temporary+=".pending";
101 {
102 std::ofstream file(temporary,std::ios::binary|std::ios::trunc);
103 if(!file)return std::unexpected("Cannot write player history temporary INI");
104 file<<preserved_;
105 for(std::size_t i=0;i<entries.size();++i)
106 {
107 const auto& entry=entries[i];if(entry.name.empty())continue;
108 auto name=narrow(entry.name);
109 if(name.find_first_of("\r\n")!=std::string::npos)return std::unexpected("Player history name contains a line break");
110 file<<"[Player"<<i+1<<"]\r\nName="<<name<<"\r\nWins="<<entry.wins
111 <<"\r\nGreatestNetWorth="<<entry.greatestNetWorth<<"\r\n";
112 }
113 file.flush();if(!file)return std::unexpected("Failed writing player history INI");
114 }
115#ifdef _WIN32
117 return std::unexpected("Cannot publish player history INI: "+std::to_string(GetLastError()));
118#else
119 std::filesystem::rename(temporary,path_,error);
120 if(error)return std::unexpected(error.message());
121#endif
122 return {};
123 }
124
125 std::expected<void,std::string> PlayerSelectionHistory::gameStarted(std::span<const HistoryPlayer> players)
126 {
127 auto next=entries_;
128 for(const auto& player:players)
129 {
130 // Exact active UDPSEL_GameHasJustStarted condition (including its
131 // remote-AI quirk): skip only an AI whose slot is local.
132 if(player.name.empty() || (player.aiLevel!=0 && player.local))continue;
133 if(std::any_of(next.begin(),next.end(),[&](const auto& e){return e.name==player.name;}))continue;
134 auto free=std::find_if(next.begin(),next.end(),[](const auto& e){return e.name.empty();});
135 if(free==next.end())free=next.begin();
136 *free={player.name,0,0};
137 }
138 if(auto r=save(next);!r)return r;
139 entries_=std::move(next);winnerProcessed_=false;return {};
140 }
141
142 std::expected<void,std::string> PlayerSelectionHistory::gameOver(std::wstring_view name,int worth,int aiLevel)
143 {
144 if(winnerProcessed_)return {};
145 auto next=entries_;
146 if(aiLevel==0)
147 {
148 auto winner=std::find_if(next.begin(),next.end(),[&](const auto& e){return e.name==name;});
149 if(winner!=next.end())
150 {
151 if(winner->wins<std::numeric_limits<int>::max())++winner->wins;
152 winner->greatestNetWorth=std::max(winner->greatestNetWorth,worth);
153 if(auto r=save(next);!r)return r;
154 }
155 }
156 entries_=std::move(next);winnerProcessed_=true;return {};
157 }
158
159 std::vector<HistoryEntry> PlayerSelectionHistory::highScores() const
160 {
161 std::vector<HistoryEntry> result;
162 for(const auto& entry:entries_)if(!entry.name.empty() && entry.wins>0)result.push_back(entry);
163 std::stable_sort(result.begin(),result.end(),[](const auto& a,const auto& b)
164 {return a.wins>b.wins || (a.wins==b.wins && a.greatestNetWorth>b.greatestNetWorth);});
165 if(result.size()>8)result.resize(8);
166 return result;
167 }
168 std::vector<std::wstring> PlayerSelectionHistory::names() const
169 {
170 std::vector<std::wstring> result;
171 for(const auto& entry:entries_)if(!entry.name.empty())result.push_back(entry.name.substr(0,10));
172 return result;
173 }
174}
std::array< rules::PlayerNumber, rules::MaxPlayers > players
const char * name
std::uint64_t key
unsigned index
std::string path
std::string error
Bytes input
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
const char * suffix
const std::array< HistoryEntry, 100 > & entries() const noexcept
std::expected< void, std::string > gameStarted(std::span< const HistoryPlayer > players)
std::expected< void, std::string > gameOver(std::wstring_view name, int worth, int aiLevel)
std::expected< void, std::string > open(std::filesystem::path path)