23 #include "demo.h" |
23 #include "demo.h" |
24 #include "misc.h" |
24 #include "misc.h" |
25 #include "ui_demoprompt.h" |
25 #include "ui_demoprompt.h" |
26 #include "prompts.h" |
26 #include "prompts.h" |
27 |
27 |
28 EXTERN_CONFIG (Map, binaryPaths) |
28 // |
29 EXTERN_CONFIG (List, wadpaths) |
29 // ------------------------------------------------------------------------------------------------- |
30 EXTERN_CONFIG (Bool, noprompt) |
30 // |
31 |
31 |
32 static const uint32 g_demoSignature = makeByteID ('Z', 'C', 'L', 'D'); |
32 QString uncolorize (const QString& in) |
33 |
33 { |
34 // ============================================================================= |
34 // TODO: Handle long-form colors like \c[Red] |
35 // ----------------------------------------------------------------------------- |
35 QString out; |
36 str uncolorize (const str& in) { |
|
37 str out; |
|
38 int skip = 0; |
36 int skip = 0; |
39 |
37 |
40 for (const QChar& c : in) { |
38 for (QChar c : in) |
|
39 { |
41 if (skip-- > 0) |
40 if (skip-- > 0) |
42 continue; |
41 continue; |
43 |
42 |
44 if (c == QChar ('\034')) { |
43 if (c == QChar ('\034')) |
|
44 { |
45 skip = 1; |
45 skip = 1; |
46 continue; |
46 continue; |
47 } |
47 } |
48 |
48 |
49 out += c; |
49 out += c; |
50 } |
50 } |
51 |
51 |
52 return out; |
52 return out; |
53 } |
53 } |
54 |
54 |
55 // ============================================================================= |
55 // |
56 // ----------------------------------------------------------------------------- |
56 // ------------------------------------------------------------------------------------------------- |
57 static str tr (const char* msg) { |
57 // |
|
58 |
|
59 static QString tr (const char* msg) |
|
60 { |
58 return QObject::tr (msg); |
61 return QObject::tr (msg); |
59 } |
62 } |
60 |
63 |
61 // ============================================================================= |
64 // |
62 // ----------------------------------------------------------------------------- |
65 // ------------------------------------------------------------------------------------------------- |
63 static void error (str msg) { |
66 // |
64 QMessageBox::critical (null, "Error", msg); |
67 |
65 } |
68 static void error (QString msg) |
66 |
69 { |
67 // ============================================================================= |
70 QMessageBox::critical (NULL, "Error", msg); |
68 // ----------------------------------------------------------------------------- |
71 } |
69 static bool isKnownVersion (str ver) { |
72 |
|
73 // |
|
74 // ------------------------------------------------------------------------------------------------- |
|
75 // |
|
76 |
|
77 static bool isKnownVersion (QString ver) |
|
78 { |
70 for (const QVariant& it : getVersions()) |
79 for (const QVariant& it : getVersions()) |
|
80 { |
71 if (it.toString() == ver || it.toString() + 'M' == ver) |
81 if (it.toString() == ver || it.toString() + 'M' == ver) |
72 return true; |
82 return true; |
|
83 } |
73 |
84 |
74 return false; |
85 return false; |
75 } |
86 } |
76 |
87 |
77 // ============================================================================= |
88 // |
78 // ----------------------------------------------------------------------------- |
89 // ------------------------------------------------------------------------------------------------- |
79 static str findWAD (str name) { |
90 // |
80 if (cfg::wadpaths.size() == 0) { |
91 |
|
92 static QString findWAD (QString name) |
|
93 { |
|
94 QStringList wadpaths = Config::get ("wadpaths").toStringList(); |
|
95 |
|
96 if (wadpaths.empty()) |
|
97 { |
81 error (tr ("No WAD paths configured!")); |
98 error (tr ("No WAD paths configured!")); |
82 |
99 |
83 // Cannot just return an empty string here since that'd trigger |
100 // Cannot just return an empty string here since that'd trigger |
84 // another error prompt - skip ahead and exit. |
101 // another error prompt - skip ahead and exit. |
85 exit (9); |
102 exit (9); |
86 } |
103 } |
87 |
104 |
88 for (const QVariant& it : cfg::wadpaths) { |
105 for (int i = 0; i < wadpaths.size(); ++i) |
89 str fullpath = fmt ("%1/%2", it.toString(), name); |
106 { |
|
107 QString fullpath = QString ("%1/%2").arg (wadpaths[i]).arg (name); |
90 QFile f (fullpath); |
108 QFile f (fullpath); |
91 |
109 |
92 if (f.exists()) |
110 if (f.exists()) |
93 return fullpath; |
111 return fullpath; |
94 } |
112 } |
95 |
113 |
96 return ""; |
114 return ""; |
97 } |
115 } |
98 |
116 |
99 // ============================================================================= |
117 // |
100 // ----------------------------------------------------------------------------- |
118 // ------------------------------------------------------------------------------------------------- |
101 str readString (QDataStream& stream) { |
119 // |
102 str out; |
120 |
103 uint8 c; |
121 QString readString (QDataStream& stream) |
104 |
122 { |
105 for (stream >> c; c != '\0'; stream >> c) |
123 QString out; |
106 out += c; |
124 uint8 ch; |
107 |
125 |
|
126 for (stream >> ch; ch != 0; stream >> ch) |
|
127 out += QChar (ch); |
|
128 |
108 return out; |
129 return out; |
109 } |
130 } |
110 |
131 |
111 // ============================================================================= |
132 // |
112 // ----------------------------------------------------------------------------- |
133 // ------------------------------------------------------------------------------------------------- |
113 int launchDemo (str path) { |
134 // |
|
135 |
|
136 struct UserInfo |
|
137 { |
|
138 QString netname; |
|
139 QString skin; |
|
140 QString className; |
|
141 uint32 color; |
|
142 uint32 aimdist; |
|
143 uint32 railcolor; |
|
144 uint8 gender; |
|
145 uint8 handicap; |
|
146 uint8 unlagged; |
|
147 uint8 respawnOnFire; |
|
148 uint8 ticsPerUpdate; |
|
149 uint8 connectionType; |
|
150 }; |
|
151 |
|
152 int launchDemo (QString path) |
|
153 { |
114 QFile f (path); |
154 QFile f (path); |
115 |
155 |
116 if (!f.open (QIODevice::ReadOnly)) { |
156 if (not f.open (QIODevice::ReadOnly)) |
117 error (fmt (tr ("Couldn't open '%1' for reading: %2"), path, strerror (errno))); |
157 { |
|
158 error (tr ("Couldn't open '%1' for reading: %2").arg (path).arg (strerror (errno))); |
118 return 1; |
159 return 1; |
119 } |
160 } |
120 |
161 |
121 QDataStream stream (&f); |
162 QDataStream stream (&f); |
122 stream.setByteOrder (QDataStream::LittleEndian); |
163 stream.setByteOrder (QDataStream::LittleEndian); |
123 |
164 |
124 uint8 offset; |
165 uint8 offset; |
125 uint32 length; |
166 uint32 length; |
126 uint16 zanversionID, numWads; |
167 uint16 zanversionID, numWads; |
127 uint32 longSink; |
168 uint32 longSink; |
128 str zanversion; |
169 QString zanversion; |
129 list<str> wads; |
170 QStringList wads; |
130 BuildType buildID; |
171 UserInfo userinfo; |
|
172 |
|
173 // Assume a release build if the build ID not supplied. The demo only got the "ZCLD" signature |
|
174 // in the 1.1 release build, 1.1.1 had no testing binaries and the build ID is included in 1.2 |
|
175 // onward. |
|
176 BuildType buildID = ReleaseBuild; |
|
177 |
131 bool ready = false; |
178 bool ready = false; |
132 |
179 |
133 struct { |
|
134 str netname, skin, className; |
|
135 uint8 gender, handicap, unlagged, respawnOnFire, ticsPerUpdate, connectionType; |
|
136 uint32 color, aimdist, railcolor; |
|
137 } userinfo; |
|
138 |
|
139 // Check signature |
180 // Check signature |
140 { |
181 { |
141 uint32 sig; |
182 uint32 demosignature; |
142 |
183 stream >> demosignature; |
143 stream >> sig; |
184 |
144 if (sig != g_demoSignature) { |
185 if (demosignature != makeByteID ('Z', 'C', 'L', 'D')) |
145 error (fmt (tr ("'%1' is not a Zandronum demo file!"), path)); |
186 { |
146 return 3; |
187 error (tr ("'%1' is not a valid Zandronum demo file!").arg (path)); |
|
188 return 1; |
147 } |
189 } |
148 } |
190 } |
149 |
191 |
150 // Zandronum stores CLD_DEMOLENGTH after the signature. This is also the |
192 // Zandronum stores CLD_DEMOLENGTH after the signature. This is also the |
151 // first demo enumerator, so we can determine the offset (which is variable!) |
193 // first demo enumerator, so we can determine the offset (which is variable!) |
152 // from this byte |
194 // from this byte |
153 stream >> offset |
195 stream >> offset |
154 >> length; |
196 >> length; |
155 |
197 |
156 // Read the demo header and get data |
198 // Read the demo header and get data |
157 for (;;) { |
199 for (;;) |
|
200 { |
158 uint8 header; |
201 uint8 header; |
159 stream >> header; |
202 stream >> header; |
160 |
203 |
161 if (header == DemoBodyStart + offset) { |
204 if (header == DemoBodyStart + offset) |
|
205 { |
162 ready = true; |
206 ready = true; |
163 break; |
207 break; |
164 } elif (header == DemoVersion + offset) { |
208 } |
|
209 else if (header == DemoVersion + offset) |
|
210 { |
165 stream >> zanversionID; |
211 stream >> zanversionID; |
166 zanversion = readString (stream); |
212 zanversion = readString (stream); |
167 |
213 |
168 if (!zanversion.startsWith ("1.1-") && !zanversion.startsWith ("1.1.1-")) { |
214 if (not zanversion.startsWith ("1.1-") and not zanversion.startsWith ("1.1.1-")) |
|
215 { |
169 uint8 a; |
216 uint8 a; |
170 stream >> a; |
217 stream >> a; |
171 buildID = (BuildType) a; |
218 buildID = (BuildType) a; |
172 } else { |
|
173 // Assume a release build if not supplied. The demo only got the |
|
174 // "ZCLD" signature in the 1.1 release build, 1.1.1 had no testing |
|
175 // binaries and the build ID is included in 1.2 onward. |
|
176 buildID = ReleaseBuild; |
|
177 } |
219 } |
178 |
220 |
179 stream >> longSink; // rng seed - we don't need it |
221 stream >> longSink; // rng seed - we don't need it |
180 } elif (header == DemoUserInfo + offset) { |
222 } |
|
223 else if (header == DemoUserInfo + offset) |
|
224 { |
181 userinfo.netname = readString (stream); |
225 userinfo.netname = readString (stream); |
182 stream >> userinfo.gender |
226 stream >> userinfo.gender; |
183 >> userinfo.color |
227 stream >> userinfo.color; |
184 >> userinfo.aimdist; |
228 stream >> userinfo.aimdist; |
185 userinfo.skin = readString (stream); |
229 userinfo.skin = readString (stream); |
186 stream >> userinfo.railcolor |
230 stream >> userinfo.railcolor; |
187 >> userinfo.handicap |
231 stream >> userinfo.handicap; |
188 >> userinfo.unlagged |
232 stream >> userinfo.unlagged; |
189 >> userinfo.respawnOnFire |
233 stream >> userinfo.respawnOnFire; |
190 >> userinfo.ticsPerUpdate |
234 stream >> userinfo.ticsPerUpdate; |
191 >> userinfo.connectionType; |
235 stream >> userinfo.connectionType; |
192 userinfo.className = readString (stream); |
236 userinfo.className = readString (stream); |
193 } elif (header == DemoWads + offset) { |
237 } |
194 str sink; |
238 else if (header == DemoWads + offset) |
|
239 { |
|
240 QString sink; |
195 stream >> numWads; |
241 stream >> numWads; |
196 |
242 |
197 for (uint8 i = 0; i < numWads; ++i) { |
243 for (uint8 i = 0; i < numWads; ++i) { |
198 str wad = readString (stream); |
244 QString wad = readString (stream); |
199 wads << wad; |
245 wads << wad; |
200 } |
246 } |
201 |
247 |
202 // The demo has two checksum strings. We're not interested |
248 // The demo has two checksum strings. We're not interested |
203 // in them though. Down the sink they go... |
249 // in them though. Down the sink they go... |
204 for (int i = 0; i < 2; ++i) |
250 for (int i = 0; i < 2; ++i) |
205 sink = readString (stream); |
251 sink = readString (stream); |
206 } else { |
252 } |
207 error (fmt (tr ("Unknown header %1!\n"), (int) header)); |
253 else |
208 return 3; |
254 { |
209 } |
255 error (tr ("Unknown header %1!\n").arg (int (header))); |
210 } |
256 return 1; |
211 |
257 } |
212 if (!ready) { |
258 } |
213 error (fmt (tr ("Incomplete demo header in '%s'!"), path)); |
259 |
214 return 3; |
260 if (not ready) |
215 } |
261 { |
216 |
262 error (tr ("Incomplete demo header in '%s'!").arg (path)); |
217 if (!isKnownVersion (zanversion)) { |
263 return 1; |
218 UnknownVersionPrompt* prompt = new UnknownVersionPrompt (path, zanversion, |
264 } |
219 (buildID == ReleaseBuild)); |
265 |
220 |
266 if (not isKnownVersion (zanversion)) |
221 if (!prompt->exec()) |
267 { |
222 return 6; |
268 QDialog* prompt = new UnknownVersionPrompt (path, zanversion, (buildID == ReleaseBuild)); |
223 |
269 |
224 if (!isKnownVersion (zanversion)) { |
270 if (not prompt->exec()) |
225 error (tr ("Failure in configuration! This shouldn't happen.")); |
271 return 1; |
226 return 6; |
272 } |
227 } |
273 |
228 } |
274 QString binarypath = Config::get ("binarypaths").toMap()[zanversion].toString(); |
229 |
275 |
230 str binarypath = cfg::binaryPaths [zanversion].toString(); |
276 if (binarypath.isEmpty()) |
231 |
277 { |
232 if (binarypath.isEmpty()) { |
278 error (tr ("No binary path specified for Zandronum version %1!").arg (zanversion)); |
233 error (fmt (tr ("No binary path specified for Zandronum version %1!"), zanversion)); |
279 return 1; |
234 return 7; |
280 } |
235 } |
281 |
236 |
282 QString iwadpath; |
237 str iwadpath; |
283 QStringList pwadpaths; |
238 list<str> pwadpaths; |
|
239 |
284 |
240 // Find the WADs |
285 // Find the WADs |
241 for (const str& wad : wads) { |
286 for (const QString& wad : wads) |
242 str path = findWAD (wad); |
287 { |
243 |
288 QString path = findWAD (wad); |
244 // IWAD names can appear in uppercase too. Linux is case-sensitive, so.. |
289 |
245 if (&wad == &wads[0] && path.isEmpty()) |
290 // WAD names are case-sensitive under non-Windows and they can appear in uppercase |
|
291 // so we need to test that too. |
|
292 if (path.isEmpty()) |
246 path = findWAD (wad.toUpper()); |
293 path = findWAD (wad.toUpper()); |
247 |
294 |
248 if (path.isEmpty()) { |
295 if (path.isEmpty()) |
249 error (fmt (tr ("Couldn't find %1!"), wad)); |
296 { |
250 return 8; |
297 error (tr ("Couldn't find %1!").arg (wad)); |
251 } |
298 return 1; |
252 |
299 } |
253 if (&wad == &wads[0]) |
300 |
|
301 if (&wad == &wads.first()) |
254 iwadpath = path; |
302 iwadpath = path; |
255 else |
303 else |
256 pwadpaths << path; |
304 pwadpaths << path; |
257 } |
305 } |
258 |
306 |
259 if (!cfg::noprompt) { |
307 if (not Config::get ("noprompt").toBool()) |
260 str pwadtext; |
308 { |
261 |
309 QString pwadtext; |
262 for (const str& pwad : wads) { |
310 |
263 if (&pwad == &wads[0]) |
311 for (const QString& wad : wads) |
|
312 { |
|
313 if (&wad == &wads.first()) |
264 continue; // skip the IWAD |
314 continue; // skip the IWAD |
265 |
315 |
266 if (!pwadtext.isEmpty()) |
316 if (not pwadtext.isEmpty()) |
267 pwadtext += "<br />"; |
317 pwadtext += "<br />"; |
268 |
318 |
269 pwadtext += pwad; |
319 pwadtext += wad; |
270 } |
320 } |
271 |
321 |
272 QDialog* dlg = new QDialog; |
322 QDialog* dlg = new QDialog; |
273 Ui_DemoPrompt ui; |
323 Ui_DemoPrompt ui; |
274 ui.setupUi (dlg); |
324 ui.setupUi (dlg); |