Open source Star Ruler 2 source code!
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
#include "main/references.h"
|
||||
#include "main/logging.h"
|
||||
#include "main/initialization.h"
|
||||
#include "render/render_state.h"
|
||||
#include "resource/library.h"
|
||||
#include "compat/misc.h"
|
||||
#include "str_util.h"
|
||||
#include "num_util.h"
|
||||
#include "threads.h"
|
||||
#include "files.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <tuple>
|
||||
#include <queue>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "main/profiler.h"
|
||||
|
||||
extern bool cancelAssets;
|
||||
|
||||
namespace resource {
|
||||
|
||||
umap<std::string, render::SpriteMode> INIT_VAR(spriteModes) {
|
||||
spriteModes["Horizontal"] = render::SM_Horizontal;
|
||||
spriteModes["Vertical"] = render::SM_Vertical;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::FaceCulling> INIT_VAR(cullingModes) {
|
||||
cullingModes["None"] = render::FC_None;
|
||||
cullingModes["Front"] = render::FC_Front;
|
||||
cullingModes["Back"] = render::FC_Back;
|
||||
cullingModes["Both"] = render::FC_Both;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::DepthTest> INIT_VAR(depthTests) {
|
||||
depthTests["Never"] = render::DT_Never;
|
||||
depthTests["Less"] = render::DT_Less;
|
||||
depthTests["LessEqual"] = render::DT_LessEqual;
|
||||
depthTests["Equal"] = render::DT_Equal;
|
||||
depthTests["GreaterEqual"] = render::DT_GreaterEqual;
|
||||
depthTests["Greater"] = render::DT_Greater;
|
||||
depthTests["Always"] = render::DT_Always;
|
||||
depthTests["NoDepthTest"] = render::DT_NoDepthTest;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::TextureWrap> INIT_VAR(textureWraps) {
|
||||
textureWraps["Repeat"] = render::TW_Repeat;
|
||||
textureWraps["Clamp"] = render::TW_Clamp;
|
||||
textureWraps["ClampEdge"] = render::TW_ClampEdge;
|
||||
textureWraps["Mirror"] = render::TW_Mirror;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::TextureFilter> INIT_VAR(textureFilters) {
|
||||
textureFilters["Linear"] = render::TF_Linear;
|
||||
textureFilters["Nearest"] = render::TF_Nearest;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::DrawMode> INIT_VAR(drawModes) {
|
||||
drawModes["Line"] = render::DM_Line;
|
||||
drawModes["Fill"] = render::DM_Fill;
|
||||
} INIT_VAR_END;
|
||||
|
||||
umap<std::string, render::BaseMaterial> INIT_VAR(baseMats) {
|
||||
baseMats["Solid"] = render::MAT_Solid;
|
||||
baseMats["Add"] = render::MAT_Add;
|
||||
baseMats["Alpha"] = render::MAT_Alpha;
|
||||
baseMats["Font"] = render::MAT_Font;
|
||||
baseMats["Overlay"] = render::MAT_Overlay;
|
||||
} INIT_VAR_END;
|
||||
|
||||
const unsigned DRIVER_MIPMAP_SIZE = 512 * 512;
|
||||
|
||||
struct QueuedTexture {
|
||||
int priority;
|
||||
std::vector<Image*> images;
|
||||
render::Texture* tex;
|
||||
std::string filename;
|
||||
bool mipmap;
|
||||
bool cachePixels;
|
||||
mutable unsigned row, lod;
|
||||
|
||||
QueuedTexture(render::Texture* dest, int Priority, Image* source, bool Mipmap, bool CachePixels)
|
||||
: priority(Priority), tex(dest), mipmap(Mipmap), cachePixels(CachePixels), row(0), lod(0)
|
||||
{
|
||||
images.push_back(source);
|
||||
}
|
||||
|
||||
QueuedTexture(render::Texture* dest, int Priority, const std::string& source, bool Mipmap, bool CachePixels)
|
||||
: priority(Priority), tex(dest), filename(source), mipmap(Mipmap), cachePixels(CachePixels), row(0), lod(0)
|
||||
{
|
||||
}
|
||||
|
||||
//Loads the image, returning the number of bytes loaded
|
||||
// If true was returned, the image is done loading or there was a failure
|
||||
// The number of bytes loaded may exceed maxBytes - it is only a soft limit
|
||||
bool load(unsigned maxBytes, unsigned& loadedBytes);
|
||||
|
||||
bool operator<(const QueuedTexture& other) const {
|
||||
return priority < other.priority;
|
||||
}
|
||||
|
||||
void clearImages() {
|
||||
for(auto i = images.begin(); i != images.end(); ++i)
|
||||
delete *i;
|
||||
images.clear();
|
||||
}
|
||||
};
|
||||
|
||||
threads::Mutex queuedImagesLock;
|
||||
std::priority_queue<QueuedTexture> queuedImages;
|
||||
|
||||
threads::Mutex queuedTexturesLock;
|
||||
std::priority_queue<QueuedTexture> queuedTextures;
|
||||
|
||||
threads::Mutex texIdentLock;
|
||||
std::unordered_map<std::string,render::Texture*> texNames;
|
||||
|
||||
unsigned maxTexSize = 0;
|
||||
|
||||
void Library::clearTextures() {
|
||||
texNames.clear();
|
||||
maxTexSize = 0;
|
||||
}
|
||||
|
||||
bool Library::hasQueuedImages() {
|
||||
return !queuedImages.empty();
|
||||
}
|
||||
|
||||
unsigned getMaxTextureSize() {
|
||||
if(maxTexSize == 0) {
|
||||
auto* texQuality = devices.settings.engine.getSetting("iTextureQuality");
|
||||
if(texQuality) {
|
||||
switch(texQuality->getInteger()) {
|
||||
case 0: maxTexSize = 256; break;
|
||||
case 1: maxTexSize = 512; break;
|
||||
case 2: maxTexSize = 1024; break;
|
||||
case 3: maxTexSize = 2048; break;
|
||||
case 4: maxTexSize = 4096; break;
|
||||
default:
|
||||
case 5: maxTexSize = 8192; break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
maxTexSize = 65536;
|
||||
}
|
||||
}
|
||||
|
||||
return maxTexSize * maxTexSize;
|
||||
}
|
||||
|
||||
bool Library::processImages(int maxPriority, int amount) {
|
||||
bool processedAny = false;
|
||||
int processed = 0;
|
||||
while(!queuedImages.empty()) {
|
||||
queuedImagesLock.lock();
|
||||
if(queuedImages.empty()) {
|
||||
queuedImagesLock.release();
|
||||
break;
|
||||
}
|
||||
|
||||
auto elem = queuedImages.top();
|
||||
if(elem.priority < maxPriority) {
|
||||
queuedImagesLock.release();
|
||||
break;
|
||||
}
|
||||
|
||||
if(elem.tex)
|
||||
textures.push_back(elem.tex);
|
||||
|
||||
queuedImages.pop();
|
||||
queuedImagesLock.release();
|
||||
|
||||
if(cancelAssets)
|
||||
continue;
|
||||
|
||||
processedAny = true;
|
||||
|
||||
if(!elem.filename.empty()) {
|
||||
Image* img = loadImage(elem.filename.c_str());
|
||||
|
||||
if(!img) {
|
||||
error("Error: Could not load image '%s'", elem.filename.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned texSize = getMaxTextureSize();
|
||||
while(img->width * img->height > texSize) {
|
||||
auto* prev = img;
|
||||
img = img->makeMipmap();
|
||||
delete prev;
|
||||
}
|
||||
|
||||
elem.images.push_back(img);
|
||||
|
||||
//Loading images this way causes weirdness
|
||||
/*if(elem.mipmap && img->width * img->height > DRIVER_MIPMAP_SIZE) {
|
||||
while(img->width > 2 && img->height > 2) {
|
||||
img = img->makeMipmap();
|
||||
if(img)
|
||||
elem.images.push_back(img);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
++processed;
|
||||
|
||||
queuedTexturesLock.lock();
|
||||
queuedTextures.push(elem);
|
||||
queuedTexturesLock.release();
|
||||
|
||||
if(processed >= amount)
|
||||
break;
|
||||
}
|
||||
|
||||
return processedAny;
|
||||
}
|
||||
|
||||
bool Library::hasQueuedTextures() {
|
||||
return !queuedTextures.empty();
|
||||
}
|
||||
|
||||
bool QueuedTexture::load(unsigned maxBytes, unsigned& loadedBytes) {
|
||||
if(images.empty())
|
||||
return true;
|
||||
if(!tex) {
|
||||
clearImages();
|
||||
return true;
|
||||
}
|
||||
|
||||
if(loadedBytes >= maxBytes)
|
||||
return false;
|
||||
|
||||
Image* img = images.front();
|
||||
unsigned rowBytes = img->width * ColorDepths[img->format];
|
||||
unsigned loadRows = (maxBytes - loadedBytes) / rowBytes;
|
||||
if(loadRows == 0)
|
||||
loadRows = 1;
|
||||
if(loadRows > img->height - row)
|
||||
loadRows = img->height - row;
|
||||
loadedBytes += rowBytes * loadRows;
|
||||
|
||||
if(lod == 0/* && loadRows == img->height && img->width * img->height <= DRIVER_MIPMAP_SIZE*/) {
|
||||
tex->load(*img, mipmap, cachePixels);
|
||||
clearImages();
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if(row == 0)
|
||||
tex->loadStart(*img, mipmap, cachePixels, lod);
|
||||
tex->loadPartial(*img, recti(0, row, img->width, row+loadRows), cachePixels, lod);
|
||||
row += loadRows;
|
||||
|
||||
if(row == img->height) {
|
||||
delete img;
|
||||
images.erase(images.begin());
|
||||
|
||||
tex->loadFinish(mipmap && images.empty(), lod);
|
||||
if(!mipmap || images.empty())
|
||||
return true;
|
||||
|
||||
//Reset for the next LOD
|
||||
lod += 1;
|
||||
row = 0;
|
||||
return load(maxBytes, loadedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Library::processTextures(int maxPriority, bool singleFrame) {
|
||||
bool processedAny = false;
|
||||
const unsigned frameByteLimit = 1024 * 1024 * 4;
|
||||
int64_t byteLimit = singleFrame ? frameByteLimit : 0xffffffff;
|
||||
|
||||
while(!queuedTextures.empty()) {
|
||||
queuedTexturesLock.lock();
|
||||
if(queuedTextures.empty()) {
|
||||
queuedTexturesLock.release();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
auto tex = queuedTextures.top();
|
||||
if(tex.priority < maxPriority) {
|
||||
queuedTexturesLock.release();
|
||||
break;
|
||||
}
|
||||
|
||||
queuedTextures.pop();
|
||||
|
||||
//Track filenames even if we can't load the resource at the moment, in case the file is created while we're running
|
||||
if(tex.tex && !tex.filename.empty())
|
||||
texture_files[tex.filename] = tex.tex;
|
||||
|
||||
queuedTexturesLock.release();
|
||||
|
||||
if(cancelAssets)
|
||||
continue;
|
||||
|
||||
processedAny = true;
|
||||
|
||||
unsigned loaded = 0;
|
||||
if(!tex.load(byteLimit, loaded)) {
|
||||
threads::Lock lock(queuedTexturesLock);
|
||||
queuedTextures.push(tex);
|
||||
}
|
||||
|
||||
byteLimit -= (int64_t)loaded;
|
||||
|
||||
if(byteLimit <= 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return processedAny;
|
||||
}
|
||||
|
||||
render::Texture* queueImage(const std::string& abs_file, int priority = -20, bool mipmap = true, bool cachePixels = false, bool cubemap = false) {
|
||||
bool queue = false;
|
||||
render::Texture* tex;
|
||||
{
|
||||
texIdentLock.lock();
|
||||
render::Texture*& pTex = texNames[abs_file];
|
||||
if(pTex == 0) {
|
||||
if(cubemap)
|
||||
pTex = render::RenderDriver::createCubemap();
|
||||
else
|
||||
pTex = render::RenderDriver::createTexture();
|
||||
queue = true;
|
||||
}
|
||||
tex = pTex;
|
||||
texIdentLock.release();
|
||||
}
|
||||
|
||||
if(queue) {
|
||||
queuedImagesLock.lock();
|
||||
resource::QueuedTexture queued = resource::QueuedTexture(tex, priority, abs_file, mipmap, cachePixels);
|
||||
queuedImages.push(queued);
|
||||
queuedImagesLock.release();
|
||||
}
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
render::Texture* queueImage(Image* img, int priority, bool mipmap, bool cachePixels) {
|
||||
render::Texture* tex = render::RenderDriver::createTexture();
|
||||
|
||||
queuedTexturesLock.lock();
|
||||
resource::QueuedTexture queued = resource::QueuedTexture(tex, priority, img, mipmap, cachePixels);
|
||||
queuedTextures.push(queued);
|
||||
queuedTexturesLock.release();
|
||||
|
||||
return tex;
|
||||
}
|
||||
|
||||
void queueTextureUpdate(render::Texture* tex, Image* img, int priority, bool mipmap, bool cachePixels) {
|
||||
queuedTexturesLock.lock();
|
||||
resource::QueuedTexture queued = resource::QueuedTexture(tex, priority, img, mipmap, cachePixels);
|
||||
queuedTextures.push(queued);
|
||||
queuedTexturesLock.release();
|
||||
}
|
||||
|
||||
void Library::loadMaterials(const std::string& filename) {
|
||||
DataHandler datahandler;
|
||||
|
||||
render::RenderState* state = nullptr;
|
||||
render::SpriteSheet* sheet = nullptr;
|
||||
render::MaterialGroup* group = nullptr;
|
||||
std::string matName;
|
||||
int priority = -61;
|
||||
bool texdefs = false;
|
||||
|
||||
//Initialization handling
|
||||
datahandler("SpriteSheet", [&](std::string& value) {
|
||||
matName = value;
|
||||
priority = -61;
|
||||
group = nullptr;
|
||||
sheet = new render::SpriteSheet();
|
||||
state = &sheet->material;
|
||||
spritesheets[matName] = sheet;
|
||||
sheet_indices[sheet] = (unsigned)spritesheet_names.size();
|
||||
spritesheet_names.push_back(matName);
|
||||
texdefs = false;
|
||||
});
|
||||
|
||||
datahandler("Material", [&](std::string& value) {
|
||||
matName = value;
|
||||
sheet = 0;
|
||||
group = nullptr;
|
||||
priority = -61;
|
||||
if(materials.find(matName) != materials.end()) {
|
||||
error("Duplicate material: %s", matName.c_str());
|
||||
state = materials[matName];
|
||||
*state = render::RenderState();
|
||||
}
|
||||
else {
|
||||
state = new render::RenderState();
|
||||
state->constant = true;
|
||||
}
|
||||
|
||||
materials[matName] = state;
|
||||
mat_indices[state] = (unsigned)material_names.size();
|
||||
material_names.push_back(matName);
|
||||
texdefs = false;
|
||||
});
|
||||
|
||||
datahandler("MaterialGroup", [&](std::string& value) {
|
||||
matName = value;
|
||||
sheet = nullptr;
|
||||
state = nullptr;
|
||||
group = new render::MaterialGroup();
|
||||
group->prefix = value + "_";
|
||||
matGroups[value] = group;
|
||||
});
|
||||
|
||||
datahandler("Template", [&](std::string& value) {
|
||||
if(!group)
|
||||
return;
|
||||
|
||||
auto it = materials.find(value);
|
||||
if(it != materials.end())
|
||||
group->base = *it->second;
|
||||
else
|
||||
error("Could not find template material '%s' for '%s'", value.c_str(), matName.c_str());
|
||||
});
|
||||
|
||||
datahandler("Prefix", [&](std::string& value) {
|
||||
if(!group)
|
||||
return;
|
||||
|
||||
group->prefix = value;
|
||||
});
|
||||
|
||||
datahandler("Folder", [&](std::string& value) {
|
||||
if(!group)
|
||||
return;
|
||||
|
||||
auto& g = *group;
|
||||
auto& mats = materials;
|
||||
auto& inds = mat_indices;
|
||||
auto& names = material_names;
|
||||
int texPriority = priority;
|
||||
|
||||
devices.mods.listFiles(value, "*.png", [&](const std::string& filename) {
|
||||
std::string id = g.prefix + getBasename(filename, false);
|
||||
makeIdentifier(id);
|
||||
auto* mat = new render::RenderState();
|
||||
|
||||
*mat = g.base;
|
||||
mat->constant = true;
|
||||
|
||||
mats[id] = mat;
|
||||
inds[mat] = (unsigned)names.size();
|
||||
names.push_back(id);
|
||||
|
||||
g.names.push_back(id);
|
||||
g.materials.push_back(mat);
|
||||
|
||||
if(load_resources) {
|
||||
mat->textures[0] = queueImage(filename, texPriority, mat->mipmap, mat->cachePixels);
|
||||
if(watch_resources)
|
||||
devices.library.watchTexture(filename);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
datahandler("Inherit", [&](std::string& value) {
|
||||
if(!state)
|
||||
return;
|
||||
|
||||
auto it = materials.find(value);
|
||||
if(it != materials.end())
|
||||
*state = *it->second;
|
||||
else
|
||||
error("Could not find material '%s' to inherit for '%s'", value.c_str(), matName.c_str());
|
||||
});
|
||||
|
||||
//Renderstate members
|
||||
HANDLE_BOOL(datahandler, "DepthWrite", state, depthWrite);
|
||||
HANDLE_ENUM(datahandler, "DepthTest", state, depthTest, depthTests);
|
||||
HANDLE_ENUM(datahandler, "Culling", state, culling, cullingModes);
|
||||
HANDLE_BOOL(datahandler, "Lighting", state, lighting);
|
||||
HANDLE_BOOL(datahandler, "NormalizeNormals", state, normalizeNormals);
|
||||
HANDLE_ENUM_W(datahandler, "Shader", state, shader, shaders, load_resources);
|
||||
HANDLE_NUM(datahandler, "Shininess", state, shininess);
|
||||
HANDLE_ENUM(datahandler, "WrapVertical", state, wrapVertical, textureWraps);
|
||||
HANDLE_ENUM(datahandler, "WrapHorizontal", state, wrapHorizontal, textureWraps);
|
||||
HANDLE_ENUM(datahandler, "FilterMin", state, filterMin, textureFilters);
|
||||
HANDLE_ENUM(datahandler, "FilterMag", state, filterMag, textureFilters);
|
||||
HANDLE_ENUM(datahandler, "DrawMode", state, drawMode, drawModes);
|
||||
HANDLE_ENUM(datahandler, "Blend", state, baseMat, baseMats);
|
||||
|
||||
datahandler("Mipmap", [&](std::string& value) {
|
||||
if(!state)
|
||||
return;
|
||||
if(texdefs) {
|
||||
warn("Warning: Mipmap statement should be before texture"
|
||||
" declarations.\n %s", datahandler.position().c_str());
|
||||
}
|
||||
state->mipmap = toBool(value);
|
||||
});
|
||||
|
||||
datahandler("CachePixels", [&](std::string& value) {
|
||||
if(!state)
|
||||
return;
|
||||
state->cachePixels = toBool(value);
|
||||
});
|
||||
|
||||
datahandler("LoadPriority", [&](std::string& value) {
|
||||
if(value == "Critical" || value == "Menu")
|
||||
priority = 10;
|
||||
else if(value == "Game")
|
||||
priority = -10;
|
||||
else if(value == "High")
|
||||
priority = -31;
|
||||
else if(value == "Low")
|
||||
priority = -91;
|
||||
else
|
||||
priority = -111 + toNumber<int>(value);
|
||||
});
|
||||
|
||||
datahandler("Alpha", [&](std::string& value) {
|
||||
if(!state)
|
||||
return;
|
||||
|
||||
if(toBool(value))
|
||||
state->baseMat = render::MAT_Alpha;
|
||||
else
|
||||
state->baseMat = render::MAT_Solid;
|
||||
});
|
||||
|
||||
datahandler("Diffuse", [&](std::string& value) {
|
||||
int r,g,b,a;
|
||||
if(int args = sscanf(value.c_str(),"#%2x%2x%2x%2x", &r,&g,&b,&a)) {
|
||||
if(args == 3)
|
||||
state->diffuse = Colorf(Color(r,g,b));
|
||||
else if(args == 4)
|
||||
state->diffuse = Colorf(Color(r,g,b,a));
|
||||
}
|
||||
else {
|
||||
sscanf(value.c_str(), "%f,%f,%f,%f", &state->diffuse.r, &state->diffuse.g, &state->diffuse.b, &state->diffuse.a);
|
||||
}
|
||||
});
|
||||
|
||||
datahandler("Specular", [&](std::string& value) {
|
||||
int r,g,b,a;
|
||||
if(int args = sscanf(value.c_str(),"#%2x%2x%2x%2x", &r,&g,&b,&a)) {
|
||||
if(args == 3)
|
||||
state->specular = Colorf(Color(r,g,b));
|
||||
else if(args == 4)
|
||||
state->specular = Colorf(Color(r,g,b,a));
|
||||
}
|
||||
else {
|
||||
sscanf(value.c_str(), "%f,%f,%f,%f", &state->specular.r, &state->specular.g, &state->specular.b, &state->specular.a);
|
||||
}
|
||||
});
|
||||
|
||||
//Spritesheet members
|
||||
HANDLE_ENUM(datahandler, "Mode", sheet, mode, spriteModes);
|
||||
|
||||
datahandler("Size", [&](std::string& value) {
|
||||
if(!sheet)
|
||||
return;
|
||||
|
||||
std::vector<std::string> args;
|
||||
split(value, args, ',');
|
||||
|
||||
if(args.size() != 2)
|
||||
return;
|
||||
|
||||
sheet->width = toNumber<int>(args[0]);
|
||||
sheet->height = toNumber<int>(args[1]);
|
||||
});
|
||||
|
||||
datahandler("Spacing", [&](std::string& value) {
|
||||
if(!sheet)
|
||||
return;
|
||||
|
||||
sheet->spacing = toNumber<int>(value);
|
||||
});
|
||||
|
||||
datahandler.defaultHandler([&](std::string& key, std::string& value) {
|
||||
if(key.size() >= 7 && (key.compare(0, 7, "Texture") == 0 || key.compare(0, 7, "Cubemap") == 0)) {
|
||||
//Figure out the texture number to set
|
||||
int texNum = 0;
|
||||
texdefs = true;
|
||||
if (key.size() > 7) {
|
||||
std::string num = key.substr(7, key.size() - 7);
|
||||
texNum = min_(toNumber<int>(num)-1, RENDER_MAX_TEXTURES);
|
||||
}
|
||||
|
||||
value = getAbsolutePath( devices.mods.resolve(value) );
|
||||
if(load_resources) {
|
||||
state->textures[texNum] = queueImage(value, priority, state->mipmap, state->cachePixels, key[0] == 'C');
|
||||
if(watch_resources)
|
||||
watchTexture(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
datahandler.read(filename);
|
||||
}
|
||||
|
||||
std::string Library::getSpriteDesc(const render::Sprite& sprt) const {
|
||||
std::string ret;
|
||||
if(sprt.mat) {
|
||||
auto it = mat_indices.find(sprt.mat);
|
||||
if(it != mat_indices.end())
|
||||
ret = material_names[it->second];
|
||||
}
|
||||
else {
|
||||
auto it = sheet_indices.find(sprt.sheet);
|
||||
if(it == sheet_indices.end())
|
||||
return ret;
|
||||
ret += spritesheet_names[it->second];
|
||||
ret += "::";
|
||||
ret += toString(sprt.index);
|
||||
}
|
||||
if(sprt.color.color != 0xffffffff) {
|
||||
ret += "*";
|
||||
ret += toString(sprt.color);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user