[Scummvm-git-logs] scummvm master -> 8a42b59f4c20e235bd39f73da1a7ea6247889328

Scorpeg noreply at scummvm.org
Thu Jul 16 14:00:06 UTC 2026


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

Summary:
0a85596fed PHOENIXVR: Add Messenger Louvre plugins
8af2829821 PHOENIXVR: Improve Messenger Louvre handling
5bf3a727cf PHOENIXVR: Add Messenger game variants
d0071eae92 PHOENIXVR: Fix Messenger save return handling
be7795688f PHOENIXVR: Keep script sounds playing during movies
b1aa7f5199 PHOENIXVR: Fix Messenger inventory overlay positions
74708adda3 PHOENIXVR: Tidy Messenger Louvre helpers
8a42b59f4c PHOENIXVR: Add debugger variable command


Commit: 0a85596fed1e133c485fecbad624f4cd9c8feb92
    https://github.com/scummvm/scummvm/commit/0a85596fed1e133c485fecbad624f4cd9c8feb92
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:37+03:00

Commit Message:
PHOENIXVR: Add Messenger Louvre plugins

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


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index 6600e76004f..a797f766b70 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -24,6 +24,7 @@
 
 #include "common/debug.h"
 #include "common/stream.h"
+#include "common/system.h"
 #include "common/textconsole.h"
 #include "phoenixvr/phoenixvr.h"
 #include "phoenixvr/script.h"
@@ -31,6 +32,430 @@
 namespace PhoenixVR {
 
 namespace {
+constexpr int kMessengerInventorySlots = 12;
+constexpr int kMessengerDisplayedInventorySlots = 8;
+constexpr int kLouvreChestSize = 128;
+
+struct LouvrePluginState {
+	int objectChest[kLouvreChestSize];
+	int monde4;
+	uint32 rolloverBlockedUntil;
+	bool initialized;
+};
+
+LouvrePluginState g_louvrePluginState;
+
+void initLouvrePluginState() {
+	if (g_louvrePluginState.initialized)
+		return;
+
+	for (int i = 0; i < kLouvreChestSize; ++i)
+		g_louvrePluginState.objectChest[i] = 0;
+	g_louvrePluginState.monde4 = 0;
+	g_louvrePluginState.rolloverBlockedUntil = 0;
+	g_louvrePluginState.initialized = true;
+}
+
+Common::String messengerInventorySlotName(int slot) {
+	return Common::String::format("Pos%d", slot);
+}
+
+int messengerInventorySlot(int slot) {
+	if (slot < 1 || slot > kMessengerInventorySlots)
+		return 0;
+
+	return g_engine->getVariable(messengerInventorySlotName(slot));
+}
+
+void setMessengerInventorySlot(int slot, int objectId) {
+	if (slot >= 1 && slot <= kMessengerInventorySlots)
+		g_engine->setVariable(messengerInventorySlotName(slot), objectId);
+}
+
+int messengerSpecialObjectSlot(int objectId) {
+	switch (objectId) {
+	case 100:
+		return 10;
+	case 200:
+		return 11;
+	case 300:
+		return 9;
+	case 400:
+		return 12;
+	default:
+		return 0;
+	}
+}
+
+Common::String louvreObjectImage(int objectId, int suffix) {
+	return Common::String::format("Obj%02d%02d.bmp", objectId / 100, suffix);
+}
+
+void drawLouvreImage(const Common::String &image, int x, int y) {
+	g_engine->drawArchiveImage(image, x, y);
+}
+
+void drawLouvreText(int textId, uint16 color = 1987) {
+	static const Common::Rect kTextRect(62, 402, 354, 466);
+	drawLouvreImage("EffaceText.bmp", kTextRect.left, kTextRect.top);
+	g_engine->clearArchiveText(kTextRect);
+	g_engine->drawArchiveText(textId, kTextRect, 14, true, color);
+}
+
+void clearLouvreText() {
+	static const Common::Rect kTextRect(62, 402, 354, 466);
+	drawLouvreImage("EffaceText.bmp", kTextRect.left, kTextRect.top);
+	g_engine->clearArchiveText(kTextRect);
+}
+
+bool louvreTextBlocked() {
+	return g_louvrePluginState.rolloverBlockedUntil > g_system->getMillis();
+}
+
+bool louvreObjectCanView(int objectId) {
+	static const int kViewableObjects[] = {
+		3000, 3100, 4200, 4300, 5500, 5900, 7600, 7700, 7800, 8500, 8600, 8800};
+
+	for (int viewableObject : kViewableObjects)
+		if (viewableObject == objectId)
+			return true;
+	return false;
+}
+
+int louvreCombineObjects(int selectedObjectId, int objectId) {
+	struct CombineEntry {
+		int left;
+		int right;
+		int result;
+	};
+	static const CombineEntry kCombineTable[] = {
+		{500, 1200, 1300},
+		{500, 600, 1400},
+		{500, 700, 9600},
+		{9600, 600, 1500},
+		{1400, 700, 1500},
+		{3000, 2900, 3100},
+		{2300, 2200, 3500},
+		{2100, 2200, 3500},
+		{2100, 2300, 3500},
+		{3500, 2100, 3600},
+		{3500, 2200, 3600},
+		{3500, 2300, 3600},
+		{1800, 2600, 3700},
+		{1800, 1700, 3700},
+		{2600, 1700, 3700},
+		{3700, 1800, 3800},
+		{3700, 2600, 3800},
+		{3700, 1700, 3800},
+		{4200, 3400, 4300},
+		{4400, 6100, 6200},
+		{4400, 6000, 6300},
+		{6200, 6000, 6400},
+		{6300, 6100, 6400},
+		{7700, 3400, 7800},
+		{6900, 8200, 9800},
+		{9300, 7000, 9900}};
+
+	for (const CombineEntry &entry : kCombineTable) {
+		if ((entry.left == selectedObjectId && entry.right == objectId) ||
+			(entry.left == objectId && entry.right == selectedObjectId)) {
+			if (entry.result == 3500) {
+				g_engine->setVariable("RedPastryA", selectedObjectId);
+				g_engine->setVariable("RedPastryB", objectId);
+			} else if (entry.result == 3700) {
+				g_engine->setVariable("YellowSeedA", selectedObjectId);
+				g_engine->setVariable("YellowSeedB", objectId);
+			}
+			return entry.result;
+		}
+	}
+
+	return 0;
+}
+
+int louvreSeparateObject(int objectId, int *parts) {
+	struct SeparateEntry {
+		int object;
+		int parts[3];
+	};
+	static const SeparateEntry kSeparateTable[] = {
+		{1300, {500, 1200, 0}},
+		{1400, {500, 600, 0}},
+		{1500, {1400, 700, 0}},
+		{3100, {3000, 2900, 0}},
+		{3600, {3500, 2100, 0}},
+		{3800, {3700, 1800, 0}},
+		{4300, {4200, 3400, 0}},
+		{6200, {4400, 6100, 0}},
+		{6300, {4400, 6000, 0}},
+		{6400, {6200, 6000, 0}},
+		{7800, {7700, 3400, 0}},
+		{9600, {500, 700, 0}},
+		{9800, {6900, 8200, 0}},
+		{9900, {9300, 7000, 0}},
+	};
+
+	for (const SeparateEntry &entry : kSeparateTable) {
+		if (entry.object == objectId) {
+			Common::copy(entry.parts, entry.parts + ARRAYSIZE(entry.parts), parts);
+			return entry.parts[2] != 0 ? 3 : 2;
+		}
+	}
+
+	return 0;
+}
+
+bool louvreObjectCanCombine(int objectId) {
+	static const int kCombinableObjects[] = {
+		500, 600, 700, 1200, 1300, 1400, 1500, 1700, 1800, 2100, 2200, 2300, 2600, 2900,
+		3000, 3100, 3400, 3500, 3600, 3700, 3800, 4200, 4300, 4400, 6000, 6100, 6200,
+		6300, 6400, 6900, 7000, 7700, 7800, 8200, 9300, 9600, 9800, 9900};
+
+	for (int combinableObject : kCombinableObjects)
+		if (combinableObject == objectId)
+			return true;
+	return false;
+}
+
+int louvreObjectActionMask(int objectId) {
+	if (objectId == 0)
+		return 0;
+	if (messengerSpecialObjectSlot(objectId) != 0)
+		return 1;
+
+	int mask = 1;
+	if (louvreObjectCanView(objectId))
+		mask |= 2;
+	if (louvreObjectCanCombine(objectId))
+		mask |= 4;
+	return mask;
+}
+
+int louvreSelectedObjectActionMask() {
+	int selection = g_engine->getVariable("Selection");
+	if (selection > 100)
+		selection -= 100;
+	return louvreObjectActionMask(messengerInventorySlot(selection));
+}
+
+void drawLouvreActionButtons() {
+	static const int kActionButtonX[] = {21, 22, 25, 24};
+	static const int kActionButtonY[] = {145, 191, 270, 318};
+	static const int kActionValues[] = {1, 2, 4, 8};
+
+	int mask = louvreSelectedObjectActionMask();
+	int currentAction = g_engine->getVariable("CurrentAction");
+	for (uint i = 0; i < ARRAYSIZE(kActionValues); ++i) {
+		int button = i + 1;
+		if ((mask & kActionValues[i]) != 0)
+			button += 10;
+		if (currentAction == kActionValues[i])
+			button += 20;
+		drawLouvreImage(Common::String::format("Bout%04d.bmp", button), kActionButtonX[i], kActionButtonY[i]);
+	}
+}
+
+void drawLouvreSelectionMarker(int slot, int imageNumber, bool selected) {
+	static const Common::Point kSlotSelectedPos[kMessengerInventorySlots + 1] = {
+		Common::Point(273, 15),
+		Common::Point(389, 82),
+		Common::Point(482, 83),
+		Common::Point(574, 84),
+		Common::Point(456, 170),
+		Common::Point(556, 178),
+		Common::Point(476, 258),
+		Common::Point(567, 268),
+		Common::Point(552, 352),
+		Common::Point(8, 32),
+		Common::Point(93, 32),
+		Common::Point(182, 27),
+		Common::Point(264, 28)};
+
+	if (slot >= 1 && slot <= kMessengerInventorySlots && imageNumber > 0)
+		drawLouvreImage(Common::String::format(selected ? "Sel%04d.bmp" : "DSel%04d.bmp", imageNumber), kSlotSelectedPos[slot].x, kSlotSelectedPos[slot].y);
+}
+
+void drawLouvreSelectedObject(int objectSlot) {
+	static const int kSelectedObjectX = 103;
+	static const int kSelectedObjectY = 123;
+
+	initLouvrePluginState();
+	if (objectSlot > 100)
+		objectSlot -= 100;
+	if (objectSlot < 1 || objectSlot > kMessengerInventorySlots)
+		return;
+
+	int objectId = messengerInventorySlot(objectSlot);
+	if (objectId == 0)
+		return;
+
+	drawLouvreImage(louvreObjectImage(objectId, 0), kSelectedObjectX, kSelectedObjectY);
+	clearLouvreText();
+	drawLouvreActionButtons();
+	drawLouvreSelectionMarker(objectSlot, objectSlot, true);
+}
+
+void clearLouvreSelection() {
+	int selection = g_engine->getVariable("Selection");
+	int slot = selection > 100 ? selection - 100 : selection;
+
+	g_engine->setVariable("Selection", 0);
+	drawLouvreImage(louvreObjectImage(0, 0), 103, 123);
+	clearLouvreText();
+	drawLouvreActionButtons();
+	drawLouvreSelectionMarker(slot, selection, false);
+}
+
+void setLouvreSelectedSlot(int slot, int flags) {
+	int previousSelection = g_engine->getVariable("Selection");
+	int previousSlot = previousSelection > 100 ? previousSelection - 100 : previousSelection;
+	int targetSlot = slot > 100 ? slot - 100 : slot;
+
+	if (previousSelection == slot && (flags & 2) == 0) {
+		clearLouvreSelection();
+		return;
+	}
+
+	g_engine->setVariable("Selection", slot);
+	drawLouvreSelectionMarker(previousSlot, previousSelection, false);
+	drawLouvreSelectionMarker(targetSlot, slot, true);
+
+	if ((slot < 100 || (flags & 2) != 0) && (flags & 5) == 0)
+		drawLouvreSelectedObject(slot);
+}
+
+Common::String louvreNumberedImageName(const char *prefix, int value) {
+	return Common::String::format("%s%04d", prefix, value);
+}
+
+int louvreChestLastUsedIndex() {
+	for (int i = kLouvreChestSize - 1; i >= 0; --i)
+		if (g_louvrePluginState.objectChest[i] != 0)
+			return i + 1;
+	return 0;
+}
+
+void drawLouvreChestScrollButtons() {
+	static const Common::Point kUpButtonPos(191, 17);
+	static const Common::Point kDownButtonPos(198, 375);
+
+	int offset = g_engine->getVariable("CoffreOffset");
+	int count = louvreChestLastUsedIndex();
+	drawLouvreImage(offset <= 0 ? "CBou0001" : "CBou0011", kUpButtonPos.x, kUpButtonPos.y);
+	drawLouvreImage(offset >= count - 1 || offset >= 124 ? "CBou0002" : "CBou0012", kDownButtonPos.x, kDownButtonPos.y);
+}
+
+void drawLouvreChestSlot(int slot) {
+	static const Common::Point kChestImagePos[] = {
+		Common::Point(80, 82), Common::Point(80, 145), Common::Point(80, 210), Common::Point(80, 273)};
+	static const Common::Rect kChestTextRect[] = {
+		Common::Rect(138, 80, 324, 134),
+		Common::Rect(138, 145, 324, 199),
+		Common::Rect(138, 208, 324, 262),
+		Common::Rect(138, 272, 324, 326)};
+
+	int index = g_engine->getVariable("CoffreOffset") + slot - 1;
+	int objectId = (index >= 0 && index < kLouvreChestSize) ? g_louvrePluginState.objectChest[index] : 0;
+	drawLouvreImage(louvreNumberedImageName("Cof", objectId + 1), kChestImagePos[slot - 1].x, kChestImagePos[slot - 1].y);
+	drawLouvreImage(louvreNumberedImageName("CText", slot), kChestTextRect[slot - 1].left, kChestTextRect[slot - 1].top);
+	g_engine->clearArchiveText(kChestTextRect[slot - 1]);
+	if (objectId != 0)
+		g_engine->drawArchiveText(objectId, kChestTextRect[slot - 1], 8, false, 1987);
+}
+
+void drawLouvreChest() {
+	for (int slot = 1; slot < 5; ++slot)
+		drawLouvreChestSlot(slot);
+}
+
+void drawLouvreChestSelection(int slot) {
+	static const Common::Point kChestSelectionPos[] = {
+		Common::Point(69, 70), Common::Point(70, 134), Common::Point(70, 199), Common::Point(70, 262)};
+
+	int current = g_engine->getVariable("CoffreSelect");
+	if (current != 0 && current <= 4) {
+		drawLouvreImage(louvreNumberedImageName("CDel", current), kChestSelectionPos[current - 1].x, kChestSelectionPos[current - 1].y);
+		drawLouvreImage(louvreNumberedImageName("CDel", current + 1), kChestSelectionPos[current].x, kChestSelectionPos[current].y);
+	}
+
+	if (slot == 0 || current == slot) {
+		g_engine->setVariable("CoffreSelect", 0);
+		return;
+	}
+
+	drawLouvreImage(louvreNumberedImageName("Cel", slot), kChestSelectionPos[slot - 1].x, kChestSelectionPos[slot - 1].y);
+	drawLouvreImage(louvreNumberedImageName("Cel", slot + 1), kChestSelectionPos[slot].x, kChestSelectionPos[slot].y);
+	g_engine->setVariable("CoffreSelect", slot);
+}
+
+void drawLouvreInventorySlotObject(int slot, int objectId) {
+	static const Common::Point kInventoryObjectPos[] = {
+		Common::Point(381, 28),
+		Common::Point(477, 28),
+		Common::Point(565, 28),
+		Common::Point(452, 116),
+		Common::Point(549, 123),
+		Common::Point(468, 204),
+		Common::Point(558, 212),
+		Common::Point(548, 300),
+		Common::Point(18, 13),
+		Common::Point(104, 20),
+		Common::Point(193, 12),
+		Common::Point(273, 15)};
+
+	if (slot >= 1 && slot <= kMessengerInventorySlots && (slot <= kMessengerDisplayedInventorySlots || objectId != 0))
+		drawLouvreImage(louvreNumberedImageName("Obj", objectId + 1), kInventoryObjectPos[slot - 1].x, kInventoryObjectPos[slot - 1].y);
+}
+
+void drawLouvreInventoryObjects(bool includeSpecialSlots) {
+	for (int slot = 1; slot < (includeSpecialSlots ? 13 : 9); ++slot)
+		drawLouvreInventorySlotObject(slot, messengerInventorySlot(slot));
+}
+
+void updateLouvreChestScroll(int direction) {
+	int offset = g_engine->getVariable("CoffreOffset");
+	int count = louvreChestLastUsedIndex();
+	if (direction == 1)
+		offset -= 3;
+	else if (direction == 2)
+		offset += 3;
+
+	offset = CLIP(offset, 0, MIN(124, MAX(0, count - 1)));
+	if (offset != g_engine->getVariable("CoffreOffset")) {
+		g_engine->setVariable("CoffreOffset", offset);
+		drawLouvreChest();
+	}
+	drawLouvreChestScrollButtons();
+}
+
+void exchangeLouvreChestObject(int chestSlot, int inventorySlot) {
+	if (chestSlot == 0)
+		chestSlot = g_engine->getVariable("CoffreSelect");
+	if (inventorySlot == 0)
+		inventorySlot = g_engine->getVariable("Selection");
+	if (inventorySlot > 100)
+		inventorySlot -= 100;
+
+	if (chestSlot < 1 || chestSlot > 4 || inventorySlot < 1 || inventorySlot > kMessengerInventorySlots)
+		return;
+
+	int chestIndex = g_engine->getVariable("CoffreOffset") + chestSlot - 1;
+	if (chestIndex < 0 || chestIndex >= kLouvreChestSize)
+		return;
+	if (louvreChestLastUsedIndex() == 0)
+		g_engine->setVariable("CoffreOffset", 0);
+
+	int inventoryObject = messengerInventorySlot(inventorySlot);
+	int chestObject = g_louvrePluginState.objectChest[chestIndex];
+	g_louvrePluginState.objectChest[chestIndex] = inventoryObject;
+	setMessengerInventorySlot(inventorySlot, chestObject);
+	drawLouvreInventorySlotObject(inventorySlot, chestObject);
+	drawLouvreChestSlot(chestSlot);
+	drawLouvreChestSelection(0);
+	g_engine->setVariable("Selection", 0);
+	updateLouvreChestScroll(0);
+}
+
 struct MultiCD_Use_Install_Path : public Script::Command {
 	Common::String path;
 
@@ -242,9 +667,9 @@ struct GetMonde4 : public Script::Command {
 
 	GetMonde4(const Common::Array<Common::String> &args) : var(args[0]), negativeVar(args[1]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("GetMonde4 %s %s", var.c_str(), negativeVar.c_str());
-		g_engine->setVariable(var, 0);
-		g_engine->setVariable(negativeVar, 1);
+		initLouvrePluginState();
+		g_engine->setVariable(var, g_louvrePluginState.monde4);
+		g_engine->setVariable(negativeVar, g_louvrePluginState.monde4 == 0);
 	}
 };
 
@@ -253,7 +678,8 @@ struct SetMonde4 : public Script::Command {
 
 	SetMonde4(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("SetMonde4 %d", value);
+		initLouvrePluginState();
+		g_louvrePluginState.monde4 = value;
 	}
 };
 
@@ -264,7 +690,31 @@ struct AddObject : public Script::Command {
 
 	AddObject(const Common::Array<Common::String> &args) : object(atoi(args[0].c_str())), var(args[1]), negativeVar(args[2]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("AddObject %d %s %s", object, var.c_str(), negativeVar.c_str());
+		initLouvrePluginState();
+
+		g_engine->setVariable(var, 1);
+		g_engine->setVariable(negativeVar, 0);
+		g_engine->setVariable("AddObjectOk", 0);
+
+		int slot = messengerSpecialObjectSlot(object);
+		if (slot != 0) {
+			setMessengerInventorySlot(slot, object);
+			g_engine->setVariable("Selection", slot);
+			return;
+		}
+
+		for (slot = 1; slot < 9; ++slot) {
+			if (messengerInventorySlot(slot) == 0) {
+				setMessengerInventorySlot(slot, object);
+				g_engine->setVariable("Selection", slot);
+				return;
+			}
+		}
+
+		g_engine->setVariable("Selection", 0);
+		g_engine->setVariable("AddObjectOk", 1);
+		g_engine->setVariable(var, 0);
+		g_engine->setVariable(negativeVar, 1);
 	}
 };
 
@@ -273,7 +723,13 @@ struct AddCoffreObject : public Script::Command {
 
 	AddCoffreObject(const Common::Array<Common::String> &args) : object(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("AddCoffreObject %d", object);
+		initLouvrePluginState();
+		for (int i = 0; i < kLouvreChestSize; ++i) {
+			if (g_louvrePluginState.objectChest[i] == 0) {
+				g_louvrePluginState.objectChest[i] = object;
+				return;
+			}
+		}
 	}
 };
 
@@ -284,16 +740,29 @@ struct IsPresent : public Script::Command {
 
 	IsPresent(const Common::Array<Common::String> &args) : object(atoi(args[0].c_str())), var(args[1]), negativeVar(args[2]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("IsPresent %d %s %s", object, var.c_str(), negativeVar.c_str());
+		initLouvrePluginState();
+		bool found = false;
+		for (int i = 1; i <= kMessengerInventorySlots; ++i) {
+			if (messengerInventorySlot(i) == object) {
+				found = true;
+				break;
+			}
+		}
+		g_engine->setVariable(var, found);
+		g_engine->setVariable(negativeVar, !found);
 	}
 };
 
 struct RemoveObject : public Script::Command {
-	int object;
+	Common::String object;
 
-	RemoveObject(const Common::Array<Common::String> &args) : object(atoi(args[0].c_str())) {}
+	RemoveObject(const Common::Array<Common::String> &args) : object(args[0]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("RemoveObject %d", object);
+		initLouvrePluginState();
+		int objectId = Script::Command::valueOf(object);
+		for (int i = 1; i <= kMessengerInventorySlots; ++i)
+			if (messengerInventorySlot(i) == objectId)
+				setMessengerInventorySlot(i, 0);
 	}
 };
 
@@ -347,7 +816,32 @@ struct Select : public Script::Command {
 	Select(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())), arg0(args[1]), arg1(args[2]) {}
 
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("Select %d %s %s", value, arg0.c_str(), arg1.c_str());
+		initLouvrePluginState();
+		g_engine->setVariable(arg1, 0);
+
+		int objectId = messengerInventorySlot(value);
+		if (objectId == 0)
+			return;
+
+		if (g_engine->getVariable("CurrentAction") == 4) {
+			g_engine->setVariable(arg0, 1);
+			g_engine->setVariable(arg1, 0);
+
+			int selectedSlot = g_engine->getVariable("Selection") - 100;
+			int selectedObjectId = messengerInventorySlot(selectedSlot);
+			int combinedObjectId = louvreCombineObjects(selectedObjectId, objectId);
+			if (combinedObjectId != 0) {
+				g_engine->setVariable(arg1, 1);
+				g_engine->setVariable("CurrentAction", 0);
+				setMessengerInventorySlot(selectedSlot, combinedObjectId);
+				setMessengerInventorySlot(value, 0);
+				drawLouvreActionButtons();
+				setLouvreSelectedSlot(selectedSlot, 0);
+			}
+			return;
+		}
+
+		setLouvreSelectedSlot(value, 0);
 	}
 };
 
@@ -358,35 +852,98 @@ struct DoAction : public Script::Command {
 	DoAction(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())), arg(args[1]) {}
 
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("DoAction %d %s", value, arg.c_str());
+		initLouvrePluginState();
+		g_engine->setVariable(arg, 0);
+
+		int selection = g_engine->getVariable("Selection");
+		if (selection > 100)
+			selection -= 100;
+		if (selection == 0)
+			return;
+
+		int objectId = messengerInventorySlot(selection);
+		int actionMask = louvreSelectedObjectActionMask();
+
+		int currentAction = g_engine->getVariable("CurrentAction");
+		if (!((currentAction == value || currentAction == 0) && (actionMask & value) != 0))
+			return;
+
+		if (currentAction != 0) {
+			setLouvreSelectedSlot(selection, 4);
+			g_engine->setVariable("CurrentAction", 0);
+			drawLouvreActionButtons();
+			return;
+		}
+
+		g_engine->setVariable(arg, 1);
+		switch (value) {
+		case 1:
+			g_engine->setVariable("Inventaire", objectId);
+			break;
+		case 2:
+			if (louvreObjectCanView(objectId))
+				g_engine->setVariable(arg, objectId);
+			break;
+		case 4:
+			g_engine->setVariable("CurrentAction", value);
+			setLouvreSelectedSlot(selection + 100, 0);
+			break;
+		case 8:
+			g_engine->setVariable("CurrentAction", value);
+			drawLouvreActionButtons();
+			break;
+		default:
+			break;
+		}
 	}
 };
 
 struct IsHere : public Script::Command {
 	IsHere(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("IsHere");
+		int comefrom = g_engine->getVariable("comefrom");
+		if (comefrom > 10000000)
+			comefrom -= 10000000;
+
+		Common::String value = Common::String::format("%d", comefrom);
+		if (value.size() >= 3) {
+			Common::String name = Common::String::format("P0%cS%c%c", value[0], value[1], value[2]);
+			g_engine->playAnimation(name, "y", 20, 20);
+		}
 	}
 };
 
 struct InitCoffre : public Script::Command {
 	InitCoffre(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("InitCoffre");
+		initLouvrePluginState();
+		int compacted[kLouvreChestSize];
+		Common::fill(compacted, compacted + kLouvreChestSize, 0);
+
+		int dst = 0;
+		for (int i = 0; i < kLouvreChestSize; ++i)
+			if (g_louvrePluginState.objectChest[i] != 0)
+				compacted[dst++] = g_louvrePluginState.objectChest[i];
+
+		Common::copy(compacted, compacted + kLouvreChestSize, g_louvrePluginState.objectChest);
 	}
 };
 
 struct LoadCoffre : public Script::Command {
 	LoadCoffre(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("LoadCoffre");
+		initLouvrePluginState();
+		for (int i = 0; i < kLouvreChestSize; ++i)
+			g_louvrePluginState.objectChest[i] = g_engine->getVariable(Common::String::format("coffre%d", i + 1));
 	}
 };
 
 struct SaveCoffre : public Script::Command {
 	SaveCoffre(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("SaveCoffre");
+		initLouvrePluginState();
+		for (int i = 0; i < kLouvreChestSize; ++i)
+			g_engine->setVariable(Common::String::format("coffre%d", i + 1), g_louvrePluginState.objectChest[i]);
 	}
 };
 
@@ -459,14 +1016,20 @@ struct TestCible : public Script::Command {
 struct AfficheCoffre : public Script::Command {
 	AfficheCoffre(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("AfficheCoffre");
+		initLouvrePluginState();
+		drawLouvreChest();
 	}
 };
 
 struct AfficheSelection : public Script::Command {
 	AfficheSelection(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("AfficheSelection");
+		initLouvrePluginState();
+		int selection = g_engine->getVariable("Selection");
+		if (selection > 100)
+			selection -= 100;
+		if (selection != 0)
+			drawLouvreSelectedObject(selection);
 	}
 };
 
@@ -474,7 +1037,8 @@ struct AffichePorteF : public Script::Command {
 	int value;
 	AffichePorteF(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("AffichePorteF %d", value);
+		initLouvrePluginState();
+		drawLouvreInventoryObjects(true);
 	}
 };
 
@@ -483,7 +1047,14 @@ struct SelectPorteF : public Script::Command {
 	Common::String var;
 	SelectPorteF(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())), var(args[1]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("SelectPorteF %d %s", value, var.c_str());
+		initLouvrePluginState();
+		g_engine->setVariable(var, 0);
+		if (g_engine->getVariable("CoffreSelect") != 0) {
+			exchangeLouvreChestObject(0, value);
+		} else if (messengerInventorySlot(value) != 0) {
+			setLouvreSelectedSlot(value, 1);
+		}
+		g_engine->setVariable(var, g_engine->getVariable("Selection") != 0);
 	}
 };
 
@@ -492,7 +1063,16 @@ struct SelectCoffre : public Script::Command {
 	Common::String var;
 	SelectCoffre(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())), var(args[1]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("SelectCoffre %d %s", value, var.c_str());
+		initLouvrePluginState();
+		g_engine->setVariable(var, 0);
+		if (g_engine->getVariable("Selection") != 0) {
+			exchangeLouvreChestObject(value, 0);
+		} else {
+			int index = g_engine->getVariable("CoffreOffset") + value - 1;
+			if (index >= 0 && index < kLouvreChestSize && g_louvrePluginState.objectChest[index] != 0)
+				drawLouvreChestSelection(value);
+		}
+		g_engine->setVariable(var, g_engine->getVariable("CoffreSelect") != 0);
 	}
 };
 
@@ -565,7 +1145,8 @@ struct Set_Global_Pan : public Script::Command {
 	Set_Global_Pan(const Common::Array<Common::String> &args) : arg(args[0]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		auto value = valueOf(arg);
-		warning("set_global_pan %s -> %d", arg.c_str(), value);
+		debug("set_global_pan %s -> %d", arg.c_str(), value);
+		g_engine->setGlobalPan(value);
 	}
 };
 
@@ -750,16 +1331,70 @@ struct LoadSave_Set_Context_Label : public Script::Command {
 };
 
 struct Discocier : public Script::Command {
-	Discocier(const Common::Array<Common::String> &args) {}
+	Common::String var;
+	Discocier(const Common::Array<Common::String> &args) : var(args[0]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("Discocier");
+		initLouvrePluginState();
+		g_engine->setVariable(var, 0);
+
+		int selection = g_engine->getVariable("Selection");
+		int slot = selection > 100 ? selection - 100 : selection;
+		int objectId = messengerInventorySlot(slot);
+
+		int parts[3] = {0, 0, 0};
+		int partCount = 0;
+		if (objectId == 3500) {
+			partCount = 2;
+			parts[0] = g_engine->getVariable("RedPastryA");
+			parts[1] = g_engine->getVariable("RedPastryB");
+		} else if (objectId == 3700) {
+			partCount = 2;
+			parts[0] = g_engine->getVariable("YellowSeedA");
+			parts[1] = g_engine->getVariable("YellowSeedB");
+		} else {
+			partCount = louvreSeparateObject(objectId, parts);
+		}
+
+		g_engine->setVariable("CurrentAction", 0);
+		int emptySlots = 0;
+		for (int i = 1; i < 9; ++i)
+			if (messengerInventorySlot(i) == 0)
+				++emptySlots;
+
+		if (partCount > emptySlots + 1) {
+			drawLouvreActionButtons();
+			drawLouvreText(28, 63687);
+			g_louvrePluginState.rolloverBlockedUntil = g_system->getMillis() + 5000;
+			return;
+		}
+
+		if (partCount == 0)
+			return;
+
+		setMessengerInventorySlot(slot, 0);
+		for (int i = partCount - 1; i >= 0; --i) {
+			for (int dstSlot = 1; dstSlot < 9; ++dstSlot) {
+				if (messengerInventorySlot(dstSlot) == 0) {
+					setMessengerInventorySlot(dstSlot, parts[i]);
+					break;
+				}
+			}
+		}
+
+		setLouvreSelectedSlot(g_engine->getVariable("Selection"), 2);
+		drawLouvreInventoryObjects(true);
+		g_engine->setVariable(var, 1);
 	}
 };
 
 struct Reset : public Script::Command {
 	Reset(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("Reset");
+		initLouvrePluginState();
+		for (int i = 0; i < kLouvreChestSize; ++i)
+			g_louvrePluginState.objectChest[i] = 0;
+		for (int i = 1; i <= kMessengerInventorySlots; ++i)
+			setMessengerInventorySlot(i, 0);
 	}
 };
 
@@ -773,14 +1408,28 @@ struct Restart : public Script::Command {
 struct MemoryRelease : public Script::Command {
 	MemoryRelease(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("MemoryRelease");
+		debug("MemoryRelease");
 	}
 };
 
 struct DrawTextSelection : public Script::Command {
 	DrawTextSelection(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("DrawTextSelection");
+		initLouvrePluginState();
+		if (louvreTextBlocked())
+			return;
+
+		static const Common::Rect kTextRect(62, 402, 354, 466);
+		clearLouvreText();
+
+		int selection = g_engine->getVariable("Selection");
+		if (selection > 100)
+			selection -= 100;
+		if (selection != 0) {
+			int objectId = messengerInventorySlot(selection);
+			if (objectId != 0)
+				g_engine->drawArchiveText(objectId, kTextRect, 14, true, 1987);
+		}
 	}
 };
 
@@ -789,7 +1438,15 @@ struct CarteDestination : public Script::Command {
 	Common::String varY;
 	CarteDestination(const Common::Array<Common::String> &args) : varX(args[0]), varY(args[1]) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("CarteDestination %s %s", varX.c_str(), varY.c_str());
+		int comefrom = g_engine->getVariable("comefrom");
+		if (comefrom > 10000000)
+			comefrom -= 10000000;
+
+		Common::String value = Common::String::format("%d", comefrom);
+		if (value.size() >= 3) {
+			g_engine->setVariable(varX, value[0] - '0');
+			g_engine->setVariable(varY, atoi(value.c_str() + 1));
+		}
 	}
 };
 
@@ -797,7 +1454,8 @@ struct Scroll : public Script::Command {
 	int value;
 	Scroll(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("Scroll");
+		initLouvrePluginState();
+		updateLouvreChestScroll(value);
 	}
 };
 
@@ -831,7 +1489,16 @@ struct PorteFRollover : public Script::Command {
 
 	PorteFRollover(const Common::Array<Common::String> &args) : arg(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("PorteFRollover %d", arg);
+		initLouvrePluginState();
+		if (louvreTextBlocked())
+			return;
+
+		if (arg == 28) {
+			drawLouvreText(arg, 63687);
+			g_louvrePluginState.rolloverBlockedUntil = g_system->getMillis() + 5000;
+		} else {
+			drawLouvreText(arg);
+		}
 	}
 };
 
@@ -849,6 +1516,24 @@ struct LoadVariable : public Script::Command {
 	}
 };
 
+struct Init2 : public Script::Command {
+	Common::String var;
+	Init2(const Common::Array<Common::String> &args) : var(args[0]) {}
+	void exec(Script::ExecutionContext &ctx) const override {
+		g_engine->setVariable(var, 0);
+	}
+};
+
+struct Init : public Script::Command {
+	int value;
+	Common::String var;
+	Init(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())), var(args[1]) {}
+	void exec(Script::ExecutionContext &ctx) const override {
+		initLouvrePluginState();
+		g_engine->setVariable(var, value == 2 ? 1 : 0);
+	}
+};
+
 struct End : public Script::Command {
 	End(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
@@ -875,6 +1560,8 @@ struct End : public Script::Command {
 	E(ExeDemo)                       \
 	E(GetMonde4)                     \
 	E(SetMonde4)                     \
+	E(Init)                          \
+	E(Init2)                         \
 	E(IsPresent)                     \
 	E(KillTimer)                     \
 	E(InitCoffre)                    \
@@ -1170,7 +1857,8 @@ struct PlayRandomSound : public PlaySound {
 	PlayRandomSound(Common::String s, int v, int u, int l) : PlaySound(Common::move(s), v, l), unk(u) {}
 
 	void exec(Script::ExecutionContext &ctx) const override {
-		warning("PlayRandomSound %s %d %d %d", sound.c_str(), volume, unk, loops);
+		debug("PlayRandomSound %s %d %d %d", sound.c_str(), volume, unk, loops);
+		PlaySound::exec(ctx);
 	}
 };
 
diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index e08aba75f11..b34396d3ef3 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -215,7 +215,7 @@ const ADGameDescription gameDescriptions[] = {
 				   "textes.txt", "23f577d1201bc3024ca49cb11f9f7347", 5261),
 		Common::EN_ANY,
 		Common::kPlatformWindows,
-		ADGF_DROPPLATFORM | ADGF_UNSUPPORTED,
+		ADGF_DROPPLATFORM,
 		GUIO1(GUIO_NONE)
 	},
 
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index d2d631c9de3..f79657d2d47 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -719,6 +719,7 @@ bool PhoenixVREngine::goToWarp(const Common::String &warp, bool savePrev) {
 		_nextWarp = _script->getWarp(warp);
 
 	_hoverIndex = -1;
+	_messengerInventoryHover = -1;
 	if (savePrev) {
 		assert(_warpIdx >= 0);
 		_prevWarp = _warpIdx;
@@ -804,6 +805,11 @@ int PhoenixVREngine::getVariable(const Common::String &name) const {
 	return _variables.getValOrDefault(name, 0);
 }
 
+static int8 panToBalance(int pan) {
+	pan = CLIP(pan, 0, 255);
+	return static_cast<int8>((pan * 254 + 127) / 255 - 127);
+}
+
 void PhoenixVREngine::playSound(const Common::String &sound, Audio::Mixer::SoundType type, uint8 volume, int loops, bool spatial, float angle) {
 	const bool music = type == Audio::Mixer::kMusicSoundType;
 	debug("play sound %s %d %d, music: %d, 3d: %d, angle: %g", sound.c_str(), volume, loops, music, spatial, angle);
@@ -825,7 +831,7 @@ void PhoenixVREngine::playSound(const Common::String &sound, Audio::Mixer::Sound
 		_currentMusicVolume = volume;
 	}
 
-	_mixer->playStream(type, &h, Audio::makeWAVStream(stream.release(), DisposeAfterUse::YES), -1, volume);
+	_mixer->playStream(type, &h, Audio::makeWAVStream(stream.release(), DisposeAfterUse::YES), -1, volume, spatial ? 0 : panToBalance(_globalPan));
 	if (loops < 0 || music)
 		_mixer->loopChannel(h);
 	Common::SharedPtr<Video::Subtitles> subtitles;
@@ -997,6 +1003,50 @@ void PhoenixVREngine::resetLockKey() {
 	_prevWarp = -1; // original game does only this o_O
 }
 
+void PhoenixVREngine::drawArchiveImage(const Common::String &image, int x, int y) {
+	const Graphics::Surface *surface = _arn ? _arn->get(image) : nullptr;
+	if (!surface && !image.contains('.'))
+		surface = _arn ? _arn->get(image + ".bmp") : nullptr;
+	if (!surface) {
+		warning("can't find archive image %s", image.c_str());
+		return;
+	}
+
+	_screen->simpleBlitFrom(*surface, Common::Point(x, y));
+
+	Common::Point pos(x, y);
+	for (ArchiveImage &archiveImage : _archiveImages) {
+		if (archiveImage.pos == pos) {
+			archiveImage.image = image;
+			return;
+		}
+	}
+
+	_archiveImages.push_back({image, pos});
+}
+
+void PhoenixVREngine::drawArchiveText(int textId, const Common::Rect &dstRect, int size, bool bold, uint16 color) {
+	for (TextState &archiveText : _archiveTexts) {
+		if (archiveText.rect == dstRect) {
+			archiveText = {textId, dstRect, size, bold, color};
+			paintText(archiveText);
+			return;
+		}
+	}
+
+	_archiveTexts.push_back({textId, dstRect, size, bold, color});
+	paintText(_archiveTexts.back());
+}
+
+void PhoenixVREngine::clearArchiveText(const Common::Rect &dstRect) {
+	for (uint i = 0; i < _archiveTexts.size(); ++i) {
+		if (_archiveTexts[i].rect == dstRect) {
+			_archiveTexts.remove_at(i);
+			return;
+		}
+	}
+}
+
 void PhoenixVREngine::showImageOverlay(const Common::String &image, int x, int y) {
 	debug("AfficheImage %s %d %d", image.c_str(), x, y);
 	_imageOverlay.reset();
@@ -1197,32 +1247,57 @@ void PhoenixVREngine::renderTimer() {
 
 void PhoenixVREngine::renderVR(float dt) {
 	_vr.render(_screen, _angleX.angle(), _angleY.angle(), _fov, dt, _showRegions ? _regSet.get() : nullptr);
-	if (_textId >= 0 && _textes.contains(_textId)) {
-		const Graphics::Font *font = getFont(_textSize, _textBold);
-		if (font) {
-			Common::Array<Common::U32String> lines;
-			font->wordWrapText(_textes.getVal(_textId), _textRect.width(), lines, Graphics::kWordWrapDefault);
-
-			const int fontH = font->getFontHeight();
-			int textW = 0;
-			for (const Common::U32String &line : lines)
-				textW = MAX(textW, font->getStringWidth(line));
-
-			byte r, g, b;
-			_rgb565.colorToRGB(_textColor, r, g, b);
-			const uint32 textColor = _screen->format.RGBToColor(r, g, b);
-			const int16 textX = _textRect.left + (_textRect.width() - textW) / 2;
-			const int16 textY = _textRect.top + (_textRect.height() - fontH * lines.size()) / 2;
-			for (uint i = 0; i < lines.size(); ++i) {
-				const int16 lineX = textX + (textW - font->getStringWidth(lines[i])) / 2;
-				font->drawString(_screen, lines[i], lineX, textY + i * fontH, _textRect.right - lineX, textColor, Graphics::kTextAlignLeft);
-			}
-		}
-	}
+	paintText(_rolloverText);
+	renderArchiveImages();
+	renderArchiveTexts();
 	renderImageOverlay();
 	renderTimer();
 }
 
+void PhoenixVREngine::renderArchiveImages() {
+	for (const ArchiveImage &archiveImage : _archiveImages) {
+		const Graphics::Surface *surface = _arn ? _arn->get(archiveImage.image) : nullptr;
+		if (!surface && !archiveImage.image.contains('.'))
+			surface = _arn ? _arn->get(archiveImage.image + ".bmp") : nullptr;
+		if (!surface)
+			continue;
+
+		_screen->simpleBlitFrom(*surface, archiveImage.pos);
+	}
+}
+
+void PhoenixVREngine::renderArchiveTexts() {
+	for (const TextState &archiveText : _archiveTexts)
+		paintText(archiveText);
+}
+
+void PhoenixVREngine::paintText(const TextState &textState) {
+	auto *font = getFont(textState.size, textState.bold);
+	if (!font || !_textes.contains(textState.textId))
+		return;
+
+	Common::Array<Common::U32String> lines;
+	font->wordWrapText(_textes.getVal(textState.textId), textState.rect.width(), lines, Graphics::kWordWrapDefault);
+	if (lines.empty())
+		return;
+
+	int fontH = font->getFontHeight();
+	int textW = 0;
+	for (uint i = 0; i < lines.size(); ++i) {
+		textW = MAX(textW, font->getStringWidth(lines[i]));
+	}
+
+	byte r, g, b;
+	_rgb565.colorToRGB(textState.color, r, g, b);
+	uint32 textColor = _screen->format.RGBToColor(r, g, b);
+	int16 dstX = textState.rect.left + (textState.rect.width() - textW) / 2;
+	int16 dstY = textState.rect.top + (textState.rect.height() - fontH * lines.size()) / 2;
+	for (uint i = 0; i < lines.size(); ++i) {
+		int x = (textW - font->getStringWidth(lines[i])) / 2;
+		font->drawString(_screen, lines[i], dstX + x, dstY + i * fontH, textW, textColor, Graphics::kTextAlignLeft);
+	}
+}
+
 void PhoenixVREngine::renderImageOverlay() {
 	if (_imageOverlay)
 		_screen->simpleBlitFrom(*_imageOverlay, _imageOverlayPos);
@@ -1314,17 +1389,17 @@ void PhoenixVREngine::rollover(int textId, RolloverType type) {
 
 	if (!_textes.contains(textId)) {
 		debug("text cleared");
-		_textId = -1;
+		_rolloverText = TextState();
 		return;
 	}
 	auto &text = _textes.getVal(textId);
 	debug("rollover %s, %s font size: %d, bold: %d, color: %02x", dstRect.toString().c_str(), text.encode(Common::kUtf8).c_str(), size, bold, color);
 
-	_textId = textId;
-	_textRect = dstRect;
-	_textSize = size;
-	_textBold = bold;
-	_textColor = color;
+	_rolloverText = TextState(textId, dstRect, size, bold, color);
+}
+
+void PhoenixVREngine::clearText() {
+	_rolloverText = TextState();
 }
 
 void PhoenixVREngine::tick(float dt) {
@@ -1370,7 +1445,9 @@ void PhoenixVREngine::tick(float dt) {
 		goToWarp(_script->getInitScript()->vrFile);
 	}
 	if (_nextWarp >= 0) {
-		_textId = -1;
+		_rolloverText = TextState();
+		_archiveImages.clear();
+		_archiveTexts.clear();
 		_warpIdx = _nextWarp;
 		_warp = _script->getWarp(_nextWarp);
 		debug("warp %d -> %s %s", _nextWarp, _warp->vrFile.c_str(), _warp->testFile.c_str());
@@ -1423,6 +1500,7 @@ void PhoenixVREngine::tick(float dt) {
 	Graphics::ManagedSurface *cursor = nullptr;
 	auto &cursors = _cursors[_warpIdx];
 	bool anyMatched = false;
+	int messengerInventoryHover = -1;
 	for (int i = 0, n = cursors.size(); i != n; ++i) {
 		auto *region = getRegion(i);
 		if (!region)
@@ -1430,6 +1508,8 @@ void PhoenixVREngine::tick(float dt) {
 
 		if (_vr.isVR() ? region->contains3D(currentVRPos()) : region->contains2D(_mousePos.x, _mousePos.y)) {
 			anyMatched = true;
+			if (gameIdMatches("messenger") && _warp && _warp->vrFile.equalsIgnoreCase("portef.vr") && i >= 0 && i < 12)
+				messengerInventoryHover = i;
 
 			auto test = _warp->getTest(i);
 			if (test && test->hover == 1 && _hoverIndex < 0) {
@@ -1452,6 +1532,29 @@ void PhoenixVREngine::tick(float dt) {
 		}
 	}
 
+	if (messengerInventoryHover != _messengerInventoryHover) {
+		_messengerInventoryHover = messengerInventoryHover;
+		if (gameIdMatches("messenger") && _warp && _warp->vrFile.equalsIgnoreCase("portef.vr")) {
+			const Common::Rect textRect(62, 402, 354, 466);
+			drawArchiveImage("EffaceText.bmp", textRect.left, textRect.top);
+			clearArchiveText(textRect);
+
+			int textId = 0;
+			if (_messengerInventoryHover >= 0)
+				textId = getVariable(Common::String::format("Pos%d", _messengerInventoryHover + 1));
+			else {
+				int selection = getVariable("Selection");
+				if (selection > 100)
+					selection -= 100;
+				if (selection >= 1 && selection <= 12)
+					textId = getVariable(Common::String::format("Pos%d", selection));
+			}
+
+			if (textId != 0)
+				drawArchiveText(textId, textRect, 14, true, 1987);
+		}
+	}
+
 	if (!cursor)
 		cursor = loadCursor(anyMatched ? _defaultCursor[1] : _defaultCursor[0]);
 	if (cursor) {
@@ -2086,6 +2189,16 @@ void PhoenixVREngine::setGlobalVolume(int volume) {
 	syncSoundSettings();
 }
 
+void PhoenixVREngine::setGlobalPan(int pan) {
+	_globalPan = CLIP(pan, 0, 255);
+	int8 balance = panToBalance(_globalPan);
+
+	for (auto &kv : _sounds) {
+		if (!kv._value.spatial && _mixer->isSoundHandleActive(kv._value.handle))
+			_mixer->setChannelBalance(kv._value.handle, balance);
+	}
+}
+
 void PhoenixVREngine::syncSoundSettings() {
 	int musicVolume = ConfMan.getInt("music_volume");
 	int sfxVolume = ConfMan.getInt("sfx_volume");
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 32ed68d1fde..7ef22297fe4 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -204,11 +204,16 @@ public:
 	void loadVariables();
 
 	void rollover(int textId, RolloverType type);
+	void clearText();
 	void showWaves();
 	void restart();
 	bool setNextLevel();
 
 	void setGlobalVolume(int vol);
+	void setGlobalPan(int pan);
+	void drawArchiveImage(const Common::String &image, int x, int y);
+	void drawArchiveText(int textId, const Common::Rect &dstRect, int size, bool bold, uint16 color);
+	void clearArchiveText(const Common::Rect &dstRect);
 	void showImageOverlay(const Common::String &image, int x, int y);
 	void stopImageOverlay();
 	void updateStage();
@@ -217,6 +222,22 @@ public:
 	void testCible(const Common::String &insideVar, const Common::String &outsideVar);
 
 private:
+	struct ArchiveImage {
+		Common::String image;
+		Common::Point pos;
+	};
+
+	struct TextState {
+		TextState() : textId(-1), size(0), bold(false), color(0) {}
+		TextState(int textId_, const Common::Rect &rect_, int size_, bool bold_, uint16 color_) : textId(textId_), rect(rect_), size(size_), bold(bold_), color(color_) {}
+
+		int textId;
+		Common::Rect rect;
+		int size;
+		bool bold;
+		uint16 color;
+	};
+
 	static Common::String removeDrive(const Common::String &path);
 	Common::SeekableReadStream *open(const Common::String &name, Common::String *origName = nullptr);
 	Common::SeekableReadStream *tryOpen(const Common::Path &name, Common::String *origName);
@@ -230,6 +251,9 @@ private:
 	void tickTimer(float dt);
 	void loadNextScript();
 	void renderVR(float dt);
+	void renderArchiveImages();
+	void renderArchiveTexts();
+	void paintText(const TextState &textState);
 	void renderImageOverlay();
 	void renderTimer();
 	void renderFade(int color);
@@ -253,6 +277,7 @@ private:
 	int _nextWarp = -1;
 	int _prevWarp = -1;
 	int _hoverIndex = -1;
+	int _messengerInventoryHover = -1;
 	int _nextTest = -1;
 
 	struct KeyCodeHash : public Common::UnaryFunction<Common::KeyCode, uint> {
@@ -282,6 +307,7 @@ private:
 	Common::String _defaultCursor[2];
 	Common::String _currentMusic;
 	int _currentMusicVolume = 0;
+	int _globalPan = 128;
 
 	VR _vr;
 	float _fov;
@@ -307,13 +333,11 @@ private:
 	Common::ScopedPtr<Graphics::Font> _regularFonts[kFontSizeCount];
 	Common::ScopedPtr<Graphics::Font> _boldFonts[kFontSizeCount];
 
-	int _textId = -1;
-	Common::Rect _textRect;
-	int _textSize = 0;
-	bool _textBold = false;
-	uint16 _textColor = 0;
+	TextState _rolloverText;
 	Common::ScopedPtr<Graphics::ManagedSurface> _imageOverlay;
 	Common::Point _imageOverlayPos;
+	Common::Array<ArchiveImage> _archiveImages;
+	Common::Array<TextState> _archiveTexts;
 	bool _cibleActive = false;
 	uint32 _cibleStartMillis = 0;
 	int _ciblePeriodSeconds = 0;


Commit: 8af2829821e2fe245588842043d008d296bb7d49
    https://github.com/scummvm/scummvm/commit/8af2829821e2fe245588842043d008d296bb7d49
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:37+03:00

Commit Message:
PHOENIXVR: Improve Messenger Louvre handling

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


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index a797f766b70..ffb4526f480 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -217,6 +217,14 @@ bool louvreObjectCanCombine(int objectId) {
 	return false;
 }
 
+bool louvreObjectCanSeparate(int objectId) {
+	if (objectId == 3500 || objectId == 3700)
+		return true;
+
+	int parts[3] = {0, 0, 0};
+	return louvreSeparateObject(objectId, parts) != 0;
+}
+
 int louvreObjectActionMask(int objectId) {
 	if (objectId == 0)
 		return 0;
@@ -228,6 +236,8 @@ int louvreObjectActionMask(int objectId) {
 		mask |= 2;
 	if (louvreObjectCanCombine(objectId))
 		mask |= 4;
+	if (louvreObjectCanSeparate(objectId))
+		mask |= 8;
 	return mask;
 }
 
@@ -295,14 +305,16 @@ void drawLouvreSelectedObject(int objectSlot) {
 	drawLouvreSelectionMarker(objectSlot, objectSlot, true);
 }
 
-void clearLouvreSelection() {
+void clearLouvreSelection(bool clearObjectPreview = true) {
 	int selection = g_engine->getVariable("Selection");
 	int slot = selection > 100 ? selection - 100 : selection;
 
 	g_engine->setVariable("Selection", 0);
-	drawLouvreImage(louvreObjectImage(0, 0), 103, 123);
-	clearLouvreText();
-	drawLouvreActionButtons();
+	if (clearObjectPreview) {
+		drawLouvreImage(louvreObjectImage(0, 0), 103, 123);
+		clearLouvreText();
+		drawLouvreActionButtons();
+	}
 	drawLouvreSelectionMarker(slot, selection, false);
 }
 
@@ -312,7 +324,7 @@ void setLouvreSelectedSlot(int slot, int flags) {
 	int targetSlot = slot > 100 ? slot - 100 : slot;
 
 	if (previousSelection == slot && (flags & 2) == 0) {
-		clearLouvreSelection();
+		clearLouvreSelection((flags & 1) == 0);
 		return;
 	}
 
@@ -360,7 +372,7 @@ void drawLouvreChestSlot(int slot) {
 	drawLouvreImage(louvreNumberedImageName("CText", slot), kChestTextRect[slot - 1].left, kChestTextRect[slot - 1].top);
 	g_engine->clearArchiveText(kChestTextRect[slot - 1]);
 	if (objectId != 0)
-		g_engine->drawArchiveText(objectId, kChestTextRect[slot - 1], 8, false, 1987);
+		g_engine->drawArchiveText(objectId, kChestTextRect[slot - 1], 12, false, 1987);
 }
 
 void drawLouvreChest() {
@@ -370,7 +382,7 @@ void drawLouvreChest() {
 
 void drawLouvreChestSelection(int slot) {
 	static const Common::Point kChestSelectionPos[] = {
-		Common::Point(69, 70), Common::Point(70, 134), Common::Point(70, 199), Common::Point(70, 262)};
+		Common::Point(69, 70), Common::Point(70, 134), Common::Point(70, 199), Common::Point(70, 262), Common::Point(70, 326)};
 
 	int current = g_engine->getVariable("CoffreSelect");
 	if (current != 0 && current <= 4) {
@@ -422,6 +434,7 @@ void updateLouvreChestScroll(int direction) {
 
 	offset = CLIP(offset, 0, MIN(124, MAX(0, count - 1)));
 	if (offset != g_engine->getVariable("CoffreOffset")) {
+		g_engine->setVariable("CoffreSelect", 0);
 		g_engine->setVariable("CoffreOffset", offset);
 		drawLouvreChest();
 	}
@@ -777,7 +790,7 @@ struct Cmp : public Script::Command {
 		if (args.size() == 5) {
 			arg1 = args[4];
 		} else {
-			uint opLength = (op.size() > 1 && op[1] == '=') ? 2 : 1;
+			uint opLength = (op.size() > 1 && (op[1] == '=' || op[1] == '>')) ? 2 : 1;
 			arg1 = op.substr(opLength);
 			op = op.substr(0, opLength);
 		}
@@ -790,7 +803,7 @@ struct Cmp : public Script::Command {
 		auto value1 = valueOf(arg1);
 		if (op == "==") {
 			r = value0 == value1;
-		} else if (op == "!=") {
+		} else if (op == "!=" || op == "<>") {
 			r = value0 != value1;
 		} else if (op == "<") {
 			r = value0 < value1;
@@ -835,6 +848,8 @@ struct Select : public Script::Command {
 				g_engine->setVariable("CurrentAction", 0);
 				setMessengerInventorySlot(selectedSlot, combinedObjectId);
 				setMessengerInventorySlot(value, 0);
+				drawLouvreInventorySlotObject(selectedSlot, combinedObjectId);
+				drawLouvreInventorySlotObject(value, 0);
 				drawLouvreActionButtons();
 				setLouvreSelectedSlot(selectedSlot, 0);
 			}
@@ -1038,7 +1053,7 @@ struct AffichePorteF : public Script::Command {
 	AffichePorteF(const Common::Array<Common::String> &args) : value(atoi(args[0].c_str())) {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		initLouvrePluginState();
-		drawLouvreInventoryObjects(true);
+		drawLouvreInventoryObjects(value != 0);
 	}
 };
 
@@ -1443,10 +1458,10 @@ struct CarteDestination : public Script::Command {
 			comefrom -= 10000000;
 
 		Common::String value = Common::String::format("%d", comefrom);
-		if (value.size() >= 3) {
+		if (value.size() >= 1)
 			g_engine->setVariable(varX, value[0] - '0');
-			g_engine->setVariable(varY, atoi(value.c_str() + 1));
-		}
+		if (value.size() >= 3)
+			g_engine->setVariable(varY, atoi(value.substr(1, 2).c_str()));
 	}
 };
 
@@ -1684,12 +1699,17 @@ struct GoSub : public Script::Command {
 	GoSub(const Common::String &l) : label(l) {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		debug("gosub %s", label.c_str());
-		assert(ctx.scope);
-		auto *labelPtr = ctx.scope->findLabel(label);
-		assert(labelPtr);
+		auto *scope = ctx.rootScope ? ctx.rootScope : ctx.scope;
+		assert(scope);
+		auto *labelPtr = scope->findLabel(label);
+		if (!labelPtr) {
+			warning("gosub: label %s not found", label.c_str());
+			return;
+		}
 		Script::ExecutionContext sub = {};
 		sub.subroutine = true;
-		ctx.scope->exec(sub, labelPtr->offset);
+		sub.rootScope = scope;
+		scope->exec(sub, labelPtr->offset);
 	}
 };
 
@@ -1706,12 +1726,10 @@ struct Return : public Script::Command {
 	Return() {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		ctx.running = false;
-		if (ctx.subroutine) {
+		if (ctx.subroutine)
 			debug("return to caller");
-		} else {
-			debug("return to previous warp");
-			g_engine->returnToWarp();
-		}
+		else
+			debug("return from script");
 	}
 };
 
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index f79657d2d47..0af66729aca 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -1220,7 +1220,7 @@ void PhoenixVREngine::tickTimer(float dt) {
 }
 
 void PhoenixVREngine::renderTimer() {
-	if (_timerFlags == 0 || !_showTimer || !_arn)
+	if ((_timerFlags & 4) == 0 || !_showTimer || !_arn)
 		return;
 	auto timerBg = _arn->get("cadre.bmp");
 	auto timerFg = _arn->get("cadreB.bmp");
@@ -1230,7 +1230,7 @@ void PhoenixVREngine::renderTimer() {
 	// Necronomicon has timer in scripts, but does not contain bitmaps for timers.
 	Common::Rect bgRect{320, 16, 632, 44};
 	Common::Rect fgRect{333, 23, 619, 38};
-	if (gameIdMatches("dracula2")) {
+	if (gameIdMatches("dracula2") || gameIdMatches("messenger")) {
 		bgRect = Common::Rect(165, 15, 474, 48);
 		fgRect = Common::Rect(177, 15, 461, 48);
 	}
@@ -1293,8 +1293,8 @@ void PhoenixVREngine::paintText(const TextState &textState) {
 	int16 dstX = textState.rect.left + (textState.rect.width() - textW) / 2;
 	int16 dstY = textState.rect.top + (textState.rect.height() - fontH * lines.size()) / 2;
 	for (uint i = 0; i < lines.size(); ++i) {
-		int x = (textW - font->getStringWidth(lines[i])) / 2;
-		font->drawString(_screen, lines[i], dstX + x, dstY + i * fontH, textW, textColor, Graphics::kTextAlignLeft);
+		int16 lineX = dstX + (textW - font->getStringWidth(lines[i])) / 2;
+		font->drawString(_screen, lines[i], lineX, dstY + i * fontH, textState.rect.right - lineX, textColor, Graphics::kTextAlignLeft);
 	}
 }
 
@@ -1444,7 +1444,7 @@ void PhoenixVREngine::tick(float dt) {
 		loadNextScript();
 		goToWarp(_script->getInitScript()->vrFile);
 	}
-	if (_nextWarp >= 0) {
+	while (_nextWarp >= 0) {
 		_rolloverText = TextState();
 		_archiveImages.clear();
 		_archiveTexts.clear();
@@ -1486,7 +1486,6 @@ void PhoenixVREngine::tick(float dt) {
 		else
 			warning("no default script!");
 		_restarted = false;
-		return;
 	}
 
 	if (_nextTest >= 0) {
@@ -1501,7 +1500,8 @@ void PhoenixVREngine::tick(float dt) {
 	auto &cursors = _cursors[_warpIdx];
 	bool anyMatched = false;
 	int messengerInventoryHover = -1;
-	for (int i = 0, n = cursors.size(); i != n; ++i) {
+	int regionCount = _regSet ? _regSet->size() : 0;
+	for (int i = 0, n = MAX<int>(regionCount, cursors.size()); i != n; ++i) {
 		auto *region = getRegion(i);
 		if (!region)
 			continue;
@@ -1518,15 +1518,17 @@ void PhoenixVREngine::tick(float dt) {
 				executeTest(i);
 			}
 
-			auto &name = cursors[i];
-			if (!cursor) {
+			if (!cursor && i < static_cast<int>(cursors.size())) {
+				auto &name = cursors[i];
 				cursor = loadCursor(name);
 			}
 		} else if (i == _hoverIndex) {
 			debug("leaving hover region");
-			auto leave = _warp->getTest(i + 1);
+			auto leave = _warp->getTest(i - 1);
+			if (!leave || leave->hover != 2)
+				leave = _warp->getTest(i + 1);
 			if (leave && leave->hover == 2) {
-				executeTest(i + 1);
+				executeTest(leave->idx);
 			}
 			_hoverIndex = -1;
 		}
@@ -1715,8 +1717,12 @@ Common::Error PhoenixVREngine::run() {
 				}
 			} break;
 			case Common::EVENT_RBUTTONUP: {
-				if (!_hasFocus || _prevWarp != -1)
+				if (!_hasFocus)
 					break;
+				if (_prevWarp != -1) {
+					returnToWarp();
+					break;
+				}
 				debug("right click");
 				auto &rclick = _lockKey[12];
 				if (!rclick.empty())
@@ -1740,7 +1746,8 @@ Common::Error PhoenixVREngine::run() {
 				if (_warpIdx < 0)
 					break;
 				auto &cursors = _cursors[_warpIdx];
-				for (uint i = 0, n = cursors.size(); i != n; ++i) {
+				int regionCount = _regSet ? _regSet->size() : 0;
+				for (uint i = 0, n = MAX<int>(regionCount, cursors.size()); i != n; ++i) {
 					auto *region = getRegion(i);
 					if (!region)
 						continue;
diff --git a/engines/phoenixvr/script.cpp b/engines/phoenixvr/script.cpp
index 55fd722dd9c..f8e8c37b756 100644
--- a/engines/phoenixvr/script.cpp
+++ b/engines/phoenixvr/script.cpp
@@ -273,8 +273,11 @@ public:
 				return CommandPtr(new SetCursor(Common::move(var), Common::move(warp), idx));
 			}
 			int value = 0;
-			if (maybe('='))
-				value = nextInt();
+			if (maybe('=')) {
+				skip();
+				if (!atEnd() && peek() != '!')
+					value = nextInt();
+			}
 			return CommandPtr(new SetVar(Common::move(var), value));
 		} else if (keyword("not")) {
 			auto var = nextWord();
@@ -298,6 +301,8 @@ void Script::Scope::exec(ExecutionContext &ctx) const {
 
 void Script::Scope::exec(ExecutionContext &ctx, uint offset) const {
 	auto oldScope = ctx.scope;
+	if (!ctx.rootScope)
+		ctx.rootScope = this;
 	ctx.scope = this;
 	for (uint i = offset, n = commands.size(); i < n; ++i) {
 		if (!ctx.running)
@@ -448,7 +453,15 @@ Script::~Script() {
 
 int Script::getWarp(const Common::String &name) const {
 	auto it = _warpsIndex.find(name);
-	return it != _warpsIndex.end() ? it->_value : -1;
+	if (it != _warpsIndex.end())
+		return it->_value;
+
+	for (uint i = 0; i < _warps.size(); ++i) {
+		if (_warps[i]->vrFile.equalsIgnoreCase(name))
+			return i;
+	}
+
+	return -1;
 }
 
 Script::ConstWarpPtr Script::getWarp(int idx) const {
diff --git a/engines/phoenixvr/script.h b/engines/phoenixvr/script.h
index dc7e93ef0dd..37032b20e31 100644
--- a/engines/phoenixvr/script.h
+++ b/engines/phoenixvr/script.h
@@ -56,6 +56,7 @@ public:
 		bool running = true;
 		bool subroutine = false;
 		const Scope *scope = nullptr;
+		const Scope *rootScope = nullptr;
 	};
 	struct Command {
 		virtual ~Command() {}


Commit: 5bf3a727cf49d827af1bf46604f58d73153fd9c8
    https://github.com/scummvm/scummvm/commit/5bf3a727cf49d827af1bf46604f58d73153fd9c8
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:37+03:00

Commit Message:
PHOENIXVR: Add Messenger game variants

Changed paths:
    engines/phoenixvr/detection_tables.h


diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index b34396d3ef3..3a78be90771 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -211,11 +211,38 @@ const ADGameDescription gameDescriptions[] = {
 
 	{"messenger",
 		nullptr,
-		AD_ENTRY2s("script.pak", "1e0f9cb47bc203e9e2983b03ffa85174", 185,
-				   "textes.txt", "23f577d1201bc3024ca49cb11f9f7347", 5261),
+		AD_ENTRY5s("script.pak", "1e0f9cb47bc203e9e2983b03ffa85174", 185,
+				   "textes.txt", "23f577d1201bc3024ca49cb11f9f7347", 5261,
+				   "cd1/Data/interface.vr", "110c55142ec95dcbdc0dd86a68013d2a", 188422,
+				   "cd1/Data/Script1.pak", "e0cf6ed93ea338dc91d639c6b70b75c1", 54920,
+				   "cd2/Data/Script2.pak", "26fe454a74da294ff622cdc5663f2c8c", 82085),
 		Common::EN_ANY,
 		Common::kPlatformWindows,
-		ADGF_DROPPLATFORM,
+		ADGF_DROPPLATFORM | ADGF_CD | ADGF_UNSTABLE,
+		GUIO1(GUIO_NONE)
+	},
+
+	{"messenger",
+		nullptr,
+		AD_ENTRY4s("script.pak", "0c9c4b2bfa3b1399b89f6b62063a2e80", 210,
+				   "cd1/Data/interface.vr", "c128d4c10122ae50ad50e2d3c4ccfbc5", 218081,
+				   "cd1/Data/Script.pak", "b8bf0acf2202f1929ed34305346303d9", 54752,
+				   "cd2/Data/Script2.pak", "01bccbbb33e901c9addfa0de51d1bea3", 81955),
+		Common::RU_RUS,
+		Common::kPlatformWindows,
+		ADGF_DROPPLATFORM | ADGF_CD | ADGF_UNSTABLE,
+		GUIO1(GUIO_NONE)
+	},
+
+	{"messenger",
+		nullptr,
+		AD_ENTRY4s("script.pak", "0c9c4b2bfa3b1399b89f6b62063a2e80", 210,
+				   "cd1/Data/interface.vr", "a11ca0314af1d863bbd289e3af4a52c9", 214009,
+				   "cd1/Data/Script.pak", "62980f95be48448d738747a3cac9b2b2", 54944,
+				   "cd2/Data/Script2.pak", "ea95c1be10f24c4ad5ad4cb4ad130699", 82104),
+		Common::FR_FRA,
+		Common::kPlatformWindows,
+		ADGF_DROPPLATFORM | ADGF_CD | ADGF_UNSTABLE,
 		GUIO1(GUIO_NONE)
 	},
 


Commit: d0071eae929c99ebc59cdc57082f814cc1ed9c5a
    https://github.com/scummvm/scummvm/commit/d0071eae929c99ebc59cdc57082f814cc1ed9c5a
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:37+03:00

Commit Message:
PHOENIXVR: Fix Messenger save return handling

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


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index ffb4526f480..fed4a7f43a7 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -1726,10 +1726,12 @@ struct Return : public Script::Command {
 	Return() {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		ctx.running = false;
-		if (ctx.subroutine)
+		if (ctx.subroutine) {
 			debug("return to caller");
-		else
-			debug("return from script");
+		} else {
+			debug("return to previous warp");
+			g_engine->returnToWarp();
+		}
 	}
 };
 
diff --git a/engines/phoenixvr/detection_tables.h b/engines/phoenixvr/detection_tables.h
index 3a78be90771..5e23dd36937 100644
--- a/engines/phoenixvr/detection_tables.h
+++ b/engines/phoenixvr/detection_tables.h
@@ -214,8 +214,8 @@ const ADGameDescription gameDescriptions[] = {
 		AD_ENTRY5s("script.pak", "1e0f9cb47bc203e9e2983b03ffa85174", 185,
 				   "textes.txt", "23f577d1201bc3024ca49cb11f9f7347", 5261,
 				   "cd1/Data/interface.vr", "110c55142ec95dcbdc0dd86a68013d2a", 188422,
-				   "cd1/Data/Script1.pak", "e0cf6ed93ea338dc91d639c6b70b75c1", 54920,
-				   "cd2/Data/Script2.pak", "26fe454a74da294ff622cdc5663f2c8c", 82085),
+				   "cd1/Data/Script1.pak", "f599fc4da33515e414ddb97d8ef9010c", 54920,
+				   "cd2/Data/Script2.pak", "d7af7fa917912626c9d13503a2a1178c", 82085),
 		Common::EN_ANY,
 		Common::kPlatformWindows,
 		ADGF_DROPPLATFORM | ADGF_CD | ADGF_UNSTABLE,
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 0af66729aca..59556cd76b9 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -2065,8 +2065,7 @@ Common::Error PhoenixVREngine::saveGameStream(Common::WriteStream *slot, bool is
 	state.dibHeader.assign(dib.getData(), dib.getData() + dib.size());
 
 	state.thumbnail.assign(thumbnailPixels, thumbnailPixels + thumbnailSize);
-	state.state = Common::move(_capturedState);
-	_capturedState.clear();
+	state.state = _capturedState;
 
 	state.save(*slot);
 	return Common::kNoError;


Commit: be7795688fbc0974682c1e06fdb76413b7140506
    https://github.com/scummvm/scummvm/commit/be7795688fbc0974682c1e06fdb76413b7140506
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:37+03:00

Commit Message:
PHOENIXVR: Keep script sounds playing during movies

Changed paths:
    engines/phoenixvr/phoenixvr.cpp


diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 59556cd76b9..6531ebbc2c2 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -937,7 +937,6 @@ void PhoenixVREngine::playMovie(const Common::String &movie) {
 		return;
 	}
 
-	_mixer->pauseAll(true);
 	_system->lockMouse(false);
 	dec->start();
 
@@ -987,7 +986,6 @@ void PhoenixVREngine::playMovie(const Common::String &movie) {
 	if (subtitles)
 		g_system->hideOverlay();
 	_system->lockMouse(_vr.isVR());
-	_mixer->pauseAll(false);
 }
 
 void PhoenixVREngine::playAnimation(const Common::String &name, const Common::String &var, int varValue, float speed) {


Commit: b1aa7f51996382b18765c7a7037710a6fe5f5314
    https://github.com/scummvm/scummvm/commit/b1aa7f51996382b18765c7a7037710a6fe5f5314
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:38+03:00

Commit Message:
PHOENIXVR: Fix Messenger inventory overlay positions

Changed paths:
    engines/phoenixvr/commands.h


diff --git a/engines/phoenixvr/commands.h b/engines/phoenixvr/commands.h
index fed4a7f43a7..f9073e828b5 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -266,8 +266,7 @@ void drawLouvreActionButtons() {
 }
 
 void drawLouvreSelectionMarker(int slot, int imageNumber, bool selected) {
-	static const Common::Point kSlotSelectedPos[kMessengerInventorySlots + 1] = {
-		Common::Point(273, 15),
+	static const Common::Point kSlotSelectedPos[kMessengerInventorySlots] = {
 		Common::Point(389, 82),
 		Common::Point(482, 83),
 		Common::Point(574, 84),
@@ -282,7 +281,7 @@ void drawLouvreSelectionMarker(int slot, int imageNumber, bool selected) {
 		Common::Point(264, 28)};
 
 	if (slot >= 1 && slot <= kMessengerInventorySlots && imageNumber > 0)
-		drawLouvreImage(Common::String::format(selected ? "Sel%04d.bmp" : "DSel%04d.bmp", imageNumber), kSlotSelectedPos[slot].x, kSlotSelectedPos[slot].y);
+		drawLouvreImage(Common::String::format(selected ? "Sel%04d.bmp" : "DSel%04d.bmp", imageNumber), kSlotSelectedPos[slot - 1].x, kSlotSelectedPos[slot - 1].y);
 }
 
 void drawLouvreSelectedObject(int objectSlot) {


Commit: 74708adda352eb7dfde5f0f69b8b13c40965a100
    https://github.com/scummvm/scummvm/commit/74708adda352eb7dfde5f0f69b8b13c40965a100
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:38+03:00

Commit Message:
PHOENIXVR: Tidy Messenger Louvre helpers

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 f9073e828b5..ae04ba7790b 100644
--- a/engines/phoenixvr/commands.h
+++ b/engines/phoenixvr/commands.h
@@ -35,6 +35,10 @@ namespace {
 constexpr int kMessengerInventorySlots = 12;
 constexpr int kMessengerDisplayedInventorySlots = 8;
 constexpr int kLouvreChestSize = 128;
+const Common::Rect kLouvreTextRect(62, 402, 354, 466);
+const uint16 kLouvreTextColor = 1987;
+const uint16 kLouvreWarningTextColor = 63687;
+constexpr uint32 kLouvreTextBlockMillis = 5000;
 
 struct LouvrePluginState {
 	int objectChest[kLouvreChestSize];
@@ -60,6 +64,10 @@ Common::String messengerInventorySlotName(int slot) {
 	return Common::String::format("Pos%d", slot);
 }
 
+int louvreSelectionSlot(int selection) {
+	return selection > 100 ? selection - 100 : selection;
+}
+
 int messengerInventorySlot(int slot) {
 	if (slot < 1 || slot > kMessengerInventorySlots)
 		return 0;
@@ -73,18 +81,13 @@ void setMessengerInventorySlot(int slot, int objectId) {
 }
 
 int messengerSpecialObjectSlot(int objectId) {
-	switch (objectId) {
-	case 100:
-		return 10;
-	case 200:
-		return 11;
-	case 300:
-		return 9;
-	case 400:
-		return 12;
-	default:
-		return 0;
+	static const int kSpecialObjects[] = {300, 100, 200, 400};
+
+	for (uint i = 0; i < ARRAYSIZE(kSpecialObjects); ++i) {
+		if (kSpecialObjects[i] == objectId)
+			return i + 9;
 	}
+	return 0;
 }
 
 Common::String louvreObjectImage(int objectId, int suffix) {
@@ -95,33 +98,39 @@ void drawLouvreImage(const Common::String &image, int x, int y) {
 	g_engine->drawArchiveImage(image, x, y);
 }
 
-void drawLouvreText(int textId, uint16 color = 1987) {
-	static const Common::Rect kTextRect(62, 402, 354, 466);
-	drawLouvreImage("EffaceText.bmp", kTextRect.left, kTextRect.top);
-	g_engine->clearArchiveText(kTextRect);
-	g_engine->drawArchiveText(textId, kTextRect, 14, true, color);
+void drawLouvreText(int textId, uint16 color = kLouvreTextColor) {
+	drawLouvreImage("EffaceText.bmp", kLouvreTextRect.left, kLouvreTextRect.top);
+	g_engine->clearArchiveText(kLouvreTextRect);
+	g_engine->drawArchiveText(textId, kLouvreTextRect, 14, true, color);
 }
 
 void clearLouvreText() {
-	static const Common::Rect kTextRect(62, 402, 354, 466);
-	drawLouvreImage("EffaceText.bmp", kTextRect.left, kTextRect.top);
-	g_engine->clearArchiveText(kTextRect);
+	drawLouvreImage("EffaceText.bmp", kLouvreTextRect.left, kLouvreTextRect.top);
+	g_engine->clearArchiveText(kLouvreTextRect);
 }
 
 bool louvreTextBlocked() {
 	return g_louvrePluginState.rolloverBlockedUntil > g_system->getMillis();
 }
 
-bool louvreObjectCanView(int objectId) {
-	static const int kViewableObjects[] = {
-		3000, 3100, 4200, 4300, 5500, 5900, 7600, 7700, 7800, 8500, 8600, 8800};
+void drawBlockedLouvreText(int textId) {
+	drawLouvreText(textId, kLouvreWarningTextColor);
+	g_louvrePluginState.rolloverBlockedUntil = g_system->getMillis() + kLouvreTextBlockMillis;
+}
 
-	for (int viewableObject : kViewableObjects)
-		if (viewableObject == objectId)
+template<size_t N>
+bool louvreContainsObject(const int (&objects)[N], int objectId) {
+	for (int object : objects)
+		if (object == objectId)
 			return true;
 	return false;
 }
 
+bool louvreObjectCanView(int objectId) {
+	static const int kViewableObjects[] = {3000, 3100, 4200, 4300, 5500, 5900, 7600, 7700, 7800, 8500, 8600, 8800};
+	return louvreContainsObject(kViewableObjects, objectId);
+}
+
 int louvreCombineObjects(int selectedObjectId, int objectId) {
 	struct CombineEntry {
 		int left;
@@ -210,11 +219,7 @@ bool louvreObjectCanCombine(int objectId) {
 		500, 600, 700, 1200, 1300, 1400, 1500, 1700, 1800, 2100, 2200, 2300, 2600, 2900,
 		3000, 3100, 3400, 3500, 3600, 3700, 3800, 4200, 4300, 4400, 6000, 6100, 6200,
 		6300, 6400, 6900, 7000, 7700, 7800, 8200, 9300, 9600, 9800, 9900};
-
-	for (int combinableObject : kCombinableObjects)
-		if (combinableObject == objectId)
-			return true;
-	return false;
+	return louvreContainsObject(kCombinableObjects, objectId);
 }
 
 bool louvreObjectCanSeparate(int objectId) {
@@ -242,10 +247,7 @@ int louvreObjectActionMask(int objectId) {
 }
 
 int louvreSelectedObjectActionMask() {
-	int selection = g_engine->getVariable("Selection");
-	if (selection > 100)
-		selection -= 100;
-	return louvreObjectActionMask(messengerInventorySlot(selection));
+	return louvreObjectActionMask(messengerInventorySlot(louvreSelectionSlot(g_engine->getVariable("Selection"))));
 }
 
 void drawLouvreActionButtons() {
@@ -289,8 +291,7 @@ void drawLouvreSelectedObject(int objectSlot) {
 	static const int kSelectedObjectY = 123;
 
 	initLouvrePluginState();
-	if (objectSlot > 100)
-		objectSlot -= 100;
+	objectSlot = louvreSelectionSlot(objectSlot);
 	if (objectSlot < 1 || objectSlot > kMessengerInventorySlots)
 		return;
 
@@ -306,7 +307,7 @@ void drawLouvreSelectedObject(int objectSlot) {
 
 void clearLouvreSelection(bool clearObjectPreview = true) {
 	int selection = g_engine->getVariable("Selection");
-	int slot = selection > 100 ? selection - 100 : selection;
+	int slot = louvreSelectionSlot(selection);
 
 	g_engine->setVariable("Selection", 0);
 	if (clearObjectPreview) {
@@ -319,8 +320,8 @@ void clearLouvreSelection(bool clearObjectPreview = true) {
 
 void setLouvreSelectedSlot(int slot, int flags) {
 	int previousSelection = g_engine->getVariable("Selection");
-	int previousSlot = previousSelection > 100 ? previousSelection - 100 : previousSelection;
-	int targetSlot = slot > 100 ? slot - 100 : slot;
+	int previousSlot = louvreSelectionSlot(previousSelection);
+	int targetSlot = louvreSelectionSlot(slot);
 
 	if (previousSelection == slot && (flags & 2) == 0) {
 		clearLouvreSelection((flags & 1) == 0);
@@ -445,8 +446,7 @@ void exchangeLouvreChestObject(int chestSlot, int inventorySlot) {
 		chestSlot = g_engine->getVariable("CoffreSelect");
 	if (inventorySlot == 0)
 		inventorySlot = g_engine->getVariable("Selection");
-	if (inventorySlot > 100)
-		inventorySlot -= 100;
+	inventorySlot = louvreSelectionSlot(inventorySlot);
 
 	if (chestSlot < 1 || chestSlot > 4 || inventorySlot < 1 || inventorySlot > kMessengerInventorySlots)
 		return;
@@ -869,9 +869,7 @@ struct DoAction : public Script::Command {
 		initLouvrePluginState();
 		g_engine->setVariable(arg, 0);
 
-		int selection = g_engine->getVariable("Selection");
-		if (selection > 100)
-			selection -= 100;
+		int selection = louvreSelectionSlot(g_engine->getVariable("Selection"));
 		if (selection == 0)
 			return;
 
@@ -1039,9 +1037,7 @@ struct AfficheSelection : public Script::Command {
 	AfficheSelection(const Common::Array<Common::String> &args) {}
 	void exec(Script::ExecutionContext &ctx) const override {
 		initLouvrePluginState();
-		int selection = g_engine->getVariable("Selection");
-		if (selection > 100)
-			selection -= 100;
+		int selection = louvreSelectionSlot(g_engine->getVariable("Selection"));
 		if (selection != 0)
 			drawLouvreSelectedObject(selection);
 	}
@@ -1352,7 +1348,7 @@ struct Discocier : public Script::Command {
 		g_engine->setVariable(var, 0);
 
 		int selection = g_engine->getVariable("Selection");
-		int slot = selection > 100 ? selection - 100 : selection;
+		int slot = louvreSelectionSlot(selection);
 		int objectId = messengerInventorySlot(slot);
 
 		int parts[3] = {0, 0, 0};
@@ -1377,8 +1373,7 @@ struct Discocier : public Script::Command {
 
 		if (partCount > emptySlots + 1) {
 			drawLouvreActionButtons();
-			drawLouvreText(28, 63687);
-			g_louvrePluginState.rolloverBlockedUntil = g_system->getMillis() + 5000;
+			drawBlockedLouvreText(28);
 			return;
 		}
 
@@ -1433,16 +1428,13 @@ struct DrawTextSelection : public Script::Command {
 		if (louvreTextBlocked())
 			return;
 
-		static const Common::Rect kTextRect(62, 402, 354, 466);
 		clearLouvreText();
 
-		int selection = g_engine->getVariable("Selection");
-		if (selection > 100)
-			selection -= 100;
+		int selection = louvreSelectionSlot(g_engine->getVariable("Selection"));
 		if (selection != 0) {
 			int objectId = messengerInventorySlot(selection);
 			if (objectId != 0)
-				g_engine->drawArchiveText(objectId, kTextRect, 14, true, 1987);
+				g_engine->drawArchiveText(objectId, kLouvreTextRect, 14, true, kLouvreTextColor);
 		}
 	}
 };
@@ -1508,8 +1500,7 @@ struct PorteFRollover : public Script::Command {
 			return;
 
 		if (arg == 28) {
-			drawLouvreText(arg, 63687);
-			g_louvrePluginState.rolloverBlockedUntil = g_system->getMillis() + 5000;
+			drawBlockedLouvreText(arg);
 		} else {
 			drawLouvreText(arg);
 		}
diff --git a/engines/phoenixvr/phoenixvr.cpp b/engines/phoenixvr/phoenixvr.cpp
index 6531ebbc2c2..8a748e631e9 100644
--- a/engines/phoenixvr/phoenixvr.cpp
+++ b/engines/phoenixvr/phoenixvr.cpp
@@ -1001,10 +1001,15 @@ void PhoenixVREngine::resetLockKey() {
 	_prevWarp = -1; // original game does only this o_O
 }
 
-void PhoenixVREngine::drawArchiveImage(const Common::String &image, int x, int y) {
+const Graphics::Surface *PhoenixVREngine::findArchiveImage(const Common::String &image) const {
 	const Graphics::Surface *surface = _arn ? _arn->get(image) : nullptr;
 	if (!surface && !image.contains('.'))
 		surface = _arn ? _arn->get(image + ".bmp") : nullptr;
+	return surface;
+}
+
+void PhoenixVREngine::drawArchiveImage(const Common::String &image, int x, int y) {
+	const Graphics::Surface *surface = findArchiveImage(image);
 	if (!surface) {
 		warning("can't find archive image %s", image.c_str());
 		return;
@@ -1049,9 +1054,7 @@ void PhoenixVREngine::showImageOverlay(const Common::String &image, int x, int y
 	debug("AfficheImage %s %d %d", image.c_str(), x, y);
 	_imageOverlay.reset();
 
-	const Graphics::Surface *surface = _arn ? _arn->get(image) : nullptr;
-	if (!surface && !image.contains('.'))
-		surface = _arn ? _arn->get(image + ".bmp") : nullptr;
+	const Graphics::Surface *surface = findArchiveImage(image);
 	if (!surface) {
 		warning("can't find image overlay %s", image.c_str());
 		return;
@@ -1254,9 +1257,7 @@ void PhoenixVREngine::renderVR(float dt) {
 
 void PhoenixVREngine::renderArchiveImages() {
 	for (const ArchiveImage &archiveImage : _archiveImages) {
-		const Graphics::Surface *surface = _arn ? _arn->get(archiveImage.image) : nullptr;
-		if (!surface && !archiveImage.image.contains('.'))
-			surface = _arn ? _arn->get(archiveImage.image + ".bmp") : nullptr;
+		const Graphics::Surface *surface = findArchiveImage(archiveImage.image);
 		if (!surface)
 			continue;
 
diff --git a/engines/phoenixvr/phoenixvr.h b/engines/phoenixvr/phoenixvr.h
index 7ef22297fe4..5eb688bcef8 100644
--- a/engines/phoenixvr/phoenixvr.h
+++ b/engines/phoenixvr/phoenixvr.h
@@ -250,6 +250,7 @@ private:
 	void tick(float dt);
 	void tickTimer(float dt);
 	void loadNextScript();
+	const Graphics::Surface *findArchiveImage(const Common::String &image) const;
 	void renderVR(float dt);
 	void renderArchiveImages();
 	void renderArchiveTexts();


Commit: 8a42b59f4c20e235bd39f73da1a7ea6247889328
    https://github.com/scummvm/scummvm/commit/8a42b59f4c20e235bd39f73da1a7ea6247889328
Author: Scorp (scorp at mrs.mn)
Date: 2026-07-16T16:58:38+03:00

Commit Message:
PHOENIXVR: Add debugger variable command

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


diff --git a/engines/phoenixvr/console.cpp b/engines/phoenixvr/console.cpp
index 2f7b5e20ed1..31a6c48c415 100644
--- a/engines/phoenixvr/console.cpp
+++ b/engines/phoenixvr/console.cpp
@@ -27,6 +27,7 @@ namespace PhoenixVR {
 Console::Console() : GUI::Debugger() {
 	registerCmd("warp", WRAP_METHOD(Console, cmdWarp));
 	registerCmd("script", WRAP_METHOD(Console, cmdScript));
+	registerCmd("var", WRAP_METHOD(Console, cmdVar));
 	registerCmd("stop_all_sounds", WRAP_METHOD(Console, cmdStopAllSounds));
 	registerCmd("next_level", WRAP_METHOD(Console, cmdNextLevel));
 }
@@ -52,6 +53,22 @@ bool Console::cmdScript(int argc, const char **argv) {
 	return false;
 }
 
+bool Console::cmdVar(int argc, const char **argv) {
+	if (argc < 2) {
+		debugPrintf("var <name> [value]\n");
+		return true;
+	}
+
+	if (argc >= 3) {
+		const int value = atoi(argv[2]);
+		g_engine->declareVariable(argv[1]);
+		g_engine->setVariable(argv[1], value);
+	}
+
+	debugPrintf("%s = %d\n", argv[1], g_engine->getVariable(argv[1]));
+	return true;
+}
+
 bool Console::cmdStopAllSounds(int argc, const char **argv) {
 	g_engine->stopAllSounds();
 	return false;
diff --git a/engines/phoenixvr/console.h b/engines/phoenixvr/console.h
index 2fdd7fd8ce0..6ef00d29424 100644
--- a/engines/phoenixvr/console.h
+++ b/engines/phoenixvr/console.h
@@ -31,6 +31,7 @@ class Console : public GUI::Debugger {
 private:
 	bool cmdWarp(int argc, const char **argv);
 	bool cmdScript(int argc, const char **argv);
+	bool cmdVar(int argc, const char **argv);
 	bool cmdStopAllSounds(int argc, const char **argv);
 	bool cmdNextLevel(int argc, const char **argv);
 




More information about the Scummvm-git-logs mailing list