[Scummvm-git-logs] scummvm master -> 27dc8075fb287df0ff659eea2be1a71cef060d25

dreammaster noreply at scummvm.org
Fri Mar 18 04:51:10 UTC 2022


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

Summary:
ea65fe3c69 AGS: Moved SpriteFile class to a separate code unit for convenience
659be0a4e6 AGS: Fixed few warnings
ddb38177f9 AGS: Updated build version (3.6.0.8)
fc9473d175 AGS: Picked a sprite file writing code into SpriteFileWriter class
6e695f7817 AGS: Only set default character's idle delay for games < 3.6.0
4b68aa31f9 AGS: Process idleview based on real game speed, not hardcoded 40 fps
4293a267cb AGS: Corrected font outline upgrade condition
08560a1dbf AGS: Forgot to also clear guibgbmp array in dispose_game_drawdata()
74254fdcf4 AGS: Simplified couple of set_volume calls
3167bac67b AGS: More clear use of the "max channels" constants
aa91cf1161 AGS: Increased the max number of audio channels to 16
ee311d660f AGS: Make game audiochannels limit defined by a game version
bb1d0849fa AGS: Read and write RoomObject's fields explicitly
78b9068a91 AGS: Replace "localuserconf" option with "user-conf-dir"
27dc8075fb AGS: Configurable user paths support $GAMENAME$ token


Commit: ea65fe3c6998a70d173e78b24e37e1aa28f01ed9
    https://github.com/scummvm/scummvm/commit/ea65fe3c6998a70d173e78b24e37e1aa28f01ed9
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:22-07:00

Commit Message:
AGS: Moved SpriteFile class to a separate code unit for convenience

>From upstream 393ef2c24bbc835d8e7a7229d40f638a6f172304

Changed paths:
  A engines/ags/shared/ac/sprite_file.cpp
  A engines/ags/shared/ac/sprite_file.h
    engines/ags/engine/ac/sprite_cache_engine.cpp
    engines/ags/globals.cpp
    engines/ags/globals.h
    engines/ags/module.mk
    engines/ags/shared/ac/sprite_cache.cpp
    engines/ags/shared/ac/sprite_cache.h


diff --git a/engines/ags/engine/ac/sprite_cache_engine.cpp b/engines/ags/engine/ac/sprite_cache_engine.cpp
index 31a0a9e68c5..fdcefb8e871 100644
--- a/engines/ags/engine/ac/sprite_cache_engine.cpp
+++ b/engines/ags/engine/ac/sprite_cache_engine.cpp
@@ -25,13 +25,6 @@
 //
 //=============================================================================
 
-// Headers, as they are in sprcache.cpp
-#ifdef _MANAGED
-// ensure this doesn't get compiled to .NET IL
-#pragma unmanaged
-#pragma warning (disable: 4996 4312)  // disable deprecation warnings
-#endif
-
 #include "ags/shared/ac/game_struct_defines.h"
 #include "ags/shared/ac/sprite_cache.h"
 #include "ags/shared/util/compress.h"
@@ -42,7 +35,7 @@ namespace AGS3 {
 // Engine-specific implementation split out of sprcache.cpp
 //=============================================================================
 
-void SpriteCache::InitNullSpriteParams(sprkey_t index) {
+void AGS::Shared::SpriteCache::InitNullSpriteParams(sprkey_t index) {
 	// make it a blue cup, to avoid crashes
 	_sprInfos[index].Width = _sprInfos[0].Width;
 	_sprInfos[index].Height = _sprInfos[0].Height;
diff --git a/engines/ags/globals.cpp b/engines/ags/globals.cpp
index 2e42791721d..69b5b78ac9a 100644
--- a/engines/ags/globals.cpp
+++ b/engines/ags/globals.cpp
@@ -220,7 +220,7 @@ Globals::Globals() {
 	_guis = new std::vector<AGS::Shared::GUIMain>();
 	_play = new GameState();
 	_game = new GameSetupStruct();
-	_spriteset = new SpriteCache(_game->SpriteInfos);
+	_spriteset = new AGS::Shared::SpriteCache(_game->SpriteInfos);
 	_thisroom = new AGS::Shared::RoomStruct();
 	_troom = new RoomStatus();
 	_usetup = new GameSetup();
diff --git a/engines/ags/globals.h b/engines/ags/globals.h
index 15cde98d733..f15e523ff27 100644
--- a/engines/ags/globals.h
+++ b/engines/ags/globals.h
@@ -75,6 +75,7 @@ class GUITextBox;
 struct InteractionVariable;
 struct PlaneScaling;
 class RoomStruct;
+class SpriteCache;
 struct Translation;
 
 } // namespace Shared
@@ -100,7 +101,6 @@ class EngineExports;
 
 class Navigation;
 class SplitLines;
-class SpriteCache;
 class TTFFontRenderer;
 class WFNFontRenderer;
 
@@ -709,7 +709,7 @@ public:
 
 	GameSetupStruct *_game;
 	GameState *_play;
-	SpriteCache *_spriteset;
+	AGS::Shared::SpriteCache *_spriteset;
 	AGS::Shared::RoomStruct *_thisroom;
 	RoomStatus *_troom; // used for non-saveable rooms, eg. intro
 
diff --git a/engines/ags/module.mk b/engines/ags/module.mk
index dab643747ff..257df04c895 100644
--- a/engines/ags/module.mk
+++ b/engines/ags/module.mk
@@ -39,6 +39,7 @@ MODULE_OBJS = \
 	shared/ac/keycode.o \
 	shared/ac/mouse_cursor.o \
 	shared/ac/sprite_cache.o \
+	shared/ac/sprite_file.o \
 	shared/ac/view.o \
 	shared/ac/words_dictionary.o \
 	shared/core/asset.o \
diff --git a/engines/ags/shared/ac/sprite_cache.cpp b/engines/ags/shared/ac/sprite_cache.cpp
index 59440303950..60b6b145f26 100644
--- a/engines/ags/shared/ac/sprite_cache.cpp
+++ b/engines/ags/shared/ac/sprite_cache.cpp
@@ -25,23 +25,14 @@
 //
 //=============================================================================
 
-#ifdef _MANAGED
-// ensure this doesn't get compiled to .NET IL
-#pragma unmanaged
-#pragma warning (disable: 4996 4312)  // disable deprecation warnings
-#endif
-
 #include "common/system.h"
+#include "ags/shared/util/stream.h"
 #include "ags/lib/std/algorithm.h"
+#include "ags/shared/ac/sprite_cache.h"
 #include "ags/shared/ac/common.h" // quit
 #include "ags/shared/ac/game_struct_defines.h"
-#include "ags/shared/ac/sprite_cache.h"
-#include "ags/shared/core/asset_manager.h"
 #include "ags/shared/debugging/out.h"
 #include "ags/shared/gfx/bitmap.h"
-#include "ags/shared/util/compress.h"
-#include "ags/shared/util/file.h"
-#include "ags/shared/util/stream.h"
 #include "ags/globals.h"
 
 namespace AGS3 {
@@ -56,18 +47,15 @@ extern void get_new_size_for_sprite(int, int, int, int &, int &);
 #define START_OF_LIST -1
 #define END_OF_LIST   -1
 
-const char *spindexid = "SPRINDEX";
-
-// TODO: should not be part of SpriteCache, but rather some asset management class?
-const char *const SpriteFile::DefaultSpriteFileName = "acsprset.spr";
-const char *const SpriteFile::DefaultSpriteIndexName = "sprindex.dat";
-
 SpriteInfo::SpriteInfo()
 	: Flags(0)
 	, Width(0)
 	, Height(0) {
 }
 
+namespace AGS {
+namespace Shared {
+
 SpriteCache::SpriteData::SpriteData()
 	: Size(0)
 	, Flags(0)
@@ -79,12 +67,6 @@ SpriteCache::SpriteData::~SpriteData() {
 	// (some of these bitmaps may be assigned from outside of the cache)
 }
 
-
-SpriteFile::SpriteFile() {
-	_compressed = false;
-	_curPos = -2;
-}
-
 SpriteCache::SpriteCache(std::vector<SpriteInfo> &sprInfos)
 	: _sprInfos(sprInfos) {
 	Init();
@@ -110,14 +92,6 @@ size_t SpriteCache::GetSpriteSlotCount() const {
 	return _spriteData.size();
 }
 
-sprkey_t SpriteFile::FindTopmostSprite(const std::vector<Bitmap *> &sprites) {
-	sprkey_t topmost = -1;
-	for (sprkey_t i = 0; i < static_cast<sprkey_t>(sprites.size()); ++i)
-		if (sprites[i])
-			topmost = i;
-	return topmost;
-}
-
 void SpriteCache::SetMaxCacheSize(size_t size) {
 	_maxCacheSize = size;
 }
@@ -379,14 +353,6 @@ sprkey_t SpriteCache::GetDataIndex(sprkey_t index) {
 	return (_spriteData[index].Flags & SPRCACHEFLAG_REMAPPED) == 0 ? index : 0;
 }
 
-void SpriteFile::SeekToSprite(sprkey_t index) {
-	// If we didn't just load the previous sprite, seek to it
-	if (index != _curPos) {
-		_stream->Seek(_spriteData[index].Offset, kSeekBegin);
-		_curPos = index;
-	}
-}
-
 size_t SpriteCache::LoadSprite(sprkey_t index) {
 	int hh = 0;
 
@@ -457,119 +423,6 @@ void SpriteCache::RemapSpriteToSprite0(sprkey_t index) {
 #endif
 }
 
-const char *spriteFileSig = " Sprite File ";
-
-int SpriteFile::SaveToFile(const String &save_to_file,
-	const std::vector<Bitmap *> &sprites,
-	SpriteFile *read_from_file,
-	bool compressOutput, SpriteFileIndex &index) {
-	std::unique_ptr<Stream> output(File::CreateFile(save_to_file));
-	if (output == nullptr)
-		return -1;
-
-	int spriteFileIDCheck = g_system->getMillis();
-
-	// sprite file version
-	output->WriteInt16(kSprfVersion_Current);
-
-	output->WriteArray(spriteFileSig, strlen(spriteFileSig), 1);
-
-	output->WriteInt8(compressOutput ? 1 : 0);
-	output->WriteInt32(spriteFileIDCheck);
-
-	sprkey_t lastslot = read_from_file ? read_from_file->GetTopmostSprite() : 0;
-	lastslot = std::max(lastslot, FindTopmostSprite(sprites));
-	output->WriteInt32(lastslot);
-
-	// allocate buffers to store the indexing info
-	sprkey_t numsprits = lastslot + 1;
-	std::vector<int16_t> spritewidths, spriteheights;
-	std::vector<soff_t> spriteoffs;
-	spritewidths.resize(numsprits);
-	spriteheights.resize(numsprits);
-	spriteoffs.resize(numsprits);
-	std::unique_ptr<Bitmap> temp_bmp; // for disposing temp sprites
-	std::vector<char> membuf; // for loading raw sprite data
-
-	const bool diff_compress =
-		read_from_file && read_from_file->IsFileCompressed() != compressOutput;
-
-	for (sprkey_t i = 0; i <= lastslot; ++i) {
-		soff_t sproff = output->GetPosition();
-
-		Bitmap *image = (size_t)i < sprites.size() ? sprites[i] : nullptr;
-
-		// if compression setting is different, load the sprite into memory
-		// (otherwise we will be able to simply copy bytes from one file to another
-		if ((image == nullptr) && diff_compress) {
-			read_from_file->LoadSprite(i, image);
-			temp_bmp.reset(image);
-		}
-
-		// if managed to load an image - save it according the new compression settings
-		if (image != nullptr) {
-			// image in memory -- write it out
-			int bpp = image->GetColorDepth() / 8;
-			spriteoffs[i] = sproff;
-			spritewidths[i] = image->GetWidth();
-			spriteheights[i] = image->GetHeight();
-			output->WriteInt16(bpp);
-			output->WriteInt16(spritewidths[i]);
-			output->WriteInt16(spriteheights[i]);
-
-			if (compressOutput) {
-				soff_t lenloc = output->GetPosition();
-				// write some space for the length data
-				output->WriteInt32(0);
-
-				rle_compress(image, output.get());
-
-				soff_t fileSizeSoFar = output->GetPosition();
-				// write the length of the compressed data
-				output->Seek(lenloc, kSeekBegin);
-				output->WriteInt32((fileSizeSoFar - lenloc) - 4);
-				output->Seek(0, kSeekEnd);
-			} else {
-				output->WriteArray(image->GetDataForWriting(), spritewidths[i] * bpp, spriteheights[i]);
-			}
-			continue;
-		} else if (diff_compress) {
-			// sprite doesn't exist
-			output->WriteInt16(0); // colour depth
-			continue;
-		}
-
-		// Not in memory - and same compression option;
-		// Directly copy the sprite bytes from the input file to the output
-		Size metric;
-		int bpp;
-		read_from_file->LoadSpriteData(i, metric, bpp, membuf);
-
-		output->WriteInt16(bpp);
-		if (bpp == 0)
-			continue; // empty slot
-
-		spriteoffs[i] = sproff;
-		spritewidths[i] = metric.Width;
-		spriteheights[i] = metric.Height;
-		output->WriteInt16(metric.Width);
-		output->WriteInt16(metric.Height);
-		if (compressOutput)
-			output->WriteInt32(membuf.size());
-		if (membuf.size() == 0)
-			continue; // bad data?
-		output->Write(&membuf[0], membuf.size());
-	}
-
-	index.SpriteFileIDCheck = spriteFileIDCheck;
-	index.LastSlot = lastslot;
-	index.SpriteCount = numsprits;
-	index.Widths = spritewidths;
-	index.Heights = spriteheights;
-	index.Offsets = spriteoffs;
-	return 0;
-}
-
 int SpriteCache::SaveToFile(const String &filename, bool compressOutput, SpriteFileIndex &index) {
 	std::vector<Bitmap *> sprites;
 	for (const auto &data : _spriteData) {
@@ -584,29 +437,6 @@ int SpriteCache::SaveToFile(const String &filename, bool compressOutput, SpriteF
 	return _file.SaveToFile(filename, sprites, &_file, compressOutput, index);
 }
 
-int SpriteFile::SaveSpriteIndex(const String &filename, const SpriteFileIndex &index) {
-	// write the sprite index file
-	Stream *out = File::CreateFile(filename);
-	if (!out)
-		return -1;
-	// write "SPRINDEX" id
-	out->WriteArray(spindexid, strlen(spindexid), 1);
-	// write version
-	out->WriteInt32(kSpridxfVersion_Current);
-	out->WriteInt32(index.SpriteFileIDCheck);
-	// write last sprite number and num sprites, to verify that
-	// it matches the spr file
-	out->WriteInt32(index.LastSlot);
-	out->WriteInt32(index.SpriteCount);
-	if (index.SpriteCount > 0) {
-		out->WriteArrayOfInt16(&index.Widths.front(), index.Widths.size());
-		out->WriteArrayOfInt16(&index.Heights.front(), index.Heights.size());
-		out->WriteArrayOfInt64(&index.Offsets.front(), index.Offsets.size());
-	}
-	delete out;
-	return 0;
-}
-
 HError SpriteCache::InitFile(const String &filename, const String &sprindex_filename) {
 	std::vector<Size> metrics;
 	HError err = _file.OpenFile(filename, sprindex_filename, metrics);
@@ -634,277 +464,10 @@ HError SpriteCache::InitFile(const String &filename, const String &sprindex_file
 	return HError::None();
 }
 
-HError SpriteFile::RebuildSpriteIndex(Stream *in, sprkey_t topmost,
-	SpriteFileVersion vers, std::vector<Size> &metrics) {
-	for (sprkey_t i = 0; i <= topmost; ++i) {
-		_spriteData[i].Offset = in->GetPosition();
-
-		int coldep = in->ReadInt16();
-
-		if (coldep == 0) {
-			if (in->EOS())
-				break;
-			continue;
-		}
-
-		if (in->EOS())
-			break;
-
-		if ((size_t)i >= _spriteData.size())
-			break;
-
-		int wdd = in->ReadInt16();
-		int htt = in->ReadInt16();
-		metrics[i].Width = wdd;
-		metrics[i].Height = htt;
-
-		size_t spriteDataSize;
-		if (vers == kSprfVersion_Compressed) {
-			spriteDataSize = in->ReadInt32();
-		} else if (vers >= kSprfVersion_Last32bit) {
-			spriteDataSize = this->_compressed ? in->ReadInt32() : wdd * coldep * htt;
-		} else {
-			spriteDataSize = wdd * coldep * htt;
-		}
-		in->Seek(spriteDataSize);
-	}
-	return HError::None();
-}
-
-bool SpriteFile::LoadSpriteIndexFile(const String &filename, int expectedFileID,
-	soff_t spr_initial_offs, sprkey_t topmost, std::vector<Size> &metrics) {
-	Stream *fidx = _GP(AssetMgr)->OpenAsset(filename);
-	if (fidx == nullptr) {
-		return false;
-	}
-
-	char buffer[9];
-	// check "SPRINDEX" id
-	fidx->ReadArray(&buffer[0], strlen(spindexid), 1);
-	buffer[8] = 0;
-	if (strcmp(buffer, spindexid)) {
-		delete fidx;
-		return false;
-	}
-	// check version
-	SpriteIndexFileVersion vers = (SpriteIndexFileVersion)fidx->ReadInt32();
-	if (vers < kSpridxfVersion_Initial || vers > kSpridxfVersion_Current) {
-		delete fidx;
-		return false;
-	}
-	if (vers >= kSpridxfVersion_Last32bit) {
-		if (fidx->ReadInt32() != expectedFileID) {
-			delete fidx;
-			return false;
-		}
-	}
-
-	sprkey_t topmost_index = fidx->ReadInt32();
-	// end index+1 should be the same as num sprites
-	if (fidx->ReadInt32() != topmost_index + 1) {
-		delete fidx;
-		return false;
-	}
-
-	if (topmost_index != topmost) {
-		delete fidx;
-		return false;
-	}
-
-	sprkey_t numsprits = topmost_index + 1;
-	std::vector<int16_t> rspritewidths; rspritewidths.resize(numsprits);
-	std::vector<int16_t> rspriteheights; rspriteheights.resize(numsprits);
-	std::vector<soff_t>  spriteoffs; spriteoffs.resize(numsprits);
-
-	fidx->ReadArrayOfInt16(&rspritewidths[0], numsprits);
-	fidx->ReadArrayOfInt16(&rspriteheights[0], numsprits);
-	if (vers <= kSpridxfVersion_Last32bit) {
-		for (sprkey_t i = 0; i < numsprits; ++i)
-			spriteoffs[i] = fidx->ReadInt32();
-	} else // large file support
-	{
-		fidx->ReadArrayOfInt64(&spriteoffs[0], numsprits);
-	}
-	delete fidx;
-
-	for (sprkey_t i = 0; i <= topmost_index; ++i) {
-		if (spriteoffs[i] != 0) {
-			_spriteData[i].Offset = spriteoffs[i] + spr_initial_offs;
-			metrics[i].Width = rspritewidths[i];
-			metrics[i].Height = rspriteheights[i];
-		}
-	}
-	return true;
-}
-
 void SpriteCache::DetachFile() {
 	_file.Reset();
 }
 
-bool SpriteFile::IsFileCompressed() const {
-	return _compressed;
-}
-
-sprkey_t SpriteFile::GetTopmostSprite() const {
-	return (sprkey_t)_spriteData.size() - 1;
-}
-
-void SpriteFile::Reset() {
-	_stream.reset();
-	_curPos = -2;
-}
-
-HAGSError SpriteFile::LoadSprite(sprkey_t index, Shared::Bitmap *&sprite) {
-	sprite = nullptr;
-	if (index < 0 || (size_t)index >= _spriteData.size())
-		new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
-			index, 0, _spriteData.size() - 1));
-
-	if (_spriteData[index].Offset == 0)
-		return HError::None(); // sprite is not in file
-
-	SeekToSprite(index);
-	_curPos = -2; // mark undefined pos
-
-	int coldep = _stream->ReadInt16();
-	if (coldep == 0) { // empty slot, this is normal
-		return HError::None();
-	}
-
-	int wdd = _stream->ReadInt16();
-	int htt = _stream->ReadInt16();
-	Bitmap *image = BitmapHelper::CreateBitmap(wdd, htt, coldep * 8);
-	if (image == nullptr) {
-		return new Error(String::FromFormat("LoadSprite: failed to allocate bitmap %d (%dx%d%d).",
-			index, wdd, htt, coldep * 8));
-	}
-
-	if (_compressed) {
-		size_t data_size = _stream->ReadInt32();
-		if (data_size == 0) {
-			delete image;
-			return new Error(String::FromFormat("LoadSprite: bad compressed data for sprite %d.", index));
-		}
-		rle_decompress(image, _stream.get());
-	} else {
-		if (coldep == 1) {
-			for (int h = 0; h < htt; ++h)
-				_stream->ReadArray(&image->GetScanLineForWriting(h)[0], coldep, wdd);
-		} else if (coldep == 2) {
-			for (int h = 0; h < htt; ++h)
-				_stream->ReadArrayOfInt16((int16_t *)&image->GetScanLineForWriting(h)[0], wdd);
-		} else {
-			for (int h = 0; h < htt; ++h)
-				_stream->ReadArrayOfInt32((int32_t *)&image->GetScanLineForWriting(h)[0], wdd);
-		}
-	}
-	sprite = image;
-	_curPos = index + 1; // mark correct pos
-	return HError::None();
-}
-
-HError SpriteFile::LoadSpriteData(sprkey_t index, Size &metric, int &bpp,
-		std::vector<char> &data) {
-	metric = Size();
-	bpp = 0;
-
-	if (index < 0 || (size_t)index >= _spriteData.size())
-		new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
-			index, 0, _spriteData.size() - 1));
-
-	if (_spriteData[index].Offset == 0)
-		return HError::None(); // sprite is not in file
-
-	SeekToSprite(index);
-	_curPos = -2; // mark undefined pos
-
-	int coldep = _stream->ReadInt16();
-	if (coldep == 0) { // empty slot, this is normal
-		metric = Size();
-		bpp = 0;
-		data.resize(0);
-		return HError::None();
-	}
-
-	int width = _stream->ReadInt16();
-	int height = _stream->ReadInt16();
-
-	size_t data_size;
-	if (_compressed)
-		data_size = _stream->ReadInt32();
-	else
-		data_size = width * height * coldep;
-	data.resize(data_size);
-	_stream->Read(&data[0], data_size);
-	metric = Size(width, height);
-	bpp = coldep;
-	_curPos = index + 1; // mark correct pos
-	return HError::None();
-}
-
-HAGSError SpriteFile::OpenFile(const String &filename, const String &sprindex_filename,
-	std::vector<Size> &metrics) {
-	SpriteFileVersion vers;
-	char buff[20];
-	soff_t spr_initial_offs = 0;
-	int spriteFileID = 0;
-
-	_stream.reset(_GP(AssetMgr)->OpenAsset(filename));
-	if (_stream == nullptr)
-		return new Error(String::FromFormat("Failed to open spriteset file '%s'.", filename.GetCStr()));
-
-	spr_initial_offs = _stream->GetPosition();
-
-	vers = (SpriteFileVersion)_stream->ReadInt16();
-	// read the "Sprite File" signature
-	_stream->ReadArray(&buff[0], 13, 1);
-
-	if (vers < kSprfVersion_Uncompressed || vers > kSprfVersion_Current) {
-		_stream.reset();
-		return new Error(String::FromFormat("Unsupported spriteset format (requested %d, supported %d - %d).", vers, kSprfVersion_Uncompressed, kSprfVersion_Current));
-	}
-
-	// unknown version
-	buff[13] = 0;
-	if (strcmp(buff, spriteFileSig)) {
-		_stream.reset();
-		return new Error("Unknown spriteset format.");
-	}
-
-	if (vers == kSprfVersion_Uncompressed) {
-		this->_compressed = false;
-	} else if (vers == kSprfVersion_Compressed) {
-		this->_compressed = true;
-	} else if (vers >= kSprfVersion_Last32bit) {
-		this->_compressed = (_stream->ReadInt8() == 1);
-		spriteFileID = _stream->ReadInt32();
-	}
-
-	if (vers < kSprfVersion_Compressed) {
-		// skip the palette
-		_stream->Seek(256 * 3); // sizeof(RGB) * 256
-	}
-
-	sprkey_t topmost;
-	if (vers < kSprfVersion_HighSpriteLimit)
-		topmost = (uint16_t)_stream->ReadInt16();
-	else
-		topmost = _stream->ReadInt32();
-	if (vers < kSprfVersion_Uncompressed)
-		topmost = 200;
-
-	_spriteData.resize(topmost + 1);
-	metrics.resize(topmost + 1);
-
-	// if there is a sprite index file, use it
-	if (LoadSpriteIndexFile(sprindex_filename, spriteFileID,
-		spr_initial_offs, topmost, metrics)) {
-		// Succeeded
-		return HError::None();
-	}
-
-	// Failed, index file is invalid; index sprites manually
-	return RebuildSpriteIndex(_stream.get(), topmost, vers, metrics);
-}
-
+} // namespace Shared
+} // namespace AGS
 } // namespace AGS3
diff --git a/engines/ags/shared/ac/sprite_cache.h b/engines/ags/shared/ac/sprite_cache.h
index 2b19e88edc1..fb830b963e2 100644
--- a/engines/ags/shared/ac/sprite_cache.h
+++ b/engines/ags/shared/ac/sprite_cache.h
@@ -43,6 +43,7 @@
 
 #include "ags/lib/std/memory.h"
 #include "ags/lib/std/vector.h"
+#include "ags/shared/ac/sprite_file.h"
 #include "ags/shared/core/platform.h"
 #include "ags/shared/util/error.h"
 #include "ags/shared/util/geometry.h"
@@ -76,91 +77,10 @@ struct SpriteInfo;
 #define DEFAULTCACHESIZE_KB (128 * 1024)
 #endif
 
-// TODO: research old version differences
-enum SpriteFileVersion {
-	kSprfVersion_Uncompressed = 4,
-	kSprfVersion_Compressed = 5,
-	kSprfVersion_Last32bit = 6,
-	kSprfVersion_64bit = 10,
-	kSprfVersion_HighSpriteLimit = 11,
-	kSprfVersion_Current = kSprfVersion_HighSpriteLimit
-};
-
-enum SpriteIndexFileVersion {
-	kSpridxfVersion_Initial = 1,
-	kSpridxfVersion_Last32bit = 2,
-	kSpridxfVersion_64bit = 10,
-	kSpridxfVersion_HighSpriteLimit = 11,
-	kSpridxfVersion_Current = kSpridxfVersion_HighSpriteLimit
-};
-
-
-typedef int32_t sprkey_t;
-
-// SpriteFileIndex contains sprite file's table of contents
-struct SpriteFileIndex {
-	int SpriteFileIDCheck = 0; // tag matching sprite file and index file
-	sprkey_t LastSlot = -1;
-	size_t SpriteCount = 0u;
-	std::vector<int16_t> Widths;
-	std::vector<int16_t> Heights;
-	std::vector<soff_t>  Offsets;
-};
-
-class SpriteFile {
-public:
-	// Standart sprite file and sprite index names
-	static const char *const DefaultSpriteFileName;
-	static const char *const DefaultSpriteIndexName;
-
-	SpriteFile();
-	// Loads sprite reference information and inits sprite stream
-	HAGSError   OpenFile(const Shared::String &filename, const Shared::String &sprindex_filename,
-		std::vector<Size> &metrics);
-	void        Reset();
-
-	// Tells if bitmaps in the file are compressed
-	bool        IsFileCompressed() const;
-	// Tells the highest known sprite index
-	sprkey_t    GetTopmostSprite() const;
-
-	// Loads sprite index file
-	bool        LoadSpriteIndexFile(const Shared::String &filename, int expectedFileID,
-		soff_t spr_initial_offs, sprkey_t topmost, std::vector<Size> &metrics);
-	// Rebuilds sprite index from the main sprite file
-	HAGSError   RebuildSpriteIndex(AGS::Shared::Stream *in, sprkey_t topmost, SpriteFileVersion vers,
-		std::vector<Size> &metrics);
-
-	HAGSError LoadSprite(sprkey_t index, Shared::Bitmap *&sprite);
-	HAGSError LoadSpriteData(sprkey_t index, Size &metric, int &bpp, std::vector<char> &data);
-
-	// Saves all sprites to file; fills in index data for external use
-	// TODO: refactor to be able to save main file and index file separately (separate function for gather data?)
-	static int  SaveToFile(const Shared::String &save_to_file,
-		const std::vector<Shared::Bitmap *> &sprites, // available sprites (may contain nullptrs)
-		SpriteFile *read_from_file, // optional file to read missing sprites from
-		bool compressOutput, SpriteFileIndex &index);
-	// Saves sprite index table in a separate file
-	static int  SaveSpriteIndex(const Shared::String &filename, const SpriteFileIndex &index);
-
-private:
-	// Finds the topmost occupied slot index. Warning: may be slow.
-	static sprkey_t FindTopmostSprite(const std::vector<Shared::Bitmap *> &sprites);
-	// Seek stream to sprite
-	void        SeekToSprite(sprkey_t index);
-
-	// Internal sprite reference
-	struct SpriteRef {
-		soff_t Offset = 0; // data offset
-		size_t Size = 0;   // cache size of element, in bytes
-	};
+struct SpriteInfo;
 
-	// Array of sprite references
-	std::vector<SpriteRef> _spriteData;
-	std::unique_ptr<Shared::Stream> _stream; // the sprite stream
-	bool _compressed; // are sprites compressed
-	sprkey_t _curPos; // current stream position (sprite slot)
-};
+namespace AGS {
+namespace Shared {
 
 class SpriteCache {
 public:
@@ -172,7 +92,7 @@ public:
 	~SpriteCache();
 
 	// Loads sprite reference information and inits sprite stream
-	HAGSError   InitFile(const Shared::String &filename, const Shared::String &sprindex_filename);
+	HError   InitFile(const Shared::String &filename, const Shared::String &sprindex_filename);
 	// Saves current cache contents to the file
 	int         SaveToFile(const Shared::String &filename, bool compressOutput, SpriteFileIndex &index);
 	// Closes an active sprite file stream
@@ -277,6 +197,8 @@ private:
 	void        InitNullSpriteParams(sprkey_t index);
 };
 
+} // namespace Shared
+} // namespace AGS
 } // namespace AGS3
 
 #endif
diff --git a/engines/ags/shared/ac/sprite_file.cpp b/engines/ags/shared/ac/sprite_file.cpp
new file mode 100644
index 00000000000..aef315b54ec
--- /dev/null
+++ b/engines/ags/shared/ac/sprite_file.cpp
@@ -0,0 +1,467 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "ags/shared/ac/sprite_file.h"
+#include "ags/lib/std/algorithm.h"
+#include "ags/shared/core/asset_manager.h"
+#include "ags/shared/gfx/bitmap.h"
+#include "ags/shared/util/compress.h"
+#include "ags/shared/util/file.h"
+#include "ags/shared/util/stream.h"
+
+namespace AGS3 {
+namespace AGS {
+namespace Shared {
+
+static const char *spriteFileSig = " Sprite File ";
+static const char *spindexid = "SPRINDEX";
+
+// TODO: should not be part of SpriteFile, but rather some asset management class?
+const char *SpriteFile::DefaultSpriteFileName = "acsprset.spr";
+const char *SpriteFile::DefaultSpriteIndexName = "sprindex.dat";
+
+
+SpriteFile::SpriteFile() {
+	_compressed = false;
+	_curPos = -2;
+}
+
+HError SpriteFile::OpenFile(const String &filename, const String &sprindex_filename,
+	std::vector<Size> &metrics) {
+	SpriteFileVersion vers;
+	char buff[20];
+	soff_t spr_initial_offs = 0;
+	int spriteFileID = 0;
+
+	_stream.reset(_GP(AssetMgr)->OpenAsset(filename));
+	if (_stream == nullptr)
+		return new Error(String::FromFormat("Failed to open spriteset file '%s'.", filename.GetCStr()));
+
+	spr_initial_offs = _stream->GetPosition();
+
+	vers = (SpriteFileVersion)_stream->ReadInt16();
+	// read the "Sprite File" signature
+	_stream->ReadArray(&buff[0], 13, 1);
+
+	if (vers < kSprfVersion_Uncompressed || vers > kSprfVersion_Current) {
+		_stream.reset();
+		return new Error(String::FromFormat("Unsupported spriteset format (requested %d, supported %d - %d).", vers, kSprfVersion_Uncompressed, kSprfVersion_Current));
+	}
+
+	// unknown version
+	buff[13] = 0;
+	if (strcmp(buff, spriteFileSig)) {
+		_stream.reset();
+		return new Error("Uknown spriteset format.");
+	}
+
+	if (vers == kSprfVersion_Uncompressed) {
+		this->_compressed = false;
+	} else if (vers == kSprfVersion_Compressed) {
+		this->_compressed = true;
+	} else if (vers >= kSprfVersion_Last32bit) {
+		this->_compressed = (_stream->ReadInt8() == 1);
+		spriteFileID = _stream->ReadInt32();
+	}
+
+	if (vers < kSprfVersion_Compressed) {
+		// skip the palette
+		_stream->Seek(256 * 3); // sizeof(RGB) * 256
+	}
+
+	sprkey_t topmost;
+	if (vers < kSprfVersion_HighSpriteLimit)
+		topmost = (uint16_t)_stream->ReadInt16();
+	else
+		topmost = _stream->ReadInt32();
+	if (vers < kSprfVersion_Uncompressed)
+		topmost = 200;
+
+	_spriteData.resize(topmost + 1);
+	metrics.resize(topmost + 1);
+
+	// if there is a sprite index file, use it
+	if (LoadSpriteIndexFile(sprindex_filename, spriteFileID,
+		spr_initial_offs, topmost, metrics)) {
+		// Succeeded
+		return HError::None();
+	}
+
+	// Failed, index file is invalid; index sprites manually
+	return RebuildSpriteIndex(_stream.get(), topmost, vers, metrics);
+}
+
+void SpriteFile::Reset() {
+	_stream.reset();
+	_curPos = -2;
+}
+
+bool SpriteFile::IsFileCompressed() const {
+	return _compressed;
+}
+
+sprkey_t SpriteFile::GetTopmostSprite() const {
+	return (sprkey_t)_spriteData.size() - 1;
+}
+
+bool SpriteFile::LoadSpriteIndexFile(const String &filename, int expectedFileID,
+	soff_t spr_initial_offs, sprkey_t topmost, std::vector<Size> &metrics) {
+	Stream *fidx = _GP(AssetMgr)->OpenAsset(filename);
+	if (fidx == nullptr) {
+		return false;
+	}
+
+	char buffer[9];
+	// check "SPRINDEX" id
+	fidx->ReadArray(&buffer[0], strlen(spindexid), 1);
+	buffer[8] = 0;
+	if (strcmp(buffer, spindexid)) {
+		delete fidx;
+		return false;
+	}
+	// check version
+	SpriteIndexFileVersion vers = (SpriteIndexFileVersion)fidx->ReadInt32();
+	if (vers < kSpridxfVersion_Initial || vers > kSpridxfVersion_Current) {
+		delete fidx;
+		return false;
+	}
+	if (vers >= kSpridxfVersion_Last32bit) {
+		if (fidx->ReadInt32() != expectedFileID) {
+			delete fidx;
+			return false;
+		}
+	}
+
+	sprkey_t topmost_index = fidx->ReadInt32();
+	// end index+1 should be the same as num sprites
+	if (fidx->ReadInt32() != topmost_index + 1) {
+		delete fidx;
+		return false;
+	}
+
+	if (topmost_index != topmost) {
+		delete fidx;
+		return false;
+	}
+
+	sprkey_t numsprits = topmost_index + 1;
+	std::vector<int16_t> rspritewidths; rspritewidths.resize(numsprits);
+	std::vector<int16_t> rspriteheights; rspriteheights.resize(numsprits);
+	std::vector<soff_t>  spriteoffs; spriteoffs.resize(numsprits);
+
+	fidx->ReadArrayOfInt16(&rspritewidths[0], numsprits);
+	fidx->ReadArrayOfInt16(&rspriteheights[0], numsprits);
+	if (vers <= kSpridxfVersion_Last32bit) {
+		for (sprkey_t i = 0; i < numsprits; ++i)
+			spriteoffs[i] = fidx->ReadInt32();
+	} else // large file support
+	{
+		fidx->ReadArrayOfInt64(&spriteoffs[0], numsprits);
+	}
+	delete fidx;
+
+	for (sprkey_t i = 0; i <= topmost_index; ++i) {
+		if (spriteoffs[i] != 0) {
+			_spriteData[i].Offset = spriteoffs[i] + spr_initial_offs;
+			metrics[i].Width = rspritewidths[i];
+			metrics[i].Height = rspriteheights[i];
+		}
+	}
+	return true;
+}
+
+HError SpriteFile::RebuildSpriteIndex(Stream *in, sprkey_t topmost,
+	SpriteFileVersion vers, std::vector<Size> &metrics) {
+	for (sprkey_t i = 0; i <= topmost; ++i) {
+		_spriteData[i].Offset = in->GetPosition();
+
+		int coldep = in->ReadInt16();
+
+		if (coldep == 0) {
+			if (in->EOS())
+				break;
+			continue;
+		}
+
+		if (in->EOS())
+			break;
+
+		if ((size_t)i >= _spriteData.size())
+			break;
+
+		int wdd = in->ReadInt16();
+		int htt = in->ReadInt16();
+		metrics[i].Width = wdd;
+		metrics[i].Height = htt;
+
+		size_t spriteDataSize;
+		if (vers == kSprfVersion_Compressed) {
+			spriteDataSize = in->ReadInt32();
+		} else if (vers >= kSprfVersion_Last32bit) {
+			spriteDataSize = this->_compressed ? in->ReadInt32() : wdd * coldep * htt;
+		} else {
+			spriteDataSize = wdd * coldep * htt;
+		}
+		in->Seek(spriteDataSize);
+	}
+	return HError::None();
+}
+
+HError SpriteFile::LoadSprite(sprkey_t index, Shared::Bitmap *&sprite) {
+	sprite = nullptr;
+	if (index < 0 || (size_t)index >= _spriteData.size())
+		new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
+			index, 0, _spriteData.size() - 1));
+
+	if (_spriteData[index].Offset == 0)
+		return HError::None(); // sprite is not in file
+
+	SeekToSprite(index);
+	_curPos = -2; // mark undefined pos
+
+	int coldep = _stream->ReadInt16();
+	if (coldep == 0) { // empty slot, this is normal
+		return HError::None();
+	}
+
+	int wdd = _stream->ReadInt16();
+	int htt = _stream->ReadInt16();
+	Bitmap *image = BitmapHelper::CreateBitmap(wdd, htt, coldep * 8);
+	if (image == nullptr) {
+		return new Error(String::FromFormat("LoadSprite: failed to allocate bitmap %d (%dx%d%d).",
+			index, wdd, htt, coldep * 8));
+	}
+
+	if (_compressed) {
+		size_t data_size = _stream->ReadInt32();
+		if (data_size == 0) {
+			delete image;
+			return new Error(String::FromFormat("LoadSprite: bad compressed data for sprite %d.", index));
+		}
+		rle_decompress(image, _stream.get());
+	} else {
+		if (coldep == 1) {
+			for (int h = 0; h < htt; ++h)
+				_stream->ReadArray(&image->GetScanLineForWriting(h)[0], coldep, wdd);
+		} else if (coldep == 2) {
+			for (int h = 0; h < htt; ++h)
+				_stream->ReadArrayOfInt16((int16_t *)&image->GetScanLineForWriting(h)[0], wdd);
+		} else {
+			for (int h = 0; h < htt; ++h)
+				_stream->ReadArrayOfInt32((int32_t *)&image->GetScanLineForWriting(h)[0], wdd);
+		}
+	}
+	sprite = image;
+	_curPos = index + 1; // mark correct pos
+	return HError::None();
+}
+
+HError SpriteFile::LoadSpriteData(sprkey_t index, Size &metric, int &bpp,
+	std::vector<char> &data) {
+	metric = Size();
+	bpp = 0;
+	if (index < 0 || (size_t)index >= _spriteData.size())
+		new Error(String::FromFormat("LoadSprite: slot index %d out of bounds (%d - %d).",
+			index, 0, _spriteData.size() - 1));
+
+	if (_spriteData[index].Offset == 0)
+		return HError::None(); // sprite is not in file
+
+	SeekToSprite(index);
+	_curPos = -2; // mark undefined pos
+
+	int coldep = _stream->ReadInt16();
+	if (coldep == 0) { // empty slot, this is normal
+		metric = Size();
+		bpp = 0;
+		data.resize(0);
+		return HError::None();
+	}
+
+	int width = _stream->ReadInt16();
+	int height = _stream->ReadInt16();
+
+	size_t data_size;
+	if (_compressed)
+		data_size = _stream->ReadInt32();
+	else
+		data_size = width * height * coldep;
+	data.resize(data_size);
+	_stream->Read(&data[0], data_size);
+	metric = Size(width, height);
+	bpp = coldep;
+	_curPos = index + 1; // mark correct pos
+	return HError::None();
+}
+
+sprkey_t SpriteFile::FindTopmostSprite(const std::vector<Bitmap *> &sprites) {
+	sprkey_t topmost = -1;
+	for (sprkey_t i = 0; i < static_cast<sprkey_t>(sprites.size()); ++i)
+		if (sprites[i])
+			topmost = i;
+	return topmost;
+}
+
+void SpriteFile::SeekToSprite(sprkey_t index) {
+	// If we didn't just load the previous sprite, seek to it
+	if (index != _curPos) {
+		_stream->Seek(_spriteData[index].Offset, kSeekBegin);
+		_curPos = index;
+	}
+}
+
+int SpriteFile::SaveToFile(const String &save_to_file,
+	const std::vector<Bitmap *> &sprites,
+	SpriteFile *read_from_file,
+	bool compressOutput, SpriteFileIndex &index) {
+	std::unique_ptr<Stream> output(File::CreateFile(save_to_file));
+	if (output == nullptr)
+		return -1;
+
+	int spriteFileIDCheck = g_system->getMillis();
+
+	// sprite file version
+	output->WriteInt16(kSprfVersion_Current);
+
+	output->WriteArray(spriteFileSig, strlen(spriteFileSig), 1);
+
+	output->WriteInt8(compressOutput ? 1 : 0);
+	output->WriteInt32(spriteFileIDCheck);
+
+	sprkey_t lastslot = read_from_file ? read_from_file->GetTopmostSprite() : 0;
+	lastslot = std::max(lastslot, FindTopmostSprite(sprites));
+	output->WriteInt32(lastslot);
+
+	// allocate buffers to store the indexing info
+	sprkey_t numsprits = lastslot + 1;
+	std::vector<int16_t> spritewidths, spriteheights;
+	std::vector<soff_t> spriteoffs;
+	spritewidths.resize(numsprits);
+	spriteheights.resize(numsprits);
+	spriteoffs.resize(numsprits);
+	std::unique_ptr<Bitmap> temp_bmp; // for disposing temp sprites
+	std::vector<char> membuf; // for loading raw sprite data
+
+	const bool diff_compress =
+		read_from_file && read_from_file->IsFileCompressed() != compressOutput;
+
+	for (sprkey_t i = 0; i <= lastslot; ++i) {
+		soff_t sproff = output->GetPosition();
+
+		Bitmap *image = (size_t)i < sprites.size() ? sprites[i] : nullptr;
+
+		// if compression setting is different, load the sprite into memory
+		// (otherwise we will be able to simply copy bytes from one file to another
+		if ((image == nullptr) && diff_compress) {
+			read_from_file->LoadSprite(i, image);
+			temp_bmp.reset(image);
+		}
+
+		// if managed to load an image - save it according the new compression settings
+		if (image != nullptr) {
+			// image in memory -- write it out
+			int bpp = image->GetColorDepth() / 8;
+			spriteoffs[i] = sproff;
+			spritewidths[i] = image->GetWidth();
+			spriteheights[i] = image->GetHeight();
+			output->WriteInt16(bpp);
+			output->WriteInt16(spritewidths[i]);
+			output->WriteInt16(spriteheights[i]);
+
+			if (compressOutput) {
+				soff_t lenloc = output->GetPosition();
+				// write some space for the length data
+				output->WriteInt32(0);
+
+				rle_compress(image, output.get());
+
+				soff_t fileSizeSoFar = output->GetPosition();
+				// write the length of the compressed data
+				output->Seek(lenloc, kSeekBegin);
+				output->WriteInt32((fileSizeSoFar - lenloc) - 4);
+				output->Seek(0, kSeekEnd);
+			} else {
+				output->WriteArray(image->GetDataForWriting(), spritewidths[i] * bpp, spriteheights[i]);
+			}
+			continue;
+		} else if (diff_compress) {
+			// sprite doesn't exist
+			output->WriteInt16(0); // colour depth
+			continue;
+		}
+
+		// Not in memory - and same compression option;
+		// Directly copy the sprite bytes from the input file to the output
+		Size metric;
+		int bpp;
+		read_from_file->LoadSpriteData(i, metric, bpp, membuf);
+
+		output->WriteInt16(bpp);
+		if (bpp == 0)
+			continue; // empty slot
+
+		spriteoffs[i] = sproff;
+		spritewidths[i] = metric.Width;
+		spriteheights[i] = metric.Height;
+		output->WriteInt16(metric.Width);
+		output->WriteInt16(metric.Height);
+		if (compressOutput)
+			output->WriteInt32(membuf.size());
+		if (membuf.size() == 0)
+			continue; // bad data?
+		output->Write(&membuf[0], membuf.size());
+	}
+
+	index.SpriteFileIDCheck = spriteFileIDCheck;
+	index.LastSlot = lastslot;
+	index.SpriteCount = numsprits;
+	index.Widths = spritewidths;
+	index.Heights = spriteheights;
+	index.Offsets = spriteoffs;
+	return 0;
+}
+
+int SpriteFile::SaveSpriteIndex(const String &filename, const SpriteFileIndex &index) {
+	// write the sprite index file
+	Stream *out = File::CreateFile(filename);
+	if (!out)
+		return -1;
+	// write "SPRINDEX" id
+	out->WriteArray(spindexid, strlen(spindexid), 1);
+	// write version
+	out->WriteInt32(kSpridxfVersion_Current);
+	out->WriteInt32(index.SpriteFileIDCheck);
+	// write last sprite number and num sprites, to verify that
+	// it matches the spr file
+	out->WriteInt32(index.LastSlot);
+	out->WriteInt32(index.SpriteCount);
+	if (index.SpriteCount > 0) {
+		out->WriteArrayOfInt16(&index.Widths.front(), index.Widths.size());
+		out->WriteArrayOfInt16(&index.Heights.front(), index.Heights.size());
+		out->WriteArrayOfInt64(&index.Offsets.front(), index.Offsets.size());
+	}
+	delete out;
+	return 0;
+}
+
+} // namespace Shared
+} // namespace AGS
+} // namespace AGS3
diff --git a/engines/ags/shared/ac/sprite_file.h b/engines/ags/shared/ac/sprite_file.h
new file mode 100644
index 00000000000..90cea21d35f
--- /dev/null
+++ b/engines/ags/shared/ac/sprite_file.h
@@ -0,0 +1,131 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+//=============================================================================
+//
+// SpriteFile class handles sprite file loading and streaming.
+//
+//=============================================================================
+
+#ifndef AGS_SHARED_AC_SPRITE_FILE_H
+#define AGS_SHARED_AC_SPRITE_FILE_H
+
+#include "ags/shared/core/types.h"
+#include "ags/lib/std/vector.h"
+#include "ags/globals.h"
+
+namespace AGS3 {
+namespace AGS {
+namespace Shared {
+
+class Bitmap;
+
+// TODO: research old version differences
+enum SpriteFileVersion {
+	kSprfVersion_Uncompressed = 4,
+	kSprfVersion_Compressed = 5,
+	kSprfVersion_Last32bit = 6,
+	kSprfVersion_64bit = 10,
+	kSprfVersion_HighSpriteLimit = 11,
+	kSprfVersion_Current = kSprfVersion_HighSpriteLimit
+};
+
+enum SpriteIndexFileVersion {
+	kSpridxfVersion_Initial = 1,
+	kSpridxfVersion_Last32bit = 2,
+	kSpridxfVersion_64bit = 10,
+	kSpridxfVersion_HighSpriteLimit = 11,
+	kSpridxfVersion_Current = kSpridxfVersion_HighSpriteLimit
+};
+
+typedef int32_t sprkey_t;
+
+// SpriteFileIndex contains sprite file's table of contents
+struct SpriteFileIndex {
+	int SpriteFileIDCheck = 0; // tag matching sprite file and index file
+	sprkey_t LastSlot = -1;
+	size_t SpriteCount = 0u;
+	std::vector<int16_t> Widths;
+	std::vector<int16_t> Heights;
+	std::vector<soff_t>  Offsets;
+};
+
+
+class SpriteFile {
+public:
+	// Standart sprite file and sprite index names
+	static const char *DefaultSpriteFileName;
+	static const char *DefaultSpriteIndexName;
+
+	SpriteFile();
+	// Loads sprite reference information and inits sprite stream
+	HError      OpenFile(const String &filename, const String &sprindex_filename,
+		std::vector<Size> &metrics);
+	void        Reset();
+
+	// Tells if bitmaps in the file are compressed
+	bool        IsFileCompressed() const;
+	// Tells the highest known sprite index
+	sprkey_t    GetTopmostSprite() const;
+
+	// Loads sprite index file
+	bool        LoadSpriteIndexFile(const String &filename, int expectedFileID,
+		soff_t spr_initial_offs, sprkey_t topmost, std::vector<Size> &metrics);
+	// Rebuilds sprite index from the main sprite file
+	HError      RebuildSpriteIndex(Stream *in, sprkey_t topmost, SpriteFileVersion vers,
+		std::vector<Size> &metrics);
+
+	HError      LoadSprite(sprkey_t index, Bitmap *&sprite);
+	HError      LoadSpriteData(sprkey_t index, Size &metric, int &bpp, std::vector<char> &data);
+
+	// Saves all sprites to file; fills in index data for external use
+	// TODO: refactor to be able to save main file and index file separately (separate function for gather data?)
+	static int  SaveToFile(const String &save_to_file,
+		const std::vector<Bitmap *> &sprites, // available sprites (may contain nullptrs)
+		SpriteFile *read_from_file, // optional file to read missing sprites from
+		bool compressOutput, SpriteFileIndex &index);
+	// Saves sprite index table in a separate file
+	static int  SaveSpriteIndex(const String &filename, const SpriteFileIndex &index);
+
+private:
+	// Finds the topmost occupied slot index. Warning: may be slow.
+	static sprkey_t FindTopmostSprite(const std::vector<Bitmap *> &sprites);
+	// Seek stream to sprite
+	void        SeekToSprite(sprkey_t index);
+
+	// Internal sprite reference
+	struct SpriteRef {
+		soff_t Offset = 0; // data offset
+		size_t Size = 0;   // cache size of element, in bytes
+	};
+
+	// Array of sprite references
+	std::vector<SpriteRef> _spriteData;
+	std::unique_ptr<Stream> _stream; // the sprite stream
+	bool _compressed; // are sprites compressed
+	sprkey_t _curPos; // current stream position (sprite slot)
+};
+
+} // namespace Shared
+} // namespace AGS
+} // namespace AGS3
+
+#endif


Commit: 659be0a4e67857b46a422a0c7d158d07f3631252
    https://github.com/scummvm/scummvm/commit/659be0a4e67857b46a422a0c7d158d07f3631252
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:23-07:00

Commit Message:
AGS: Fixed few warnings

>From upstream b416d55eb74745db0da5a3cb2654e063cd057ded

Changed paths:
    engines/ags/engine/ac/drawing_surface.cpp
    engines/ags/engine/ac/global_game.cpp
    engines/ags/engine/ac/overlay.cpp


diff --git a/engines/ags/engine/ac/drawing_surface.cpp b/engines/ags/engine/ac/drawing_surface.cpp
index 1c06f801a62..5ccd8d16c59 100644
--- a/engines/ags/engine/ac/drawing_surface.cpp
+++ b/engines/ags/engine/ac/drawing_surface.cpp
@@ -226,7 +226,7 @@ void DrawingSurface_DrawSurfaceEx(ScriptDrawingSurface *target, ScriptDrawingSur
 		int dst_x, int dst_y, int dst_width, int dst_height,
 		int src_x, int src_y, int src_width, int src_height) {
 	DrawingSurface_DrawImageImpl(target, source->GetBitmapSurface(), dst_x, dst_y, trans, dst_width, dst_height,
-		src_x, src_y, src_width, src_height, -1, source->hasAlphaChannel);
+		src_x, src_y, src_width, src_height, -1, source->hasAlphaChannel != 0);
 }
 
 void DrawingSurface_DrawSurface(ScriptDrawingSurface *target, ScriptDrawingSurface *source, int trans) {
diff --git a/engines/ags/engine/ac/global_game.cpp b/engines/ags/engine/ac/global_game.cpp
index ddc8ac3a9ca..a6dc4757357 100644
--- a/engines/ags/engine/ac/global_game.cpp
+++ b/engines/ags/engine/ac/global_game.cpp
@@ -780,7 +780,7 @@ int WaitImpl(int skip_type, int nloops) {
 
 	if (_GP(game).options[OPT_BASESCRIPTAPI] < kScriptAPI_v360) {
 		// < 3.6.0 return 1 is skipped by user input, otherwise 0
-		return (_GP(play).wait_skipped_by & (SKIP_KEYPRESS | SKIP_MOUSECLICK)) != 0 ? 1 : 0;
+		return ((_GP(play).wait_skipped_by & (SKIP_KEYPRESS | SKIP_MOUSECLICK)) != 0) ? 1 : 0;
 	}
 	// >= 3.6.0 return positive keycode, negative mouse button code, or 0 as time-out
 	return _GP(play).GetWaitSkipResult();
diff --git a/engines/ags/engine/ac/overlay.cpp b/engines/ags/engine/ac/overlay.cpp
index 2811fe99fa1..75538220ebc 100644
--- a/engines/ags/engine/ac/overlay.cpp
+++ b/engines/ags/engine/ac/overlay.cpp
@@ -273,7 +273,7 @@ size_t add_screen_overlay(int x, int y, int type, Bitmap *piccy, bool alphaChann
 size_t add_screen_overlay(int x, int y, int type, Shared::Bitmap *piccy, int pic_offx, int pic_offy, bool alphaChannel) {
 	if (type == OVER_CUSTOM) {
 		// find an unused custom ID; TODO: find a better approach!
-		for (uint id = OVER_CUSTOM + 1; id <= _GP(screenover).size() + OVER_CUSTOM + 1; ++id) {
+		for (int id = OVER_CUSTOM + 1; (size_t)id <= _GP(screenover).size() + OVER_CUSTOM + 1; ++id) {
 			if (find_overlay_of_type(id) == -1) {
 				type = id; break;
 			}


Commit: ddb38177f925cc10966f7419866b1dd6b4c8ce9e
    https://github.com/scummvm/scummvm/commit/ddb38177f925cc10966f7419866b1dd6b4c8ce9e
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:23-07:00

Commit Message:
AGS: Updated build version (3.6.0.8)

>From upstream 836fb0adbc7cc1a5a78563266f4ef0b53a33f0df

Changed paths:
    engines/ags/shared/core/def_version.h


diff --git a/engines/ags/shared/core/def_version.h b/engines/ags/shared/core/def_version.h
index 923151398b1..3f75c72c1d3 100644
--- a/engines/ags/shared/core/def_version.h
+++ b/engines/ags/shared/core/def_version.h
@@ -22,9 +22,9 @@
 #ifndef AGS_SHARED_CORE_DEFVERSION_H
 #define AGS_SHARED_CORE_DEFVERSION_H
 
-#define ACI_VERSION_STR      "3.6.0.3"
+#define ACI_VERSION_STR      "3.6.0.8"
 #if defined (RC_INVOKED) // for MSVC resource compiler
-#define ACI_VERSION_MSRC_DEF  3,6,0,3
+#define ACI_VERSION_MSRC_DEF  3,6,0,8
 #endif
 
 #define SPECIAL_VERSION ""


Commit: fc9473d175ee0643aa763b8dfdb614829cf40885
    https://github.com/scummvm/scummvm/commit/fc9473d175ee0643aa763b8dfdb614829cf40885
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:23-07:00

Commit Message:
AGS: Picked a sprite file writing code into SpriteFileWriter class

>From upstream 62a87664fc9249bad2886b44d10affc4d60071c3

Changed paths:
    engines/ags/shared/ac/sprite_cache.cpp
    engines/ags/shared/ac/sprite_file.cpp
    engines/ags/shared/ac/sprite_file.h


diff --git a/engines/ags/shared/ac/sprite_cache.cpp b/engines/ags/shared/ac/sprite_cache.cpp
index 60b6b145f26..a90f311241d 100644
--- a/engines/ags/shared/ac/sprite_cache.cpp
+++ b/engines/ags/shared/ac/sprite_cache.cpp
@@ -105,7 +105,7 @@ void SpriteCache::Init() {
 }
 
 void SpriteCache::Reset() {
-	_file.Reset();
+	_file.Close();
 	// TODO: find out if it's safe to simply always delete _spriteData.Image with array element
 	for (size_t i = 0; i < _spriteData.size(); ++i) {
 		if (_spriteData[i].Image) {
@@ -434,7 +434,7 @@ int SpriteCache::SaveToFile(const String &filename, bool compressOutput, SpriteF
 		pre_save_sprite(data.Image);
 		sprites.push_back(data.Image);
 	}
-	return _file.SaveToFile(filename, sprites, &_file, compressOutput, index);
+	return SaveSpriteFile(filename, sprites, &_file, compressOutput, index);
 }
 
 HError SpriteCache::InitFile(const String &filename, const String &sprindex_filename) {
@@ -465,7 +465,7 @@ HError SpriteCache::InitFile(const String &filename, const String &sprindex_file
 }
 
 void SpriteCache::DetachFile() {
-	_file.Reset();
+	_file.Close();
 }
 
 } // namespace Shared
diff --git a/engines/ags/shared/ac/sprite_file.cpp b/engines/ags/shared/ac/sprite_file.cpp
index aef315b54ec..2aa4806b6d5 100644
--- a/engines/ags/shared/ac/sprite_file.cpp
+++ b/engines/ags/shared/ac/sprite_file.cpp
@@ -25,6 +25,7 @@
 #include "ags/shared/gfx/bitmap.h"
 #include "ags/shared/util/compress.h"
 #include "ags/shared/util/file.h"
+#include "ags/shared/util/memory_stream.h"
 #include "ags/shared/util/stream.h"
 
 namespace AGS3 {
@@ -109,7 +110,7 @@ HError SpriteFile::OpenFile(const String &filename, const String &sprindex_filen
 	return RebuildSpriteIndex(_stream.get(), topmost, vers, metrics);
 }
 
-void SpriteFile::Reset() {
+void SpriteFile::Close() {
 	_stream.reset();
 	_curPos = -2;
 }
@@ -312,14 +313,6 @@ HError SpriteFile::LoadSpriteData(sprkey_t index, Size &metric, int &bpp,
 	return HError::None();
 }
 
-sprkey_t SpriteFile::FindTopmostSprite(const std::vector<Bitmap *> &sprites) {
-	sprkey_t topmost = -1;
-	for (sprkey_t i = 0; i < static_cast<sprkey_t>(sprites.size()); ++i)
-		if (sprites[i])
-			topmost = i;
-	return topmost;
-}
-
 void SpriteFile::SeekToSprite(sprkey_t index) {
 	// If we didn't just load the previous sprite, seek to it
 	if (index != _curPos) {
@@ -328,7 +321,16 @@ void SpriteFile::SeekToSprite(sprkey_t index) {
 	}
 }
 
-int SpriteFile::SaveToFile(const String &save_to_file,
+// Finds the topmost occupied slot index. Warning: may be slow.
+static sprkey_t FindTopmostSprite(const std::vector<Bitmap *> &sprites) {
+	sprkey_t topmost = -1;
+	for (sprkey_t i = 0; i < static_cast<sprkey_t>(sprites.size()); ++i)
+		if (sprites[i])
+			topmost = i;
+	return topmost;
+}
+
+int SaveSpriteFile(const String &save_to_file,
 	const std::vector<Bitmap *> &sprites,
 	SpriteFile *read_from_file,
 	bool compressOutput, SpriteFileIndex &index) {
@@ -336,27 +338,12 @@ int SpriteFile::SaveToFile(const String &save_to_file,
 	if (output == nullptr)
 		return -1;
 
-	int spriteFileIDCheck = g_system->getMillis();
-
-	// sprite file version
-	output->WriteInt16(kSprfVersion_Current);
-
-	output->WriteArray(spriteFileSig, strlen(spriteFileSig), 1);
-
-	output->WriteInt8(compressOutput ? 1 : 0);
-	output->WriteInt32(spriteFileIDCheck);
-
 	sprkey_t lastslot = read_from_file ? read_from_file->GetTopmostSprite() : 0;
 	lastslot = std::max(lastslot, FindTopmostSprite(sprites));
-	output->WriteInt32(lastslot);
-
-	// allocate buffers to store the indexing info
-	sprkey_t numsprits = lastslot + 1;
-	std::vector<int16_t> spritewidths, spriteheights;
-	std::vector<soff_t> spriteoffs;
-	spritewidths.resize(numsprits);
-	spriteheights.resize(numsprits);
-	spriteoffs.resize(numsprits);
+
+	SpriteFileWriter writer(output);
+	writer.Begin(compressOutput, lastslot);
+
 	std::unique_ptr<Bitmap> temp_bmp; // for disposing temp sprites
 	std::vector<char> membuf; // for loading raw sprite data
 
@@ -364,8 +351,6 @@ int SpriteFile::SaveToFile(const String &save_to_file,
 		read_from_file && read_from_file->IsFileCompressed() != compressOutput;
 
 	for (sprkey_t i = 0; i <= lastslot; ++i) {
-		soff_t sproff = output->GetPosition();
-
 		Bitmap *image = (size_t)i < sprites.size() ? sprites[i] : nullptr;
 
 		// if compression setting is different, load the sprite into memory
@@ -377,34 +362,11 @@ int SpriteFile::SaveToFile(const String &save_to_file,
 
 		// if managed to load an image - save it according the new compression settings
 		if (image != nullptr) {
-			// image in memory -- write it out
-			int bpp = image->GetColorDepth() / 8;
-			spriteoffs[i] = sproff;
-			spritewidths[i] = image->GetWidth();
-			spriteheights[i] = image->GetHeight();
-			output->WriteInt16(bpp);
-			output->WriteInt16(spritewidths[i]);
-			output->WriteInt16(spriteheights[i]);
-
-			if (compressOutput) {
-				soff_t lenloc = output->GetPosition();
-				// write some space for the length data
-				output->WriteInt32(0);
-
-				rle_compress(image, output.get());
-
-				soff_t fileSizeSoFar = output->GetPosition();
-				// write the length of the compressed data
-				output->Seek(lenloc, kSeekBegin);
-				output->WriteInt32((fileSizeSoFar - lenloc) - 4);
-				output->Seek(0, kSeekEnd);
-			} else {
-				output->WriteArray(image->GetDataForWriting(), spritewidths[i] * bpp, spriteheights[i]);
-			}
+			writer.WriteBitmap(image);
 			continue;
 		} else if (diff_compress) {
 			// sprite doesn't exist
-			output->WriteInt16(0); // colour depth
+			writer.WriteEmptySlot();
 			continue;
 		}
 
@@ -413,33 +375,19 @@ int SpriteFile::SaveToFile(const String &save_to_file,
 		Size metric;
 		int bpp;
 		read_from_file->LoadSpriteData(i, metric, bpp, membuf);
-
-		output->WriteInt16(bpp);
-		if (bpp == 0)
+		if (bpp == 0) {
+			writer.WriteEmptySlot();
 			continue; // empty slot
-
-		spriteoffs[i] = sproff;
-		spritewidths[i] = metric.Width;
-		spriteheights[i] = metric.Height;
-		output->WriteInt16(metric.Width);
-		output->WriteInt16(metric.Height);
-		if (compressOutput)
-			output->WriteInt32(membuf.size());
-		if (membuf.size() == 0)
-			continue; // bad data?
-		output->Write(&membuf[0], membuf.size());
+		}
+		writer.WriteSpriteData(membuf, metric.Width, metric.Height, bpp);
 	}
+	writer.Finalize();
 
-	index.SpriteFileIDCheck = spriteFileIDCheck;
-	index.LastSlot = lastslot;
-	index.SpriteCount = numsprits;
-	index.Widths = spritewidths;
-	index.Heights = spriteheights;
-	index.Offsets = spriteoffs;
+	index = writer.GetIndex();
 	return 0;
 }
 
-int SpriteFile::SaveSpriteIndex(const String &filename, const SpriteFileIndex &index) {
+int SaveSpriteIndex(const String &filename, const SpriteFileIndex &index) {
 	// write the sprite index file
 	Stream *out = File::CreateFile(filename);
 	if (!out)
@@ -451,9 +399,9 @@ int SpriteFile::SaveSpriteIndex(const String &filename, const SpriteFileIndex &i
 	out->WriteInt32(index.SpriteFileIDCheck);
 	// write last sprite number and num sprites, to verify that
 	// it matches the spr file
-	out->WriteInt32(index.LastSlot);
-	out->WriteInt32(index.SpriteCount);
-	if (index.SpriteCount > 0) {
+	out->WriteInt32(index.GetLastSlot());
+	out->WriteInt32(index.GetCount());
+	if (index.GetCount() > 0) {
 		out->WriteArrayOfInt16(&index.Widths.front(), index.Widths.size());
 		out->WriteArrayOfInt16(&index.Heights.front(), index.Heights.size());
 		out->WriteArrayOfInt64(&index.Offsets.front(), index.Offsets.size());
@@ -462,6 +410,83 @@ int SpriteFile::SaveSpriteIndex(const String &filename, const SpriteFileIndex &i
 	return 0;
 }
 
+SpriteFileWriter::SpriteFileWriter(std::unique_ptr<Stream> &out) : _out(out) {
+}
+
+void SpriteFileWriter::Begin(bool compressed, sprkey_t last_slot) {
+	if (!_out) return;
+	_index.SpriteFileIDCheck = g_system->getMillis();
+	_compress = compressed;
+
+	// sprite file version
+	_out->WriteInt16(kSprfVersion_Current);
+	_out->WriteArray(spriteFileSig, strlen(spriteFileSig), 1);
+	_out->WriteInt8(_compress ? 1 : 0);
+	_out->WriteInt32(_index.SpriteFileIDCheck);
+
+	// Remember and write provided "last slot" index,
+	// but if it's not set (< 0) then we will have to return back later
+	// and write correct one; this is done in Finalize().
+	_lastSlotPos = _out->GetPosition();
+	_out->WriteInt32(last_slot);
+
+	if (last_slot >= 0) { // allocate buffers to store the indexing info
+		sprkey_t numsprits = last_slot + 1;
+		_index.Offsets.reserve(numsprits);
+		_index.Widths.reserve(numsprits);
+		_index.Heights.reserve(numsprits);
+	}
+}
+
+void SpriteFileWriter::WriteBitmap(Bitmap *image) {
+	if (!_out) return;
+	int bpp = image->GetColorDepth() / 8;
+	int w = image->GetWidth();
+	int h = image->GetHeight();
+	if (_compress) {
+		MemoryStream mems(_membuf, kStream_Write);
+		rle_compress(image, &mems);
+		WriteSpriteData(_membuf, w, h, bpp);
+		_membuf.clear();
+	} else {
+		WriteSpriteData((const char *)image->GetData(), w * h * bpp, w, h, bpp);
+	}
+}
+
+void SpriteFileWriter::WriteEmptySlot() {
+	if (!_out) return;
+	soff_t sproff = _out->GetPosition();
+	_out->WriteInt16(0); // write invalid color depth to mark empty slot
+	_index.Offsets.push_back(sproff);
+	_index.Widths.push_back(0);
+	_index.Heights.push_back(0);
+}
+
+void SpriteFileWriter::WriteSpriteData(const char *pbuf, size_t len,
+	int w, int h, int bpp) {
+	if (!_out) return;
+	soff_t sproff = _out->GetPosition();
+	_index.Offsets.push_back(sproff);
+	_index.Widths.push_back(w);
+	_index.Heights.push_back(h);
+	_out->WriteInt16(bpp);
+	_out->WriteInt16(w);
+	_out->WriteInt16(h);
+	// if not compressed, then the data size could be calculated from the
+	// image metrics, therefore no need to write one
+	if (_compress)
+		_out->WriteInt32(len);
+	if (len == 0) return; // bad data?
+	_out->Write(pbuf, len); // write data itself
+}
+
+void SpriteFileWriter::Finalize() {
+	if (!_out || _lastSlotPos < 0) return;
+	_out->Seek(_lastSlotPos, kSeekBegin);
+	_out->WriteInt32(_index.GetLastSlot());
+	_out.reset();
+}
+
 } // namespace Shared
 } // namespace AGS
 } // namespace AGS3
diff --git a/engines/ags/shared/ac/sprite_file.h b/engines/ags/shared/ac/sprite_file.h
index 90cea21d35f..8052aff5f50 100644
--- a/engines/ags/shared/ac/sprite_file.h
+++ b/engines/ags/shared/ac/sprite_file.h
@@ -21,7 +21,10 @@
 
 //=============================================================================
 //
-// SpriteFile class handles sprite file loading and streaming.
+// SpriteFile class handles sprite file parsing and streaming sprites.
+// SpriteFileWriter manages writing sprites into the output stream one by one,
+// accumulating index information, and may therefore be suitable for a variety
+// of situations.
 //
 //=============================================================================
 
@@ -29,7 +32,9 @@
 #define AGS_SHARED_AC_SPRITE_FILE_H
 
 #include "ags/shared/core/types.h"
+#include "ags/lib/std/memory.h"
 #include "ags/lib/std/vector.h"
+#include "ags/shared/util/stream.h"
 #include "ags/globals.h"
 
 namespace AGS3 {
@@ -61,14 +66,16 @@ typedef int32_t sprkey_t;
 // SpriteFileIndex contains sprite file's table of contents
 struct SpriteFileIndex {
 	int SpriteFileIDCheck = 0; // tag matching sprite file and index file
-	sprkey_t LastSlot = -1;
-	size_t SpriteCount = 0u;
 	std::vector<int16_t> Widths;
 	std::vector<int16_t> Heights;
 	std::vector<soff_t>  Offsets;
-};
 
+	inline size_t GetCount() const { return Offsets.size(); }
+	inline sprkey_t GetLastSlot() const { return (sprkey_t)GetCount() - 1; }
+};
 
+// SpriteFile opens a sprite file for reading, reports general information,
+// and lets read sprites in any order.
 class SpriteFile {
 public:
 	// Standart sprite file and sprite index names
@@ -79,7 +86,8 @@ public:
 	// Loads sprite reference information and inits sprite stream
 	HError      OpenFile(const String &filename, const String &sprindex_filename,
 		std::vector<Size> &metrics);
-	void        Reset();
+	// Closes stream; no reading will be possible unless opened again
+	void        Close();
 
 	// Tells if bitmaps in the file are compressed
 	bool        IsFileCompressed() const;
@@ -93,21 +101,12 @@ public:
 	HError      RebuildSpriteIndex(Stream *in, sprkey_t topmost, SpriteFileVersion vers,
 		std::vector<Size> &metrics);
 
+	// Loads an image data and creates a ready bitmap
 	HError      LoadSprite(sprkey_t index, Bitmap *&sprite);
+	// Loads an image data into the buffer, reports the bitmap metrics and color depth
 	HError      LoadSpriteData(sprkey_t index, Size &metric, int &bpp, std::vector<char> &data);
 
-	// Saves all sprites to file; fills in index data for external use
-	// TODO: refactor to be able to save main file and index file separately (separate function for gather data?)
-	static int  SaveToFile(const String &save_to_file,
-		const std::vector<Bitmap *> &sprites, // available sprites (may contain nullptrs)
-		SpriteFile *read_from_file, // optional file to read missing sprites from
-		bool compressOutput, SpriteFileIndex &index);
-	// Saves sprite index table in a separate file
-	static int  SaveSpriteIndex(const String &filename, const SpriteFileIndex &index);
-
 private:
-	// Finds the topmost occupied slot index. Warning: may be slow.
-	static sprkey_t FindTopmostSprite(const std::vector<Bitmap *> &sprites);
 	// Seek stream to sprite
 	void        SeekToSprite(sprkey_t index);
 
@@ -124,6 +123,49 @@ private:
 	sprkey_t _curPos; // current stream position (sprite slot)
 };
 
+// SpriteFileWriter class writes a sprite file in a requested format.
+// Start using it by calling Begin, write ready bitmaps or copy raw sprite data
+// over slot by slot, then call Finalize to let it close the format correctly.
+class SpriteFileWriter {
+public:
+	SpriteFileWriter(std::unique_ptr<Stream> &out);
+	~SpriteFileWriter() {}
+
+    // Get the sprite index, accumulated after write
+    const SpriteFileIndex &GetIndex() const { return _index; }
+
+    // Initializes new sprite file format
+    void Begin(bool compress, sprkey_t last_slot = -1);
+    // Writes a bitmap into file, compressing if necessary
+    void WriteBitmap(Bitmap *image);
+    // Writes an empty slot marker
+    void WriteEmptySlot();
+    // Writes a raw sprite data without additional processing
+    void WriteSpriteData(const char *pbuf, size_t len, int w, int h, int bpp);
+    void WriteSpriteData(const std::vector<char> &buf, int w, int h, int bpp)
+        { WriteSpriteData(&buf[0], buf.size(), w, h, bpp); }
+    // Finalizes current format; no further writing is possible after this
+    void Finalize();
+
+private:
+    std::unique_ptr<Stream> &_out;
+    bool _compress = false;
+    soff_t _lastSlotPos = -1; // last slot save position in file
+    // sprite index accumulated on write for reporting back to user
+    SpriteFileIndex _index;
+    // compression buffer
+    std::vector<char> _membuf;
+};
+
+// Saves all sprites to file; fills in index data for external use
+// TODO: refactor to be able to save main file and index file separately (separate function for gather data?)
+int SaveSpriteFile(const String &save_to_file,
+    const std::vector<Bitmap*> &sprites, // available sprites (may contain nullptrs)
+    SpriteFile *read_from_file, // optional file to read missing sprites from
+    bool compressOutput, SpriteFileIndex &index);
+// Saves sprite index table in a separate file
+int SaveSpriteIndex(const String &filename, const SpriteFileIndex &index);
+
 } // namespace Shared
 } // namespace AGS
 } // namespace AGS3


Commit: 6e695f7817c7e1389508d5c7ae44191b29a15582
    https://github.com/scummvm/scummvm/commit/6e695f7817c7e1389508d5c7ae44191b29a15582
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:24-07:00

Commit Message:
AGS: Only set default character's idle delay for games < 3.6.0

>From upstream 8e6152837a6bc73ff50f6a304ed58b38997b5418

Changed paths:
    engines/ags/engine/main/engine.cpp


diff --git a/engines/ags/engine/main/engine.cpp b/engines/ags/engine/main/engine.cpp
index 1dca064cbce..6b83a18a009 100644
--- a/engines/ags/engine/main/engine.cpp
+++ b/engines/ags/engine/main/engine.cpp
@@ -618,8 +618,8 @@ void engine_init_game_settings() {
 		_GP(game).chars[ee].activeinv = -1;
 		_GP(game).chars[ee].following = -1;
 		_GP(game).chars[ee].followinfo = 97 | (10 << 8);
-		_GP(game).chars[ee].idletime = 20; // can be overridden later with SetIdle or summink
-		_GP(game).chars[ee].idleleft = _GP(game).chars[ee].idletime;
+		if (_G(loaded_game_file_version) < kGameVersion_360)
+			_GP(game).chars[ee].idletime = 20; // default to 20 seconds		_GP(game).chars[ee].idleleft = _GP(game).chars[ee].idletime;
 		_GP(game).chars[ee].transparency = 0;
 		_GP(game).chars[ee].baseline = -1;
 		_GP(game).chars[ee].walkwaitcounter = 0;


Commit: 4b68aa31f968733af54f6a043793236672d19adc
    https://github.com/scummvm/scummvm/commit/4b68aa31f968733af54f6a043793236672d19adc
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:24-07:00

Commit Message:
AGS: Process idleview based on real game speed, not hardcoded 40 fps

>From upstream 2e0d5116bce07a098166d6f6311fc87b27352e47

Changed paths:
    engines/ags/engine/ac/character_info_engine.cpp


diff --git a/engines/ags/engine/ac/character_info_engine.cpp b/engines/ags/engine/ac/character_info_engine.cpp
index bdfe1faa891..05a7edc370d 100644
--- a/engines/ags/engine/ac/character_info_engine.cpp
+++ b/engines/ags/engine/ac/character_info_engine.cpp
@@ -26,6 +26,7 @@
 #include "ags/engine/ac/character_extras.h"
 #include "ags/engine/ac/game_state.h"
 #include "ags/engine/ac/global_character.h"
+#include "ags/engine/ac/global_game.h"
 #include "ags/engine/ac/math.h"
 #include "ags/engine/ac/view_frame.h"
 #include "ags/engine/debugging/debug_log.h"
@@ -455,7 +456,7 @@ void CharacterInfo::update_character_idle(CharacterExtras *chex, int &doing_noth
 	else if ((doing_nothing == 0) || ((flags & CHF_FIXVIEW) != 0))
 		idleleft = idletime;
 	// count idle time
-	else if ((_G(loopcounter) % 40 == 0) || (chex->process_idle_this_time == 1)) {
+	else if ((_G(loopcounter) % GetGameSpeed() == 0) || (chex->process_idle_this_time == 1)) {
 		idleleft--;
 		if (idleleft == -1) {
 			int useloop = loop;


Commit: 4293a267cb1f2bd1a62c15ceed1c32be863566b8
    https://github.com/scummvm/scummvm/commit/4293a267cb1f2bd1a62c15ceed1c32be863566b8
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:24-07:00

Commit Message:
AGS: Corrected font outline upgrade condition

>From upstream 070b3ff3d0040f65ef42764033cf0d01742926ff

Changed paths:
    engines/ags/engine/game/game_init.cpp


diff --git a/engines/ags/engine/game/game_init.cpp b/engines/ags/engine/game/game_init.cpp
index 55fa74b409a..c47a7c7bdb4 100644
--- a/engines/ags/engine/game/game_init.cpp
+++ b/engines/ags/engine/game/game_init.cpp
@@ -265,8 +265,8 @@ void LoadFonts(GameDataVersion data_ver) {
 
 		const bool is_wfn = is_bitmap_font(i);
 		// Outline thickness corresponds to 1 game pixel by default;
-		// but if it's a scaled up bitmap font in a legacy hires game, then it equals to scale
-		if ((data_ver < kGameVersion_360) && _GP(game).IsLegacyHiRes()) {
+		// but if it's a scaled up bitmap font, then it equals to scale
+		if (data_ver < kGameVersion_360) {
 			if (is_wfn && (finfo.Outline == FONT_OUTLINE_AUTO)) {
 				set_font_outline(i, FONT_OUTLINE_AUTO, FontInfo::kSquared, get_font_scaling_mul(i));
 			}


Commit: 08560a1dbfbcfd819593e3bea87866cd807a40b3
    https://github.com/scummvm/scummvm/commit/08560a1dbfbcfd819593e3bea87866cd807a40b3
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:24-07:00

Commit Message:
AGS: Forgot to also clear guibgbmp array in dispose_game_drawdata()

>From upstream 1e4b73ad7b4ae5bf69641f08940d88b0fe828080

Changed paths:
    engines/ags/engine/ac/draw.cpp


diff --git a/engines/ags/engine/ac/draw.cpp b/engines/ags/engine/ac/draw.cpp
index 7e446775295..b5fe5223faa 100644
--- a/engines/ags/engine/ac/draw.cpp
+++ b/engines/ags/engine/ac/draw.cpp
@@ -413,6 +413,7 @@ void dispose_game_drawdata() {
 	_GP(actspswbbmp).clear();
 	_GP(actspswbcache).clear();
 	_GP(guibg).clear();
+	_GP(guibgbmp).clear();
 }
 
 void dispose_room_drawdata() {


Commit: 74254fdcf4897adce9d1cab5c56b40da8835313b
    https://github.com/scummvm/scummvm/commit/74254fdcf4897adce9d1cab5c56b40da8835313b
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:25-07:00

Commit Message:
AGS: Simplified couple of set_volume calls

>From upstream 3d4187e64237a7fa6f7f7bf4fb6477369bae1af4

Changed paths:
    engines/ags/engine/media/audio/audio.cpp


diff --git a/engines/ags/engine/media/audio/audio.cpp b/engines/ags/engine/media/audio/audio.cpp
index 507ff022ebc..b7664487a05 100644
--- a/engines/ags/engine/media/audio/audio.cpp
+++ b/engines/ags/engine/media/audio/audio.cpp
@@ -265,7 +265,7 @@ static void audio_update_polled_stuff() {
 		SOUNDCLIP *ch = lock.GetChannel(_GP(play).crossfading_out_channel);
 		int newVolume = ch ? ch->get_volume() - _GP(play).crossfade_out_volume_per_step : 0;
 		if (newVolume > 0) {
-			AudioChannel_SetVolume(&_G(scrAudioChannel)[_GP(play).crossfading_out_channel], newVolume);
+			ch->set_volume_percent(newVolume);
 		} else {
 			stop_and_destroy_channel(_GP(play).crossfading_out_channel);
 			_GP(play).crossfading_out_channel = 0;
@@ -282,7 +282,7 @@ static void audio_update_polled_stuff() {
 			newVolume = _GP(play).crossfade_final_volume_in;
 		}
 
-		AudioChannel_SetVolume(&_G(scrAudioChannel)[_GP(play).crossfading_in_channel], newVolume);
+		ch->set_volume_percent(newVolume);
 
 		if (newVolume >= _GP(play).crossfade_final_volume_in) {
 			_GP(play).crossfading_in_channel = 0;


Commit: 3167bac67bc61a67f17b29f3d872d93878bdf84d
    https://github.com/scummvm/scummvm/commit/3167bac67bc61a67f17b29f3d872d93878bdf84d
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:25-07:00

Commit Message:
AGS: More clear use of the "max channels" constants

>From upstream 2fd582a2a5fc9d38643277903854a977f8995e07

Changed paths:
    engines/ags/engine/ac/audio_clip.cpp
    engines/ags/engine/ac/game.cpp
    engines/ags/engine/ac/global_audio.cpp
    engines/ags/engine/ac/global_video.cpp
    engines/ags/engine/ac/room.cpp
    engines/ags/engine/ac/system.cpp
    engines/ags/engine/game/game_init.cpp
    engines/ags/engine/game/savegame.cpp
    engines/ags/engine/game/savegame_components.cpp
    engines/ags/engine/game/savegame_internal.h
    engines/ags/engine/game/savegame_v321.cpp
    engines/ags/engine/media/audio/audio.cpp
    engines/ags/engine/media/audio/audio_defines.h
    engines/ags/globals.cpp


diff --git a/engines/ags/engine/ac/audio_clip.cpp b/engines/ags/engine/ac/audio_clip.cpp
index 7c0dd438728..92d1401dea1 100644
--- a/engines/ags/engine/ac/audio_clip.cpp
+++ b/engines/ags/engine/ac/audio_clip.cpp
@@ -48,7 +48,7 @@ int AudioClip_GetIsAvailable(ScriptAudioClip *clip) {
 
 void AudioClip_Stop(ScriptAudioClip *clip) {
 	AudioChannelsLock lock;
-	for (int i = 0; i < MAX_SOUND_CHANNELS; i++) {
+	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if ((ch != nullptr) && (ch->_sourceClip == clip)) {
 			AudioChannel_Stop(&_G(scrAudioChannel)[i]);
@@ -72,8 +72,9 @@ ScriptAudioChannel *AudioClip_PlayQueued(ScriptAudioClip *clip, int priority, in
 }
 
 ScriptAudioChannel *AudioClip_PlayOnChannel(ScriptAudioClip *clip, int chan, int priority, int repeat) {
-	if (chan < 1 || chan >= MAX_SOUND_CHANNELS)
-		quitprintf("!AudioClip.PlayOnChannel: invalid channel %d, the range is %d - %d", chan, 1, MAX_SOUND_CHANNELS - 1);
+	if (chan < NUM_SPEECH_CHANS || chan >= MAX_GAME_CHANNELS)
+		quitprintf("!AudioClip.PlayOnChannel: invalid channel %d, the range is %d - %d",
+			chan, NUM_SPEECH_CHANS, MAX_GAME_CHANNELS - 1);
 	if (priority == SCR_NO_VALUE)
 		priority = clip->defaultPriority;
 	if (repeat == SCR_NO_VALUE)
diff --git a/engines/ags/engine/ac/game.cpp b/engines/ags/engine/ac/game.cpp
index 0e55ad221d2..984e38176ce 100644
--- a/engines/ags/engine/ac/game.cpp
+++ b/engines/ags/engine/ac/game.cpp
@@ -97,7 +97,7 @@ void Game_StopAudio(int audioType) {
 		quitprintf("!Game.StopAudio: invalid audio type %d", audioType);
 	int aa;
 
-	for (aa = 0; aa < MAX_SOUND_CHANNELS; aa++) {
+	for (aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
 		if (audioType == SCR_NO_VALUE) {
 			stop_or_fade_out_channel(aa);
 		} else {
@@ -117,7 +117,7 @@ int Game_IsAudioPlaying(int audioType) {
 	if (_GP(play).fast_forward)
 		return 0;
 
-	for (int aa = 0; aa < MAX_SOUND_CHANNELS; aa++) {
+	for (int aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
 		ScriptAudioClip *clip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[aa]);
 		if (clip != nullptr) {
 			if ((clip->type == audioType) || (audioType == SCR_NO_VALUE)) {
@@ -147,7 +147,7 @@ void Game_SetAudioTypeVolume(int audioType, int volume, int changeType) {
 	if ((changeType == VOL_CHANGEEXISTING) ||
 	        (changeType == VOL_BOTH)) {
 		AudioChannelsLock lock;
-		for (int aa = 0; aa < MAX_SOUND_CHANNELS; aa++) {
+		for (int aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
 			ScriptAudioClip *clip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[aa]);
 			if ((clip != nullptr) && (clip->type == audioType)) {
 				auto *ch = lock.GetChannel(aa);
@@ -1133,7 +1133,7 @@ void stop_fast_forwarding() {
 		AudioChannelsLock lock;
 
 		// Restore actual volume of sounds
-		for (int aa = 0; aa <= MAX_SOUND_CHANNELS; aa++) {
+		for (int aa = 0; aa < TOTAL_AUDIO_CHANNELS; aa++) {
 			auto *ch = lock.GetChannelIfPlaying(aa);
 			if (ch) {
 				ch->set_mute(false);
@@ -1242,7 +1242,7 @@ void display_switch_out_suspend() {
 	{
 		// stop the sound stuttering
 		AudioChannelsLock lock;
-		for (int i = 0; i <= MAX_SOUND_CHANNELS; i++) {
+		for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
 			auto *ch = lock.GetChannelIfPlaying(i);
 			if (ch) {
 				ch->pause();
@@ -1271,7 +1271,7 @@ void display_switch_in_resume() {
 
 	{
 		AudioChannelsLock lock;
-		for (int i = 0; i <= MAX_SOUND_CHANNELS; i++) {
+		for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
 			auto *ch = lock.GetChannelIfPlaying(i);
 			if (ch) {
 				ch->resume();
diff --git a/engines/ags/engine/ac/global_audio.cpp b/engines/ags/engine/ac/global_audio.cpp
index a543cc7b861..2c6763f96f1 100644
--- a/engines/ags/engine/ac/global_audio.cpp
+++ b/engines/ags/engine/ac/global_audio.cpp
@@ -40,8 +40,9 @@ namespace AGS3 {
 using namespace AGS::Shared;
 
 void StopAmbientSound(int channel) {
-	if ((channel < 0) || (channel >= MAX_SOUND_CHANNELS))
-		quit("!StopAmbientSound: invalid channel");
+	if ((channel < NUM_SPEECH_CHANS) || (channel >= MAX_GAME_CHANNELS))
+		quitprintf("!StopAmbientSound: invalid channel %d, supported %d - %d",
+			channel, NUM_SPEECH_CHANS, MAX_GAME_CHANNELS - 1);
 
 	if (_GP(ambient)[channel].channel == 0)
 		return;
@@ -52,7 +53,7 @@ void StopAmbientSound(int channel) {
 
 void PlayAmbientSound(int channel, int sndnum, int vol, int x, int y) {
 	// the channel parameter is to allow multiple ambient sounds in future
-	if ((channel < 1) || (channel == SCHAN_SPEECH) || (channel >= MAX_SOUND_CHANNELS))
+	if ((channel < 1) || (channel == SCHAN_SPEECH) || (channel >= MAX_GAME_CHANNELS))
 		quit("!PlayAmbientSound: invalid channel number");
 	if ((vol < 1) || (vol > 255))
 		quit("!PlayAmbientSound: volume must be 1 to 255");
@@ -95,7 +96,7 @@ int IsChannelPlaying(int chan) {
 	if (_GP(play).fast_forward)
 		return 0;
 
-	if ((chan < 0) || (chan >= MAX_SOUND_CHANNELS))
+	if ((chan < 0) || (chan >= MAX_GAME_CHANNELS))
 		quit("!IsChannelPlaying: invalid sound channel");
 
 	if (channel_is_playing(chan))
@@ -110,7 +111,7 @@ int IsSoundPlaying() {
 
 	// find if there's a sound playing
 	AudioChannelsLock lock;
-	for (int i = SCHAN_NORMAL; i < MAX_SOUND_CHANNELS; i++) {
+	for (int i = SCHAN_NORMAL; i < MAX_GAME_CHANNELS; i++) {
 		if (lock.GetChannelIfPlaying(i))
 			return 1;
 	}
@@ -128,8 +129,8 @@ int PlaySoundEx(int val1, int channel) {
 	if (aclip && !is_audiotype_allowed_to_play((AudioFileType)aclip->fileType))
 		return -1; // if sound is off, ignore it
 
-	if ((channel < SCHAN_NORMAL) || (channel >= MAX_SOUND_CHANNELS))
-		quit("!PlaySoundEx: invalid channel specified, must be 3-7");
+	if ((channel < SCHAN_NORMAL) || (channel >= MAX_GAME_CHANNELS))
+		quitprintf("!PlaySoundEx: invalid channel specified, must be %d-%d", SCHAN_NORMAL, MAX_GAME_CHANNELS - 1);
 
 	// if an ambient sound is playing on this channel, abort it
 	StopAmbientSound(channel);
@@ -337,7 +338,7 @@ void SetSoundVolume(int newvol) {
 void SetChannelVolume(int chan, int newvol) {
 	if ((newvol < 0) || (newvol > 255))
 		quit("!SetChannelVolume: invalid volume - must be from 0-255");
-	if ((chan < 0) || (chan >= MAX_SOUND_CHANNELS))
+	if ((chan < 0) || (chan >= MAX_GAME_CHANNELS))
 		quit("!SetChannelVolume: invalid channel id");
 
 	AudioChannelsLock lock;
diff --git a/engines/ags/engine/ac/global_video.cpp b/engines/ags/engine/ac/global_video.cpp
index c5ca0376e37..16a261caaae 100644
--- a/engines/ags/engine/ac/global_video.cpp
+++ b/engines/ags/engine/ac/global_video.cpp
@@ -62,8 +62,8 @@ void scrPlayVideo(const char *name, int skip, int flags) {
 
 void pause_sound_if_necessary_and_play_video(const char *name, int skip, int flags) {
 	int musplaying = _GP(play).cur_music_number, i;
-	int ambientWas[MAX_SOUND_CHANNELS];
-	for (i = 1; i < MAX_SOUND_CHANNELS; i++)
+	int ambientWas[MAX_GAME_CHANNELS];
+	for (i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++)
 		ambientWas[i] = _GP(ambient)[i].channel;
 
 	if ((strlen(name) > 3) && (ags_stricmp(&name[strlen(name) - 3], "ogv") == 0)) {
@@ -85,9 +85,10 @@ void pause_sound_if_necessary_and_play_video(const char *name, int skip, int fla
 		// restart the music
 		if (musplaying >= 0)
 			newmusic(musplaying);
-		for (i = 1; i < MAX_SOUND_CHANNELS; i++) {
+		for (i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
 			if (ambientWas[i] > 0)
-				PlayAmbientSound(ambientWas[i], _GP(ambient)[i].num, _GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
+				PlayAmbientSound(ambientWas[i], _GP(ambient)[i].num,
+					_GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
 		}
 	}
 }
diff --git a/engines/ags/engine/ac/room.cpp b/engines/ags/engine/ac/room.cpp
index 7e80bc5ad0f..41e808bcbb9 100644
--- a/engines/ags/engine/ac/room.cpp
+++ b/engines/ags/engine/ac/room.cpp
@@ -240,7 +240,7 @@ void unload_old_room() {
 		_G(objs)[ff].moving = 0;
 
 	if (!_GP(play).ambient_sounds_persist) {
-		for (ff = 1; ff < MAX_SOUND_CHANNELS; ff++)
+		for (ff = NUM_SPEECH_CHANS; ff < MAX_GAME_CHANNELS; ff++)
 			StopAmbientSound(ff);
 	}
 
diff --git a/engines/ags/engine/ac/system.cpp b/engines/ags/engine/ac/system.cpp
index b5cbf152882..3626f5e726d 100644
--- a/engines/ags/engine/ac/system.cpp
+++ b/engines/ags/engine/ac/system.cpp
@@ -162,11 +162,11 @@ void System_SetGamma(int newValue) {
 }
 
 int System_GetAudioChannelCount() {
-	return MAX_SOUND_CHANNELS;
+	return MAX_GAME_CHANNELS;
 }
 
 ScriptAudioChannel *System_GetAudioChannels(int index) {
-	if ((index < 0) || (index >= MAX_SOUND_CHANNELS))
+	if ((index < 0) || (index >= MAX_GAME_CHANNELS))
 		quit("!System.AudioChannels: invalid sound channel index");
 
 	return &_G(scrAudioChannel)[index];
diff --git a/engines/ags/engine/game/game_init.cpp b/engines/ags/engine/game/game_init.cpp
index c47a7c7bdb4..ab5993651b9 100644
--- a/engines/ags/engine/game/game_init.cpp
+++ b/engines/ags/engine/game/game_init.cpp
@@ -89,7 +89,7 @@ String GetGameInitErrorText(GameInitErrorType err) {
 
 // Initializes audio channels and clips and registers them in the script system
 void InitAndRegisterAudioObjects() {
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+	for (int i = 0; i < MAX_GAME_CHANNELS; ++i) {
 		_G(scrAudioChannel)[i].id = i;
 		ccRegisterManagedObject(&_G(scrAudioChannel)[i], &_GP(ccDynamicAudio));
 	}
diff --git a/engines/ags/engine/game/savegame.cpp b/engines/ags/engine/game/savegame.cpp
index ccb9f296fe3..5e3acd6b40a 100644
--- a/engines/ags/engine/game/savegame.cpp
+++ b/engines/ags/engine/game/savegame.cpp
@@ -386,7 +386,7 @@ void DoBeforeRestore(PreservedParams &pp) {
 	ccUnregisterAllObjects();
 
 	// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
 		stop_and_destroy_channel_ex(i, false);
 	}
 
@@ -555,7 +555,7 @@ HSaveError DoAfterRestore(const PreservedParams &pp, const RestoredData &r_data)
 	{
 		AudioChannelsLock lock;
 		// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
-		for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+		for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
 			const RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
 			if (chan_info.ClipID < 0)
 				continue;
@@ -585,7 +585,7 @@ HSaveError DoAfterRestore(const PreservedParams &pp, const RestoredData &r_data)
 		// If there were synced audio tracks, the time taken to load in the
 		// different channels will have thrown them out of sync, so re-time it
 		// NOTE: channels are array of MAX_SOUND_CHANNELS+1 size
-		for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+		for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
 			auto *ch = lock.GetChannelIfPlaying(i);
 			int pos = r_data.AudioChans[i].Pos;
 			if ((pos > 0) && (ch != nullptr)) {
@@ -595,7 +595,7 @@ HSaveError DoAfterRestore(const PreservedParams &pp, const RestoredData &r_data)
 	} // -- AudioChannelsLock
 
 	// TODO: investigate loop range
-	for (int i = 1; i < MAX_SOUND_CHANNELS; ++i) {
+	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; ++i) {
 		if (r_data.DoAmbient[i])
 			PlayAmbientSound(i, r_data.DoAmbient[i], _GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
 	}
diff --git a/engines/ags/engine/game/savegame_components.cpp b/engines/ags/engine/game/savegame_components.cpp
index d8c687b0e4b..5c4b1d62dd4 100644
--- a/engines/ags/engine/game/savegame_components.cpp
+++ b/engines/ags/engine/game/savegame_components.cpp
@@ -344,7 +344,7 @@ HSaveError WriteAudio(Stream *out) {
 	}
 
 	// Audio clips and crossfade
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; i++) {
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if ((ch != nullptr) && (ch->_sourceClip != nullptr)) {
 			out->WriteInt32(((ScriptAudioClip *)ch->_sourceClip)->id);
@@ -372,7 +372,7 @@ HSaveError WriteAudio(Stream *out) {
 	out->WriteInt32(_G(current_music_type));
 
 	// Ambient sound
-	for (int i = 0; i < MAX_SOUND_CHANNELS; ++i)
+	for (int i = 0; i < MAX_GAME_CHANNELS; ++i)
 		_GP(ambient)[i].WriteToFile(out);
 	return HSaveError::None();
 }
@@ -394,7 +394,7 @@ HSaveError ReadAudio(Stream *in, int32_t cmp_ver, const PreservedParams &pp, Res
 	}
 
 	// Audio clips and crossfade
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
 		RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
 		chan_info.Pos = 0;
 		chan_info.ClipID = in->ReadInt32();
@@ -425,9 +425,9 @@ HSaveError ReadAudio(Stream *in, int32_t cmp_ver, const PreservedParams &pp, Res
 	_G(current_music_type) = in->ReadInt32();
 
 	// Ambient sound
-	for (int i = 0; i < MAX_SOUND_CHANNELS; ++i)
+	for (int i = 0; i < MAX_GAME_CHANNELS; ++i)
 		_GP(ambient)[i].ReadFromFile(in);
-	for (int i = 1; i < MAX_SOUND_CHANNELS; ++i) {
+	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; ++i) {
 		if (_GP(ambient)[i].channel == 0) {
 			r_data.DoAmbient[i] = 0;
 		} else {
diff --git a/engines/ags/engine/game/savegame_internal.h b/engines/ags/engine/game/savegame_internal.h
index 50691559a08..0a7fee951bf 100644
--- a/engines/ags/engine/game/savegame_internal.h
+++ b/engines/ags/engine/game/savegame_internal.h
@@ -105,9 +105,9 @@ struct RestoredData {
 		int YSource = -1;
 		int MaxDist = 0;
 	};
-	ChannelInfo             AudioChans[MAX_SOUND_CHANNELS + 1];
+	ChannelInfo             AudioChans[TOTAL_AUDIO_CHANNELS];
 	// Ambient sounds
-	int                     DoAmbient[MAX_SOUND_CHANNELS];
+	int                     DoAmbient[MAX_GAME_CHANNELS];
 	// Viewport and camera data, has to be preserved and applied only after
 	// room gets loaded, because we must clamp these to room parameters.
 	struct ViewportData {
diff --git a/engines/ags/engine/game/savegame_v321.cpp b/engines/ags/engine/game/savegame_v321.cpp
index 8e849caef7d..c65102ae6b9 100644
--- a/engines/ags/engine/game/savegame_v321.cpp
+++ b/engines/ags/engine/game/savegame_v321.cpp
@@ -253,11 +253,11 @@ static void restore_game_thisroom(Stream *in, RestoredData &r_data) {
 }
 
 static void restore_game_ambientsounds(Stream *in, RestoredData &r_data) {
-	for (int i = 0; i < MAX_SOUND_CHANNELS; ++i) {
+	for (int i = 0; i < MAX_GAME_CHANNELS; ++i) {
 		_GP(ambient)[i].ReadFromFile(in);
 	}
 
-	for (int bb = 1; bb < MAX_SOUND_CHANNELS; bb++) {
+	for (int bb = NUM_SPEECH_CHANS; bb < MAX_GAME_CHANNELS; bb++) {
 		if (_GP(ambient)[bb].channel == 0)
 			r_data.DoAmbient[bb] = 0;
 		else {
@@ -359,7 +359,7 @@ static HSaveError restore_game_audioclips_and_crossfade(Stream *in, RestoredData
 		return new SavegameError(kSvgErr_GameContentAssertion, "Mismatching number of Audio Clips.");
 	}
 
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; ++i) {
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
 		RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
 		chan_info.Pos = 0;
 		chan_info.ClipID = in->ReadInt32();
diff --git a/engines/ags/engine/media/audio/audio.cpp b/engines/ags/engine/media/audio/audio.cpp
index b7664487a05..c1fc39d25a8 100644
--- a/engines/ags/engine/media/audio/audio.cpp
+++ b/engines/ags/engine/media/audio/audio.cpp
@@ -156,6 +156,7 @@ static void move_track_to_crossfade_channel(int currentChannel, int crossfadeSpe
 	}
 }
 
+// NOTE: this function assumes one of the user channels
 void stop_or_fade_out_channel(int fadeOutChannel, int fadeInChannel, ScriptAudioClip *newSound) {
 	ScriptAudioClip *sourceClip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[fadeOutChannel]);
 	if ((sourceClip != nullptr) && (_GP(game).audioClipTypes[sourceClip->type].crossfadeSpeed > 0)) {
@@ -176,7 +177,7 @@ static int find_free_audio_channel(ScriptAudioClip *clip, int priority, bool int
 		priority--;
 
 	int startAtChannel = _G(reserved_channel_count);
-	int endBeforeChannel = MAX_SOUND_CHANNELS;
+	int endBeforeChannel = MAX_GAME_CHANNELS;
 
 	if (_GP(game).audioClipTypes[clip->type].reservedChannels > 0) {
 		startAtChannel = 0;
@@ -450,7 +451,7 @@ ScriptAudioChannel *play_audio_clip_by_index(int audioClipIndex) {
 }
 
 void stop_and_destroy_channel_ex(int chid, bool resetLegacyMusicSettings) {
-	if ((chid < 0) || (chid > MAX_SOUND_CHANNELS))
+	if ((chid < 0) || (chid >= TOTAL_AUDIO_CHANNELS))
 		quit("!StopChannel: invalid channel ID");
 
 	AudioChannelsLock lock;
@@ -470,8 +471,10 @@ void stop_and_destroy_channel_ex(int chid, bool resetLegacyMusicSettings) {
 	// don't update '_G(crossFading)' here as it is updated in all the cross-fading functions.
 
 	// destroyed an ambient sound channel
-	if (_GP(ambient)[chid].channel > 0)
-		_GP(ambient)[chid].channel = 0;
+	if (chid < MAX_GAME_CHANNELS) {
+		if (_GP(ambient)[chid].channel > 0)
+			_GP(ambient)[chid].channel = 0;
+	}
 
 	if ((chid == SCHAN_MUSIC) && (resetLegacyMusicSettings)) {
 		_GP(play).cur_music_number = -1;
@@ -537,7 +540,7 @@ int get_volume_adjusted_for_distance(int volume, int sndX, int sndY, int sndMaxD
 void update_directional_sound_vol() {
 	AudioChannelsLock lock;
 
-	for (int chnum = 1; chnum < MAX_SOUND_CHANNELS; chnum++) {
+	for (int chnum = NUM_SPEECH_CHANS; chnum < MAX_GAME_CHANNELS; chnum++) {
 		auto *ch = lock.GetChannelIfPlaying(chnum);
 		if ((ch != nullptr) && (ch->_xSource >= 0)) {
 			ch->apply_directional_modifier(
@@ -553,8 +556,7 @@ void update_directional_sound_vol() {
 void update_ambient_sound_vol() {
 	AudioChannelsLock lock;
 
-	for (int chan = 1; chan < MAX_SOUND_CHANNELS; chan++) {
-
+	for (int chan = NUM_SPEECH_CHANS; chan < MAX_GAME_CHANNELS; chan++) {
 		AmbientSound *thisSound = &_GP(ambient)[chan];
 
 		if (thisSound->channel == 0)
@@ -614,7 +616,7 @@ void stop_all_sound_and_music() {
 	// make sure it doesn't start crossfading when it comes back
 	_G(crossFading) = 0;
 	// any ambient sound will be aborted
-	for (int i = 0; i <= MAX_SOUND_CHANNELS; i++)
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i)
 		stop_and_destroy_channel(i);
 }
 
@@ -633,7 +635,7 @@ static int play_sound_priority(int val1, int priority) {
 	AudioChannelsLock lock;
 
 	// find a free channel to play it on
-	for (int i = SCHAN_NORMAL; i < MAX_SOUND_CHANNELS; i++) {
+	for (int i = SCHAN_NORMAL; i < MAX_GAME_CHANNELS; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if (val1 < 0) {
 			// Playing sound -1 means iterate through and stop all sound
@@ -773,7 +775,7 @@ int calculate_max_volume() {
 void apply_volume_drop_modifier(bool applyModifier) {
 	AudioChannelsLock lock;
 
-	for (int i = 0; i < MAX_SOUND_CHANNELS; i++) {
+	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if (ch && ch->_sourceClip != nullptr) {
 			if (applyModifier)
diff --git a/engines/ags/engine/media/audio/audio_defines.h b/engines/ags/engine/media/audio/audio_defines.h
index 46263af2d70..7b9786ca8ee 100644
--- a/engines/ags/engine/media/audio/audio_defines.h
+++ b/engines/ags/engine/media/audio/audio_defines.h
@@ -28,8 +28,13 @@
 #define MUS_MOD  4
 #define MUS_OGG  5
 
-#define MAX_SOUND_CHANNELS 8
-#define SPECIAL_CROSSFADE_CHANNEL 8
+ // Max channels that are distributed among game's audio types
+#define MAX_GAME_CHANNELS         8
+#define SPECIAL_CROSSFADE_CHANNEL (MAX_GAME_CHANNELS)
+// Total number of channels: game chans + utility chans
+#define TOTAL_AUDIO_CHANNELS      (MAX_GAME_CHANNELS + 1)
+// Number of game channels reserved for speech voice-over
+#define NUM_SPEECH_CHANS          1
 
 #define SCHAN_SPEECH  0
 #define SCHAN_AMBIENT 1
diff --git a/engines/ags/globals.cpp b/engines/ags/globals.cpp
index 69b5b78ac9a..a8de8218d09 100644
--- a/engines/ags/globals.cpp
+++ b/engines/ags/globals.cpp
@@ -121,10 +121,9 @@ Globals::Globals() {
 	_AssetMgr = new std::unique_ptr<Shared::AssetManager>();
 
 	// audio.cpp globals
-	_audioChannels = new std::array<SOUNDCLIP *>(MAX_SOUND_CHANNELS + 1);
-	// TODO: double check that ambient sounds array actually needs +1
-	_ambient = new std::array<AmbientSound>(MAX_SOUND_CHANNELS + 1);
-	_scrAudioChannel = new ScriptAudioChannel[MAX_SOUND_CHANNELS + 1];
+	_audioChannels = new std::array<SOUNDCLIP *>(TOTAL_AUDIO_CHANNELS);
+	_ambient = new std::array<AmbientSound>(MAX_GAME_CHANNELS);
+	_scrAudioChannel = new ScriptAudioChannel[MAX_GAME_CHANNELS];
 
 	// button.cpp globals
 	_animbuts = new AnimatingGUIButton[MAX_ANIMATING_BUTTONS];


Commit: aa91cf116156fa397996f6749f837ef0ef508047
    https://github.com/scummvm/scummvm/commit/aa91cf116156fa397996f6749f837ef0ef508047
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:25-07:00

Commit Message:
AGS: Increased the max number of audio channels to 16

>From upstream ebddc45c2a2252f5eb927c96b5c6065eebea1ed4

Changed paths:
    engines/ags/engine/game/savegame_components.cpp
    engines/ags/engine/game/savegame_v321.cpp
    engines/ags/engine/media/audio/audio_defines.h


diff --git a/engines/ags/engine/game/savegame_components.cpp b/engines/ags/engine/game/savegame_components.cpp
index 5c4b1d62dd4..a08749b29e7 100644
--- a/engines/ags/engine/game/savegame_components.cpp
+++ b/engines/ags/engine/game/savegame_components.cpp
@@ -336,7 +336,10 @@ HSaveError WriteAudio(Stream *out) {
 
 	// Game content assertion
 	out->WriteInt32(_GP(game).audioClipTypes.size());
-	out->WriteInt32(_GP(game).audioClips.size()); // [ivan-mogilko] not necessary, kept only to avoid changing save format
+	out->WriteInt8(TOTAL_AUDIO_CHANNELS);
+	out->WriteInt8(MAX_GAME_CHANNELS);
+	out->WriteInt16(0); // reserved 2 bytes (remains of int32)
+
 	// Audio types
 	for (size_t i = 0; i < _GP(game).audioClipTypes.size(); ++i) {
 		_GP(game).audioClipTypes[i].WriteToSavegame(out);
@@ -382,10 +385,19 @@ HSaveError ReadAudio(Stream *in, int32_t cmp_ver, const PreservedParams &pp, Res
 	// Game content assertion
 	if (!AssertGameContent(err, in->ReadInt32(), _GP(game).audioClipTypes.size(), "Audio Clip Types"))
 		return err;
-	in->ReadInt32(); // audio clip count
-	/* [ivan-mogilko] looks like it's not necessary to assert, as there's no data serialized for clips
-	if (!AssertGameContent(err, in->ReadInt32(), _GP(game).audioClips.size(), "Audio Clips"))
-	    return err;*/
+	int total_channels, max_game_channels;
+	if (cmp_ver >= 2) {
+		total_channels = in->ReadInt8();
+		max_game_channels = in->ReadInt8();
+		in->ReadInt16(); // reserved 2 bytes
+		if (!AssertCompatLimit(err, total_channels, TOTAL_AUDIO_CHANNELS, "System Audio Channels") ||
+			!AssertCompatLimit(err, max_game_channels, MAX_GAME_CHANNELS, "Game Audio Channels"))
+			return err;
+	} else {
+		total_channels = TOTAL_AUDIO_CHANNELS_v320;
+		max_game_channels = MAX_GAME_CHANNELS_v320;
+		in->ReadInt32(); // unused in prev format ver
+	}
 
 	// Audio types
 	for (size_t i = 0; i < _GP(game).audioClipTypes.size(); ++i) {
@@ -394,7 +406,7 @@ HSaveError ReadAudio(Stream *in, int32_t cmp_ver, const PreservedParams &pp, Res
 	}
 
 	// Audio clips and crossfade
-	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; i++) {
+	for (int i = 0; i < total_channels; ++i) {
 		RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
 		chan_info.Pos = 0;
 		chan_info.ClipID = in->ReadInt32();
@@ -425,9 +437,9 @@ HSaveError ReadAudio(Stream *in, int32_t cmp_ver, const PreservedParams &pp, Res
 	_G(current_music_type) = in->ReadInt32();
 
 	// Ambient sound
-	for (int i = 0; i < MAX_GAME_CHANNELS; ++i)
+	for (int i = 0; i < max_game_channels; ++i)
 		_GP(ambient)[i].ReadFromFile(in);
-	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; ++i) {
+	for (int i = NUM_SPEECH_CHANS; i < max_game_channels; ++i) {
 		if (_GP(ambient)[i].channel == 0) {
 			r_data.DoAmbient[i] = 0;
 		} else {
@@ -1014,7 +1026,7 @@ ComponentHandler ComponentHandlers[] = {
 	},
 	{
 		"Audio",
-		1,
+		2,
 		0,
 		WriteAudio,
 		ReadAudio
diff --git a/engines/ags/engine/game/savegame_v321.cpp b/engines/ags/engine/game/savegame_v321.cpp
index c65102ae6b9..61fd8d37276 100644
--- a/engines/ags/engine/game/savegame_v321.cpp
+++ b/engines/ags/engine/game/savegame_v321.cpp
@@ -253,11 +253,11 @@ static void restore_game_thisroom(Stream *in, RestoredData &r_data) {
 }
 
 static void restore_game_ambientsounds(Stream *in, RestoredData &r_data) {
-	for (int i = 0; i < MAX_GAME_CHANNELS; ++i) {
+	for (int i = 0; i < MAX_GAME_CHANNELS_v320; ++i) {
 		_GP(ambient)[i].ReadFromFile(in);
 	}
 
-	for (int bb = NUM_SPEECH_CHANS; bb < MAX_GAME_CHANNELS; bb++) {
+	for (int bb = NUM_SPEECH_CHANS; bb < MAX_GAME_CHANNELS_v320; bb++) {
 		if (_GP(ambient)[bb].channel == 0)
 			r_data.DoAmbient[bb] = 0;
 		else {
@@ -359,7 +359,7 @@ static HSaveError restore_game_audioclips_and_crossfade(Stream *in, RestoredData
 		return new SavegameError(kSvgErr_GameContentAssertion, "Mismatching number of Audio Clips.");
 	}
 
-	for (int i = 0; i < TOTAL_AUDIO_CHANNELS; ++i) {
+	for (int i = 0; i < TOTAL_AUDIO_CHANNELS_v320; ++i) {
 		RestoredData::ChannelInfo &chan_info = r_data.AudioChans[i];
 		chan_info.Pos = 0;
 		chan_info.ClipID = in->ReadInt32();
diff --git a/engines/ags/engine/media/audio/audio_defines.h b/engines/ags/engine/media/audio/audio_defines.h
index 7b9786ca8ee..8d9e428108b 100644
--- a/engines/ags/engine/media/audio/audio_defines.h
+++ b/engines/ags/engine/media/audio/audio_defines.h
@@ -29,12 +29,15 @@
 #define MUS_OGG  5
 
  // Max channels that are distributed among game's audio types
-#define MAX_GAME_CHANNELS         8
+#define MAX_GAME_CHANNELS         16
 #define SPECIAL_CROSSFADE_CHANNEL (MAX_GAME_CHANNELS)
 // Total number of channels: game chans + utility chans
 #define TOTAL_AUDIO_CHANNELS      (MAX_GAME_CHANNELS + 1)
 // Number of game channels reserved for speech voice-over
 #define NUM_SPEECH_CHANS          1
+// Legacy channel numbers
+#define MAX_GAME_CHANNELS_v320    8
+#define TOTAL_AUDIO_CHANNELS_v320 (MAX_GAME_CHANNELS_v320 + 1)
 
 #define SCHAN_SPEECH  0
 #define SCHAN_AMBIENT 1


Commit: ee311d660fb7dd3b95d895d0f1c68bbb989ea7a8
    https://github.com/scummvm/scummvm/commit/ee311d660fb7dd3b95d895d0f1c68bbb989ea7a8
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:25-07:00

Commit Message:
AGS: Make game audiochannels limit defined by a game version

>From upstream 65f0b843dfd85967135eca1bda46976aa1ddcf26

Changed paths:
    engines/ags/engine/ac/audio_clip.cpp
    engines/ags/engine/ac/game.cpp
    engines/ags/engine/ac/global_audio.cpp
    engines/ags/engine/ac/global_video.cpp
    engines/ags/engine/ac/room.cpp
    engines/ags/engine/ac/system.cpp
    engines/ags/engine/game/game_init.cpp
    engines/ags/engine/game/savegame.cpp
    engines/ags/engine/game/savegame_components.cpp
    engines/ags/engine/media/audio/audio.cpp
    engines/ags/shared/ac/game_setup_struct.h


diff --git a/engines/ags/engine/ac/audio_clip.cpp b/engines/ags/engine/ac/audio_clip.cpp
index 92d1401dea1..b7ef9b78a4a 100644
--- a/engines/ags/engine/ac/audio_clip.cpp
+++ b/engines/ags/engine/ac/audio_clip.cpp
@@ -48,7 +48,7 @@ int AudioClip_GetIsAvailable(ScriptAudioClip *clip) {
 
 void AudioClip_Stop(ScriptAudioClip *clip) {
 	AudioChannelsLock lock;
-	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
+	for (int i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if ((ch != nullptr) && (ch->_sourceClip == clip)) {
 			AudioChannel_Stop(&_G(scrAudioChannel)[i]);
@@ -72,9 +72,9 @@ ScriptAudioChannel *AudioClip_PlayQueued(ScriptAudioClip *clip, int priority, in
 }
 
 ScriptAudioChannel *AudioClip_PlayOnChannel(ScriptAudioClip *clip, int chan, int priority, int repeat) {
-	if (chan < NUM_SPEECH_CHANS || chan >= MAX_GAME_CHANNELS)
+	if (chan < NUM_SPEECH_CHANS || chan >= _GP(game).numGameChannels)
 		quitprintf("!AudioClip.PlayOnChannel: invalid channel %d, the range is %d - %d",
-			chan, NUM_SPEECH_CHANS, MAX_GAME_CHANNELS - 1);
+			chan, NUM_SPEECH_CHANS, _GP(game).numGameChannels - 1);
 	if (priority == SCR_NO_VALUE)
 		priority = clip->defaultPriority;
 	if (repeat == SCR_NO_VALUE)
diff --git a/engines/ags/engine/ac/game.cpp b/engines/ags/engine/ac/game.cpp
index 984e38176ce..37dd33d9be4 100644
--- a/engines/ags/engine/ac/game.cpp
+++ b/engines/ags/engine/ac/game.cpp
@@ -95,9 +95,8 @@ using namespace AGS::Engine;
 void Game_StopAudio(int audioType) {
 	if (((audioType < 0) || ((size_t)audioType >= _GP(game).audioClipTypes.size())) && (audioType != SCR_NO_VALUE))
 		quitprintf("!Game.StopAudio: invalid audio type %d", audioType);
-	int aa;
 
-	for (aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
+	for (int aa = 0; aa < _GP(game).numGameChannels; aa++) {
 		if (audioType == SCR_NO_VALUE) {
 			stop_or_fade_out_channel(aa);
 		} else {
@@ -117,7 +116,7 @@ int Game_IsAudioPlaying(int audioType) {
 	if (_GP(play).fast_forward)
 		return 0;
 
-	for (int aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
+	for (int aa = 0; aa < _GP(game).numGameChannels; aa++) {
 		ScriptAudioClip *clip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[aa]);
 		if (clip != nullptr) {
 			if ((clip->type == audioType) || (audioType == SCR_NO_VALUE)) {
@@ -147,7 +146,7 @@ void Game_SetAudioTypeVolume(int audioType, int volume, int changeType) {
 	if ((changeType == VOL_CHANGEEXISTING) ||
 	        (changeType == VOL_BOTH)) {
 		AudioChannelsLock lock;
-		for (int aa = 0; aa < MAX_GAME_CHANNELS; aa++) {
+		for (int aa = 0; aa < _GP(game).numGameChannels; aa++) {
 			ScriptAudioClip *clip = AudioChannel_GetPlayingClip(&_G(scrAudioChannel)[aa]);
 			if ((clip != nullptr) && (clip->type == audioType)) {
 				auto *ch = lock.GetChannel(aa);
diff --git a/engines/ags/engine/ac/global_audio.cpp b/engines/ags/engine/ac/global_audio.cpp
index 2c6763f96f1..d9fcbabb1f6 100644
--- a/engines/ags/engine/ac/global_audio.cpp
+++ b/engines/ags/engine/ac/global_audio.cpp
@@ -40,9 +40,9 @@ namespace AGS3 {
 using namespace AGS::Shared;
 
 void StopAmbientSound(int channel) {
-	if ((channel < NUM_SPEECH_CHANS) || (channel >= MAX_GAME_CHANNELS))
+	if ((channel < NUM_SPEECH_CHANS) || (channel >= _GP(game).numGameChannels))
 		quitprintf("!StopAmbientSound: invalid channel %d, supported %d - %d",
-			channel, NUM_SPEECH_CHANS, MAX_GAME_CHANNELS - 1);
+			channel, NUM_SPEECH_CHANS, _GP(game).numGameChannels - 1);
 
 	if (_GP(ambient)[channel].channel == 0)
 		return;
@@ -53,7 +53,7 @@ void StopAmbientSound(int channel) {
 
 void PlayAmbientSound(int channel, int sndnum, int vol, int x, int y) {
 	// the channel parameter is to allow multiple ambient sounds in future
-	if ((channel < 1) || (channel == SCHAN_SPEECH) || (channel >= MAX_GAME_CHANNELS))
+	if ((channel < 1) || (channel == SCHAN_SPEECH) || (channel >= _GP(game).numGameChannels))
 		quit("!PlayAmbientSound: invalid channel number");
 	if ((vol < 1) || (vol > 255))
 		quit("!PlayAmbientSound: volume must be 1 to 255");
@@ -96,7 +96,7 @@ int IsChannelPlaying(int chan) {
 	if (_GP(play).fast_forward)
 		return 0;
 
-	if ((chan < 0) || (chan >= MAX_GAME_CHANNELS))
+	if ((chan < 0) || (chan >= _GP(game).numGameChannels))
 		quit("!IsChannelPlaying: invalid sound channel");
 
 	if (channel_is_playing(chan))
@@ -111,7 +111,7 @@ int IsSoundPlaying() {
 
 	// find if there's a sound playing
 	AudioChannelsLock lock;
-	for (int i = SCHAN_NORMAL; i < MAX_GAME_CHANNELS; i++) {
+	for (int i = SCHAN_NORMAL; i < _GP(game).numGameChannels; i++) {
 		if (lock.GetChannelIfPlaying(i))
 			return 1;
 	}
@@ -129,8 +129,8 @@ int PlaySoundEx(int val1, int channel) {
 	if (aclip && !is_audiotype_allowed_to_play((AudioFileType)aclip->fileType))
 		return -1; // if sound is off, ignore it
 
-	if ((channel < SCHAN_NORMAL) || (channel >= MAX_GAME_CHANNELS))
-		quitprintf("!PlaySoundEx: invalid channel specified, must be %d-%d", SCHAN_NORMAL, MAX_GAME_CHANNELS - 1);
+	if ((channel < SCHAN_NORMAL) || (channel >= _GP(game).numGameChannels))
+		quitprintf("!PlaySoundEx: invalid channel specified, must be %d-%d", SCHAN_NORMAL, _GP(game).numGameChannels - 1);
 
 	// if an ambient sound is playing on this channel, abort it
 	StopAmbientSound(channel);
@@ -338,7 +338,7 @@ void SetSoundVolume(int newvol) {
 void SetChannelVolume(int chan, int newvol) {
 	if ((newvol < 0) || (newvol > 255))
 		quit("!SetChannelVolume: invalid volume - must be from 0-255");
-	if ((chan < 0) || (chan >= MAX_GAME_CHANNELS))
+	if ((chan < 0) || (chan >= _GP(game).numGameChannels))
 		quit("!SetChannelVolume: invalid channel id");
 
 	AudioChannelsLock lock;
diff --git a/engines/ags/engine/ac/global_video.cpp b/engines/ags/engine/ac/global_video.cpp
index 16a261caaae..a31d89ee39b 100644
--- a/engines/ags/engine/ac/global_video.cpp
+++ b/engines/ags/engine/ac/global_video.cpp
@@ -26,6 +26,7 @@
 #include "ags/engine/ac/global_game.h"
 #include "ags/engine/ac/global_video.h"
 #include "ags/engine/ac/path_helper.h"
+#include "ags/shared/ac/game_setup_struct.h"
 #include "ags/shared/core/asset_manager.h"
 #include "ags/engine/debugging/debugger.h"
 #include "ags/engine/debugging/debug_log.h"
@@ -62,8 +63,8 @@ void scrPlayVideo(const char *name, int skip, int flags) {
 
 void pause_sound_if_necessary_and_play_video(const char *name, int skip, int flags) {
 	int musplaying = _GP(play).cur_music_number, i;
-	int ambientWas[MAX_GAME_CHANNELS];
-	for (i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++)
+	int ambientWas[MAX_GAME_CHANNELS]{ 0 };
+	for (i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; i++)
 		ambientWas[i] = _GP(ambient)[i].channel;
 
 	if ((strlen(name) > 3) && (ags_stricmp(&name[strlen(name) - 3], "ogv") == 0)) {
@@ -85,7 +86,7 @@ void pause_sound_if_necessary_and_play_video(const char *name, int skip, int fla
 		// restart the music
 		if (musplaying >= 0)
 			newmusic(musplaying);
-		for (i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
+		for (i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; i++) {
 			if (ambientWas[i] > 0)
 				PlayAmbientSound(ambientWas[i], _GP(ambient)[i].num,
 					_GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
diff --git a/engines/ags/engine/ac/room.cpp b/engines/ags/engine/ac/room.cpp
index 41e808bcbb9..a29c520c9a0 100644
--- a/engines/ags/engine/ac/room.cpp
+++ b/engines/ags/engine/ac/room.cpp
@@ -240,7 +240,7 @@ void unload_old_room() {
 		_G(objs)[ff].moving = 0;
 
 	if (!_GP(play).ambient_sounds_persist) {
-		for (ff = NUM_SPEECH_CHANS; ff < MAX_GAME_CHANNELS; ff++)
+		for (ff = NUM_SPEECH_CHANS; ff < _GP(game).numGameChannels; ff++)
 			StopAmbientSound(ff);
 	}
 
diff --git a/engines/ags/engine/ac/system.cpp b/engines/ags/engine/ac/system.cpp
index 3626f5e726d..7c51648478f 100644
--- a/engines/ags/engine/ac/system.cpp
+++ b/engines/ags/engine/ac/system.cpp
@@ -162,12 +162,13 @@ void System_SetGamma(int newValue) {
 }
 
 int System_GetAudioChannelCount() {
-	return MAX_GAME_CHANNELS;
+	return _GP(game).numGameChannels;
 }
 
 ScriptAudioChannel *System_GetAudioChannels(int index) {
-	if ((index < 0) || (index >= MAX_GAME_CHANNELS))
-		quit("!System.AudioChannels: invalid sound channel index");
+	if ((index < 0) || (index >= _GP(game).numGameChannels))
+		quitprintf("!System.AudioChannels: invalid sound channel index %d, supported %d - %d",
+			0, _GP(game).numGameChannels);
 
 	return &_G(scrAudioChannel)[index];
 }
diff --git a/engines/ags/engine/game/game_init.cpp b/engines/ags/engine/game/game_init.cpp
index ab5993651b9..6cf7a960e6f 100644
--- a/engines/ags/engine/game/game_init.cpp
+++ b/engines/ags/engine/game/game_init.cpp
@@ -89,7 +89,7 @@ String GetGameInitErrorText(GameInitErrorType err) {
 
 // Initializes audio channels and clips and registers them in the script system
 void InitAndRegisterAudioObjects() {
-	for (int i = 0; i < MAX_GAME_CHANNELS; ++i) {
+	for (int i = 0; i < _GP(game).numGameChannels; ++i) {
 		_G(scrAudioChannel)[i].id = i;
 		ccRegisterManagedObject(&_G(scrAudioChannel)[i], &_GP(ccDynamicAudio));
 	}
@@ -378,6 +378,13 @@ HGameInitError InitGameState(const LoadedGameEntities &ents, GameDataVersion dat
 	_GP(play).charProps.resize(_GP(game).numcharacters);
 	_G(old_dialog_scripts) = ents.OldDialogScripts;
 	_G(old_speech_lines) = ents.OldSpeechLines;
+
+	// Set number of game channels corresponding to the loaded game version
+	if (_G(loaded_game_file_version) < kGameVersion_360)
+		_GP(game).numGameChannels = MAX_GAME_CHANNELS_v320;
+	else
+		_GP(game).numGameChannels = MAX_GAME_CHANNELS;
+
 	HError err = InitAndRegisterGameEntities();
 	if (!err)
 		return new GameInitError(kGameInitErr_EntityInitFail, err);
diff --git a/engines/ags/engine/game/savegame.cpp b/engines/ags/engine/game/savegame.cpp
index 5e3acd6b40a..76b5f3121d6 100644
--- a/engines/ags/engine/game/savegame.cpp
+++ b/engines/ags/engine/game/savegame.cpp
@@ -594,8 +594,7 @@ HSaveError DoAfterRestore(const PreservedParams &pp, const RestoredData &r_data)
 		}
 	} // -- AudioChannelsLock
 
-	// TODO: investigate loop range
-	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; ++i) {
+	for (int i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; ++i) {
 		if (r_data.DoAmbient[i])
 			PlayAmbientSound(i, r_data.DoAmbient[i], _GP(ambient)[i].vol, _GP(ambient)[i].x, _GP(ambient)[i].y);
 	}
diff --git a/engines/ags/engine/game/savegame_components.cpp b/engines/ags/engine/game/savegame_components.cpp
index a08749b29e7..94bf11bd1ef 100644
--- a/engines/ags/engine/game/savegame_components.cpp
+++ b/engines/ags/engine/game/savegame_components.cpp
@@ -337,7 +337,7 @@ HSaveError WriteAudio(Stream *out) {
 	// Game content assertion
 	out->WriteInt32(_GP(game).audioClipTypes.size());
 	out->WriteInt8(TOTAL_AUDIO_CHANNELS);
-	out->WriteInt8(MAX_GAME_CHANNELS);
+	out->WriteInt8(_GP(game).numGameChannels);
 	out->WriteInt16(0); // reserved 2 bytes (remains of int32)
 
 	// Audio types
@@ -375,7 +375,7 @@ HSaveError WriteAudio(Stream *out) {
 	out->WriteInt32(_G(current_music_type));
 
 	// Ambient sound
-	for (int i = 0; i < MAX_GAME_CHANNELS; ++i)
+	for (int i = 0; i < _GP(game).numGameChannels; ++i)
 		_GP(ambient)[i].WriteToFile(out);
 	return HSaveError::None();
 }
diff --git a/engines/ags/engine/media/audio/audio.cpp b/engines/ags/engine/media/audio/audio.cpp
index c1fc39d25a8..435cec56042 100644
--- a/engines/ags/engine/media/audio/audio.cpp
+++ b/engines/ags/engine/media/audio/audio.cpp
@@ -177,7 +177,7 @@ static int find_free_audio_channel(ScriptAudioClip *clip, int priority, bool int
 		priority--;
 
 	int startAtChannel = _G(reserved_channel_count);
-	int endBeforeChannel = MAX_GAME_CHANNELS;
+	int endBeforeChannel = _GP(game).numGameChannels;
 
 	if (_GP(game).audioClipTypes[clip->type].reservedChannels > 0) {
 		startAtChannel = 0;
@@ -471,7 +471,7 @@ void stop_and_destroy_channel_ex(int chid, bool resetLegacyMusicSettings) {
 	// don't update '_G(crossFading)' here as it is updated in all the cross-fading functions.
 
 	// destroyed an ambient sound channel
-	if (chid < MAX_GAME_CHANNELS) {
+	if (chid < _GP(game).numGameChannels) {
 		if (_GP(ambient)[chid].channel > 0)
 			_GP(ambient)[chid].channel = 0;
 	}
@@ -540,7 +540,7 @@ int get_volume_adjusted_for_distance(int volume, int sndX, int sndY, int sndMaxD
 void update_directional_sound_vol() {
 	AudioChannelsLock lock;
 
-	for (int chnum = NUM_SPEECH_CHANS; chnum < MAX_GAME_CHANNELS; chnum++) {
+	for (int chnum = NUM_SPEECH_CHANS; chnum < _GP(game).numGameChannels; chnum++) {
 		auto *ch = lock.GetChannelIfPlaying(chnum);
 		if ((ch != nullptr) && (ch->_xSource >= 0)) {
 			ch->apply_directional_modifier(
@@ -556,7 +556,7 @@ void update_directional_sound_vol() {
 void update_ambient_sound_vol() {
 	AudioChannelsLock lock;
 
-	for (int chan = NUM_SPEECH_CHANS; chan < MAX_GAME_CHANNELS; chan++) {
+	for (int chan = NUM_SPEECH_CHANS; chan < _GP(game).numGameChannels; chan++) {
 		AmbientSound *thisSound = &_GP(ambient)[chan];
 
 		if (thisSound->channel == 0)
@@ -635,7 +635,7 @@ static int play_sound_priority(int val1, int priority) {
 	AudioChannelsLock lock;
 
 	// find a free channel to play it on
-	for (int i = SCHAN_NORMAL; i < MAX_GAME_CHANNELS; i++) {
+	for (int i = SCHAN_NORMAL; i < _GP(game).numGameChannels; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if (val1 < 0) {
 			// Playing sound -1 means iterate through and stop all sound
@@ -775,7 +775,7 @@ int calculate_max_volume() {
 void apply_volume_drop_modifier(bool applyModifier) {
 	AudioChannelsLock lock;
 
-	for (int i = NUM_SPEECH_CHANS; i < MAX_GAME_CHANNELS; i++) {
+	for (int i = NUM_SPEECH_CHANS; i < _GP(game).numGameChannels; i++) {
 		auto *ch = lock.GetChannelIfPlaying(i);
 		if (ch && ch->_sourceClip != nullptr) {
 			if (applyModifier)
diff --git a/engines/ags/shared/ac/game_setup_struct.h b/engines/ags/shared/ac/game_setup_struct.h
index ecea16eba6a..3f1a259f790 100644
--- a/engines/ags/shared/ac/game_setup_struct.h
+++ b/engines/ags/shared/ac/game_setup_struct.h
@@ -88,6 +88,8 @@ struct GameSetupStruct : public GameSetupStructBase {
 	// A clip to play when player gains score in game
 	// TODO: find out why OPT_SCORESOUND option cannot be used to store this in >=3.2 games
 	int               scoreClipID;
+	// number of allowed game audio channels (the ones under direct user control)
+	int               numGameChannels = 0;
 
 	// TODO: I converted original array of sprite infos to vector here, because
 	// statistically in most games sprites go in long continious sequences with minimal


Commit: bb1d0849fa2d92e67e011f644290d66c89b072f9
    https://github.com/scummvm/scummvm/commit/bb1d0849fa2d92e67e011f644290d66c89b072f9
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:26-07:00

Commit Message:
AGS: Read and write RoomObject's fields explicitly

>From upstream bb9ae639f9e28b59af4bbc9b613cd9455ea0db3e

Changed paths:
    engines/ags/engine/ac/room_object.cpp


diff --git a/engines/ags/engine/ac/room_object.cpp b/engines/ags/engine/ac/room_object.cpp
index 2eeee9d4691..4153950ec4a 100644
--- a/engines/ags/engine/ac/room_object.cpp
+++ b/engines/ags/engine/ac/room_object.cpp
@@ -157,27 +157,54 @@ void RoomObject::ReadFromFile(Stream *in) {
 	x = in->ReadInt32();
 	y = in->ReadInt32();
 	transparent = in->ReadInt32();
-
-	in->ReadArrayOfInt16(&tint_r, 15);
-	cycling = in->ReadByte();
-	overall_speed = in->ReadByte();
-	on = in->ReadByte();
-	flags = in->ReadByte();
-	in->ReadArrayOfInt16(&blocking_width, 2);
+	tint_r = in->ReadInt16();
+	tint_g = in->ReadInt16();
+	tint_b = in->ReadInt16();
+	tint_level = in->ReadInt16();
+	tint_light = in->ReadInt16();
+	zoom = in->ReadInt16();
+	last_width = in->ReadInt16();
+	last_height = in->ReadInt16();
+	num = in->ReadInt16();
+	baseline = in->ReadInt16();
+	view = in->ReadInt16();
+	loop = in->ReadInt16();
+	frame = in->ReadInt16();
+	wait = in->ReadInt16();
+	moving = in->ReadInt16();
+	cycling = in->ReadInt8();
+	overall_speed = in->ReadInt8();
+	on = in->ReadInt8();
+	flags = in->ReadInt8();
+	blocking_width = in->ReadInt16();
+	blocking_height = in->ReadInt16();
 }
 
 void RoomObject::WriteToFile(Stream *out) const {
 	out->WriteInt32(x);
 	out->WriteInt32(y);
 	out->WriteInt32(transparent);
-
-	// TODO: Split up array write to properly write fields separately
-	out->WriteArrayOfInt16(&tint_r, 15);
-	out->WriteByte(cycling);
-	out->WriteByte(overall_speed);
-	out->WriteByte(on);
-	out->WriteByte(flags);
-	out->WriteArrayOfInt16(&blocking_width, 2);
+	out->WriteInt16(tint_r);
+	out->WriteInt16(tint_g);
+	out->WriteInt16(tint_b);
+	out->WriteInt16(tint_level);
+	out->WriteInt16(tint_light);
+	out->WriteInt16(zoom);
+	out->WriteInt16(last_width);
+	out->WriteInt16(last_height);
+	out->WriteInt16(num);
+	out->WriteInt16(baseline);
+	out->WriteInt16(view);
+	out->WriteInt16(loop);
+	out->WriteInt16(frame);
+	out->WriteInt16(wait);
+	out->WriteInt16(moving);
+	out->WriteInt8(cycling);
+	out->WriteInt8(overall_speed);
+	out->WriteInt8(on);
+	out->WriteInt8(flags);
+	out->WriteInt16(blocking_width);
+	out->WriteInt16(blocking_height);
 }
 
 } // namespace AGS3


Commit: 78b9068a91febb7980bf08dd157b9bce28a15188
    https://github.com/scummvm/scummvm/commit/78b9068a91febb7980bf08dd157b9bce28a15188
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:26-07:00

Commit Message:
AGS: Replace "localuserconf" option with "user-conf-dir"

>From upstream 9edb428aa028164081cd9a4f9050cf5fa0767964

Changed paths:
    engines/ags/engine/ac/file.cpp
    engines/ags/engine/ac/game_setup.cpp
    engines/ags/engine/ac/game_setup.h
    engines/ags/engine/main/engine.cpp
    engines/ags/engine/main/main.cpp


diff --git a/engines/ags/engine/ac/file.cpp b/engines/ags/engine/ac/file.cpp
index bbd0d0c1f3c..03694cf23d1 100644
--- a/engines/ags/engine/ac/file.cpp
+++ b/engines/ags/engine/ac/file.cpp
@@ -242,10 +242,10 @@ FSLocation GetGlobalUserConfigDir() {
 
 FSLocation GetGameUserConfigDir() {
 	String dir = _G(platform)->GetUserConfigDirectory();
-	if (Path::IsRelativePath(dir)) // relative dir is resolved relative to the game data dir
+	if (!_GP(usetup).user_conf_dir.IsEmpty()) // directive to use custom userconf location
+		return FSLocation(_GP(usetup).user_conf_dir);
+	else if (Path::IsRelativePath(dir)) // relative dir is resolved relative to the game data dir
 		return FSLocation(_GP(ResPaths).DataDir, dir);
-	else if (_GP(usetup).local_user_conf) // directive to use game dir location
-		return FSLocation(_GP(ResPaths).DataDir);
 	// For absolute dir, we assume it's a special directory prepared for AGS engine
 	// and therefore amend it with a game own subdir
 	return FSLocation(dir, _GP(game).saveGameFolderName);
diff --git a/engines/ags/engine/ac/game_setup.cpp b/engines/ags/engine/ac/game_setup.cpp
index 4f86308e661..15106172c69 100644
--- a/engines/ags/engine/ac/game_setup.cpp
+++ b/engines/ags/engine/ac/game_setup.cpp
@@ -24,7 +24,6 @@
 namespace AGS3 {
 
 GameSetup::GameSetup() {
-	local_user_conf = false;
 	audio_backend = 1;
 	no_speech_pack = false;
 	textheight = 0;
diff --git a/engines/ags/engine/ac/game_setup.h b/engines/ags/engine/ac/game_setup.h
index c3807d17799..16933b6efc9 100644
--- a/engines/ags/engine/ac/game_setup.h
+++ b/engines/ags/engine/ac/game_setup.h
@@ -67,8 +67,8 @@ struct GameSetup {
 	String opt_audio_dir; // optional custom install audio dir path
 	String opt_voice_dir; // optional custom install voice-over dir path
 	//
-	String conf_path; // explicitly set path to config
-	bool   local_user_conf; // search for user config in the game directory
+	String conf_path; // a read-only config path (if set the regular config is ignored)
+	String user_conf_dir; // directory to read and write user config in
 	String user_data_dir; // directory to write savedgames and user files to
 	String shared_data_dir; // directory to write shared game files to
 	String translation;
diff --git a/engines/ags/engine/main/engine.cpp b/engines/ags/engine/main/engine.cpp
index 6b83a18a009..ad66cee686e 100644
--- a/engines/ags/engine/main/engine.cpp
+++ b/engines/ags/engine/main/engine.cpp
@@ -429,6 +429,8 @@ int engine_check_register_game() {
 
 // Setup paths and directories that may be affected by user configuration
 void engine_init_user_directories() {
+	if (!_GP(usetup).user_conf_dir.IsEmpty())
+		Debug::Printf(kDbgMsg_Info, "User config directory: %s", _GP(usetup).user_conf_dir.GetCStr());
 	if (!_GP(usetup).user_data_dir.IsEmpty())
 		Debug::Printf(kDbgMsg_Info, "User data directory: %s", _GP(usetup).user_data_dir.GetCStr());
 	if (!_GP(usetup).shared_data_dir.IsEmpty())
@@ -964,14 +966,23 @@ void engine_read_config(ConfigTree &cfg) {
 	if (Path::ComparePaths(user_global_cfg_file, def_cfg_file) != 0)
 		IniUtil::Read(user_global_cfg_file, cfg);
 
-	// Handle directive to search for the user config inside the game directory;
-	// this option may come either from command line or default/global config.
-	_GP(usetup).local_user_conf |= INIreadint(cfg, "misc", "localuserconf", 0) != 0;
-	if (_GP(usetup).local_user_conf) {
-		// Test if the file is writeable, if it is then both engine and setup
-		// applications may actually use it fully as a user config, otherwise
-		// fallback to default behavior.
-		_GP(usetup).local_user_conf = File::TestWriteFile(def_cfg_file);
+	// Handle directive to search for the user config inside the custom directory;
+		// this option may come either from command line or default/global config.
+	if (_GP(usetup).user_conf_dir.IsEmpty())
+		_GP(usetup).user_conf_dir = INIreadstring(cfg, "misc", "user_conf_dir");
+	if (_GP(usetup).user_conf_dir.IsEmpty()) // also try deprecated option
+		_GP(usetup).user_conf_dir = INIreadint(cfg, "misc", "localuserconf") != 0 ? "." : "";
+	// Test if the file is writeable, if it is then both engine and setup
+	// applications may actually use it fully as a user config, otherwise
+	// fallback to default behavior.
+	if (!_GP(usetup).user_conf_dir.IsEmpty()) {
+		if (Path::IsRelativePath(_GP(usetup).user_conf_dir))
+			_GP(usetup).user_conf_dir = Path::ConcatPaths(_GP(usetup).startup_dir, _GP(usetup).user_conf_dir);
+		if (!File::TestWriteFile(Path::ConcatPaths(_GP(usetup).user_conf_dir, DefaultConfigFileName))) {
+			Debug::Printf(kDbgMsg_Warn, "Write test failed at user config dir '%s', using default path.",
+				_GP(usetup).user_conf_dir.GetCStr());
+			_GP(usetup).user_conf_dir = "";
+		}
 	}
 
 	// Read user configuration file
diff --git a/engines/ags/engine/main/main.cpp b/engines/ags/engine/main/main.cpp
index c96f0097982..a450db05d89 100644
--- a/engines/ags/engine/main/main.cpp
+++ b/engines/ags/engine/main/main.cpp
@@ -223,7 +223,9 @@ int main_process_cmdline(ConfigTree &cfg, int argc, const char *argv[]) {
 		} else if (ags_stricmp(arg, "--conf") == 0 && (argc > ee + 1)) {
 			_GP(usetup).conf_path = argv[++ee];
 		} else if (ags_stricmp(arg, "--localuserconf") == 0) {
-			_GP(usetup).local_user_conf = true;
+			_GP(usetup).user_conf_dir = ".";
+		} else if ((ags_stricmp(arg, "--user-conf-dir") == 0) && (argc > ee + 1)) {
+			_GP(usetup).user_conf_dir = argv[++ee];
 		} else if (ags_stricmp(arg, "--runfromide") == 0 && (argc > ee + 4)) {
 			_GP(usetup).install_dir = argv[ee + 1];
 			_GP(usetup).opt_data_dir = argv[ee + 2];


Commit: 27dc8075fb287df0ff659eea2be1a71cef060d25
    https://github.com/scummvm/scummvm/commit/27dc8075fb287df0ff659eea2be1a71cef060d25
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2022-03-17T21:49:27-07:00

Commit Message:
AGS: Configurable user paths support $GAMENAME$ token

>From upstream c61191084fbdc8d60d147dc4238dc43797716e2c

Changed paths:
    engines/ags/engine/main/config.cpp
    engines/ags/engine/main/engine.cpp


diff --git a/engines/ags/engine/main/config.cpp b/engines/ags/engine/main/config.cpp
index c8638e4bab3..ec861719762 100644
--- a/engines/ags/engine/main/config.cpp
+++ b/engines/ags/engine/main/config.cpp
@@ -418,9 +418,6 @@ void post_config() {
 		_GP(usetup).Screen.FsGameFrame = GameFrameSetup(kFrame_MaxProportional);
 	if (!_GP(usetup).Screen.WinGameFrame.IsValid())
 		_GP(usetup).Screen.WinGameFrame = GameFrameSetup(kFrame_MaxRound);
-
-	_GP(usetup).user_data_dir = Path::MakePathNoSlash(_GP(usetup).user_data_dir);
-	_GP(usetup).shared_data_dir = Path::MakePathNoSlash(_GP(usetup).shared_data_dir);
 }
 
 void save_config_file() {
diff --git a/engines/ags/engine/main/engine.cpp b/engines/ags/engine/main/engine.cpp
index ad66cee686e..7b8005e592d 100644
--- a/engines/ags/engine/main/engine.cpp
+++ b/engines/ags/engine/main/engine.cpp
@@ -427,8 +427,16 @@ int engine_check_register_game() {
 	return 0;
 }
 
+// Replace special tokens inside a user path option
+static void resolve_configured_path(String &option) {
+	option.Replace("$GAMENAME$", _GP(game).gamename);
+}
+
 // Setup paths and directories that may be affected by user configuration
 void engine_init_user_directories() {
+	resolve_configured_path(_GP(usetup).user_data_dir);
+	resolve_configured_path(_GP(usetup).shared_data_dir);
+
 	if (!_GP(usetup).user_conf_dir.IsEmpty())
 		Debug::Printf(kDbgMsg_Info, "User config directory: %s", _GP(usetup).user_conf_dir.GetCStr());
 	if (!_GP(usetup).user_data_dir.IsEmpty())
@@ -976,9 +984,11 @@ void engine_read_config(ConfigTree &cfg) {
 	// applications may actually use it fully as a user config, otherwise
 	// fallback to default behavior.
 	if (!_GP(usetup).user_conf_dir.IsEmpty()) {
+		resolve_configured_path(_GP(usetup).user_conf_dir);
 		if (Path::IsRelativePath(_GP(usetup).user_conf_dir))
 			_GP(usetup).user_conf_dir = Path::ConcatPaths(_GP(usetup).startup_dir, _GP(usetup).user_conf_dir);
-		if (!File::TestWriteFile(Path::ConcatPaths(_GP(usetup).user_conf_dir, DefaultConfigFileName))) {
+		if (!Directory::CreateDirectory(_GP(usetup).user_conf_dir) ||
+			!File::TestWriteFile(Path::ConcatPaths(_GP(usetup).user_conf_dir, DefaultConfigFileName))) {
 			Debug::Printf(kDbgMsg_Warn, "Write test failed at user config dir '%s', using default path.",
 				_GP(usetup).user_conf_dir.GetCStr());
 			_GP(usetup).user_conf_dir = "";




More information about the Scummvm-git-logs mailing list