1 /** @file materials.cpp
2  *
3  * @authors Copyright (c) 2016-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  *
5  * @par License
6  * GPL: http://www.gnu.org/licenses/gpl.html
7  *
8  * <small>This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by the
10  * Free Software Foundation; either version 2 of the License, or (at your
11  * option) any later version. This program is distributed in the hope that it
12  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14  * Public License for more details. You should have received a copy of the GNU
15  * General Public License along with this program; if not, see:
16  * http://www.gnu.org/licenses</small>
17  */
18 
19 #include "doomsday/world/materials.h"
20 #include "doomsday/world/MaterialScheme"
21 #include "doomsday/world/MaterialManifest"
22 #include "doomsday/world/world.h"
23 #include "doomsday/resource/resources.h"
24 
25 #include <de/memory.h>
26 #include <unordered_set>
27 
28 using namespace de;
29 
30 namespace world {
31 
32 struct SchemeHashKey
33 {
34     String scheme;
35 
SchemeHashKeyworld::SchemeHashKey36     SchemeHashKey(String const &s) : scheme(s) {}
operator ==world::SchemeHashKey37     bool operator == (SchemeHashKey const &other) const {
38         return !scheme.compare(other.scheme, Qt::CaseInsensitive);
39     }
40 };
41 
qHash(SchemeHashKey const & key)42 uint qHash(SchemeHashKey const &key)
43 {
44     return key.scheme.at(1).toLower().unicode();
45 }
46 
DENG2_PIMPL(Materials)47 DENG2_PIMPL(Materials)
48 , DENG2_OBSERVES(MaterialScheme,   ManifestDefined)
49 , DENG2_OBSERVES(MaterialManifest, MaterialDerived)
50 , DENG2_OBSERVES(MaterialManifest, Deletion)
51 , DENG2_OBSERVES(Material,         Deletion)
52 {
53     /// System subspace schemes containing the manifests/resources.
54     QHash<SchemeHashKey, MaterialScheme *> materialSchemes;
55     QList<MaterialScheme *> materialSchemeCreationOrder;
56 
57     QList<Material *> materials;       ///< From all schemes.
58     int materialManifestCount = 0;     ///< Total number of material manifests (in all schemes).
59 
60     std::unordered_set<Material *> animatedMaterialsSubset; ///< Subset of materials (not owned) that need to animate.
61 
62     MaterialManifestGroups materialGroups;
63 
64     uint materialManifestIdMapSize = 0;
65     MaterialManifest **materialManifestIdMap = nullptr;  ///< Index with materialid_t-1
66 
67     Impl(Public *i) : Base(i)
68     {
69         /// @note Order here defines the ambigious-URI search order.
70         createMaterialScheme("Sprites");
71         createMaterialScheme("Textures");
72         createMaterialScheme("Flats");
73         createMaterialScheme("System");
74     }
75 
76     ~Impl()
77     {
78         self().clearAllMaterialGroups();
79         self().clearAllMaterialSchemes();
80         clearMaterialManifests();
81     }
82 
83     void clearMaterialManifests()
84     {
85         qDeleteAll(materialSchemes);
86         materialSchemes.clear();
87         materialSchemeCreationOrder.clear();
88 
89         // Clear the manifest index/map.
90         if (materialManifestIdMap)
91         {
92             M_Free(materialManifestIdMap);
93             materialManifestIdMap = 0;
94             materialManifestIdMapSize = 0;
95         }
96         materialManifestCount = 0;
97     }
98 
99     void createMaterialScheme(String name)
100     {
101         DENG2_ASSERT(name.length() >= MaterialScheme::min_name_length);
102 
103         // Create a new scheme.
104         MaterialScheme *newScheme = new MaterialScheme(name);
105         materialSchemes.insert(name, newScheme);
106         materialSchemeCreationOrder.append(newScheme);
107 
108         // We want notification when a new manifest is defined in this scheme.
109         newScheme->audienceForManifestDefined += this;
110     }
111 
112     /// Observes MaterialScheme ManifestDefined.
113     void materialSchemeManifestDefined(MaterialScheme & /*scheme*/, MaterialManifest &manifest)
114     {
115         /// Number of elements to block-allocate in the material index to material manifest map.
116         int const MANIFESTIDMAP_BLOCK_ALLOC = 32;
117 
118         // We want notification when the manifest is derived to produce a material.
119         manifest.audienceForMaterialDerived += this;
120 
121         // We want notification when the manifest is about to be deleted.
122         manifest.audienceForDeletion += this;
123 
124         // Acquire a new unique identifier for the manifest.
125         materialid_t const id = materialid_t(++materialManifestCount); // 1-based.
126         manifest.setId(id);
127 
128         // Add the new manifest to the id index/map.
129         if (materialManifestCount > int(materialManifestIdMapSize))
130         {
131             // Allocate more memory.
132             materialManifestIdMapSize += MANIFESTIDMAP_BLOCK_ALLOC;
133             materialManifestIdMap = (MaterialManifest **) M_Realloc(materialManifestIdMap, sizeof(*materialManifestIdMap) * materialManifestIdMapSize);
134         }
135         materialManifestIdMap[materialManifestCount - 1] = &manifest;
136     }
137 
138     /// Observes MaterialManifest MaterialDerived.
139     void materialManifestMaterialDerived(MaterialManifest & /*manifest*/, Material &material)
140     {
141         // Include this new material in the scheme-agnostic list of instances.
142         materials.append(&material);
143 
144         // We want notification when the material is about to be deleted.
145         material.audienceForDeletion() += this;
146     }
147 
148     /// Observes MaterialManifest Deletion.
149     void materialManifestBeingDeleted(MaterialManifest const &manifest)
150     {
151         foreach (MaterialManifestGroup *group, materialGroups)
152         {
153             group->remove(const_cast<MaterialManifest *>(&manifest));
154         }
155         materialManifestIdMap[manifest.id() - 1 /*1-based*/] = 0;
156 
157         // There will soon be one fewer manifest in the system.
158         materialManifestCount -= 1;
159     }
160 
161     /// Observes Material Deletion.
162     void materialBeingDeleted(Material const &material)
163     {
164         Material *pMat = const_cast<Material *>(&material);
165         materials.removeOne(pMat);
166         animatedMaterialsSubset.erase(pMat);
167     }
168 };
169 
Materials()170 Materials::Materials()
171     : d(new Impl(this))
172 {}
173 
materialScheme(String name) const174 MaterialScheme &Materials::materialScheme(String name) const
175 {
176     if (!name.isEmpty())
177     {
178         auto found = d->materialSchemes.find(name);
179         if (found != d->materialSchemes.end()) return **found;
180     }
181     /// @throw UnknownSchemeError An unknown scheme was referenced.
182     throw Resources::UnknownSchemeError("Materials::materialScheme",
183                                         "No scheme found matching '" + name + "'");
184 }
185 
isKnownMaterialScheme(String name) const186 bool Materials::isKnownMaterialScheme(String name) const
187 {
188     if (!name.isEmpty())
189     {
190         return d->materialSchemes.contains(name);
191     }
192     return false;
193 }
194 
materialSchemeCount() const195 int Materials::materialSchemeCount() const
196 {
197     return d->materialSchemes.count();
198 }
199 
forAllMaterialSchemes(std::function<LoopResult (MaterialScheme &)> func) const200 LoopResult Materials::forAllMaterialSchemes(std::function<LoopResult (MaterialScheme &)> func) const
201 {
202     for (MaterialScheme *scheme : d->materialSchemes)
203     {
204         if (auto result = func(*scheme)) return result;
205     }
206     return LoopContinue;
207 }
208 
toMaterialManifest(materialid_t id) const209 MaterialManifest &Materials::toMaterialManifest(materialid_t id) const
210 {
211     duint32 idx = id - 1; // 1-based index.
212     if (idx < duint32(d->materialManifestCount))
213     {
214         if (d->materialManifestIdMap[idx])
215         {
216             return *d->materialManifestIdMap[idx];
217         }
218         // Internal bookeeping error.
219         DENG2_ASSERT(false);
220     }
221     /// @throw InvalidMaterialIdError The specified material id is invalid.
222     throw UnknownMaterialIdError("Materials::toMaterialManifest",
223                                  "Invalid material ID " + String::number(id) +
224                                  ", valid range " +
225                                  Rangei(1, d->materialManifestCount + 1).asText());
226 }
227 
materialPtr(de::Uri const & path)228 Material *Materials::materialPtr(de::Uri const &path)
229 {
230     if (auto *manifest = materialManifestPtr(path)) return manifest->materialPtr();
231     return nullptr;
232 }
233 
hasMaterialManifest(de::Uri const & path) const234 bool Materials::hasMaterialManifest(de::Uri const &path) const
235 {
236     return materialManifestPtr(path) != nullptr;
237 }
238 
materialManifest(de::Uri const & uri) const239 MaterialManifest &Materials::materialManifest(de::Uri const &uri) const
240 {
241     if (auto *mm = materialManifestPtr(uri))
242     {
243         return *mm;
244     }
245     /// @throw MissingResourceManifestError  Failed to locate a matching manifest.
246     throw Resources::MissingResourceManifestError("Materials::materialManifest",
247                                                   "Failed to locate a manifest matching \"" + uri.asText() + "\"");
248 }
249 
materialManifestPtr(de::Uri const & uri) const250 MaterialManifest *Materials::materialManifestPtr(de::Uri const &uri) const
251 {
252     // Does the user want a manifest in a specific scheme?
253     if (!uri.scheme().isEmpty())
254     {
255         MaterialScheme &specifiedScheme = materialScheme(uri.scheme());
256         return specifiedScheme.tryFind(uri.path());
257     }
258     else
259     {
260         // No, check each scheme in priority order.
261         foreach (MaterialScheme *scheme, d->materialSchemeCreationOrder)
262         {
263             if (auto *manifest = scheme->tryFind(uri.path()))
264             {
265                 return manifest;
266             }
267         }
268     }
269     return nullptr;
270 }
271 
materialCount() const272 dint Materials::materialCount() const
273 {
274     return d->materials.count();
275 }
276 
forAllMaterials(const std::function<LoopResult (Material &)> & func) const277 LoopResult Materials::forAllMaterials(const std::function<LoopResult (Material &)> &func) const
278 {
279     for (Material *mat : d.getConst()->materials)
280     {
281         if (auto result = func(*mat))
282         {
283             return result;
284         }
285     }
286     return LoopContinue;
287 }
288 
forAnimatedMaterials(const std::function<LoopResult (Material &)> & func) const289 LoopResult Materials::forAnimatedMaterials(const std::function<LoopResult (Material &)> &func) const
290 {
291     for (Material *mat : d.getConst()->animatedMaterialsSubset)
292     {
293         if (auto result = func(*mat))
294         {
295             return result;
296         }
297     }
298     return LoopContinue;
299 }
300 
updateLookup()301 void Materials::updateLookup()
302 {
303     d->animatedMaterialsSubset.clear();
304     for (auto *mat : d->materials)
305     {
306         if (mat->isAnimated())
307         {
308             d->animatedMaterialsSubset.insert(mat);
309         }
310     }
311 }
312 
newMaterialGroup()313 Materials::MaterialManifestGroup &Materials::newMaterialGroup()
314 {
315     // Allocating one by one is inefficient, but it doesn't really matter.
316     d->materialGroups.append(new MaterialManifestGroup());
317     return *d->materialGroups.back();
318 }
319 
materialGroup(dint groupIdx) const320 Materials::MaterialManifestGroup &Materials::materialGroup(dint groupIdx) const
321 {
322     groupIdx -= 1; // 1-based index.
323     if (groupIdx >= 0 && groupIdx < d->materialGroups.count())
324     {
325         return *d->materialGroups[groupIdx];
326     }
327     /// @throw UnknownMaterialGroupError An unknown material group was referenced.
328     throw UnknownMaterialGroupError("Materials::materialGroup",
329                                     "Invalid group #" + String::number(groupIdx+1) + ", valid range " +
330                                     Rangeui(1, d->materialGroups.count() + 1).asText());
331 }
332 
allMaterialGroups() const333 Materials::MaterialManifestGroups const &Materials::allMaterialGroups() const
334 {
335     return d->materialGroups;
336 }
337 
clearAllMaterialGroups()338 void Materials::clearAllMaterialGroups()
339 {
340     qDeleteAll(d->materialGroups);
341     d->materialGroups.clear();
342 }
343 
clearAllMaterialSchemes()344 void Materials::clearAllMaterialSchemes()
345 {
346     forAllMaterialSchemes([] (MaterialScheme &scheme) {
347         scheme.clear();
348         return LoopContinue;
349     });
350     DENG2_ASSERT(materialCount() == 0); // sanity check
351 }
352 
get()353 Materials &Materials::get() // static
354 {
355     return World::get().materials();
356 }
357 
358 } // namespace world
359