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
/***************************************************************************
* Copyright ( C ) 2026 by Vadim Peretokin - vadim . peretokin @ mudlet . org *
* *
* 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 . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*
* Locks in who Mudlet considers an experienced player . That decision gates all
* of the first - time guidance - the interface tour , the starter UI package and
* the one - line hints - so getting it wrong either buries a newcomer ' s
* onboarding or drops a beginner tour on top of a ten - year veteran ' s session .
*
* mudlet : : rememberFirstLaunch ( ) and mudlet : : evaluateExperiencedPlayer ( ) take
2026-08-06 14:51:37 +02:00
* their settings , profiles path and " now " as arguments , so most cases run
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
* without a mudlet instance . The last two drive a real init ( ) instead , to pin
2026-08-06 14:51:37 +02:00
* the production wiring .
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
*
* Run with : ctest - R ExperiencedPlayerGateTest - V
*/
# include <QtTest/QtTest>
# include <QTimeZone>
# include "MudletInstanceCoordinator.h"
# include "mudlet.h"
// init() reads compiled-in resources, which are not registered automatically in
// a test binary that links mudlet_core statically
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 initializeQRCResourcesForExperiencedPlayerGateTest ( ) ;
class ExperiencedPlayerGateTest : public QObject
{
Q_OBJECT
private :
QByteArray mSavedXdg ;
// Outlives the two live-singleton cases, which share one mudlet instance
QTemporaryDir mLiveConfig ;
2026-08-06 14:51:37 +02:00
// Fixed, so the six month arithmetic does not depend on the day the suite runs
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
const QDateTime mNow = QDateTime ( QDate ( 2026 , 8 , 5 ) , QTime ( 12 , 0 ) , QTimeZone : : UTC ) ;
const QString mKey = qsl ( " firstLaunchDate " ) ;
QString profilesPathIn ( const QString & configDir ) const { return qsl ( " %1/profiles " ) . arg ( configDir ) ; }
QString iniIn ( const QString & configDir ) const { return qsl ( " %1/Mudlet.ini " ) . arg ( configDir ) ; }
QString makeProfile ( const QString & configDir , const QString & name ) const
{
const QString path = qsl ( " %1/%2 " ) . arg ( profilesPathIn ( configDir ) , name ) ;
return QDir ( ) . mkpath ( path ) ? path : QString ( ) ;
}
void setFirstLaunch ( QSettings & settings , const QDateTime & when ) const { settings . setValue ( mKey , when . toUTC ( ) . toString ( Qt : : ISODate ) ) ; }
2026-08-06 14:51:37 +02:00
// setupConfig() consults portable.txt before the XDG logic
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
bool portableMarkerPresent ( ) const
{
return QFileInfo : : exists ( qsl ( " %1/portable.txt " ) . arg ( QCoreApplication : : applicationDirPath ( ) ) ) | | QFileInfo : : exists ( qsl ( " %1/.config/mudlet/portable.txt " ) . arg ( QDir : : homePath ( ) ) ) ;
}
private slots :
void initTestCase ( ) { mSavedXdg = qgetenv ( " XDG_CONFIG_HOME " ) ; }
void cleanupTestCase ( )
{
if ( mudlet : : self ( ) ) {
delete mudlet : : self ( ) ;
}
mSavedXdg . isNull ( ) ? qunsetenv ( " XDG_CONFIG_HOME " ) : qputenv ( " XDG_CONFIG_HOME " , mSavedXdg ) ;
}
2026-08-06 14:51:37 +02:00
// --- a brand new installation ---
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
void test_freshInstallRecordsTodayAndIsNew ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
QVERIFY ( ! QDir ( profilesPathIn ( config . path ( ) ) ) . exists ( ) ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QCOMPARE ( settings . value ( mKey ) . toString ( ) , mNow . toString ( Qt : : ISODate ) ) ;
QVERIFY2 ( ! mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) , " a first-ever launch must be treated as a new player " ) ;
}
void test_emptyProfilesDirectoryIsStillAFirstRun ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QVERIFY ( QDir ( ) . mkpath ( profilesPathIn ( config . path ( ) ) ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QVERIFY ( settings . contains ( mKey ) ) ;
QVERIFY ( ! mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
}
2026-08-06 14:51:37 +02:00
// --- the recorded date, once there is one ---
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
void test_recentlyRecordedFirstLaunchIsNew ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
setFirstLaunch ( settings , mNow . addMonths ( - 1 ) ) ;
QVERIFY ( ! makeProfile ( config . path ( ) , qsl ( " Achaea " ) ) . isEmpty ( ) ) ;
QVERIFY2 ( ! mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) , " a month of use is not enough to be experienced, even with a profile in hand " ) ;
}
void test_oldRecordedFirstLaunchIsExperienced ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
setFirstLaunch ( settings , mNow . addYears ( - 3 ) ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
}
void test_sixMonthBoundary ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
const QString profiles = profilesPathIn ( config . path ( ) ) ;
setFirstLaunch ( settings , mNow . addMonths ( - 6 ) . addDays ( 1 ) ) ;
QVERIFY2 ( ! mudlet : : evaluateExperiencedPlayer ( settings , profiles , mNow ) , " one day short of six months is not yet experienced " ) ;
setFirstLaunch ( settings , mNow . addMonths ( - 6 ) ) ;
QVERIFY2 ( mudlet : : evaluateExperiencedPlayer ( settings , profiles , mNow ) , " exactly six months of use is experienced " ) ;
setFirstLaunch ( settings , mNow . addMonths ( - 6 ) . addDays ( - 1 ) ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profiles , mNow ) ) ;
}
void test_futureDatedFirstLaunchIsNotExperienced ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
setFirstLaunch ( settings , mNow . addYears ( 1 ) ) ;
QVERIFY ( ! mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
}
2026-08-06 14:51:37 +02:00
// --- upgrading users, who have no recorded first launch ---
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
void test_upgraderWithFreshlyWrittenProfilesIsExperienced ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
for ( const auto & name : { qsl ( " Achaea " ) , qsl ( " StickMUD " ) , qsl ( " Legends of the Jedi " ) } ) {
const QString profile = makeProfile ( config . path ( ) , name ) ;
QVERIFY ( ! profile . isEmpty ( ) ) ;
QFile url ( qsl ( " %1/url " ) . arg ( profile ) ) ;
QVERIFY ( url . open ( QIODevice : : WriteOnly ) ) ;
url . write ( " achaea.com " ) ;
url . close ( ) ;
QVERIFY2 ( QFileInfo ( profile ) . lastModified ( ) > mNow . addMonths ( - 6 ) , " the fixture is only meaningful while the profile directory looks brand new " ) ;
}
QVERIFY2 ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) , " an installation with profiles but no recorded first launch predates the key, so it is experienced " ) ;
}
2026-08-06 14:51:37 +02:00
// A restored profile may or may not keep its modification times, and never
// keeps its birth time, so no timestamp is consulted
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
void test_restoredFromBackupIsExperienced ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
QVERIFY ( ! makeProfile ( config . path ( ) , qsl ( " Restored " ) ) . isEmpty ( ) ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
}
void test_settingsWithoutProfilesStillCountAsEarlierUse ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
settings . setValue ( qsl ( " pos " ) , QPoint ( 120 , 80 ) ) ;
QVERIFY ( ! QDir ( profilesPathIn ( config . path ( ) ) ) . exists ( ) ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QVERIFY2 ( ! settings . contains ( mKey ) , " an installation with settings on file is not on its first run " ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
}
void test_unreadableProfilesDirectoryIsTakenAsPopulated ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
const QString profiles = profilesPathIn ( config . path ( ) ) ;
QVERIFY ( ! makeProfile ( config . path ( ) , qsl ( " Achaea " ) ) . isEmpty ( ) ) ;
QVERIFY ( QFile : : setPermissions ( profiles , QFileDevice : : WriteOwner | QFileDevice : : ExeOwner ) ) ;
if ( QFileInfo ( profiles ) . isReadable ( ) ) {
QVERIFY ( QFile : : setPermissions ( profiles , QFileDevice : : ReadOwner | QFileDevice : : WriteOwner | QFileDevice : : ExeOwner ) ) ;
QSKIP ( " the profiles directory is readable despite the permissions - running as root? " ) ;
}
const bool experienced = mudlet : : evaluateExperiencedPlayer ( settings , profiles , mNow ) ;
2026-08-06 14:51:37 +02:00
// Restore before asserting, or a failure leaves QTemporaryDir unable to clean up
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 ( QFile : : setPermissions ( profiles , QFileDevice : : ReadOwner | QFileDevice : : WriteOwner | QFileDevice : : ExeOwner ) ) ;
QVERIFY ( experienced ) ;
}
void test_upgradeDoesNotRecordAFirstLaunch ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
QVERIFY ( ! makeProfile ( config . path ( ) , qsl ( " Achaea " ) ) . isEmpty ( ) ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QVERIFY ( ! settings . contains ( mKey ) ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow . addYears ( 1 ) ) ) ;
}
void test_existingRecordIsNeverOverwritten ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
const QDateTime original = mNow . addMonths ( - 3 ) ;
setFirstLaunch ( settings , original ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QCOMPARE ( settings . value ( mKey ) . toString ( ) , original . toString ( Qt : : ISODate ) ) ;
}
void test_unparseableRecordFallsBackToTheEarlierUseCheck ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
QSettings settings ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
settings . setValue ( mKey , qsl ( " not a date " ) ) ;
QVERIFY ( mudlet : : evaluateExperiencedPlayer ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ) ;
mudlet : : rememberFirstLaunch ( settings , profilesPathIn ( config . path ( ) ) , mNow ) ;
QCOMPARE ( settings . value ( mKey ) . toString ( ) , qsl ( " not a date " ) ) ;
}
void test_recordedValueSurvivesAQSettingsRoundTrip ( )
{
QTemporaryDir config ;
QVERIFY ( config . isValid ( ) ) ;
{
QSettings writer ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
mudlet : : rememberFirstLaunch ( writer , profilesPathIn ( config . path ( ) ) , mNow . addYears ( - 2 ) ) ;
}
QSettings reader ( iniIn ( config . path ( ) ) , QSettings : : IniFormat ) ;
QVERIFY2 ( mudlet : : evaluateExperiencedPlayer ( reader , profilesPathIn ( config . path ( ) ) , mNow ) , " the recorded date must be readable back out of Mudlet.ini " ) ;
QFile ini ( iniIn ( config . path ( ) ) ) ;
QVERIFY ( ini . open ( QIODevice : : ReadOnly | QIODevice : : Text ) ) ;
QVERIFY2 ( QString : : fromUtf8 ( ini . readAll ( ) ) . contains ( qsl ( " firstLaunchDate=2024-08-05T12:00:00Z " ) ) , " the date is stored as plain ISO 8601, so it can be read and edited by hand " ) ;
}
2026-08-06 14:51:37 +02:00
// --- the live singleton ---
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
2026-08-06 14:51:37 +02:00
// Nothing else in the suite notices the init() call being moved or dropped,
// which would make every fresh install take the "used before" fallback
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
void test_initRecordsTheFirstLaunchOnAFreshInstall ( )
{
if ( portableMarkerPresent ( ) ) {
QSKIP ( " portable.txt present - setupConfig() takes the portable branch " ) ;
}
QVERIFY ( mLiveConfig . isValid ( ) ) ;
Fix an empty XDG config directory hiding every profile (#9712)
#### Brief overview of PR changes/additions
- An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated
`~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran
its first-launch onboarding as though the user were new. It also stuck:
the first such launch wrote `Mudlet.ini` into that directory, which then
kept it winning.
- The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` >
exists > absent) and the stronger claim wins, with
`$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a
deliberate opt-in both still land there. A directory that cannot be
listed counts as populated rather than empty, so a permission bit cannot
re-enter the bug.
- Creating `profiles/` is now the opt-in a test harness uses; the
`mudlet` directory alone is not, because other tooling creates that by
accident. Where both roots hold profiles, `setupConfig()` names the one
it is ignoring instead of leaving those profiles apparently gone.
#### Motivation for adding to Mudlet
Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME
for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned
rather than destroyed, but a returning user sees "5.0 wiped my
profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p
"$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it.
#### Other info (issues closed, discussion etc)
Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`,
`mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the
onboarding dialog. After: both profiles listed.
`ConfigDirOverrideTest` covers the resolution table including the sticky
`Mudlet.ini` state, both-populated, symlinked and unreadable
directories; each new guard was mutation-checked. The busted suite
passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles`
root.
Not fixed here, and pre-existing rather than 5.0 regressions:
`CredentialManager` stores passwords and the OAuth reconnect token under
`AppConfigLocation` while the config root is `confPath`, so exporting
`XDG_CONFIG_HOME` strands them, and the plaintext-password migration
reads one path, writes the other and deletes the original. Both
reproduce identically on the 4.22.0 binary and need their own migration
path.
Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
// $XDG_CONFIG_HOME/mudlet/profiles is the opt-in marker, without which
2026-08-06 14:51:37 +02:00
// setupConfig() keeps using a legacy ~/.config/mudlet
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
const QString configDir = qsl ( " %1/mudlet " ) . arg ( mLiveConfig . path ( ) ) ;
Fix an empty XDG config directory hiding every profile (#9712)
#### Brief overview of PR changes/additions
- An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated
`~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran
its first-launch onboarding as though the user were new. It also stuck:
the first such launch wrote `Mudlet.ini` into that directory, which then
kept it winning.
- The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` >
exists > absent) and the stronger claim wins, with
`$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a
deliberate opt-in both still land there. A directory that cannot be
listed counts as populated rather than empty, so a permission bit cannot
re-enter the bug.
- Creating `profiles/` is now the opt-in a test harness uses; the
`mudlet` directory alone is not, because other tooling creates that by
accident. Where both roots hold profiles, `setupConfig()` names the one
it is ignoring instead of leaving those profiles apparently gone.
#### Motivation for adding to Mudlet
Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME
for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned
rather than destroyed, but a returning user sees "5.0 wiped my
profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p
"$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it.
#### Other info (issues closed, discussion etc)
Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`,
`mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the
onboarding dialog. After: both profiles listed.
`ConfigDirOverrideTest` covers the resolution table including the sticky
`Mudlet.ini` state, both-populated, symlinked and unreadable
directories; each new guard was mutation-checked. The busted suite
passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles`
root.
Not fixed here, and pre-existing rather than 5.0 regressions:
`CredentialManager` stores passwords and the OAuth reconnect token under
`AppConfigLocation` while the config root is `confPath`, so exporting
`XDG_CONFIG_HOME` strands them, and the plaintext-password migration
reads one path, writes the other and deletes the original. Both
reproduce identically on the 4.22.0 binary and need their own migration
path.
Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
QVERIFY ( QDir ( ) . mkpath ( qsl ( " %1/profiles " ) . arg ( configDir ) ) ) ;
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
qputenv ( " XDG_CONFIG_HOME " , mLiveConfig . path ( ) . toUtf8 ( ) ) ;
initializeQRCResourcesForExperiencedPlayerGateTest ( ) ;
mudlet : : start ( ) ;
mudlet : : self ( ) - > setupConfig ( ) ;
QCOMPARE ( mudlet : : getMudletPath ( enums : : mainPath ) , configDir ) ;
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
QVERIFY2 ( mudlet : : getQSettings ( ) - > allKeys ( ) . isEmpty ( ) , " a fresh config dir must start out with an empty Mudlet.ini - something wrote settings before init() " ) ;
Fix an empty XDG config directory hiding every profile (#9712)
#### Brief overview of PR changes/additions
- An empty `$XDG_CONFIG_HOME/mudlet` silently beat a populated
`~/.config/mudlet`, so a stray `mkdir` hid every profile and Mudlet ran
its first-launch onboarding as though the user were new. It also stuck:
the first such launch wrote `Mudlet.ini` into that directory, which then
kept it winning.
- The two candidate roots are now ranked (`profiles/` > `Mudlet.ini` >
exists > absent) and the stronger claim wins, with
`$XDG_CONFIG_HOME/mudlet` taking ties so a fresh install and a
deliberate opt-in both still land there. A directory that cannot be
listed counts as populated rather than empty, so a permission bit cannot
re-enter the bug.
- Creating `profiles/` is now the opt-in a test harness uses; the
`mudlet` directory alone is not, because other tooling creates that by
accident. Where both roots hold profiles, `setupConfig()` names the one
it is ignoring instead of leaving those profiles apparently gone.
#### Motivation for adding to Mudlet
Data-loss-shaped regression from #9552 "improve: honor XDG_CONFIG_HOME
for Mudlet's config directory" (`e6c268cb0`). The profiles are orphaned
rather than destroyed, but a returning user sees "5.0 wiped my
profiles". `src/mudlet-lua/tests/README.md` itself instructed `mkdir -p
"$CONFIG_DIR/mudlet"`, so following Mudlet's own test docs triggered it.
#### Other info (issues closed, discussion etc)
Test case: create `~/.config/mudlet/profiles/{AlphaGame,BetaGame}`,
`mkdir -p $XDG_CONFIG_HOME/mudlet`, launch. Before: no profiles and the
onboarding dialog. After: both profiles listed.
`ConfigDirOverrideTest` covers the resolution table including the sticky
`Mudlet.ini` state, both-populated, symlinked and unreadable
directories; each new guard was mutation-checked. The busted suite
passes 2422/0 against an isolated `$XDG_CONFIG_HOME/mudlet/profiles`
root.
Not fixed here, and pre-existing rather than 5.0 regressions:
`CredentialManager` stores passwords and the OAuth reconnect token under
`AppConfigLocation` while the config root is `confPath`, so exporting
`XDG_CONFIG_HOME` strands them, and the plaintext-password migration
reads one path, writes the other and deletes the original. Both
reproduce identically on the 4.22.0 binary and need their own migration
path.
Assisted-by: Claude:claude-opus-5
2026-08-10 22:17:09 +02:00
QVERIFY2 ( QDir ( mudlet : : getMudletPath ( enums : : profilesPath ) ) . entryList ( QDir : : Dirs | QDir : : NoDotAndDotDot ) . isEmpty ( ) , " the opt-in profiles/ dir has to be empty, or this is not a fresh install " ) ;
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
mudlet : : self ( ) - > takeOwnershipOfInstanceCoordinator ( std : : make_unique < MudletInstanceCoordinator > ( " MudletInstanceCoordinator " ) ) ;
mudlet : : self ( ) - > init ( ) ;
QVERIFY2 ( mudlet : : getQSettings ( ) - > contains ( mKey ) , " init() must record the first launch date " ) ;
QCOMPARE ( QDateTime : : fromString ( mudlet : : getQSettings ( ) - > value ( mKey ) . toString ( ) , Qt : : ISODate ) . isValid ( ) , true ) ;
}
2026-08-06 14:51:37 +02:00
// Pins the key and profiles path experiencedMudletPlayer() picks for itself,
// which the case above cannot - there both branches would answer "new".
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
// Runs last: experiencedMudletPlayer() memoises for the life of the process.
void test_experiencedThroughTheRealSettings ( )
{
if ( portableMarkerPresent ( ) ) {
QSKIP ( " portable.txt present - setupConfig() takes the portable branch " ) ;
}
QVERIFY ( mudlet : : self ( ) ) ;
auto * settings = mudlet : : getQSettings ( ) ;
QVERIFY ( settings ) ;
settings - > remove ( mKey ) ;
QVERIFY ( ! makeProfile ( mudlet : : getMudletPath ( enums : : mainPath ) , qsl ( " Achaea " ) ) . isEmpty ( ) ) ;
QVERIFY2 ( mudlet : : self ( ) - > experiencedMudletPlayer ( ) , " a profile with no recorded first launch must read as an experienced player " ) ;
}
} ;
void initializeQRCResourcesForExperiencedPlayerGateTest ( )
{
# 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 "ExperiencedPlayerGateTest.moc"
QTEST_MAIN ( ExperiencedPlayerGateTest )