[Scummvm-git-logs] scummvm master -> 3d68656634218d213a8aa3912573c85cc2d55850

whoozle noreply at scummvm.org
Thu Jun 11 22:10:35 UTC 2026


This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
c77e04e211 PHOENIXVR: Improve text font handling
b06f7828e1 VIDEO: Allow Phoenix 4XM v1 videos
855a48e3a6 PHOENIXVR: Add more game variants
a16464659b PHOENIXVR: Improve save room rendering
e07b3297ab PHOENIXVR: Fix Amerzone save room level restore
3d68656634 PHOENIXVR: Add more game variants


Commit: c77e04e2118a4b82ff732a9d8e1b2e560c82e49a
    https://github.com/scummvm/scummvm/commit/c77e04e2118a4b82ff732a9d8e1b2e560c82e49a
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
PHOENIXVR: Improve text font handling

Changed paths:
    engines/phoenixvr/phoenixvr.cpp
    engines/phoenixvr/phoenixvr.h


diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 265e9a580be..fe7163b293d 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -29,6 +29,7 @@
 #include "common/memstream.h"
 #include "common/savefile.h"
 #include "common/scummsys.h"
+#include "common/str-enc.h"
 #include "common/system.h"
 #include "engines/util.h"
 #include "graphics/font.h"
@@ -54,6 +55,15 @@ namespace PhoenixVR {
 
 PhoenixVREngine *g_engine;
 
+static Common::CodePage getTextCodePage(Common::Language language) {
+	switch (language) {
+	case Common::RU_RUS:
+		return Common::kWindows1251;
+	default:
+		return Common::kWindows1252;
+	}
+}
+
 PhoenixVREngine::PhoenixVREngine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
 																					 _frameLimiter(g_system, kFPSLimit),
 																					 _gameDescription(gameDesc),
@@ -758,12 +768,16 @@ void PhoenixVREngine::loadVariables() {
 
 const Graphics::Font *PhoenixVREngine::getFont(int size, bool bold) const {
 #ifdef USE_FREETYPE2
-	if (size < 14)
-		return _font12.get();
-	else if (size < 18)
-		return _font14.get();
-	else
-		return _font18.get();
+	const int fontMaxSizes[] = {10, 12, 14, 16, 18, INT_MAX};
+
+	for (uint i = 0; i < ARRAYSIZE(fontMaxSizes); ++i) {
+		if (size < fontMaxSizes[i]) {
+			const Graphics::Font *font = bold ? _boldFonts[i].get() : nullptr;
+			return font ? font : _regularFonts[i].get();
+		}
+	}
+
+	return _regularFonts[ARRAYSIZE(fontMaxSizes) - 1].get();
 #else
 	return FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);
 #endif
@@ -822,9 +836,9 @@ void PhoenixVREngine::rollover(int textId, RolloverType type) {
 		return;
 	}
 	auto &text = _textes.getVal(textId);
-	debug("rollover %s, %s font size: %d, bold: %d, color: %02x", dstRect.toString().c_str(), text.c_str(), size, bold, color);
+	debug("rollover %s, %s font size: %d, bold: %d, color: %02x", dstRect.toString().c_str(), text.encode(Common::kUtf8).c_str(), size, bold, color);
 
-	Common::Array<Common::String> lines;
+	Common::Array<Common::U32String> lines;
 	font->wordWrapText(text, dstRect.width(), lines, Graphics::kWordWrapDefault);
 
 	auto fontH = font->getFontHeight();
@@ -983,10 +997,13 @@ Common::Error PhoenixVREngine::run() {
 
 	_arn.reset(ARN::create());
 #ifdef USE_FREETYPE2
-	static const Common::String family("NotoSerif-Bold.ttf");
-	_font12.reset(Graphics::loadTTFFontFromArchive(family, 12));
-	_font14.reset(Graphics::loadTTFFontFromArchive(family, 14));
-	_font18.reset(Graphics::loadTTFFontFromArchive(family, 18));
+	static const Common::String regular("NotoSans-Regular.ttf");
+	static const Common::String bold("NotoSans-Bold.ttf");
+	const int fontSizes[] = {8, 10, 12, 14, 16, 18};
+	for (uint i = 0; i < ARRAYSIZE(fontSizes); ++i) {
+		_regularFonts[i].reset(Graphics::loadTTFFontFromArchive(regular, fontSizes[i]));
+		_boldFonts[i].reset(Graphics::loadTTFFontFromArchive(bold, fontSizes[i]));
+	}
 #endif
 
 	setCursorDefault(0, "Cursor1.pcx");
@@ -1010,6 +1027,7 @@ Common::Error PhoenixVREngine::run() {
 	{
 		Common::File textes;
 		if (textes.open(Common::Path("textes.txt"))) {
+			Common::CodePage textCodePage = getTextCodePage(_gameDescription->language);
 			while (!textes.eos()) {
 				auto text = textes.readLine();
 				if (text.empty() || text[0] != '*')
@@ -1022,7 +1040,7 @@ Common::Error PhoenixVREngine::run() {
 					++pos;
 				while (pos < text.size() && Common::isSpace(text[pos]))
 					++pos;
-				_textes.setVal(textId, text.substr(pos));
+				_textes.setVal(textId, Common::convertToU32String(text.c_str() + pos, textCodePage));
 			}
 			debug("loaded %u textes", _textes.size());
 		}
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 322c6be74ec..07f411c38c9 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -272,11 +272,11 @@ private:
 	Common::Array<byte> _capturedState;
 	Common::Array<byte> _loadedState;
 
-	Common::HashMap<int, Common::String> _textes;
+	Common::HashMap<int, Common::U32String> _textes;
 
-	Common::ScopedPtr<Graphics::Font> _font12;
-	Common::ScopedPtr<Graphics::Font> _font14;
-	Common::ScopedPtr<Graphics::Font> _font18;
+	static const int kFontSizeCount = 6;
+	Common::ScopedPtr<Graphics::Font> _regularFonts[kFontSizeCount];
+	Common::ScopedPtr<Graphics::Font> _boldFonts[kFontSizeCount];
 
 	Common::ScopedPtr<Graphics::ManagedSurface> _text;
 	Common::Rect _textRect;


Commit: b06f7828e18dfc6185d7faed116531418a491df9
    https://github.com/scummvm/scummvm/commit/b06f7828e18dfc6185d7faed116531418a491df9
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
VIDEO: Allow Phoenix 4XM v1 videos

Changed paths:
    video/4xm_decoder.cpp


diff --git a/video/4xm_decoder.cpp b/video/4xm_decoder.cpp
index 5c3cc2d0695..36d2e9ed977 100644
--- a/video/4xm_decoder.cpp
+++ b/video/4xm_decoder.cpp
@@ -108,8 +108,6 @@ class FourXMDecoder::FourXMVideoTrack : public FixedRateVideoTrack {
 
 public:
 	FourXMVideoTrack(FourXMDecoder *dec, const Common::Rational &frameRate, uint w, uint h, uint16 version) : _dec(dec), _frameRate(frameRate), _w(w), _h(h), _version(version) {
-		if (_version <= 1)
-			error("versions 0 and 1 are not supported");
 		_blockType[0].reset(new HuffmanType(HuffmanType::fromFrequencies({16, 8, 4, 2, 1, 1})));
 		_blockType[1].reset(new HuffmanType(HuffmanType::fromFrequencies({8, 0, 4, 2, 1, 1})));
 		_blockType[2].reset(new HuffmanType(HuffmanType::fromFrequencies({8, 4, 0, 2, 1, 1})));
@@ -418,7 +416,7 @@ void FourXMDecoder::FourXMVideoTrack::decode_pfrm_block(uint16 *dst, const uint1
 		}
 		return;
 	}
-	if (code == 3 && _version >= 2)
+	if (code == 3)
 		return;
 
 	if (code == 0) {


Commit: 855a48e3a66c4cb624a678e7a2788cf149944069
    https://github.com/scummvm/scummvm/commit/855a48e3a66c4cb624a678e7a2788cf149944069
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
PHOENIXVR: Add more game variants

Changed paths:
    engines/phoenixvr/detection_tables.h


diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index 75e68d363ef..efe84a36f11 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -75,6 +75,16 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO1(GUIO_NONE)
 	},
 
+	{"necrono",
+		nullptr,
+		AD_ENTRY2s("script.pak", "da42a18dd02fc01f116228d5c219b2fd", 215,
+				   "textes.txt", "ab8efb7f5e92d2b76863c181bf2dbd10", 4959),
+		Common::RU_RUS,
+		Common::kPlatformWindows,
+		ADGF_DROPPLATFORM,
+		GUIO1(GUIO_NONE)
+	},
+
 	{"necrono",
 		nullptr,
 		AD_ENTRY2s("script.pak", "86294b9c445c3e06e24269c84036a207", 223,
@@ -125,6 +135,16 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO1(GUIO_NONE)}
 	,
 
+	{"lochness",
+		nullptr,
+		AD_ENTRY2s("script.pak", "a7ee3aae653658f93bba7f237bcf06f3", 1904,
+				   "textes.txt", "d1546d04243ee63f9ff6c5fc551082e1", 1763),
+		Common::RU_RUS,
+		Common::kPlatformWindows,
+		ADGF_DROPPLATFORM,
+		GUIO1(GUIO_NONE)
+	},
+
 	{"lochness",
 		nullptr,
 		AD_ENTRY2s("script.pak", "a7ee3aae653658f93bba7f237bcf06f3", 1904,


Commit: a16464659b280ccb85b0f754efb5b6ce1c6d77d6
    https://github.com/scummvm/scummvm/commit/a16464659b280ccb85b0f754efb5b6ce1c6d77d6
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
PHOENIXVR: Improve save room rendering

Changed paths:
    engines/phoenixvr/commands.h
    engines/phoenixvr/game_state.cpp
    engines/phoenixvr/metaengine.cpp
    engines/phoenixvr/phoenixvr.cpp
    engines/phoenixvr/phoenixvr.h


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index 4c79268aca6..4750a3538e2 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -512,13 +512,7 @@ struct LoadSave : public Script::Command {
 		if (!status)
 			return false;
 
-		static const int faces[] = {4, 3, 5, 1};
-		int face = faces[(slot - 1) / 2];
-		bool odd = (slot - 1) & 1;
-		// taken from necronomicon - misaligned
-		int x = odd ? 275 : 97;
-		int y = 200;
-		g_engine->drawSlot(slot, face, x, y);
+		g_engine->drawSaveCard(slot);
 		return true;
 	}
 
diff --git a/engines/phoenixvr/game_state.cpp b/engines/phoenixvr/game_state.cpp
index b24e1a8abd4..89e2e45e9d5 100644
--- a/engines/phoenixvr/game_state.cpp
+++ b/engines/phoenixvr/game_state.cpp
@@ -3,19 +3,31 @@
 #include "graphics/surface.h"
 
 namespace PhoenixVR {
+
+static Common::String readSaveText(Common::SeekableReadStream &stream) {
+	const uint32 size = stream.readUint32LE();
+	Common::String result;
+
+	for (uint32 i = 0; i < size; ++i) {
+		const byte c = stream.readByte();
+		if (c == 0) {
+			result += '\n';
+		} else {
+			result += static_cast<char>(c);
+		}
+	}
+
+	return result;
+}
+
 GameState GameState::load(Common::SeekableReadStream &stream) {
 	GameState state;
 
-	auto readString = [&]() {
-		auto size = stream.readUint32LE();
-		return stream.readString(0, size);
-	};
-
-	state.script = readString();
+	state.script = readSaveText(stream);
 	debug("save.script: %s", state.script.c_str());
-	state.game = readString();
+	state.game = readSaveText(stream);
 	debug("save.game: %s", state.game.c_str());
-	state.info = readString();
+	state.info = readSaveText(stream);
 	debug("save.datetime: %s", state.info.c_str());
 	uint dibHeaderSize = stream.readUint32LE();
 	stream.seek(-4, SEEK_CUR);
diff --git a/engines/phoenixvr/metaengine.cpp b/engines/phoenixvr/metaengine.cpp
index ae4dd2e88aa..19f1d24031a 100644
--- a/engines/phoenixvr/metaengine.cpp
+++ b/engines/phoenixvr/metaengine.cpp
@@ -33,6 +33,15 @@
 
 namespace PhoenixVR {
 
+static Common::String flattenSaveText(const Common::String &text) {
+	Common::String result = text;
+	for (uint i = 0; i < result.size(); ++i) {
+		if (result[i] == '\n')
+			result.setChar(' ', i);
+	}
+	return result;
+}
+
 static const ADExtraGuiOptionsMap optionsList[] = {
 	{GAMEOPTION_ORIGINAL_SAVELOAD,
 	 {_s("Use original save/load screens"),
@@ -150,7 +159,7 @@ SaveStateDescriptor PhoenixVRMetaEngine::querySaveMetaInfos(const char *target,
 	SaveStateDescriptor desc;
 	desc.setSaveSlot(slotIdx);
 	desc.setDeletableFlag(true);
-	desc.setDescription(state.game + " " + state.info);
+	desc.setDescription(PhoenixVR::flattenSaveText(state.game) + " " + PhoenixVR::flattenSaveText(state.info));
 	Graphics::PixelFormat rgb565(2, 5, 6, 5, 0, 11, 5, 0, 0);
 	desc.setThumbnail(state.getThumbnail(rgb565, 160));
 	return desc;
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index fe7163b293d..ab34e347fb7 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -64,12 +64,237 @@ static Common::CodePage getTextCodePage(Common::Language language) {
 	}
 }
 
+static bool isAmerzoneGame(const ADGameDescription *gameDesc) {
+	return !strcmp(gameDesc->gameId, "amerzone");
+}
+
+static Common::String getAmerzoneLevelLabel(const Common::String &script) {
+	static const struct {
+		const char *prefix;
+		const char *label;
+	} levels[] = {
+		{"01VR_PHARE", "Le Phare"},
+		{"02VR_ILE", "L'Ile"},
+		{"03VR_PUEBLO", "Le Pueblo"},
+		{"04VR_FLEUVE", "Le Fleuve"},
+		{"05VR_VILLAGEMARAIS", "Le Village"},
+		{"07VRTEMPLE_VOLCAN", "Le Temple"}
+	};
+
+	for (const auto &level : levels) {
+		if (script.hasPrefixIgnoreCase(level.prefix))
+			return level.label;
+	}
+
+	return "Amerzone";
+}
+
+static const char *mfull[] = {
+	"January", "February", "March", "April", "May", "June",
+	"July", "August", "September", "October", "November", "December"
+};
+
+static const char *wday[] = {
+	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+
+static Common::String makeSaveText(const Common::String &firstLine, const Common::String &secondLine) {
+	Common::String result = firstLine;
+	result += '\0';
+	result += secondLine;
+	return result;
+}
+
+static Common::String makeSaveText(const Common::String &firstLine, const Common::String &secondLine, const Common::String &thirdLine) {
+	Common::String result = makeSaveText(firstLine, secondLine);
+	result += '\0';
+	result += thirdLine;
+	return result;
+}
+
+static Common::String formatSaveInfo(const TimeDate &td, bool longDate, const Common::String &place = Common::String()) {
+	if (longDate) {
+		return makeSaveText(
+			Common::String::format("%s, %s %d, %04d", wday[td.tm_wday], mfull[td.tm_mon], td.tm_mday, td.tm_year + 1900),
+			Common::String::format("%02d:%02d:%02d %s", td.tm_hour, td.tm_min, td.tm_sec, td.tm_hour < 12 ? "AM" : "PM"),
+			place);
+	}
+
+	return makeSaveText(
+		Common::String::format("%s %02d %02d %04d", wday[td.tm_wday], td.tm_mday, td.tm_mon + 1, td.tm_year + 1900),
+		Common::String::format("%02d h %02d", td.tm_hour, td.tm_min));
+}
+
+static int mapSaveSlotY(int y, bool splitV, int tileY) {
+	int splitLine = (tileY + 1) * 256;
+	if (splitV && y >= splitLine)
+		return (tileY + 3) * 256 + y - splitLine;
+	return y;
+}
+
+static void fillSaveSlotRect(Graphics::Surface &dst, const Common::Rect &rect, uint32 color, bool splitV, int tileY) {
+	if (splitV) {
+		int splitLine = (tileY + 1) * 256;
+		int topH = CLIP<int>(splitLine - rect.top, 0, rect.height());
+		if (topH > 0) {
+			Common::Rect top = rect;
+			top.bottom = rect.top + topH;
+			dst.fillRect(top, color);
+		}
+		if (topH < rect.height()) {
+			int bottomY = (tileY + 3) * 256 + MAX(rect.top - splitLine, 0);
+			Common::Rect bottom(rect.left, bottomY, rect.right, bottomY + rect.height() - topH);
+			dst.fillRect(bottom, color);
+		}
+	} else {
+		dst.fillRect(rect, color);
+	}
+}
+
+static int drawSaveTextBlock(Graphics::Surface &dst, const Graphics::Font *font, const Common::String &text,
+		int x, int y, int width, uint32 color, Graphics::TextAlign align, int lineHeight, bool splitV, int tileY,
+		bool reserveEmptyFinalLine = false) {
+	bool hasText = false;
+	for (uint i = 0; i < text.size(); ++i) {
+		if (text[i] != '\n' && text[i] != '\0') {
+			hasText = true;
+			break;
+		}
+	}
+	if (!hasText)
+		return y;
+
+	uint start = 0;
+	for (uint i = 0; i < text.size(); ++i) {
+		if (text[i] == '\n' || text[i] == '\0') {
+			if (i > start) {
+				Common::String line;
+				for (uint j = start; j < i; ++j)
+					line += text[j];
+				font->drawString(&dst, line, x, mapSaveSlotY(y, splitV, tileY), width, color, align);
+				y += lineHeight;
+			} else if (reserveEmptyFinalLine && i == text.size() - 1) {
+				y += lineHeight;
+			}
+			start = i + 1;
+		}
+	}
+	if (start < text.size()) {
+		Common::String line;
+		for (uint j = start; j < text.size(); ++j)
+			line += text[j];
+		font->drawString(&dst, line, x, mapSaveSlotY(y, splitV, tileY), width, color, align);
+		y += lineHeight;
+	}
+
+	return y;
+}
+
+static int saveCardTileId(int face, int x, int y) {
+	return (face << 2) + ((y < 256) ? (x < 256 ? 0 : 1) : (x < 256 ? 3 : 2));
+}
+
+static void copyCubeFaceToSurface(Graphics::ManagedSurface &faceSurface, const Graphics::Surface &vrSurface, int face) {
+	for (int y = 0; y < 512; ++y) {
+		for (int x = 0; x < 512; ++x) {
+			const int tileId = saveCardTileId(face, x, y);
+			faceSurface.setPixel(x, y, vrSurface.getPixel(x & 0xff, (tileId << 8) + (y & 0xff)));
+		}
+	}
+}
+
+static void copySurfaceToCubeFace(Graphics::Surface &vrSurface, const Graphics::ManagedSurface &faceSurface, int face) {
+	for (int y = 0; y < 512; ++y) {
+		for (int x = 0; x < 512; ++x) {
+			const int tileId = saveCardTileId(face, x, y);
+			vrSurface.setPixel(x & 0xff, (tileId << 8) + (y & 0xff), faceSurface.getPixel(x, y));
+		}
+	}
+}
+
+static void projectSaveCard(Graphics::ManagedSurface &faceSurface, const Graphics::ManagedSurface &card, float angle) {
+	struct Vertex {
+		float x;
+		float y;
+		float invW;
+		float uOverW;
+		float vOverW;
+	};
+
+	const float srcW = static_cast<float>(card.w);
+	const float srcH = static_cast<float>(card.h);
+	const float distance = srcW / 8.0f + srcW * 8.0f / 6.283100128173828f;
+	const float cosA = cosf(angle);
+	const float sinA = sinf(angle);
+
+	auto makeVertex = [&](float modelU, float modelV, float textureU, float textureV) {
+		const float modelX = modelU - srcW / 2.0f;
+		const float modelY = distance;
+		const float modelZ = srcH / 2.0f - modelV + 32.0f;
+		const float projectedW = (modelX * sinA - modelY * cosA) / 256.0f;
+		const float invW = 1.0f / projectedW;
+
+		Vertex vertex;
+		vertex.x = (modelX * (cosA + sinA) + modelY * (sinA - cosA)) * invW;
+		vertex.y = (modelX * sinA - modelY * cosA - modelZ) * invW;
+		vertex.invW = invW;
+		vertex.uOverW = textureU * invW;
+		vertex.vOverW = textureV * invW;
+		return vertex;
+	};
+
+	Vertex vertices[4] = {
+		makeVertex(0.0f, 0.0f, srcW, srcH),
+		makeVertex(static_cast<float>(card.w), 0.0f, 0.0f, srcH),
+		makeVertex(static_cast<float>(card.w), static_cast<float>(card.h), 0.0f, 0.0f),
+		makeVertex(0.0f, static_cast<float>(card.h), srcW, 0.0f)
+	};
+
+	auto rasterizeTriangle = [&](const Vertex &a, const Vertex &b, const Vertex &c) {
+		const float area = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
+		if (ABS(area) < 0.0001f)
+			return;
+
+		int minX = CLIP<int>(static_cast<int>(floorf(MIN(a.x, MIN(b.x, c.x)))), 0, faceSurface.w - 1);
+		int maxX = CLIP<int>(static_cast<int>(ceilf(MAX(a.x, MAX(b.x, c.x)))), 0, faceSurface.w - 1);
+		int minY = CLIP<int>(static_cast<int>(floorf(MIN(a.y, MIN(b.y, c.y)))), 0, faceSurface.h - 1);
+		int maxY = CLIP<int>(static_cast<int>(ceilf(MAX(a.y, MAX(b.y, c.y)))), 0, faceSurface.h - 1);
+
+		for (int y = minY; y <= maxY; ++y) {
+			for (int x = minX; x <= maxX; ++x) {
+				const float px = static_cast<float>(x) + 0.5f;
+				const float py = static_cast<float>(y) + 0.5f;
+				const float w0 = ((b.x - px) * (c.y - py) - (b.y - py) * (c.x - px)) / area;
+				const float w1 = ((c.x - px) * (a.y - py) - (c.y - py) * (a.x - px)) / area;
+				const float w2 = 1.0f - w0 - w1;
+				if (w0 < 0.0f || w1 < 0.0f || w2 < 0.0f)
+					continue;
+
+				const float invW = w0 * a.invW + w1 * b.invW + w2 * c.invW;
+				if (ABS(invW) < 0.0001f)
+					continue;
+				const float u = (w0 * a.uOverW + w1 * b.uOverW + w2 * c.uOverW) / invW;
+				const float v = (w0 * a.vOverW + w1 * b.vOverW + w2 * c.vOverW) / invW;
+				if (u < 0.0f || u > srcW || v < 0.0f || v > srcH)
+					continue;
+
+				const int srcX = CLIP<int>(static_cast<int>(floorf(u)), 0, card.w - 1);
+				const int srcY = CLIP<int>(static_cast<int>(floorf(v)), 0, card.h - 1);
+				faceSurface.setPixel(x, y, card.getPixel(srcX, srcY));
+			}
+		}
+	};
+
+	rasterizeTriangle(vertices[0], vertices[1], vertices[2]);
+	rasterizeTriangle(vertices[0], vertices[2], vertices[3]);
+}
+
 PhoenixVREngine::PhoenixVREngine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
 																					 _frameLimiter(g_system, kFPSLimit),
 																					 _gameDescription(gameDesc),
 																					 _randomSource("PhoenixVR"),
 																					 _rgb565(2, 5, 6, 5, 0, 11, 5, 0, 0),
-																					 _thumbnail(139, 103, _rgb565),
+																					 _thumbnail(isAmerzoneGame(gameDesc) ? 232 : 139, isAmerzoneGame(gameDesc) ? 174 : 103, _rgb565),
 																					 _lockKey(13),
 																					 _fov(kPi2),
 																					 _angleX(0),
@@ -1415,14 +1640,16 @@ Common::Error PhoenixVREngine::saveGameStream(Common::WriteStream *slot, bool is
 	GameState state;
 	state.script = _contextScript;
 	state.game = _contextLabel;
-
-	static const char *wday[] = {
-		"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
+	const bool isAmerzone = gameIdMatches("amerzone");
+	Common::String amerzoneLevelLabel;
+	if (isAmerzone) {
+		amerzoneLevelLabel = getAmerzoneLevelLabel(state.script);
+		state.game.clear();
+	}
 
 	TimeDate td = {};
 	g_system->getTimeAndDate(td);
-	// Saturday 03 01 2026[\x00]23 h 17
-	state.info = Common::String::format("%s %02d %02d %04d%c%02d h %02d", wday[td.tm_wday], td.tm_mday, td.tm_mon + 1, td.tm_year + 1900, 0, td.tm_hour, td.tm_min);
+	state.info = formatSaveInfo(td, isAmerzone, amerzoneLevelLabel);
 
 	state.thumbWidth = _thumbnail.w;
 	state.thumbHeight = _thumbnail.h;
@@ -1458,11 +1685,60 @@ Common::Error PhoenixVREngine::saveGameStream(Common::WriteStream *slot, bool is
 	return Common::kNoError;
 }
 
+void PhoenixVREngine::drawSaveCard(int idx) {
+	if (!gameIdMatches("amerzone")) {
+		static const int faces[] = {4, 3, 5, 1};
+		const int face = faces[(idx - 1) / 2];
+		const bool odd = (idx - 1) & 1;
+		drawSlot(idx, face, odd ? 275 : 97, 200);
+		return;
+	}
+
+	Common::ScopedPtr<Common::InSaveFile> slot(_saveFileMan->openForLoading(getSaveStateName(idx)));
+	if (!slot)
+		return;
+
+	auto state = GameState::load(*slot);
+	auto &dst = _vr.getSurface();
+	Graphics::Surface *thumbnail = state.getThumbnail(dst.format, 232);
+	const int cardW = thumbnail->w + 6;
+	const int cardH = thumbnail->w + 30;
+
+	Graphics::ManagedSurface card(cardW, cardH, dst.format);
+	const uint32 white = dst.format.RGBToColor(0xff, 0xff, 0xff);
+	const uint32 black = dst.format.RGBToColor(0, 0, 0);
+	card.fillRect(Common::Rect(0, 0, cardW, cardH), white);
+	card.fillRect(Common::Rect(0, 0, cardW, 1), black);
+	card.fillRect(Common::Rect(0, cardH - 1, cardW, cardH), black);
+	card.fillRect(Common::Rect(0, 0, 1, cardH), black);
+	card.fillRect(Common::Rect(cardW - 1, 0, cardW, cardH), black);
+	card.copyRectToSurface(*thumbnail, 3, 6, thumbnail->getRect());
+
+	const Graphics::Font *font = getFont(16, true);
+	if (font) {
+		int textY = thumbnail->h + 18;
+		textY = drawSaveTextBlock(*card.surfacePtr(), font, state.game, 0, textY, cardW, black, Graphics::kTextAlignCenter, 18, false, 0);
+		drawSaveTextBlock(*card.surfacePtr(), font, state.info, 0, textY, cardW, black, Graphics::kTextAlignCenter, 18, false, 0);
+	}
+
+	static const int faces[] = {4, 3, 5, 1};
+	const int face = faces[(idx - 1) / 2];
+	const float angle = ((idx - 1) & 1) ? -kPi / 8.0f : kPi / 8.0f;
+	Graphics::ManagedSurface faceSurface(512, 512, dst.format);
+	copyCubeFaceToSurface(faceSurface, dst, face);
+	projectSaveCard(faceSurface, card, angle);
+	copySurfaceToCubeFace(dst, faceSurface, face);
+
+	thumbnail->free();
+	delete thumbnail;
+}
+
 void PhoenixVREngine::drawSlot(int idx, int face, int x, int y) {
 	Common::ScopedPtr<Common::InSaveFile> slot(_saveFileMan->openForLoading(getSaveStateName(idx)));
 	if (!slot)
 		return;
 	auto state = GameState::load(*slot);
+	const bool isAmerzone = gameIdMatches("amerzone");
 
 	y += face * 4 * 256;
 	bool splitV = true;
@@ -1473,8 +1749,24 @@ void PhoenixVREngine::drawSlot(int idx, int face, int x, int y) {
 	}
 
 	auto &dst = _vr.getSurface();
-	auto *src = state.getThumbnail(dst.format);
+	auto *src = state.getThumbnail(dst.format, isAmerzone ? 232 : 0);
 	int tileY = y / 256;
+	if (isAmerzone) {
+		const int cardX = x - 3;
+		const int cardY = y - 6;
+		const int cardW = src->w + 6;
+		const int cardH = src->w + 30;
+		uint32 white = dst.format.RGBToColor(0xff, 0xff, 0xff);
+		uint32 black = dst.format.RGBToColor(0, 0, 0);
+		fillSaveSlotRect(dst, Common::Rect(cardX, cardY, cardX + cardW, cardY + cardH), white, splitV, tileY);
+		fillSaveSlotRect(dst, Common::Rect(cardX, cardY, cardX + cardW, cardY + 1), black, splitV, tileY);
+		fillSaveSlotRect(dst, Common::Rect(cardX, cardY + cardH - 1, cardX + cardW, cardY + cardH), black, splitV, tileY);
+		fillSaveSlotRect(dst, Common::Rect(cardX, cardY, cardX + 1, cardY + cardH), black, splitV, tileY);
+		fillSaveSlotRect(dst, Common::Rect(cardX + cardW - 1, cardY, cardX + cardW, cardY + cardH), black, splitV, tileY);
+		x = cardX + 3;
+		y = cardY + 6;
+		tileY = y / 256;
+	}
 	auto srcRect = src->getRect();
 	short srcSplitY = MIN(y + src->h, (tileY + 1) * 256) - y;
 	if (splitV)
@@ -1485,12 +1777,26 @@ void PhoenixVREngine::drawSlot(int idx, int face, int x, int y) {
 		srcRect.bottom = src->h;
 		dst.copyRectToSurface(*src, x, (tileY + 3) * 256, srcRect);
 	}
-	auto *font = getFont(12, false);
-	static int kMargin = 14;
+	auto *font = getFont(isAmerzone ? 10 : 12, isAmerzone);
 	if (font) {
 		auto color = dst.format.RGBToColor(0, 0, 0);
-		auto dstY = splitV ? (tileY + 3) * 256 - srcSplitY : y;
-		font->drawString(&dst, state.info, x, dstY + kMargin + src->h, src->w, color, Graphics::TextAlign::kTextAlignCenter);
+		int textX = x;
+		int textW = src->w;
+		Graphics::TextAlign textAlign = Graphics::kTextAlignLeft;
+		int textY = y + 0x72;
+		int lineHeight = 14;
+		if (isAmerzone) {
+			textX = x - 3;
+			textW = src->w + 6;
+			textY = y - 6 + src->h + 14;
+			textAlign = Graphics::kTextAlignCenter;
+
+			textY = drawSaveTextBlock(dst, font, state.game, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
+			drawSaveTextBlock(dst, font, state.info, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
+		} else {
+			textY = drawSaveTextBlock(dst, font, state.game, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY, true);
+			drawSaveTextBlock(dst, font, state.info, textX, textY, textW, color, textAlign, lineHeight, splitV, tileY);
+		}
 	}
 
 	src->free();
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 07f411c38c9..f5928c5328b 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -175,6 +175,7 @@ public:
 	Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
 	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override;
 	void drawSlot(int idx, int face, int x, int y);
+	void drawSaveCard(int idx);
 	void captureContext();
 
 	void setContextLabel(const Common::String &contextLabel) {


Commit: e07b3297ab63c2759d7c4077d948a7c8a356e47b
    https://github.com/scummvm/scummvm/commit/e07b3297ab63c2759d7c4077d948a7c8a356e47b
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
PHOENIXVR: Fix Amerzone save room level restore

Changed paths:
    engines/phoenixvr/commands.h
    engines/phoenixvr/phoenixvr.cpp
    engines/phoenixvr/phoenixvr.h


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index 4750a3538e2..ca03d986cd7 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -548,6 +548,15 @@ struct LoadSave : public Script::Command {
 		} else if (n == 2) {
 			auto &srcVar = args[0];
 			auto &dstVar = args[1];
+			if (!dstVar.empty() && Common::isDigit(dstVar[0])) {
+				uint level = atoi(dstVar.c_str());
+				uint currentLevel = g_engine->currentAmerzoneLevel();
+				if (currentLevel != 0) {
+					g_engine->setVariable(srcVar, currentLevel == level);
+					return;
+				}
+			}
+
 			auto value = g_engine->getVariable(srcVar);
 			g_engine->setVariable(srcVar, 0);
 			g_engine->setVariable(dstVar, value);
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index ab34e347fb7..58672d84f74 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -341,6 +341,20 @@ bool PhoenixVREngine::gameIdMatches(const char *gameId) const {
 	return strcmp(_gameDescription->gameId, gameId) == 0;
 }
 
+uint PhoenixVREngine::currentAmerzoneLevel() const {
+	if (!gameIdMatches("amerzone"))
+		return 0;
+
+	uint index = 0;
+	for (const Common::String &level : _levels) {
+		++index;
+		if (_contextScript.hasPrefixIgnoreCase(level))
+			return index;
+	}
+
+	return _currentLevel;
+}
+
 Common::String PhoenixVREngine::removeDrive(const Common::String &path) {
 	if (path.size() < 2 || path[1] != ':')
 		return path;
@@ -1613,7 +1627,7 @@ Common::Error PhoenixVREngine::loadGameStream(Common::SeekableReadStream *slot)
 			auto &level = _levels[i];
 			if (state.script.hasPrefixIgnoreCase(level)) {
 				debug("current level is %u", i);
-				_currentLevel = i + 1;
+				_currentLevel = i;
 				break;
 			}
 		}
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index f5928c5328b..0860fe659c2 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -186,6 +186,7 @@ public:
 
 	bool wasRestarted() const { return _restarted; }
 	bool wasLoaded() const { return _loaded; }
+	uint currentAmerzoneLevel() const;
 
 	void saveVariables();
 	void loadVariables();


Commit: 3d68656634218d213a8aa3912573c85cc2d55850
    https://github.com/scummvm/scummvm/commit/3d68656634218d213a8aa3912573c85cc2d55850
Author: Scorp (scorp at mrs.mn)
Date: 2026-06-11T23:10:30+01:00

Commit Message:
PHOENIXVR: Add more game variants

Changed paths:
    engines/phoenixvr/detection_tables.h


diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index efe84a36f11..b733ed3b108 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -128,8 +128,8 @@ const ADGameDescription gameDescriptions[] = {
 	{"lochness",
 		nullptr,
 		AD_ENTRY2s("script.pak", "a7ee3aae653658f93bba7f237bcf06f3", 1904,
-				   "textes.txt", "3f2deea06efed98a355ea03d69fd15ce", 1608),
-		Common::EN_USA,
+				   "textes.txt", "d1546d04243ee63f9ff6c5fc551082e1", 1763),
+		Common::RU_RUS,
 		Common::kPlatformWindows,
 		ADGF_DROPPLATFORM | ADGF_CD,
 		GUIO1(GUIO_NONE)}




More information about the Scummvm-git-logs mailing list