[Scummvm-git-logs] scummvm master -> 319ebaf47c48a73297f84f8d31413d375d968ffa

neuromancer noreply at scummvm.org
Tue Dec 9 09:59:57 UTC 2025


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

Summary:
319ebaf47c PRIVATE: Fully implement AMRadioClip and PoliceClip


Commit: 319ebaf47c48a73297f84f8d31413d375d968ffa
    https://github.com/scummvm/scummvm/commit/319ebaf47c48a73297f84f8d31413d375d968ffa
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2025-12-09T10:59:53+01:00

Commit Message:
PRIVATE: Fully implement AMRadioClip and PoliceClip

Changed paths:
    engines/private/funcs.cpp
    engines/private/private.cpp
    engines/private/private.h
    engines/private/savegame.h


diff --git a/engines/private/funcs.cpp b/engines/private/funcs.cpp
index 6ed02a06642..2c977d1d60e 100644
--- a/engines/private/funcs.cpp
+++ b/engines/private/funcs.cpp
@@ -660,34 +660,64 @@ static void fMaskDrawn(ArgArray args) {
 	_fMask(args, true);
 }
 
-static void fAddSound(Common::String sound, const char *t, Symbol *flag = nullptr, int val = 0) {
-	if (sound == "\"\"")
+static void fAMRadioClip(ArgArray args) {
+	assert(args.size() <= 4);
+	debugC(1, kPrivateDebugScript, "AMRadioClip(%s,%d,...)", args[0].u.str, args[1].u.val);
+
+	const char *name = args[0].u.str;
+	if (strcmp(name, "\"\"") == 0) {
+		int clipCount = args[1].u.val;
+		g_private->initializeAMRadioChannels(clipCount);
 		return;
+	}
 
-	if (strcmp(t, "AMRadioClip") == 0)
-		g_private->_AMRadio.push_back(sound);
-	else if (strcmp(t, "PoliceClip") == 0)
-		g_private->_policeRadio.push_back(sound);
-	else
-		error("error: invalid sound type %s", t);
-}
+	int priority = args[1].u.val;
 
-static void fAMRadioClip(ArgArray args) {
-	assert(args.size() <= 4);
-	fAddSound(args[0].u.str, "AMRadioClip");
+	// The third and fourth parameters are numbers followed by an optional '+' character.
+	// Each number is a priority and the '+' indicates that it is to be treated as a range
+	// instead of the default behavior of requiring an exact match.
+	int disabledPriority1 = (args.size() >= 3) ? args[2].u.val : 0;
+	bool exactPriorityMatch1 = (args.size() >= 3) ? (args[2].type != NUM_PLUS) : true;
+	int disabledPriority2 = (args.size() >= 4) ? args[3].u.val : 0;
+	bool exactPriorityMatch2 = (args.size() >= 4) ? (args[3].type != NUM_PLUS) : true;
+
+	Common::String flagName = (args.size() >= 6) ? *(args[4].u.sym->name) : "";
+	int flagValue = (args.size() >= 6) ? args[5].u.val : 0;
+
+	g_private->addRadioClip(g_private->_AMRadio, name, priority,
+		disabledPriority1, exactPriorityMatch1,
+		disabledPriority2, exactPriorityMatch2,
+		flagName, flagValue);
 }
 
 static void fPoliceClip(ArgArray args) {
 	assert(args.size() <= 4 || args.size() == 6);
-	fAddSound(args[0].u.str, "PoliceClip");
-	// In the original, the variable is updated when the clip is played, but here we just update
-	// the variable when the clip is added to play. The effect for the player, is mostly the same.
-	if (args.size() == 6) {
-		assert(args[4].type == NAME);
-		assert(args[5].type == NUM);
-		Symbol *flag = g_private->maps.lookupVariable(args[4].u.sym->name);
-		setSymbol(flag, args[5].u.val);
+	debugC(1, kPrivateDebugScript, "PoliceClip(%s,%d,...)", args[0].u.str, args[1].u.val);
+
+	const char *name = args[0].u.str;
+	if (strcmp(name, "\"\"") == 0) {
+		g_private->initializePoliceRadioChannels();
+		return;
 	}
+	
+	int priority = args[1].u.val;
+	if (strcmp(name, "\"DISABLE_ONLY\"") == 0) {
+		g_private->disableRadioClips(g_private->_policeRadio, priority);
+		return;
+	}
+
+	// The third and fourth parameters are numbers followed by an optional '+' character.
+	// Each number is a priority and the '+' indicates that it is to be treated as a range
+	// instead of the default behavior of requiring an exact match.
+	int disabledPriority1 = (args.size() >= 3) ? args[2].u.val : 0;
+	bool exactPriorityMatch1 = (args.size() >= 3) ? (args[2].type != NUM_PLUS) : true;
+	int disabledPriority2 = (args.size() >= 4) ? args[3].u.val : 0;
+	bool exactPriorityMatch2 = (args.size() >= 4) ? (args[3].type != NUM_PLUS) : true;
+
+	g_private->addRadioClip(g_private->_policeRadio, name, priority,
+		disabledPriority1, exactPriorityMatch1,
+		disabledPriority2, exactPriorityMatch2,
+		"", 0);
 }
 
 static void fPhoneClip(ArgArray args) {
diff --git a/engines/private/private.cpp b/engines/private/private.cpp
index b83cffb76c0..be54706921e 100644
--- a/engines/private/private.cpp
+++ b/engines/private/private.cpp
@@ -98,8 +98,8 @@ PrivateEngine::PrivateEngine(OSystem *syst, const ADGameDescription *gd)
 	_policeRadioArea.clear();
 	_AMRadioArea.clear();
 	_phoneArea.clear();
-	// TODO: use this as a default sound for radio
-	_infaceRadioPath = "inface/radio/";
+	_AMRadio.path = "inface/radio/comm_/";
+	_policeRadio.path = "inface/radio/police/";
 	_phonePrefix = "inface/telephon/";
 	_phoneCallSound = "phone.wav";
 
@@ -1253,13 +1253,8 @@ void PrivateEngine::selectAMRadioArea(Common::Point mousePos) {
 	if (_AMRadioArea.surf == nullptr)
 		return;
 
-	if (_AMRadio.empty())
-		return;
-
 	if (inMask(_AMRadioArea.surf, mousePos)) {
-		Common::String sound = _infaceRadioPath + "comm_/" + _AMRadio.back() + ".wav";
-		playSound(sound, 1, false, false);
-		_AMRadio.pop_back();
+		playRadio(_AMRadio, false);
 	}
 }
 
@@ -1267,13 +1262,8 @@ void PrivateEngine::selectPoliceRadioArea(Common::Point mousePos) {
 	if (_policeRadioArea.surf == nullptr)
 		return;
 
-	if (_policeRadio.empty())
-		return;
-
 	if (inMask(_policeRadioArea.surf, mousePos)) {
-		Common::String sound = _infaceRadioPath + "police/" + _policeRadio.back() + ".wav";
-		playSound(sound, 1, false, false);
-		_policeRadio.pop_back();
+		playRadio(_policeRadio, true);
 	}
 }
 
@@ -1394,6 +1384,222 @@ bool PrivateEngine::selectDossierPrevSuspect(Common::Point mousePos) {
 	return false;
 }
 
+void PrivateEngine::addRadioClip(
+	Radio &radio, const Common::String &name, int priority,
+	int disabledPriority1, bool exactPriorityMatch1,
+	int disabledPriority2, bool exactPriorityMatch2,
+	const Common::String &flagName, int flagValue) {
+
+	// lookup radio clip by name
+	RadioClip *clip = nullptr;
+	for (uint i = 0; i < radio.clips.size(); i++) {
+		if (radio.clips[i].name == name) {
+			clip = &radio.clips[i];
+			break;
+		}
+	}
+
+	// add clip if new
+	if (clip == nullptr) {
+		RadioClip newClip;
+		newClip.name = name;
+		newClip.played = false;
+		newClip.priority = priority;
+		newClip.disabledPriority1 = disabledPriority1;
+		newClip.exactPriorityMatch1 = exactPriorityMatch1;
+		newClip.disabledPriority2 = disabledPriority2;
+		newClip.exactPriorityMatch2 = exactPriorityMatch2;
+		newClip.flagName = flagName;
+		newClip.flagValue = flagValue;
+		radio.clips.push_back(newClip);
+		clip = &radio.clips[radio.clips.size() - 1];
+	}
+
+	// disable other clips based on the clip's priority
+	disableRadioClips(radio, clip->priority);
+}
+
+void PrivateEngine::initializeAMRadioChannels(uint clipCount) {
+	Radio &radio = _AMRadio;
+	assert(clipCount < radio.clips.size());
+
+	// clear all channels
+	for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) {
+		radio.channels[i] = -1;
+	}
+
+	// build array of playable clip indexes (up to clipCount)
+	Common::Array<uint> playableClips;
+	for (uint i = 0; i < clipCount; i++) {
+		if (!radio.clips[i].played) {
+			playableClips.push_back(i);
+		}
+	}
+
+	// place the highest priority clips in the channels (up to two)
+	uint channelCount;
+	switch (playableClips.size()) {
+	case 0: channelCount = 0; break;
+	case 1: channelCount = 1; break;
+	case 2: channelCount = 1; break;
+	case 3: channelCount = 1; break;
+	default: channelCount = 2; break;
+	}
+	uint channel = 0;
+	uint end = 0;
+	while (channel < channelCount) {
+		channel++;
+		if (channel < playableClips.size()) {
+			uint start = channel;
+			uint remainingClips = playableClips.size() - start;
+			while (remainingClips--) {
+				RadioClip &clip1 = radio.clips[playableClips[start]];
+				RadioClip &clip2 = radio.clips[playableClips[end]];
+				if (clip1.priority < clip2.priority) {
+					SWAP(playableClips[start], playableClips[end]);
+				}
+				start++;
+			}
+		}
+		radio.channels[channel - 1] = playableClips[end];
+		end++;
+	}
+
+	// build another array of playable clip indexes, starting at clipCount
+	Common::Array<uint> morePlayableClips;
+	for (uint i = clipCount; i < radio.clips.size(); i++) {
+		if (!radio.clips[i].played) {
+			morePlayableClips.push_back(i);
+		}
+	}
+
+	// shuffle second array
+	if (!morePlayableClips.empty()) {
+		for (uint i = morePlayableClips.size() - 1; i > 0; i--) {
+			uint n = _rnd->getRandomNumber(i);
+			SWAP(morePlayableClips[i], morePlayableClips[n]);
+		}
+	}
+
+	// install some of the clips from the second array into channels, starting
+	// at the end of the channel array to keep the highest priority clips.
+	uint copyCount = morePlayableClips.size();
+	if (playableClips.size() <= 3) { // not morePlayableClips
+		copyCount = MIN<uint>(copyCount, 2);
+	} else {
+		copyCount = MIN<uint>(copyCount, 1);
+	}
+	for (uint i = 0; i < copyCount; i++) {
+		radio.channels[2 - i] = morePlayableClips[i];
+	}
+
+	// shuffle channels
+	for (uint i = ARRAYSIZE(radio.channels) - 1; i > 0; i--) {
+		uint n = _rnd->getRandomNumber(i);
+		SWAP(radio.channels[i], radio.channels[n]);
+	}
+}
+
+void PrivateEngine::initializePoliceRadioChannels() {
+	Radio &radio = _policeRadio;
+
+	// clear all channels
+	for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) {
+		radio.channels[i] = -1;
+	}
+
+	// build array of playable clip indexes
+	Common::Array<uint> playableClips;
+	for (uint i = 0; i < radio.clips.size(); i++) {
+		if (!radio.clips[i].played) {
+			playableClips.push_back(i);
+		}
+	}
+
+	// place the highest priority clips in the channels (up to three)
+	uint channelCount = MIN<uint>(playableClips.size(), ARRAYSIZE(radio.channels));
+	uint channel = 0;
+	uint end = 0;
+	while (channel < channelCount) {
+		channel++;
+		if (channel < playableClips.size()) {
+			uint start = channel;
+			uint remainingClips = playableClips.size() - start;
+			while (remainingClips--) {
+				RadioClip &clip1 = radio.clips[playableClips[start]];
+				RadioClip &clip2 = radio.clips[playableClips[end]];
+				if (clip1.priority < clip2.priority) {
+					SWAP(playableClips[start], playableClips[end]);
+				}
+				start++;
+			}
+		}
+		radio.channels[channel - 1] = playableClips[end];
+		end++;
+	}
+}
+
+void PrivateEngine::disableRadioClips(Radio &radio, int priority) {
+	for (uint i = 0; i < radio.clips.size(); i++) {
+		RadioClip &clip = radio.clips[i];
+		if (clip.played) {
+			continue;
+		}
+
+		if (clip.disabledPriority1) {
+			if ((clip.exactPriorityMatch1 && priority == clip.disabledPriority1) ||
+				(!clip.exactPriorityMatch1 && priority <= clip.disabledPriority1)) {
+				clip.played = true;
+			}
+		}
+		if (clip.disabledPriority2) {
+			if ((clip.exactPriorityMatch2 && priority == clip.disabledPriority2) ||
+				(!clip.exactPriorityMatch2 && priority <= clip.disabledPriority2)) {
+				clip.played = true;
+			}
+		}
+	}
+}
+
+void PrivateEngine::playRadio(Radio &radio, bool randomlyDisableClips) {
+	// search channels for first available clip
+	for (uint i = 0; i < ARRAYSIZE(radio.channels); i++) {
+		// skip empty channels
+		if (radio.channels[i] == -1) {
+			continue;
+		}
+
+		// verify that clip hasn't been already been played
+		RadioClip &clip = radio.clips[radio.channels[i]];
+		radio.channels[i] = -1;
+		if (clip.played) {
+			continue;
+		}
+
+		// the police radio randomly disables clips (!)
+		if (randomlyDisableClips) {
+			uint r = _rnd->getRandomNumber(9);
+			if (r < 3) {
+				clip.played = true;
+				break; // play radio.wav
+			}
+		}
+
+		// play the clip
+		Common::String sound = radio.path + clip.name + ".wav";
+		playSound(sound, 1, false, false);
+		clip.played = true;
+		if (!clip.flagName.empty()) {
+			Symbol *flag = maps.lookupVariable(&(clip.flagName));
+			setSymbol(flag, clip.flagValue);
+		}
+		return;
+	}
+
+	// play default radio sound
+	playSound("inface/radio/radio.wav", 1, false, false);
+}
+
 void PrivateEngine::addPhone(const Common::String &name, bool once, int startIndex, int endIndex, const Common::String &flagName, int flagValue) {
 	// lookup phone clip by name and index range
 	PhoneInfo *phone = nullptr;
@@ -1785,18 +1991,28 @@ Common::Error PrivateEngine::loadGameStream(Common::SeekableReadStream *stream)
 	_policeBustPreviousSetting = stream->readString();
 
 	// Radios
-	size = stream->readUint32LE();
-	_AMRadio.clear();
-
-	for (uint32 i = 0; i < size; ++i) {
-		_AMRadio.push_back(stream->readString());
-	}
-
-	size = stream->readUint32LE();
-	_policeRadio.clear();
-
-	for (uint32 i = 0; i < size; ++i) {
-		_policeRadio.push_back(stream->readString());
+	Radio *radios[] = { &_AMRadio, &_policeRadio };
+	for (uint r = 0; r < ARRAYSIZE(radios); r++) {
+		Radio *radio = radios[r];
+		radio->clear();
+
+		size = stream->readUint32LE();
+		for (uint32 i = 0; i < size; ++i) {
+			RadioClip clip;
+			clip.name = stream->readString();
+			clip.played = (stream->readByte() == 1);
+			clip.priority = stream->readSint32LE();
+			clip.disabledPriority1 = stream->readSint32LE();
+			clip.exactPriorityMatch1 = (stream->readByte() == 1);
+			clip.disabledPriority2 = stream->readSint32LE();
+			clip.exactPriorityMatch2 = (stream->readByte() == 1);
+			clip.flagName = stream->readString();
+			clip.flagValue = stream->readSint32LE();
+			radio->clips.push_back(clip);
+		}
+		for (uint i = 0; i < ARRAYSIZE(radio->channels); i++) {
+			radio->channels[i] = stream->readSint32LE();
+		}
 	}
 
 	size = stream->readUint32LE();
@@ -1925,15 +2141,27 @@ Common::Error PrivateEngine::saveGameStream(Common::WriteStream *stream, bool is
 	stream->writeByte(0);
 
 	// Radios
-	stream->writeUint32LE(_AMRadio.size());
-	for (SoundList::const_iterator it = _AMRadio.begin(); it != _AMRadio.end(); ++it) {
-		stream->writeString(*it);
-		stream->writeByte(0);
-	}
-	stream->writeUint32LE(_policeRadio.size());
-	for (SoundList::const_iterator it = _policeRadio.begin(); it != _policeRadio.end(); ++it) {
-		stream->writeString(*it);
-		stream->writeByte(0);
+	Radio *radios[] = { &_AMRadio, &_policeRadio };
+	for (uint r = 0; r < ARRAYSIZE(radios); r++) {
+		Radio *radio = radios[r];
+		stream->writeUint32LE(radio->clips.size());
+		for (uint i = 0; i < radio->clips.size(); i++) {
+			RadioClip &clip = radio->clips[i];
+			stream->writeString(clip.name);
+			stream->writeByte(0);
+			stream->writeByte(clip.played ? 1 : 0);
+			stream->writeSint32LE(clip.priority);
+			stream->writeSint32LE(clip.disabledPriority1);
+			stream->writeByte(clip.exactPriorityMatch1 ? 1 : 0);
+			stream->writeSint32LE(clip.disabledPriority2);
+			stream->writeByte(clip.exactPriorityMatch2 ? 1 : 0);
+			stream->writeString(clip.flagName);
+			stream->writeByte(0);
+			stream->writeSint32LE(clip.flagValue);
+		}
+		for (uint i = 0; i < ARRAYSIZE(radio->channels); i++) {
+			stream->writeSint32LE(radio->channels[i]);
+		}
 	}
 
 	// Phone
diff --git a/engines/private/private.h b/engines/private/private.h
index b1261a2878b..41debcccf7f 100644
--- a/engines/private/private.h
+++ b/engines/private/private.h
@@ -139,6 +139,35 @@ typedef struct PhoneInfo {
 	Common::Array<Common::String> sounds;
 } PhoneInfo;
 
+typedef struct RadioClip {
+	Common::String name;
+	bool played;
+	int priority;
+	int disabledPriority1; // 0 == none
+	bool exactPriorityMatch1;
+	int disabledPriority2; // 0 == none
+	bool exactPriorityMatch2;
+	Common::String flagName;
+	int flagValue;
+} RadioClip;
+
+typedef struct Radio {
+	Common::String path;
+	Common::Array<RadioClip> clips;
+	int channels[3];
+
+	Radio() {
+		clear();
+	}
+
+	void clear() {
+		clips.clear();
+		for (uint i = 0; i < ARRAYSIZE(channels); i++) {
+			channels[i] = -1;
+		}
+	}
+} Radio;
+
 typedef struct DossierInfo {
 	Common::String page1;
 	Common::String page2;
@@ -181,7 +210,6 @@ extern const FuncTable funcTable[];
 
 typedef Common::List<ExitInfo> ExitList;
 typedef Common::List<MaskInfo> MaskList;
-typedef Common::List<Common::String> SoundList;
 typedef Common::List<PhoneInfo> PhoneList;
 typedef Common::List<InventoryItem> InvList;
 typedef Common::List<Common::Rect *> RectList;
@@ -426,11 +454,19 @@ public:
 	Common::String _sirenSound;
 
 	// Radios
-	Common::String _infaceRadioPath;
 	MaskInfo _AMRadioArea;
 	MaskInfo _policeRadioArea;
-	SoundList _AMRadio;
-	SoundList _policeRadio;
+	Radio _AMRadio;
+	Radio _policeRadio;
+	void addRadioClip(
+		Radio &radio, const Common::String &name, int priority,
+		int disabledPriority1, bool exactPriorityMatch1,
+		int disabledPriority2, bool exactPriorityMatch2,
+		const Common::String &flagName, int flagValue);
+	void initializeAMRadioChannels(uint clipCount);
+	void initializePoliceRadioChannels();
+	void disableRadioClips(Radio &radio, int priority);
+	void playRadio(Radio &radio, bool randomlyDisableClips);
 	void selectAMRadioArea(Common::Point);
 	void selectPoliceRadioArea(Common::Point);
 
diff --git a/engines/private/savegame.h b/engines/private/savegame.h
index ecf2f291873..f606d9fb823 100644
--- a/engines/private/savegame.h
+++ b/engines/private/savegame.h
@@ -36,13 +36,14 @@ namespace Private {
 //
 // Version - new/changed feature
 // =============================
+//       3 - Radio detailed state (December 2025)
 //       2 - Phone clip detailed state (December 2025)
 //       1 - Metadata header and more game state (November 2025)
 //
 // Earlier versions did not have a header and not supported.
 
-const uint16 kCurrentSavegameVersion = 2;
-const uint16 kMinimumSavegameVersion = 2;
+const uint16 kCurrentSavegameVersion = 3;
+const uint16 kMinimumSavegameVersion = 3;
 
 struct SavegameMetadata {
 	uint16 version;




More information about the Scummvm-git-logs mailing list