/* ** tilesetbuilder.cpp ** Constructs the full tile set and adds it to the texture manager. ** **--------------------------------------------------------------------------- ** Copyright 2019-2022 Christoph Oelckers ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** ** */ #include "image.h" #include "texturemanager.h" #include "m_crc32.h" #include "c_dispatch.h" #include "tiletexture.h" #include "tilesetbuilder.h" #include "gamecontrol.h" #include "startupinfo.h" #include "printf.h" #include "m_argv.h" #include "gamestruct.h" const char* G_DefFile(void); void loaddefinitionsfile(TilesetBuildInfo& info, const char* fn, bool cumulative = false, bool maingrp = false); //========================================================================== // // Returns checksum for a given tile or texture // //========================================================================== int32_t tileGetCRC32(FImageSource* image) { if (image == nullptr) return 0; auto pixels = image->GetPalettedPixels(0); if (pixels.Size() == 0) return 0; // To get proper CRCs as the calling code expects we need to put the translucent index back to 255. for (auto& p : pixels) { if (p == 0) p = 255; else if (p == 255) p = 0; } return crc32(0, (const Bytef*)pixels.Data(), pixels.Size()); } CCMD(tilecrc) { if (argv.argc() > 1) { char* p; int tile = strtol(argv[1], &p, 10); FGameTexture* tex; if (tile >= 0 && tile < MAXTILES && !*p) { tex = TexMan.GetGameTexture(tileGetTextureID(tile)); } else { tex = TexMan.FindGameTexture(argv[1], ETextureType::Any); } auto img = tex? tex->GetTexture()->GetImage() : nullptr; if (!img) Printf("%s: not a valid texture", argv[1]); else Printf("%d\n", tileGetCRC32(img)); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static void LoadDefinitions(TilesetBuildInfo& info) { for (unsigned i = 0; i < info.tile.Size(); i++) { if (info.tile[i].extinfo.tiletovox > info.nextvoxid) info.nextvoxid = info.tile[i].extinfo.tiletovox; } info.nextvoxid++; const char* defsfile = G_DefFile(); FString razedefsfile = defsfile; razedefsfile.Substitute(".def", "-raze.def"); loaddefinitionsfile(info, "engine/engine.def", true, true); // Internal stuff that is required. // check what we have. // user .defs override the default ones and are not cumulative. // if we fine even one Raze-specific file, all of those will be loaded cumulatively. // otherwise the default rules inherited from older ports apply. if (userConfig.UserDef.IsNotEmpty()) { loaddefinitionsfile(info, userConfig.UserDef, false); } else { if (fileSystem.FileExists(razedefsfile)) { loaddefinitionsfile(info, razedefsfile, true); } else if (fileSystem.FileExists(defsfile)) { loaddefinitionsfile(info, defsfile, false); } } if (userConfig.AddDefs) { for (auto& m : *userConfig.AddDefs) { loaddefinitionsfile(info, m, false); } userConfig.AddDefs.reset(); } if (GameStartupInfo.def.IsNotEmpty()) { loaddefinitionsfile(info, GameStartupInfo.def); // Stuff from gameinfo. } // load the widescreen replacements last. This ensures that mods still get the correct CRCs for their own tile replacements. if (fileSystem.FindFile("engine/widescreen.def") >= 0 && !Args->CheckParm("-nowidescreen")) { loaddefinitionsfile(info, "engine/widescreen.def"); } fileSystem.InitHashChains(); // make sure that any resources that got added can be found again. } //========================================================================== // // // //========================================================================== picanm_t tileConvertAnimFormat(int32_t const picanimraw) { // Unpack a 4 byte packed anim descriptor into something more accessible picanm_t anm; anm.num = picanimraw & 63; anm.sf = ((picanimraw >> 24) & 15) | (picanimraw & 192); anm.extra = (picanimraw >> 28) & 15; return anm; } //========================================================================== // // // //========================================================================== FImageSource* createWritableTile(int width, int height); FImageSource* makeTileWritable(FImageSource* img); void TilesetBuildInfo::MakeWritable(int tileno) { if (tile[tileno].tileimage != nullptr) { auto newtex = makeTileWritable(tile[tileno].tileimage); tile[tileno].tileimage = newtex; tile[tileno].imported = nullptr; } } void TilesetBuildInfo::CreateWritable(int tileno, int w, int h) { auto newtex = createWritableTile(w, h); tile[tileno].tileimage = newtex; tile[tileno].imported = nullptr; } //=========================================================================== // // MakeCanvas // // Turns texture into a canvas (i.e. camera texture) // //=========================================================================== void TilesetBuildInfo::MakeCanvas(int tilenum, int width, int height) { auto ftex = new FCanvasTexture(width * 4, height * 4); ftex->aspectRatio = (float)width / height; auto canvas = MakeGameTexture(ftex, FStringf("#%05d", tilenum), ETextureType::Any); canvas->SetSize(width * 4, height * 4); canvas->SetDisplaySize((float)width, (float)height); canvas->GetTexture()->SetSize(width * 4, height * 4); tile[tilenum].imported = canvas; tile[tilenum].tileimage = nullptr; } static void GenerateRotations(int firsttileid, const char* basename, int tile, int numframes, int numrotations, int order) { if (order == 0) { for (int frame = 0; frame < numframes; frame++) { for (int rotation = 0; rotation < numrotations; rotation++) { FStringf str("%s@%c%x", basename, frame + 'A', rotation + 1); TexMan.AddAlias(str, FSetTextureID(firsttileid + tile)); tile++; } } } else if (order >= 1) { for (int rotation = 0; rotation < numrotations; rotation++) { for (int frame = 0; frame < numframes; frame++) { FStringf str("%s@%c%x", basename, frame + 'A', rotation + 1); TexMan.AddAlias(str, FSetTextureID(firsttileid + tile)); tile += order; } } } } static void CompleteRotations(int firsttileid, const char* basename, const char* getname, int numframes, int numrotations) { for (int rotation = numrotations; ; rotation++) { for (int frame = 0; frame < numframes; frame++) { FStringf str("%s@%c%x", getname, frame + 'A', rotation + 1); auto texid = TexMan.CheckForTexture(str, ETextureType::Any); if (frame == 0 && !texid.isValid()) { // rotation does not exist for the first frame -> we reached the end. return; } str.Format("%s@%c%x", basename, frame + 'A', rotation + 1); TexMan.AddAlias(str, texid); } } } static void SubstituteRotations(int firsttileid, const char* basename, int numframes, int destrot, int srcrot) { for (int frame = 0; frame < numframes; frame++) { FStringf str("%s@%c%x", basename, frame + 'A', srcrot); auto texid = TexMan.CheckForTexture(str, ETextureType::Any); if (!texid.isValid()) { continue; } str.Format("%s@%c%x", basename, frame + 'A', destrot); TexMan.AddAlias(str, texid); } } void LoadAliases(int firsttileid, int maxarttile) { int lump, lastlump = 0; while ((lump = fileSystem.FindLump("TEXNAMES", &lastlump, false)) != -1) { FScanner sc; sc.OpenLumpNum(lump); sc.SetCMode(true); while (sc.GetNumber()) { int tile = sc.Number; if (tile < 0 || tile > maxarttile) tile = maxarttile; sc.MustGetStringName("="); sc.MustGetString(); TexMan.AddAlias(sc.String, FSetTextureID(firsttileid + tile)); FString basename = sc.String; if (sc.CheckString(",")) { sc.MustGetNumber(); int numframes = sc.Number; int numrotations = 1, order = 0; if (sc.CheckString(",")) { sc.MustGetNumber(); numrotations = sc.Number; if (sc.CheckString(",")) { sc.MustGetNumber(); order = sc.Number; } } if (numframes <= 0 || numframes > 26) { sc.ScriptMessage("%d: Bad number of frames\n", numframes); continue; } if (numrotations >= 16 || numrotations < 1) { sc.ScriptMessage("%d: Bad number of rotations\n", numrotations); continue; } if (order < 0) { sc.ScriptMessage("%d: Bad order\n", order); continue; } GenerateRotations(firsttileid, basename, tile, numframes, numrotations, order); if (sc.CheckString(",")) { sc.MustGetString(); if (sc.String[0] != '@') { CompleteRotations(firsttileid, basename, sc.String, numframes, numrotations); } else { sc.UnGet(); do { sc.MustGetString(); int destrot = (int)strtoll(sc.String + 1, nullptr, 10); sc.MustGetStringName("="); sc.MustGetString(); int srcrot = (int)strtoll(sc.String + 1, nullptr, 10); SubstituteRotations(firsttileid, basename, numframes, destrot, srcrot); } while (sc.CheckString(",")); } } } } } } //========================================================================== // // // //========================================================================== void ConstructTileset() { TilesetBuildInfo info {}; TArray images; TArray rawpicanm; GetArtImages(images, rawpicanm); info.tile.Resize(MAXTILES); memset(info.tile.Data(), 0, info.tile.Size() * sizeof(info.tile[0])); // fill up the arrays to the maximum allowed but remember the highest original number. for (unsigned i = 0; i < images.Size(); i++) { info.tile[i].orgimage = info.tile[i].tileimage = images[i]; if (images[i]) { auto s = images[i]->GetOffsets(); info.tile[i].leftOffset = s.first; info.tile[i].topOffset = s.second; } info.tile[i].extinfo.picanm = tileConvertAnimFormat(rawpicanm[i]); } images.Reset(); rawpicanm.Reset(); for (auto& a : info.tile) { a.alphathreshold = 0.5f; a.extinfo.tiletovox = -1; } gi->LoadTextureInfo(info); // initialize game data that must be done before loading .DEF LoadDefinitions(info); gi->SetupSpecialTextures(info); // initialize game data that needs .DEF being processed. // now that everything has been set up, we can add the textures to the texture manager. // To keep things simple everything from .ART files and its replacements will remain in order and // converting between a Build tilenum and a texture ID can done with a single addition. // Even though this requires adding quite a few empty textures to the texture manager, it makes things a lot easier, // because it ensures an unambiguous mapping and allows communicating with features that only work with tile numbers. // as long as no named textures are used. auto nulltex = TexMan.GameByIndex(0); // Use the null texture's backing data for all empty placeholders. // Only the outward facing FGameTexture needs to be different firstarttile = TexMan.NumTextures(); maxarttile = MAXTILES - 1; while (maxarttile >= 0 && info.tile[maxarttile].tileimage == nullptr) maxarttile--; if (maxarttile < 0) return; // should never happen, but who knows - maybe someone will make a game without ART files later... :D maxarttile++; // create a placeholder in the first unused spot. This will later get used for all out of range tile numbers. int lastid = firstarttile - 1; for (int i = 0; i <= maxarttile; i++) { FTexture* ftex = nullptr; FGameTexture* gtex; FStringf tname("#%05d", i); if (info.tile[i].tileimage == nullptr) { if (info.tile[i].imported == nullptr || i == 0) { ftex = nulltex->GetTexture(); gtex = MakeGameTexture(ftex, tname, ETextureType::Null); } else { // Canvas textures can be used directly without wrapping them again. gtex = info.tile[i].imported; } } else { if (info.tile[i].imported) ftex = info.tile[i].imported->GetTexture(); else ftex = new FImageTexture(info.tile[i].tileimage); gtex = MakeGameTexture(ftex, tname, i == 0? ETextureType::FirstDefined : ETextureType::Any); gtex->SetOffsets(info.tile[i].leftOffset, info.tile[i].topOffset); } if (info.tile[i].extinfo.picanm.sf & PICANM_NOFULLBRIGHT_BIT) { gtex->SetDisableFullbright(true); } auto id = TexMan.AddGameTexture(gtex, true); if (id.GetIndex() != lastid + 1) { // this should never happen unless the texture manager gets redone in an incompatible fashion. I_FatalError("Unable to assign consecutive texture IDs to tile set."); } lastid = id.GetIndex(); } // Now create the extended info. This will leave room for all regular textures as well so that later code can assign info to these, too. // Textures being added afterward will always see the default extinfo, even if they are not covered by this array. texExtInfo.Resize(TexMan.NumTextures()); memset(texExtInfo.Data(), 0, sizeof(texExtInfo[0]) * texExtInfo.Size()); for (auto& x : texExtInfo) x.tiletovox = -1; // now copy all extinfo stuff that got parsed by .DEF or some game specific setup. for (int i = 0; i <= maxarttile; i++) { texExtInfo[i + firstarttile] = info.tile[i].extinfo; } LoadAliases(firstarttile, maxarttile); for (auto& a : info.aliases) { TexMan.AddAlias(a.first.GetChars(), min(maxarttile, a.second) + firstarttile); } }