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