improve: ship every default package as an mpackage (#9626)
#### Brief overview of PR changes/additions
- Every package Mudlet preinstalls now lives in `src/packages/<name>/`,
holding its `config.lua`, `.xml` and the `.mpackage` built from them -
previously they were scattered across `src/` and `src/mudlet-lua/lua/`,
and four had no metadata at all.
- The game loaders (Carrion Fields, Icesus, MorgenGrauen, Medievia) and
the two `mudlet.org` dev packages shipped as bare xml, so the Package
Manager showed them with no version, author or description. They are
packaged now, keeping their existing package names so nothing renames on
upgrade.
- New `DefaultPackagesTest` walks the preinstall table for seven games,
checks every queued path is really compiled in, and installs all 15
archives.
#### Motivation for adding to Mudlet
An mpackage carries Mudlet's metadata and a bare xml cannot. Keeping
each package's sources next to its archive also makes the Lua reviewable
in diffs, which a committed zip on its own is not.
#### Other info
Stacked on #9624, whose check now covers all 15 packages. The IRE mapper
stays an xml because upstream publishes it that way and
`update-3rdparty.yml` overwrites it weekly. Package repository PR
Mudlet/mudlet-package-repository#746 updates the sync paths and must
merge right after this.
#### Test case
`ctest` 66/67 locally (`TKeySequenceEditTest` is the known headless
flake - passes under openbox); `DefaultPackagesTest` 28/28; `python3
CI/check-mpackage-sync.py --base-ref origin/development` clean.
Assisted-by: Claude:claude-opus-5
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-04 10:18:48 +02:00
/***************************************************************************
* Copyright ( C ) 2026 by Mudlet Developers *
* *
* 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 2 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 , write to the *
* Free Software Foundation , Inc . , *
* 59 Temple Place - Suite 330 , Boston , MA 02111 - 1307 , USA . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*
* Covers the packages Mudlet preinstalls into new profiles , which live in
* src / packages and are compiled in through mudlet . qrc .
*
* Nothing else notices when one of these breaks : a path in
* setupPreInstallPackages ( ) that no longer names a compiled - in resource , or an
* archive rebuilt without config . lua , just means the profile quietly comes up
* without the package . The build stays green either way , so this test walks
* both the preinstall table and every archive in the resource tree .
*
* Run with : ctest - R DefaultPackagesTest - V
*/
# include <QtTest/QtTest>
# include <QTemporaryDir>
# include <QTemporaryFile>
# include "Host.h"
# include "HostManager.h"
# include "MudletInstanceCoordinator.h"
# include "mudlet.h"
extern void qInitResources_mudlet ( ) ;
extern void qInitResources_qm ( ) ;
extern void qInitResources_additional_splash_screens ( ) ;
extern void qInitResources_mudlet_fonts_common ( ) ;
extern void qInitResources_mudlet_fonts_posix ( ) ;
void initializeQRCResourcesForDefaultPackagesTest ( ) ;
class DefaultPackagesTest : public QObject
{
Q_OBJECT
private :
// A package installs under the name its config.lua declares, which is
// normally the directory it lives in. These two are deliberately not: the
// tutorial uses a display name, and the Carrion Fields loader has to match
// the name its own script passes to uninstallPackage() when it is done.
inline static const QHash < QString , QString > scmInstallsAs = { { qsl ( " mudlet-tutorial " ) , qsl ( " Mudlet Tutorial " ) } , { qsl ( " CF-loader " ) , qsl ( " CF_Loader " ) } } ;
const QString mProfileName = qsl ( " DefaultPackages-Test " ) ;
QTemporaryDir mConfigDir ;
QByteArray mSavedXdg ;
Host * mpHost = nullptr ;
QStringList preinstallsFor ( const QString & gameUrl , const QString & profileName = qsl ( " test " ) )
{
mudlet : : self ( ) - > mPackagesToInstallList . clear ( ) ;
mudlet : : self ( ) - > setupPreInstallPackages ( gameUrl , profileName ) ;
return mudlet : : self ( ) - > mPackagesToInstallList ;
}
private slots :
void initTestCase ( )
{
initializeQRCResourcesForDefaultPackagesTest ( ) ;
// Keep the test hermetic: point the config dir resolution at a
// temporary directory instead of the user's real profiles.
QVERIFY ( mConfigDir . isValid ( ) ) ;
mSavedXdg = qgetenv ( " XDG_CONFIG_HOME " ) ;
QVERIFY ( QDir ( ) . mkpath ( qsl ( " %1/mudlet/profiles " ) . arg ( mConfigDir . path ( ) ) ) ) ;
qputenv ( " XDG_CONFIG_HOME " , mConfigDir . path ( ) . toUtf8 ( ) ) ;
mudlet : : start ( ) ;
mudlet : : self ( ) - > setupConfig ( ) ;
fix: brand-new installs get the new-player experience again (#9745)
#### Brief overview of PR changes/additions
- Stop importing settings from the pre-4.19 NativeFormat store (macOS
plists / Windows registry) into a freshly created Mudlet.ini
- On those platforms the old store ignores HOME/XDG overrides, so any
machine that ever ran Mudlet <= 4.18 made every fresh install inherit
stale keys and classify as an experienced player - skipping the starter
UI, tour and hints, and failing two functional tests
- Add tripwire asserts so any future write into a fresh Mudlet.ini
before init() fails with a self-explaining message
#### Motivation for adding to Mudlet
A fresh install must be recognised as a new player; anyone who ran 4.19+
was already migrated, and a direct <= 4.18 upgrade only loses UI
preferences (window geometry, appearance, storePasswordsSecurely,
deletedDefaultMuds), never profiles.
#### Other info (issues closed, discussion etc)
Fixes #9741
**Test case:** ExperiencedPlayerGateTest (18/18) and DefaultPackagesTest
(28/28) pass; on a Mac that ran Mudlet <= 4.18 (check with `defaults
read com.mudlet.Mudlet pos`), both suites now pass and a fresh profile
gets the starter UI.
Assisted-by: Claude:claude-opus-5
2026-08-12 11:14:58 +02:00
QCOMPARE ( mudlet : : getMudletPath ( enums : : mainPath ) , qsl ( " %1/mudlet " ) . arg ( mConfigDir . path ( ) ) ) ;
QVERIFY2 ( mudlet : : getQSettings ( ) - > allKeys ( ) . isEmpty ( ) , " a fresh config dir must start out with an empty Mudlet.ini - something wrote settings before init() " ) ;
improve: ship every default package as an mpackage (#9626)
#### Brief overview of PR changes/additions
- Every package Mudlet preinstalls now lives in `src/packages/<name>/`,
holding its `config.lua`, `.xml` and the `.mpackage` built from them -
previously they were scattered across `src/` and `src/mudlet-lua/lua/`,
and four had no metadata at all.
- The game loaders (Carrion Fields, Icesus, MorgenGrauen, Medievia) and
the two `mudlet.org` dev packages shipped as bare xml, so the Package
Manager showed them with no version, author or description. They are
packaged now, keeping their existing package names so nothing renames on
upgrade.
- New `DefaultPackagesTest` walks the preinstall table for seven games,
checks every queued path is really compiled in, and installs all 15
archives.
#### Motivation for adding to Mudlet
An mpackage carries Mudlet's metadata and a bare xml cannot. Keeping
each package's sources next to its archive also makes the Lua reviewable
in diffs, which a committed zip on its own is not.
#### Other info
Stacked on #9624, whose check now covers all 15 packages. The IRE mapper
stays an xml because upstream publishes it that way and
`update-3rdparty.yml` overwrites it weekly. Package repository PR
Mudlet/mudlet-package-repository#746 updates the sync paths and must
merge right after this.
#### Test case
`ctest` 66/67 locally (`TKeySequenceEditTest` is the known headless
flake - passes under openbox); `DefaultPackagesTest` 28/28; `python3
CI/check-mpackage-sync.py --base-ref origin/development` clean.
Assisted-by: Claude:claude-opus-5
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-04 10:18:48 +02:00
mudlet : : self ( ) - > takeOwnershipOfInstanceCoordinator ( std : : make_unique < MudletInstanceCoordinator > ( " MudletInstanceCoordinator " ) ) ;
mudlet : : self ( ) - > init ( ) ;
mudlet : : self ( ) - > setStorePasswordsSecurely ( false ) ;
QVERIFY2 ( mudlet : : self ( ) - > getHostManager ( ) . addHost ( mProfileName , QString ( ) , QString ( ) , QString ( ) ) , " failed to create the Host " ) ;
mpHost = mudlet : : self ( ) - > getHostManager ( ) . getHost ( mProfileName ) ;
QVERIFY ( mpHost ) ;
}
void cleanupTestCase ( )
{
mpHost = nullptr ;
delete mudlet : : self ( ) ;
mSavedXdg . isNull ( ) ? qunsetenv ( " XDG_CONFIG_HOME " ) : qputenv ( " XDG_CONFIG_HOME " , mSavedXdg ) ;
}
// Every path the preinstall table hands out has to name something that was
// actually compiled into the binary, or the profile silently misses it.
void test_preinstalledPathsResolve_data ( )
{
QTest : : addColumn < QString > ( " gameUrl " ) ;
QTest : : newRow ( " any game " ) < < qsl ( " example.com " ) ;
QTest : : newRow ( " carrion fields " ) < < qsl ( " carrionfields.net " ) ;
QTest : : newRow ( " icesus " ) < < qsl ( " icesus.org " ) ;
QTest : : newRow ( " morgengrauen " ) < < qsl ( " mg.mud.de " ) ;
QTest : : newRow ( " medievia " ) < < qsl ( " medievia.com " ) ;
QTest : : newRow ( " an IRE game " ) < < qsl ( " achaea.com " ) ;
QTest : : newRow ( " mudlet's own " ) < < qsl ( " mudlet.org " ) ;
}
void test_preinstalledPathsResolve ( )
{
QFETCH ( QString , gameUrl ) ;
const QStringList paths = preinstallsFor ( gameUrl ) ;
QVERIFY2 ( ! paths . isEmpty ( ) , qPrintable ( qsl ( " no packages queued for %1 " ) . arg ( gameUrl ) ) ) ;
for ( const QString & path : paths ) {
QVERIFY2 ( path . startsWith ( qsl ( " :/ " ) ) , qPrintable ( qsl ( " %1 is not a resource path " ) . arg ( path ) ) ) ;
QVERIFY2 ( QFile : : exists ( path ) , qPrintable ( qsl ( " %1 is queued for %2 but is not compiled in " ) . arg ( path , gameUrl ) ) ) ;
}
}
void test_tutorialProfileGetsTheTutorial ( )
{
QVERIFY ( preinstallsFor ( qsl ( " localhost " ) , qsl ( " Mudlet Tutorial " ) ) . contains ( qsl ( " :/packages/mudlet-tutorial/mudlet-tutorial.mpackage " ) ) ) ;
QVERIFY ( ! preinstallsFor ( qsl ( " localhost " ) , qsl ( " some other profile " ) ) . contains ( qsl ( " :/packages/mudlet-tutorial/mudlet-tutorial.mpackage " ) ) ) ;
}
// Games that install an interface of their own get a loader instead of the
// starter UI, which would otherwise fight it for the same screen space.
2026-08-06 14:51:37 +02:00
// This config dir has no profiles, so the player counts as new to Mudlet.
improve: ship every default package as an mpackage (#9626)
#### Brief overview of PR changes/additions
- Every package Mudlet preinstalls now lives in `src/packages/<name>/`,
holding its `config.lua`, `.xml` and the `.mpackage` built from them -
previously they were scattered across `src/` and `src/mudlet-lua/lua/`,
and four had no metadata at all.
- The game loaders (Carrion Fields, Icesus, MorgenGrauen, Medievia) and
the two `mudlet.org` dev packages shipped as bare xml, so the Package
Manager showed them with no version, author or description. They are
packaged now, keeping their existing package names so nothing renames on
upgrade.
- New `DefaultPackagesTest` walks the preinstall table for seven games,
checks every queued path is really compiled in, and installs all 15
archives.
#### Motivation for adding to Mudlet
An mpackage carries Mudlet's metadata and a bare xml cannot. Keeping
each package's sources next to its archive also makes the Lua reviewable
in diffs, which a committed zip on its own is not.
#### Other info
Stacked on #9624, whose check now covers all 15 packages. The IRE mapper
stays an xml because upstream publishes it that way and
`update-3rdparty.yml` overwrites it weekly. Package repository PR
Mudlet/mudlet-package-repository#746 updates the sync paths and must
merge right after this.
#### Test case
`ctest` 66/67 locally (`TKeySequenceEditTest` is the known headless
flake - passes under openbox); `DefaultPackagesTest` 28/28; `python3
CI/check-mpackage-sync.py --base-ref origin/development` clean.
Assisted-by: Claude:claude-opus-5
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-04 10:18:48 +02:00
void test_gamesWithTheirOwnUiSkipTheStarterUi ( )
{
fix: stop treating long-time Mudlet users as brand new players (#9695)
#### Brief overview of PR changes/additions
- `experiencedMudletPlayer()` decided veteran status from profile
**directory** mtimes. The per-profile data writes (url, port, password,
`profile.ini`, command history) all land straight in that directory and
bump its mtime every session, so only a profile *abandoned* for six
months ever looked old - the more you use Mudlet, the more certainly it
called you new. All 13 profiles on the maintainer's machine classified
as "brand new player".
- Mudlet renewed those timestamps itself: the connection dialog rewrites
`url`/`port`/`description` for the selected profile at startup, so
merely launching Mudlet reset the value the gate read.
- Replaced with a `firstLaunchDate` key recorded in QSettings on a
genuinely fresh install (written in `init()`, before anything can create
a profile or save a setting). An installation with any trace of earlier
use - a profile, or any other setting already on file - has no
recoverable start date and is treated as experienced; timestamps cannot
recover one, since a copied or restored profile keeps its modification
times only if the tool used happened to preserve them, and loses its
birth time regardless.
#### Motivation for adding to Mudlet
In 4.22.0 this gate only suppressed three one-line hints, but 5.0 hung
the full-window "Welcome to Mudlet! New here?" tour (#9385, 69d7d4169
"Add: UI tour to complement the Mudlet tutorial") and the starter UI
package (#9454, 69cd06b1c "add: starter interface with health bars, map
and chat for new players") off it, so essentially every active 4.22.0
user upgrading to 5.0 would get a beginner tour dropped on top of their
session.
#### Other info (issues closed, discussion etc)
The heuristic dates back to ae0564e6e "Improve: revise splitscreen
tutorial (try 2)" (#7341); the two 5.0 consumers above are what turned
it into a release blocker. New `ExperiencedPlayerGateTest` (18 cases)
covers fresh install, upgrader with freshly-written profiles, settings
restored without profiles, the six-month boundary, a profile restored
from backup, future-dated, unparseable and unwritable records, and two
live-singleton cases that pin the `init()` call site and the memoised
read. Both directions were mutation-tested: dropping the `init()` call
and restoring the old mtime heuristic each fail the suite. When in doubt
the gate errs towards *experienced* - a veteran shown a new-user tour is
a much worse outcome than a newcomer who misses it.
**Test case:** Seed a HOME that looks like an existing user (one
profile, `Mudlet.ini` without `uiTourShown`), launch Mudlet and connect
- before, the 6-step "Welcome to Mudlet!" tour appears; after, it does
not. A HOME whose `Mudlet.ini` has a recent `firstLaunchDate` still gets
the tour, and a genuinely empty HOME records one.
Assisted-by: Claude:claude-opus-5
2026-08-06 12:09:52 +02:00
QVERIFY ( preinstallsFor ( qsl ( " example.com " ) ) . contains ( qsl ( " :/packages/mudlet-base-ui/mudlet-base-ui.mpackage " ) ) ) ;
improve: ship every default package as an mpackage (#9626)
#### Brief overview of PR changes/additions
- Every package Mudlet preinstalls now lives in `src/packages/<name>/`,
holding its `config.lua`, `.xml` and the `.mpackage` built from them -
previously they were scattered across `src/` and `src/mudlet-lua/lua/`,
and four had no metadata at all.
- The game loaders (Carrion Fields, Icesus, MorgenGrauen, Medievia) and
the two `mudlet.org` dev packages shipped as bare xml, so the Package
Manager showed them with no version, author or description. They are
packaged now, keeping their existing package names so nothing renames on
upgrade.
- New `DefaultPackagesTest` walks the preinstall table for seven games,
checks every queued path is really compiled in, and installs all 15
archives.
#### Motivation for adding to Mudlet
An mpackage carries Mudlet's metadata and a bare xml cannot. Keeping
each package's sources next to its archive also makes the Lua reviewable
in diffs, which a committed zip on its own is not.
#### Other info
Stacked on #9624, whose check now covers all 15 packages. The IRE mapper
stays an xml because upstream publishes it that way and
`update-3rdparty.yml` overwrites it weekly. Package repository PR
Mudlet/mudlet-package-repository#746 updates the sync paths and must
merge right after this.
#### Test case
`ctest` 66/67 locally (`TKeySequenceEditTest` is the known headless
flake - passes under openbox); `DefaultPackagesTest` 28/28; `python3
CI/check-mpackage-sync.py --base-ref origin/development` clean.
Assisted-by: Claude:claude-opus-5
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-08-04 10:18:48 +02:00
QVERIFY ( ! preinstallsFor ( qsl ( " mg.mud.de " ) ) . contains ( qsl ( " :/packages/mudlet-base-ui/mudlet-base-ui.mpackage " ) ) ) ;
QVERIFY ( preinstallsFor ( qsl ( " mg.mud.de " ) ) . contains ( qsl ( " :/packages/mg-loader/mg-loader.mpackage " ) ) ) ;
}
// The generic mapper is for games that have no mapper script of their own.
void test_ireGamesGetTheirOwnMapper ( )
{
QVERIFY ( preinstallsFor ( qsl ( " achaea.com " ) ) . contains ( qsl ( " :/mudlet-mapper.xml " ) ) ) ;
QVERIFY ( ! preinstallsFor ( qsl ( " achaea.com " ) ) . contains ( qsl ( " :/packages/generic_mapper/generic_mapper.mpackage " ) ) ) ;
QVERIFY ( preinstallsFor ( qsl ( " example.com " ) ) . contains ( qsl ( " :/packages/generic_mapper/generic_mapper.mpackage " ) ) ) ;
}
// Installing is what the preinstall table ultimately does, and a package
// that unpacks but does not import leaves the profile just as empty.
void test_packagesInstall_data ( )
{
QTest : : addColumn < QString > ( " package " ) ;
for ( const QString & package : QDir ( qsl ( " :/packages " ) ) . entryList ( QDir : : Dirs | QDir : : NoDotAndDotDot ) ) {
QTest : : newRow ( qPrintable ( package ) ) < < package ;
}
}
void test_packagesInstall ( )
{
QFETCH ( QString , package ) ;
mpHost - > mBlockScriptCompile = false ;
auto [ installed , message ] = mpHost - > installPackage ( qsl ( " :/packages/%1/%1.mpackage " ) . arg ( package ) , enums : : PackageModuleType : : Package , true ) ;
QVERIFY2 ( installed , qPrintable ( qsl ( " %1 failed to install: %2 " ) . arg ( package , message ) ) ) ;
const QString installedAs = scmInstallsAs . value ( package , package ) ;
QVERIFY2 ( mpHost - > mInstalledPackages . contains ( installedAs ) , qPrintable ( qsl ( " %1 installed but is not registered as %2 " ) . arg ( package , installedAs ) ) ) ;
}
// Each package directory carries the archive Mudlet installs. Unpack every
// one the way Host::installPackage() does and check it is shaped the way
// the installer requires: metadata in config.lua, exactly one xml.
void test_everyArchiveIsWellFormed ( )
{
const QStringList packages = QDir ( qsl ( " :/packages " ) ) . entryList ( QDir : : Dirs | QDir : : NoDotAndDotDot ) ;
QVERIFY2 ( ! packages . isEmpty ( ) , " no packages found in the resource tree " ) ;
for ( const QString & package : packages ) {
const QString archive = qsl ( " :/packages/%1/%1.mpackage " ) . arg ( package ) ;
QVERIFY2 ( QFile : : exists ( archive ) , qPrintable ( qsl ( " %1 holds no archive named after it " ) . arg ( package ) ) ) ;
QTemporaryFile onDisk ;
QVERIFY ( onDisk . open ( ) ) ;
QFile resource ( archive ) ;
QVERIFY ( resource . open ( QIODevice : : ReadOnly ) ) ;
QVERIFY ( onDisk . write ( resource . readAll ( ) ) ! = - 1 ) ;
onDisk . close ( ) ;
QTemporaryDir unpacked ;
QVERIFY ( unpacked . isValid ( ) ) ;
// mudlet::unzip() joins the destination and the entry name as-is,
// so the trailing slash is what keeps the files inside the folder:
const QString destination = qsl ( " %1/ " ) . arg ( unpacked . path ( ) ) ;
QVERIFY2 ( mudlet : : unzip ( onDisk . fileName ( ) , destination , QDir ( unpacked . path ( ) ) ) , qPrintable ( qsl ( " %1 could not be unzipped " ) . arg ( archive ) ) ) ;
const QDir contents ( unpacked . path ( ) ) ;
QVERIFY2 ( contents . exists ( qsl ( " config.lua " ) ) , qPrintable ( qsl ( " %1 carries no config.lua, so it would install without any metadata " ) . arg ( archive ) ) ) ;
const QStringList xmls = contents . entryList ( QStringList { qsl ( " *.xml " ) } , QDir : : Files ) ;
QCOMPARE ( xmls . count ( ) , 1 ) ;
// Mudlet names the installed package after config.lua, so keeping
// the directory named the same is what makes the paths guessable.
const QString declaredName = mpHost - > getPackageConfig ( contents . absoluteFilePath ( qsl ( " config.lua " ) ) ) ;
QVERIFY2 ( ! declaredName . isEmpty ( ) , qPrintable ( qsl ( " %1 declares no package name in its config.lua " ) . arg ( archive ) ) ) ;
QCOMPARE ( declaredName , scmInstallsAs . value ( package , package ) ) ;
}
}
} ;
void initializeQRCResourcesForDefaultPackagesTest ( )
{
# ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens ( ) ;
# endif
# ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common ( ) ;
# if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix ( ) ;
# endif
# endif
qInitResources_mudlet ( ) ;
qInitResources_qm ( ) ;
}
# include "DefaultPackagesTest.moc"
QTEST_MAIN ( DefaultPackagesTest )