feat: customizable panorama + village fixes (#46)

Reviewed-on: https://git.neolegacy.dev/neoStudiosLCE/neoLegacy/pulls/46
This commit is contained in:
Fireblade
2026-07-13 22:21:32 +01:00
5 changed files with 323 additions and 34 deletions

View File

@@ -13,8 +13,149 @@
#include <direct.h>
#include <cmath>
#include "../../../Xbox/XML/ATGXmlParser.h"
static const wchar_t *PANORAMA_TEXTURE_RELPATH = L"Graphics/ControlType/Panorama/";
// default settings
static const float kDefaultPanoramaScrollSpeed = 0.0001f / 16.6667f;
static const float kDefaultPanoramaBlurSigma = 0.5f;
struct PanoramaConfig
{
float scrollSpeed = kDefaultPanoramaScrollSpeed;
float blurSigma = kDefaultPanoramaBlurSigma;
// grid mode (classic panorama)
bool gridEnabled = false;
float gridSizeX = 1.0f;
float gridSizeY = 1.0f;
// nearest vs bilinear
bool gridScalingNearest = false;
};
// atg xml parser my beloved
class PanoramaXmlCallback : public ATG::ISAXCallback
{
public:
PanoramaConfig config;
HRESULT StartDocument() override { return S_OK; }
HRESULT EndDocument() override { return S_OK; }
HRESULT ElementBegin(CONST WCHAR *strName, UINT NameLen, CONST ATG::XMLAttribute * /*pAttributes*/, UINT /*NumAttributes*/) override
{
wstring elementName(strName, NameLen);
if(elementName == L"grid")
{
// grid switch
config.gridEnabled = true;
}
m_elementStack.push_back(elementName);
m_currentText.clear();
return S_OK;
}
HRESULT ElementContent(CONST WCHAR *strData, UINT DataLen, BOOL More) override
{
m_currentText.append(strData, DataLen);
if(!More)
{
ApplyCurrentElement();
m_currentText.clear();
}
return S_OK;
}
HRESULT ElementEnd(CONST WCHAR * /*strName*/, UINT /*NameLen*/) override
{
ApplyCurrentElement();
m_currentText.clear();
if(!m_elementStack.empty())
{
m_elementStack.pop_back();
}
return S_OK;
}
HRESULT CDATABegin() override { return S_OK; }
HRESULT CDATAData(CONST WCHAR * /*strCDATA*/, UINT /*CDATALen*/, BOOL /*bMore*/) override { return S_OK; }
HRESULT CDATAEnd() override { return S_OK; }
VOID Error(HRESULT /*hError*/, CONST CHAR * /*strMessage*/) override { }
private:
void ApplyCurrentElement()
{
if(m_currentText.empty() || m_elementStack.empty()) return;
const wstring &name = m_elementStack.back();
const bool insideGrid = (m_elementStack.size() >= 2 &&
m_elementStack[m_elementStack.size() - 2] == L"grid");
if(name == L"panoramaSpeed")
{
config.scrollSpeed = static_cast<float>(wcstod(m_currentText.c_str(), nullptr));
}
else if(name == L"panoramaBlur")
{
config.blurSigma = static_cast<float>(wcstod(m_currentText.c_str(), nullptr));
}
else if(insideGrid && name == L"sizeX")
{
config.gridSizeX = static_cast<float>(wcstod(m_currentText.c_str(), nullptr));
}
else if(insideGrid && name == L"sizeY")
{
config.gridSizeY = static_cast<float>(wcstod(m_currentText.c_str(), nullptr));
}
else if(insideGrid && name == L"scaling")
{
config.gridScalingNearest = (_wcsicmp(m_currentText.c_str(), L"nearest") == 0);
}
}
vector<wstring> m_elementStack;
wstring m_currentText;
};
// load custom panorama data (if any) and return its settings
static PanoramaConfig LoadPanoramaConfig(const wstring &root)
{
PanoramaConfig config;
wstring xmlPath = root + L"panorama.xml";
if(GetFileAttributesW(xmlPath.c_str()) == INVALID_FILE_ATTRIBUTES)
{
return config;
}
const char *nativePath = wstringtofilename(xmlPath);
PanoramaXmlCallback callback;
ATG::XMLParser parser;
parser.RegisterSAXCallbackInterface(&callback);
HRESULT hr = parser.ParseXMLFile(nativePath);
if(FAILED(hr))
{
printf("Failed to parse panorama.xml at %ls (hr=0x%08X)\n", xmlPath.c_str(), hr);
return PanoramaConfig();
}
if(callback.config.gridSizeX <= 0.0f) callback.config.gridSizeX = 1.0f;
if(callback.config.gridSizeY <= 0.0f) callback.config.gridSizeY = 1.0f;
printf("Loaded panorama.xml overrides from %ls (speed=%f, blur=%f, grid=%s %fx%f, scaling=%s)\n",
xmlPath.c_str(), callback.config.scrollSpeed, callback.config.blurSigma,
callback.config.gridEnabled ? "on" : "off",
callback.config.gridSizeX, callback.config.gridSizeY,
callback.config.gridScalingNearest ? "nearest" : "linear");
return callback.config;
}
static wstring GetPanoramaTexturePath()
{
const wchar_t *envOverride = _wgetenv(L"PANORAMA_TEXTURE_PATH");
@@ -89,7 +230,7 @@ static wstring GetPanoramaTexturePath()
}
// opengl time :v
static int LoadPanoramaQuadTexture(const wstring &fileName, int &outWidth, int &outHeight)
static int LoadPanoramaQuadTexture(const wstring &fileName, float blurSigma, int &outWidth, int &outHeight)
{
wstring filePath = GetPanoramaTexturePath() + fileName;
const char *nativePath = wstringtofilename(filePath);
@@ -127,10 +268,20 @@ static int LoadPanoramaQuadTexture(const wstring &fileName, int &outWidth, int &
}
// gaussian blur
const float sigma = 0.5f;
const float sigma = blurSigma;
const int width = image.getWidth();
const int height = image.getHeight();
if(sigma <= 0.0f)
{
outWidth = width;
outHeight = height;
int idNoBlur = Minecraft::GetInstance()->textures->getTexture(&image, C4JRender::TEXTURE_FORMAT_RxGyBzAw, false);
printf("Loaded panorama background quad '%ls' (no blur) -> texture id %d\n", filePath.c_str(), idNoBlur);
return idNoBlur;
}
const int radius = static_cast<int>(ceilf(3.0f * sigma));
const int ksize = radius * 2 + 1;
vector<float> kernel(ksize);
@@ -217,10 +368,63 @@ static int LoadPanoramaQuadTexture(const wstring &fileName, int &outWidth, int &
outHeight = image.getHeight();
int id = Minecraft::GetInstance()->textures->getTexture(&blurredImage, C4JRender::TEXTURE_FORMAT_RxGyBzAw, false);
printf("Loaded panorama background quad '%ls' -> texture id %d\n", filePath.c_str(), id);
printf("Loaded panorama background quad '%ls' (blur sigma=%f) -> texture id %d\n", filePath.c_str(), sigma, id);
return id;
}
// original behavior
static void DrawScrollingPanoramaQuads(S32 width, S32 height, float panoramaAspect, float panoramaScroll, Tesselator *t)
{
float sourceWidth = static_cast<float>(height) * panoramaAspect;
float scroll = panoramaScroll - floorf(panoramaScroll);
float scrollPx = scroll * sourceWidth;
float startX = -scrollPx;
const float overscanY = (static_cast<float>(height) * 0.03f > 8.0f) ? (static_cast<float>(height) * 0.03f) : 8.0f;
const float drawTop = -overscanY;
const float drawBottom = static_cast<float>(height) + overscanY;
t->begin();
for(float x = startX - sourceWidth; x < static_cast<float>(width) + sourceWidth; x += sourceWidth)
{
float left = x;
float right = x + sourceWidth;
t->vertexUV(left, drawBottom, 0.0f, 0.0f, 1.0f);
t->vertexUV(right, drawBottom, 0.0f, 1.0f, 1.0f);
t->vertexUV(right, drawTop, 0.0f, 1.0f, 0.0f);
t->vertexUV(left, drawTop, 0.0f, 0.0f, 0.0f);
}
t->end();
}
// grid mode (classic panorama)
static void DrawGridPanoramaQuads(S32 width, S32 height, float tileWidth, float tileHeight, float panoramaScroll, Tesselator *t)
{
if(tileWidth <= 0.0f) tileWidth = static_cast<float>(width);
if(tileHeight <= 0.0f) tileHeight = static_cast<float>(height);
const float scroll = panoramaScroll - floorf(panoramaScroll);
const float scrollPx = scroll * tileWidth;
const float startX = -scrollPx;
t->begin();
for(float y = -tileHeight; y < static_cast<float>(height) + tileHeight; y += tileHeight)
{
const float top = y;
const float bottom = y + tileHeight;
for(float x = startX - tileWidth; x < static_cast<float>(width) + tileWidth; x += tileWidth)
{
const float left = x;
const float right = x + tileWidth;
t->vertexUV(left, bottom, 0.0f, 0.0f, 1.0f);
t->vertexUV(right, bottom, 0.0f, 1.0f, 1.0f);
t->vertexUV(right, top, 0.0f, 1.0f, 0.0f);
t->vertexUV(left, top, 0.0f, 0.0f, 0.0f);
}
}
t->end();
}
UIComponent_Panorama::UIComponent_Panorama(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer)
{
m_bShowingDay = true;
@@ -259,11 +463,10 @@ void UIComponent_Panorama::tick()
if(m_lastScrollTickMs != 0)
{
const DWORD kMaxElapsedMs = 250;
const float kScrollPerMs = 0.0001f / 16.6667f;
DWORD elapsedMs = nowMs - m_lastScrollTickMs;
if(elapsedMs > kMaxElapsedMs) elapsedMs = kMaxElapsedMs;
m_panoramaScroll += kScrollPerMs * static_cast<float>(elapsedMs);
m_panoramaScroll += m_panoramaScrollSpeed * static_cast<float>(elapsedMs);
}
m_lastScrollTickMs = nowMs;
@@ -290,19 +493,33 @@ void UIComponent_Panorama::EnsurePanoramaTexturesLoaded()
m_panoramaTextureRoot = currentRoot;
m_bPanoramaTexturesLoaded = false;
// search for panorama.xml
PanoramaConfig config = LoadPanoramaConfig(currentRoot);
m_panoramaScrollSpeed = config.scrollSpeed;
m_panoramaBlurSigma = config.blurSigma;
m_panoramaGridEnabled = config.gridEnabled;
m_panoramaGridSizeX = config.gridSizeX;
m_panoramaGridSizeY = config.gridSizeY;
m_panoramaGridScalingNearest = config.gridScalingNearest;
int dayWidth = 0;
int dayHeight = 0;
m_texPanoramaDay = LoadPanoramaQuadTexture(L"Panorama_S.png", dayWidth, dayHeight);
m_texPanoramaDay = LoadPanoramaQuadTexture(L"Panorama_S.png", m_panoramaBlurSigma, dayWidth, dayHeight);
int nightWidth = 0;
int nightHeight = 0;
m_texPanoramaNight = LoadPanoramaQuadTexture(L"Panorama_N.png", nightWidth, nightHeight);
m_texPanoramaNight = LoadPanoramaQuadTexture(L"Panorama_N.png", m_panoramaBlurSigma, nightWidth, nightHeight);
if(dayWidth > 0 && dayHeight > 0)
{
m_panoramaAspect = static_cast<float>(dayWidth) / static_cast<float>(dayHeight);
m_panoramaNativeWidth = dayWidth;
m_panoramaNativeHeight = dayHeight;
}
else if(nightWidth > 0 && nightHeight > 0)
{
m_panoramaAspect = static_cast<float>(nightWidth) / static_cast<float>(nightHeight);
m_panoramaNativeWidth = nightWidth;
m_panoramaNativeHeight = nightHeight;
}
m_bPanoramaTexturesLoaded = (m_texPanoramaDay >= 0 || m_texPanoramaNight >= 0);
}
@@ -328,31 +545,27 @@ void UIComponent_Panorama::DrawPanoramaBackgroundQuad(S32 width, S32 height)
Minecraft::GetInstance()->textures->bind(texId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
float sourceWidth = static_cast<float>(height) * m_panoramaAspect;
float scroll = m_panoramaScroll - floorf(m_panoramaScroll);
float scrollPx = scroll * sourceWidth;
float startX = -scrollPx;
float startY = 0.0f;
const float overscanY = (static_cast<float>(height) * 0.03f > 8.0f) ? (static_cast<float>(height) * 0.03f) : 8.0f;
const float drawTop = -overscanY;
const float drawBottom = static_cast<float>(height) + overscanY;
// nearest should only be used in grid mode, i dont necessarily see a purpose for it otherwise
const int panoramaFilter = (m_panoramaGridEnabled && m_panoramaGridScalingNearest) ? GL_NEAREST : GL_LINEAR;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, panoramaFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, panoramaFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
Tesselator *t = Tesselator::getInstance();
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
t->begin();
for(float x = startX - sourceWidth; x < static_cast<float>(width) + sourceWidth; x += sourceWidth)
if(m_panoramaGridEnabled)
{
float left = x;
float right = x + sourceWidth;
t->vertexUV(left, drawBottom, 0.0f, 0.0f, 1.0f);
t->vertexUV(right, drawBottom, 0.0f, 1.0f, 1.0f);
t->vertexUV(right, drawTop, 0.0f, 1.0f, 0.0f);
t->vertexUV(left, drawTop, 0.0f, 0.0f, 0.0f);
const float tileWidth = static_cast<float>(m_panoramaNativeWidth) * m_panoramaGridSizeX;
const float tileHeight = static_cast<float>(m_panoramaNativeHeight) * m_panoramaGridSizeY;
DrawGridPanoramaQuads(width, height, tileWidth, tileHeight, m_panoramaScroll, t);
}
t->end();
else
{
DrawScrollingPanoramaQuads(width, height, m_panoramaAspect, m_panoramaScroll, t);
}
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glDisable(GL_BLEND);

View File

@@ -8,14 +8,29 @@ private:
bool m_bShowingDay;
void EnsurePanoramaTexturesLoaded();
void DrawPanoramaBackgroundQuad(S32 width, S32 height);
int m_texPanoramaDay = -1;
int m_texPanoramaNight = -1;
bool m_bPanoramaTexturesLoaded = false;
wstring m_panoramaTextureRoot;
float m_panoramaAspect = 1.0f;
DWORD m_lastScrollTickMs = 0;
float m_panoramaScroll = 0.0f;
float m_panoramaScrollSpeed = 0.0001f / 16.6667f;
float m_panoramaBlurSigma = 0.5f;
bool m_panoramaGridEnabled = false;
float m_panoramaGridSizeX = 1.0f;
float m_panoramaGridSizeY = 1.0f;
int m_panoramaNativeWidth = 0;
int m_panoramaNativeHeight = 0;
bool m_panoramaGridScalingNearest = false;
public:
UIComponent_Panorama(int iPad, void *initData, UILayer *parentLayer);

View File

@@ -2206,6 +2206,29 @@ int Level::getTopSolidBlock(int x, int z)
return -1;
}
int Level::getTopSolidOrLiquidBlock(int x, int z)
{
LevelChunk *levelChunk = getChunkAt(x, z);
int y = levelChunk->getHighestSectionPosition() + 15;
x &= 15;
z &= 15;
while (y > 0)
{
int t = levelChunk->getTile(x, y, z);
if (t == 0 || Tile::tiles[t]->material == Material::leaves)
{
y--;
}
else
{
return y + 1;
}
}
return -1;
}
int Level::getLightDepth(int x, int z)
{

View File

@@ -331,6 +331,7 @@ public:
Vec3 *getFogColor(float a);
int getTopRainBlock(int x, int z);
int getTopSolidBlock(int x, int z);
int getTopSolidOrLiquidBlock(int x, int z);
bool biomeHasRain(int x, int z); // 4J added
bool biomeHasSnow(int x, int z); // 4J added
int getLightDepth(int x, int z);

View File

@@ -211,6 +211,7 @@ StructurePiece *VillagePieces::generateAndAddPiece(StartPiece *startPiece, list<
StructurePiece *newPiece = generatePieceFromSmallDoor(startPiece, pieces, random, footX, footY, footZ, direction, depth + 1);
if (newPiece != nullptr)
{
/*
int x = (newPiece->boundingBox->x0 + newPiece->boundingBox->x1) / 2;
int z = (newPiece->boundingBox->z0 + newPiece->boundingBox->z1) / 2;
int xs = newPiece->boundingBox->x1 - newPiece->boundingBox->x0;
@@ -218,11 +219,12 @@ StructurePiece *VillagePieces::generateAndAddPiece(StartPiece *startPiece, list<
int r = xs > zs ? xs : zs;
if (startPiece->getBiomeSource()->containsOnly(x, z, r / 2 + 4, VillageFeature::allowedBiomes))
{
*/
pieces->push_back(newPiece);
startPiece->pendingHouses.push_back(newPiece);
return newPiece;
}
delete newPiece;
// }
// delete newPiece;
}
return nullptr;
}
@@ -242,6 +244,7 @@ StructurePiece *VillagePieces::generateAndAddRoadPiece(StartPiece *startPiece, l
if (box != nullptr && box->y0 > LOWEST_Y_POSITION)
{
StructurePiece *newPiece = new StraightRoad(startPiece, depth, random, box, direction);
/*
int x = (newPiece->boundingBox->x0 + newPiece->boundingBox->x1) / 2;
int z = (newPiece->boundingBox->z0 + newPiece->boundingBox->z1) / 2;
int xs = newPiece->boundingBox->x1 - newPiece->boundingBox->x0;
@@ -249,12 +252,13 @@ StructurePiece *VillagePieces::generateAndAddRoadPiece(StartPiece *startPiece, l
int r = xs > zs ? xs : zs;
if (startPiece->getBiomeSource()->containsOnly(x, z, r / 2 + 4, VillageFeature::allowedBiomes))
{
*/
pieces->push_back(newPiece);
startPiece->pendingRoads.push_back(newPiece);
return newPiece;
}
// }
// 4J Stu - The dtor for newPiece will destroy box
delete newPiece;
// delete newPiece;
}
else if(box != nullptr)
{
@@ -486,6 +490,24 @@ void VillagePieces::VillagePiece::generateBox(Level *level, BoundingBox *chunkBB
void VillagePieces::VillagePiece::fillColumnDown(Level *level, int block, int data, int x, int startY, int z, BoundingBox *chunkBB)
{
// clear any and all plants
int worldX = getWorldX(x, z);
int worldZ = getWorldZ(x, z);
int worldY = getWorldY(startY);
for (int y = worldY; y > 0; y--)
{
int tile = level->getTile(worldX, y, worldZ);
if (tile == Tile::tallgrass_Id || tile == Tile::double_plant_Id || tile == Tile::deadbush_Id)
{
level->setTileAndData(worldX, y, worldZ, 0, 0, Tile::UPDATE_CLIENTS);
}
else if (tile != 0)
{
break;
}
}
int bblock = biomeBlock(block, data);
int bdata = biomeData(block, data);
StructurePiece::fillColumnDown(level, bblock, bdata, x, startY, z, chunkBB);
@@ -727,7 +749,7 @@ bool VillagePieces::StraightRoad::postProcess(Level *level, Random *random, Boun
{
if (chunkBB->isInside(x, 64, z))
{
int y = level->getTopSolidBlock(x, z) - 1;
int y = level->getTopSolidOrLiquidBlock(x, z) - 1; // was getTopSolidBlock
int tileAtY = level->getTile(x, y, z);
if (tileAtY == Tile::water_Id || tileAtY == Tile::flowing_water_Id)
{
@@ -735,13 +757,28 @@ bool VillagePieces::StraightRoad::postProcess(Level *level, Random *random, Boun
int fill = y - 1;
while (fill >= 0 && (level->getTile(x, fill, z) == Tile::water_Id || level->getTile(x, fill, z) == Tile::flowing_water_Id))
{
level->setTileAndData(x, fill, z, Tile::planks_Id, 0, Tile::UPDATE_CLIENTS);
level->setTileAndData(x, y, z, Tile::planks_Id, 0, Tile::UPDATE_CLIENTS);
fill--;
}
}
else
{
level->setTileAndData(x, y, z, roadTile, 0, Tile::UPDATE_CLIENTS);
// tall grass fixes
int tileBelow = level->getTile(x, y - 1, z);
if (tileBelow == Tile::double_plant_Id)
{
level->setTileAndData(x, y, z, 0, 0, Tile::UPDATE_CLIENTS);
level->setTileAndData(x, y - 1, z, 0, 0, Tile::UPDATE_CLIENTS);
level->setTileAndData(x, y - 2, z, roadTile, 0, Tile::UPDATE_CLIENTS);
}
else if (level->getTile(x, y, z) == Tile::tallgrass_Id)
{
level->setTileAndData(x, y, z, 0, 0, Tile::UPDATE_CLIENTS);
level->setTileAndData(x, y - 1, z, roadTile, 0, Tile::UPDATE_CLIENTS);
}
else {
level->setTileAndData(x, y, z, roadTile, 0, Tile::UPDATE_CLIENTS);
}
}
}
}