raze/source/core/textures/tilesetbuilder.cpp
2023-12-05 22:39:27 +01:00

462 lines
15 KiB
C++

/*
** 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.GetChars(), false);
}
else
{
if (fileSystem.FileExists(razedefsfile.GetChars()))
{
loaddefinitionsfile(info, razedefsfile.GetChars(), true);
}
else if (fileSystem.FileExists(defsfile))
{
loaddefinitionsfile(info, defsfile, false);
}
}
if (userConfig.AddDefs)
{
for (auto& m : *userConfig.AddDefs)
{
loaddefinitionsfile(info, m.GetChars(), false);
}
userConfig.AddDefs.reset();
}
if (GameStartupInfo.def.IsNotEmpty())
{
loaddefinitionsfile(info, GameStartupInfo.def.GetChars()); // 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).GetChars(), 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.GetChars(), 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.GetChars(), 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.GetChars(), 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.GetChars(), 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.GetChars(), ETextureType::Any);
if (!texid.isValid())
{
continue;
}
str.Format("%s@%c%x", basename, frame + 'A', destrot);
TexMan.AddAlias(str.GetChars(), 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.GetChars(), tile, numframes, numrotations, order);
if (sc.CheckString(","))
{
sc.MustGetString();
if (sc.String[0] != '@')
{
CompleteRotations(firsttileid, basename.GetChars(), 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.GetChars(), numframes, destrot, srcrot);
} while (sc.CheckString(","));
}
}
}
}
}
}
//==========================================================================
//
//
//
//==========================================================================
void ConstructTileset()
{
TilesetBuildInfo info {};
TArray<FImageSource*> images;
TArray<unsigned> 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.GetChars(), 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.GetChars(), 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);
}
}