1 /** @file sprites.cpp Sprites.
2 * @ingroup resource
3 *
4 * @authors Copyright © 2013-2015 Daniel Swanson <danij@dengine.net>
5 * @authors Copyright © 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
6 *
7 * @par License
8 * GPL: http://www.gnu.org/licenses/gpl.html
9 *
10 * <small>This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by the
12 * Free Software Foundation; either version 2 of the License, or (at your
13 * option) any later version. This program is distributed in the hope that it
14 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16 * Public License for more details. You should have received a copy of the GNU
17 * General Public License along with this program; if not, see:
18 * http://www.gnu.org/licenses</small>
19 */
20
21 #include "doomsday/resource/sprites.h"
22 #include "doomsday/resource/resources.h"
23 #include "doomsday/resource/textures.h"
24 #include "doomsday/defs/ded.h"
25 #include "doomsday/defs/sprite.h"
26
27 #include <de/types.h>
28 #include <QMap>
29
30 namespace res {
31
32 using namespace de;
33
DENG2_PIMPL_NOREF(Sprites)34 DENG2_PIMPL_NOREF(Sprites)
35 {
36 QHash<spritenum_t, SpriteSet> sprites;
37
38 ~Impl()
39 {
40 sprites.clear();
41 }
42
43 inline bool hasSpriteSet(spritenum_t id) const
44 {
45 return sprites.contains(id);
46 }
47
48 SpriteSet *tryFindSpriteSet(spritenum_t id) const
49 {
50 auto found = sprites.constFind(id);
51 return (found != sprites.constEnd()? const_cast<SpriteSet *>(&found.value()) : nullptr);
52 }
53
54 SpriteSet &findSpriteSet(spritenum_t id)
55 {
56 if (SpriteSet *frames = tryFindSpriteSet(id)) return *frames;
57 /// @throw MissingResourceError An unknown/invalid id was specified.
58 throw Resources::MissingResourceError("Sprites::findSpriteSet",
59 "Unknown sprite id " + String::number(id));
60 }
61
62 SpriteSet &addSpriteSet(spritenum_t id, SpriteSet const &frames)
63 {
64 DENG2_ASSERT(!tryFindSpriteSet(id)); // sanity check.
65 return sprites.insert(id, frames).value();
66 }
67 };
68
Sprites()69 Sprites::Sprites()
70 : d(new Impl)
71 {}
72
clear()73 void Sprites::clear()
74 {
75 d->sprites.clear();
76 }
77
addSpriteSet(spritenum_t id,SpriteSet const & frames)78 Sprites::SpriteSet &Sprites::addSpriteSet(spritenum_t id, SpriteSet const &frames)
79 {
80 return d->addSpriteSet(id, frames);
81 }
82
spriteCount() const83 dint Sprites::spriteCount() const
84 {
85 return d->sprites.count();
86 }
87
hasSprite(spritenum_t id,dint frame) const88 bool Sprites::hasSprite(spritenum_t id, dint frame) const
89 {
90 if (SpriteSet const *frames = d.getConst()->tryFindSpriteSet(id))
91 {
92 return frames->contains(frame);
93 }
94 return false;
95 }
96
sprite(spritenum_t id,dint frame)97 defn::CompiledSpriteRecord &Sprites::sprite(spritenum_t id, dint frame)
98 {
99 return d->findSpriteSet(id).find(frame).value();
100 }
101
spritePtr(spritenum_t id,de::dint frame) const102 defn::CompiledSpriteRecord const *Sprites::spritePtr(spritenum_t id, de::dint frame) const
103 {
104 if (Sprites::SpriteSet const *sprSet = tryFindSpriteSet(id))
105 {
106 auto found = sprSet->find(frame);
107 if (found != sprSet->end()) return &found.value();
108 }
109 return nullptr;
110 }
111
tryFindSpriteSet(spritenum_t id) const112 Sprites::SpriteSet const *Sprites::tryFindSpriteSet(spritenum_t id) const
113 {
114 return d->tryFindSpriteSet(id);
115 }
116
spriteSet(spritenum_t id) const117 Sprites::SpriteSet const &Sprites::spriteSet(spritenum_t id) const
118 {
119 return d->findSpriteSet(id);
120 }
121
122 struct SpriteFrameDef
123 {
124 bool mirrored = false;
125 dint angle = 0;
126 String material;
127 };
128
129 // Tempory storage, used when reading sprite definitions.
130 typedef QMultiMap<dint, SpriteFrameDef> SpriteFrameDefs; ///< frame => frame angle def.
131 typedef QHash<String, SpriteFrameDefs> SpriteDefs; ///< sprite name => frame set.
132
133 /**
134 * In DOOM, a sprite frame is a patch texture contained in a lump existing between
135 * the S_START and S_END marker lumps (in WAD) whose lump name matches the following
136 * pattern:
137 *
138 * NAME|A|R(A|R) (for example: "TROOA0" or "TROOA2A8")
139 *
140 * NAME: Four character name of the sprite.
141 * A: Animation frame ordinal 'A'... (ASCII).
142 * R: Rotation angle 0...G
143 * 0 : Use this frame for ALL angles.
144 * 1...8: Angle of rotation in 45 degree increments.
145 * A...G: Angle of rotation in 22.5 degree increments.
146 *
147 * The second set of (optional) frame and rotation characters instruct that the
148 * same sprite frame is to be used for an additional frame but that the sprite
149 * patch should be flipped horizontally (right to left) during the loading phase.
150 *
151 * Sprite view 0 is facing the viewer, rotation 1 is one half-angle turn CLOCKWISE
152 * around the axis. This is not the same as the angle, which increases
153 * counter clockwise (protractor).
154 */
buildSpriteFramesFromTextures(res::TextureScheme::Index const & texIndex)155 static SpriteDefs buildSpriteFramesFromTextures(res::TextureScheme::Index const &texIndex)
156 {
157 static dint const NAME_LENGTH = 4;
158
159 SpriteDefs frameSets;
160 frameSets.reserve(texIndex.leafNodes().count() / 8); // overestimate.
161
162 PathTreeIterator<res::TextureScheme::Index> iter(texIndex.leafNodes());
163 while (iter.hasNext())
164 {
165 res::TextureManifest const &texManifest = iter.next();
166
167 String const material = de::Uri("Sprites", texManifest.path()).compose();
168 // Decode the sprite frame descriptor.
169 String const desc = QString(QByteArray::fromPercentEncoding(texManifest.path().toUtf8()));
170
171 // Find/create a new sprite frame set.
172 String const spriteName = desc.left(NAME_LENGTH).toLower();
173 SpriteFrameDefs *frames = nullptr;
174 if (frameSets.contains(spriteName))
175 {
176 frames = &frameSets.find(spriteName).value();
177 }
178 else
179 {
180 frames = &frameSets.insert(spriteName, SpriteFrameDefs()).value();
181 }
182
183 // The descriptor may define either one or two frames.
184 bool const haveMirror = desc.length() >= 8;
185 for (dint i = 0; i < (haveMirror ? 2 : 1); ++i)
186 {
187 dint const frameNumber = desc.at(NAME_LENGTH + i * 2).toUpper().unicode() - QChar('A').unicode();
188 dint const angleNumber = Sprites::toSpriteAngle(desc.at(NAME_LENGTH + i * 2 + 1));
189
190 if (frameNumber < 0) continue;
191
192 // Find/create a new frame.
193 SpriteFrameDef *frame = nullptr;
194 auto found = frames->find(frameNumber);
195 if (found != frames->end() && found.value().angle == angleNumber)
196 {
197 frame = &found.value();
198 }
199 else
200 {
201 // Create a new frame.
202 frame = &frames->insert(frameNumber, SpriteFrameDef()).value();
203 }
204
205 // (Re)Configure the frame.
206 DENG2_ASSERT(frame);
207 frame->material = material;
208 frame->angle = angleNumber;
209 frame->mirrored = i == 1;
210 }
211 }
212
213 return frameSets;
214 }
215
216 /**
217 * Generates a set of Sprites from the given @a frameSet.
218 *
219 * @note Gaps in the frame number range will be filled with dummy Sprite instances
220 * (no view angles added).
221 *
222 * @param frameDefs SpriteFrameDefs to process.
223 *
224 * @return Newly built sprite frame-number => definition map.
225 *
226 * @todo Don't do this here (no need for two-stage construction). -ds
227 */
buildSprites(QMultiMap<dint,SpriteFrameDef> const & frameDefs)228 static Sprites::SpriteSet buildSprites(QMultiMap<dint, SpriteFrameDef> const &frameDefs)
229 {
230 static de::dint const MAX_ANGLES = 16;
231
232 Sprites::SpriteSet frames;
233
234 // Build initial Sprites and add views.
235 for (auto it = frameDefs.constBegin(); it != frameDefs.constEnd(); ++it)
236 {
237 defn::CompiledSpriteRecord *rec = nullptr;
238 auto found = frames.find(it.key());
239 if (found != frames.end())
240 {
241 rec = &found.value();
242 }
243 else
244 {
245 rec = &frames[it.key()];
246 defn::Sprite(*rec).resetToDefaults();
247 }
248
249 SpriteFrameDef const &def = it.value();
250 defn::Sprite(*rec).addView(def.material, def.angle, def.mirrored);
251 }
252
253 // Duplicate views to complete angle sets (if defined).
254 for (defn::CompiledSpriteRecord &rec : frames)
255 {
256 defn::Sprite sprite(rec);
257
258 if (sprite.viewCount() < 2)
259 continue;
260
261 for (dint angle = 0; angle < MAX_ANGLES / 2; ++angle)
262 {
263 if (!sprite.hasView(angle * 2 + 1) && sprite.hasView(angle * 2))
264 {
265 auto src = sprite.view(angle * 2);
266 sprite.addView(src.material->asText(), angle * 2 + 2, src.mirrorX);
267 }
268 if (!sprite.hasView(angle * 2) && sprite.hasView(angle * 2 + 1))
269 {
270 auto src = sprite.view(angle * 2 + 1);
271 sprite.addView(src.material->asText(), angle * 2 + 1, src.mirrorX);
272 }
273 }
274 }
275
276 return frames;
277 }
278
initSprites()279 void Sprites::initSprites()
280 {
281 LOG_AS("Sprites");
282 LOG_RES_VERBOSE("Building sprites...");
283
284 Time begunAt;
285
286 clear();
287
288 // Build Sprite sets from their definitions.
289 /// @todo It should no longer be necessary to split this into two phases -ds
290 dint customIdx = 0;
291 SpriteDefs spriteDefs = buildSpriteFramesFromTextures(res::Textures::get().textureScheme("Sprites").index());
292 for (auto it = spriteDefs.constBegin(); it != spriteDefs.constEnd(); ++it)
293 {
294 // Lookup the id for the named sprite.
295 spritenum_t id = DED_Definitions()->getSpriteNum(it.key());
296 if (id == -1)
297 {
298 // Assign a new id from the end of the range.
299 id = (DED_Definitions()->sprites.size() + customIdx++);
300 }
301
302 // Build a Sprite (frame) set from these definitions.
303 addSpriteSet(id, buildSprites(it.value()));
304 }
305
306 // We're done with the definitions.
307 spriteDefs.clear();
308
309 LOG_RES_VERBOSE("Sprites built in %.2f seconds") << begunAt.since();
310 }
311
toSpriteAngle(QChar angleCode)312 dint Sprites::toSpriteAngle(QChar angleCode) // static
313 {
314 static dint const MAX_ANGLES = 16;
315
316 dint angle = -1; // Unknown.
317 if (angleCode.isDigit())
318 {
319 angle = angleCode.digitValue();
320 }
321 else if (angleCode.isLetter())
322 {
323 char charCodeLatin1 = angleCode.toUpper().toLatin1();
324 if (charCodeLatin1 >= 'A')
325 {
326 angle = charCodeLatin1 - 'A' + 10;
327 }
328 }
329
330 if (angle < 0 || angle > MAX_ANGLES)
331 return -1;
332
333 if (angle == 0) return 0;
334
335 if (angle <= MAX_ANGLES / 2)
336 {
337 return (angle - 1) * 2 + 1;
338 }
339 else
340 {
341 return (angle - 9) * 2 + 2;
342 }
343 }
344
isValidSpriteName(String name)345 bool Sprites::isValidSpriteName(String name) // static
346 {
347 if (name.length() < 6) return false;
348
349 // Character at position 5 is a view (angle) index.
350 if (toSpriteAngle(name.at(5)) < 0) return false;
351
352 // If defined, the character at position 7 is also a rotation number.
353 return (name.length() <= 7 || toSpriteAngle(name.at(7)) >= 0);
354 }
355
get()356 Sprites &Sprites::get() // static
357 {
358 return Resources::get().sprites();
359 }
360
361 } // namespace res
362