diff --git a/Common/StringUtils.cpp b/Common/StringUtils.cpp
index a84a21ca39..3d7ca6f5df 100644
--- a/Common/StringUtils.cpp
+++ b/Common/StringUtils.cpp
@@ -105,6 +105,38 @@ int countChar(std::string_view haystack, char needle) {
 	return count;
 }
 
+std::string SanitizeString(std::string_view input, StringRestriction restriction, int maxLength) {
+	if (restriction == StringRestriction::None) {
+		return std::string(input);
+	}
+	// First, remove any chars not in A-Za-z0-9_-. This will effectively get rid of any Unicode char, emojis etc too.
+	std::string sanitized;
+	for (auto c : input) {
+		switch (restriction) {
+		case StringRestriction::None:
+			sanitized.push_back(c);
+			break;
+		case StringRestriction::AlphaNumDashUnderscore:
+			if ((c >= 'A' && c <= 'Z') ||
+				(c >= 'a' && c <= 'z') ||
+				(c >= '0' && c <= '9') || c == '-' || c == '_') {
+				// Allowed chars.
+				sanitized.push_back(c);
+			}
+			break;
+		}
+	}
+
+	if (maxLength >= 0) {
+		// TODO: Cut at whole UTF-8 chars!
+		if (sanitized.size() > maxLength) {
+			sanitized.resize(maxLength);
+		}
+	}
+
+	return sanitized;
+}
+
 bool CharArrayFromFormatV(char* out, int outsize, const char* format, va_list args)
 {
 	int writtenCount = vsnprintf(out, outsize, format, args);
diff --git a/Common/StringUtils.h b/Common/StringUtils.h
index 7f5ffaff31..dd19c6e6a9 100644
--- a/Common/StringUtils.h
+++ b/Common/StringUtils.h
@@ -71,6 +71,13 @@ inline bool equalsNoCase(std::string_view str, std::string_view key) {
 
 bool containsNoCase(std::string_view haystack, std::string_view needle);
 
+enum class StringRestriction {
+	None,
+	AlphaNumDashUnderscore,  // Used for infrastructure usernames
+};
+
+std::string SanitizeString(std::string_view username, StringRestriction restriction, int maxLength = -1);
+
 void DataToHexString(const uint8_t *data, size_t size, std::string *output);
 void DataToHexString(int indent, uint32_t startAddr, const uint8_t* data, size_t size, std::string* output);
 
diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp
index 6a1b0e942b..af484ac4a9 100644
--- a/Common/UI/PopupScreens.cpp
+++ b/Common/UI/PopupScreens.cpp
@@ -519,7 +519,7 @@ void SliderFloatPopupScreen::OnCompleted(DialogResult result) {
 }
 
 PopupTextInputChoice::PopupTextInputChoice(RequesterToken token, std::string *value, std::string_view title, std::string_view placeholder, int maxLen, ScreenManager *screenManager, LayoutParams *layoutParams)
-	: AbstractChoiceWithValueDisplay(title, layoutParams), screenManager_(screenManager), value_(value), placeHolder_(placeholder), maxLen_(maxLen), token_(token) {
+	: AbstractChoiceWithValueDisplay(title, layoutParams), screenManager_(screenManager), value_(value), placeHolder_(placeholder), maxLen_(maxLen), token_(token), restriction_(StringRestriction::None) {
 	OnClick.Handle(this, &PopupTextInputChoice::HandleClick);
 }
 
@@ -529,7 +529,7 @@ EventReturn PopupTextInputChoice::HandleClick(EventParams &e) {
 	// Choose method depending on platform capabilities.
 	if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
 		System_InputBoxGetString(token_, text_, *value_, passwordMasking_, [=](const std::string &enteredValue, int) {
-			*value_ = StripSpaces(enteredValue);
+			*value_ = SanitizeString(StripSpaces(enteredValue), restriction_, maxLen_);
 			EventParams params{};
 			OnChange.Trigger(params);
 		});
@@ -553,6 +553,7 @@ std::string PopupTextInputChoice::ValueText() const {
 }
 
 EventReturn PopupTextInputChoice::HandleChange(EventParams &e) {
+	*value_ = StripSpaces(SanitizeString(*value_, restriction_, maxLen_));
 	e.v = this;
 	OnChange.Trigger(e);
 
diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h
index 422be1efa5..54032d275f 100644
--- a/Common/UI/PopupScreens.h
+++ b/Common/UI/PopupScreens.h
@@ -8,6 +8,9 @@
 #include "Common/UI/View.h"
 #include "Common/UI/ScrollView.h"
 
+// from StringUtils
+enum class StringRestriction;
+
 namespace UI {
 
 static const float NO_DEFAULT_FLOAT = -1000000.0f;
@@ -371,6 +374,10 @@ public:
 
 	Event OnChange;
 
+	void SetRestriction(StringRestriction restriction) {
+		restriction_ = restriction;
+	}
+
 protected:
 	std::string ValueText() const override;
 
@@ -384,6 +391,7 @@ private:
 	std::string defaultText_;
 	int maxLen_;
 	bool restoreFocus_ = false;
+	StringRestriction restriction_;
 };
 
 class ChoiceWithValueDisplay : public AbstractChoiceWithValueDisplay {
diff --git a/Core/Config.cpp b/Core/Config.cpp
index 545f104ce0..2a8810e308 100644
--- a/Core/Config.cpp
+++ b/Core/Config.cpp
@@ -111,7 +111,7 @@ GPUBackend GPUBackendFromString(std::string_view backend) {
 	return GPUBackend::OPENGL;
 }
 
-const char *DefaultLangRegion() {
+std::string DefaultLangRegion() {
 	// Unfortunate default.  There's no need to use bFirstRun, since this is only a default.
 	static std::string defaultLangRegion = "en_US";
 	std::string langRegion = System_GetProperty(SYSPROP_LANGREGION);
@@ -136,7 +136,7 @@ const char *DefaultLangRegion() {
 		}
 	}
 
-	return defaultLangRegion.c_str();
+	return defaultLangRegion;
 }
 
 static int DefaultDepthRaster() {
@@ -602,6 +602,20 @@ static std::string FastForwardModeToString(int v) {
 	return "CONTINUOUS";
 }
 
+static std::string DefaultInfrastructureUsername() {
+	// If the user has already picked a Nickname that satisfies the rules and is not "PPSSPP",
+	// let's use that.
+	// NOTE: This type of dependency means that network settings must be AFTER system settings in sections[].
+	if (g_Config.sNickName != "PPSSPP" &&
+		!g_Config.sNickName.empty() &&
+		g_Config.sNickName == SanitizeString(g_Config.sNickName, StringRestriction::AlphaNumDashUnderscore, 16)) {
+		return g_Config.sNickName;
+	}
+
+	// Otherwise let's leave it empty, which will result in login failure and a warning.
+	return std::string();
+}
+
 static const ConfigSetting graphicsSettings[] = {
 	ConfigSetting("EnableCardboardVR", &g_Config.bEnableCardboardVR, false, CfgFlag::PER_GAME),
 	ConfigSetting("CardboardScreenSize", &g_Config.iCardboardScreenSize, 50, CfgFlag::PER_GAME),
@@ -893,6 +907,7 @@ static const ConfigSetting networkSettings[] = {
 	ConfigSetting("ForcedFirstConnect", &g_Config.bForcedFirstConnect, false, CfgFlag::PER_GAME),
 	ConfigSetting("EnableUPnP", &g_Config.bEnableUPnP, false, CfgFlag::PER_GAME),
 	ConfigSetting("UPnPUseOriginalPort", &g_Config.bUPnPUseOriginalPort, false, CfgFlag::PER_GAME),
+	ConfigSetting("InfrastructureUsername", &g_Config.sInfrastructureUsername, &DefaultInfrastructureUsername, CfgFlag::PER_GAME),
 
 	ConfigSetting("EnableNetworkChat", &g_Config.bEnableNetworkChat, false, CfgFlag::PER_GAME),
 	ConfigSetting("ChatButtonPosition", &g_Config.iChatButtonPosition, (int)ScreenEdgePosition::BOTTOM_LEFT, CfgFlag::PER_GAME),
@@ -993,8 +1008,8 @@ static const ConfigSectionSettings sections[] = {
 	{"Graphics", graphicsSettings, ARRAY_SIZE(graphicsSettings)},
 	{"Sound", soundSettings, ARRAY_SIZE(soundSettings)},
 	{"Control", controlSettings, ARRAY_SIZE(controlSettings)},
-	{"Network", networkSettings, ARRAY_SIZE(networkSettings)},
 	{"SystemParam", systemParamSettings, ARRAY_SIZE(systemParamSettings)},
+	{"Network", networkSettings, ARRAY_SIZE(networkSettings)},
 	{"Debugger", debuggerSettings, ARRAY_SIZE(debuggerSettings)},
 	{"JIT", jitSettings, ARRAY_SIZE(jitSettings)},
 	{"Upgrade", upgradeSettings, ARRAY_SIZE(upgradeSettings)},
diff --git a/Core/Config.h b/Core/Config.h
index 9bc5443b54..5215ab4735 100644
--- a/Core/Config.h
+++ b/Core/Config.h
@@ -443,8 +443,9 @@ public:
 	bool bDiscardRegsOnJRRA;
 
 	// SystemParam
-	std::string sNickName;
+	std::string sNickName;  // AdHoc and system nickname
 	std::string sMACAddress;
+
 	int iLanguage;
 	int iTimeFormat;
 	int iDateFormat;
@@ -459,6 +460,7 @@ public:
 	std::string proAdhocServer;
 	std::string primaryDNSServer;
 	std::string secondaryDNSServer;
+	std::string sInfrastructureUsername;  // Username used for Infrastructure play. Different restrictions.
 	bool bEnableWlan;
 	std::map<std::string, std::string> mHostToAlias; // TODO: mPostShaderSetting
 	bool bEnableAdhocServer;
diff --git a/Core/ConfigSettings.cpp b/Core/ConfigSettings.cpp
index 6de81dc3a0..ba6b83005c 100644
--- a/Core/ConfigSettings.cpp
+++ b/Core/ConfigSettings.cpp
@@ -36,7 +36,7 @@ bool ConfigSetting::Get(const Section *section) const {
 	case TYPE_FLOAT:
 		return section->Get(iniKey_, ptr_.f, cb_.f ? cb_.f() : default_.f);
 	case TYPE_STRING:
-		return section->Get(iniKey_, ptr_.s, cb_.s ? cb_.s() : default_.s);
+		return section->Get(iniKey_, ptr_.s, cb_.s ? cb_.s().c_str() : default_.s);
 	case TYPE_TOUCH_POS:
 	{
 		ConfigTouchPos defaultTouchPos = cb_.touchPos ? cb_.touchPos() : default_.touchPos;
diff --git a/Core/ConfigSettings.h b/Core/ConfigSettings.h
index 440e22ca26..2c4f373240 100644
--- a/Core/ConfigSettings.h
+++ b/Core/ConfigSettings.h
@@ -57,7 +57,7 @@ struct ConfigSetting {
 	typedef uint32_t(*Uint32DefaultCallback)();
 	typedef uint64_t(*Uint64DefaultCallback)();
 	typedef float (*FloatDefaultCallback)();
-	typedef const char *(*StringDefaultCallback)();
+	typedef std::string (*StringDefaultCallback)();
 	typedef ConfigTouchPos(*TouchPosDefaultCallback)();
 	typedef const char *(*PathDefaultCallback)();
 	typedef ConfigCustomButton(*CustomButtonDefaultCallback)();
diff --git a/Core/HLE/sceNp.cpp b/Core/HLE/sceNp.cpp
index 8b3d413665..e43ea67d53 100644
--- a/Core/HLE/sceNp.cpp
+++ b/Core/HLE/sceNp.cpp
@@ -20,7 +20,10 @@
 
 #include <mutex>
 #include <deque>
-#include <StringUtils.h>
+
+#include "Common/System/OSD.h"
+#include "Common/Data/Text/I18n.h"
+#include "Common/StringUtils.h"
 #include "Core/MemMapHelpers.h"
 #include "Core/CoreTiming.h"
 #include "Core/Config.h"
@@ -52,7 +55,6 @@ std::recursive_mutex npAuthEvtMtx;
 std::deque<NpAuthArgs> npAuthEvents;
 std::map<int, NpAuthHandler> npAuthHandlers;
 
-
 // Tickets data are in big-endian based on captured packets
 static int writeTicketParam(u8* buffer, const u16_be type, const char* data = nullptr, const u16_be size = 0) {
 	if (buffer == nullptr) return 0;
@@ -112,12 +114,16 @@ static void notifyNpAuthHandlers(u32 id, u32 result, u32 argAddr) {
 static int sceNpInit()
 {
 	ERROR_LOG(Log::sceNet, "UNIMPL %s()", __FUNCTION__);
-	npOnlineId = g_Config.sNickName;
-	// Truncate the nickname to 16 chars exactly - longer names are not support.
-	if (npOnlineId.size() > 16) {
-		npOnlineId.resize(16);
+
+	// We'll sanitize an extra time here, just to be safe from ini modifications.
+	if (g_Config.sInfrastructureUsername == SanitizeString(g_Config.sInfrastructureUsername, StringRestriction::AlphaNumDashUnderscore, 16)) {
+		npOnlineId = g_Config.sInfrastructureUsername;
+	} else {
+		npOnlineId.clear();
 	}
 
+	// NOTE: Checking validity and returning -1 here doesn't seem to work. Instead, we will fail to generate a ticket.
+
 	return 0;
 }
 
@@ -402,6 +408,14 @@ int sceNpAuthGetTicket(u32 requestId, u32 bufferAddr, u32 length)
 	if (!Memory::IsValidAddress(bufferAddr))
 		return hleLogError(Log::sceNet, SCE_NP_AUTH_ERROR_INVALID_ARGUMENT, "invalid arg");
 
+	// We have validated, and this will be empty if the ID is bad.
+	if (npOnlineId.empty()) {
+		auto n = GetI18NCategory(I18NCat::NETWORKING);
+		// Temporary message.
+		g_OSD.Show(OSDType::MESSAGE_ERROR, n->T("To play in Infrastructure Mode, you must enter a username"), 5.0f);
+		return hleLogError(Log::sceNet, SCE_NP_AUTH_ERROR_UNKNOWN, "Missing npOnlineId");
+	}
+
 	int result = length;
 	Memory::Memset(bufferAddr, 0, length, "NpAuthGetTicket");
 	SceNpTicket ticket = {};
diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp
index 8afadd7ac7..d6b40e7446 100644
--- a/UI/GameSettingsScreen.cpp
+++ b/UI/GameSettingsScreen.cpp
@@ -941,6 +941,16 @@ void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSetti
 	useOriPort->SetEnabledPtr(&g_Config.bEnableUPnP);
 
 	networkingSettings->Add(new ItemHeader(n->T("Infrastructure")));
+	if (g_Config.sInfrastructureUsername.empty()) {
+		networkingSettings->Add(new NoticeView(NoticeLevel::WARN, n->T("To play in Infrastructure Mode, you must enter a username"), ""));
+	}
+	PopupTextInputChoice *usernameChoice = networkingSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.sInfrastructureUsername, n->T("Username"), "", 16, screenManager()));
+	usernameChoice->SetRestriction(StringRestriction::AlphaNumDashUnderscore);
+	usernameChoice->OnChange.Add([this](UI::EventParams &e) {
+		RecreateViews();
+		return UI::EVENT_DONE;
+	});
+
 	networkingSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.primaryDNSServer, n->T("Primary DNS server"), "", 32, screenManager()));
 	networkingSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.secondaryDNSServer, n->T("Secondary DNS server"), "", 32, screenManager()));
 
@@ -1682,62 +1692,42 @@ UI::EventReturn GameSettingsScreen::OnAudioDevice(UI::EventParams &e) {
 }
 
 UI::EventReturn GameSettingsScreen::OnChangeQuickChat0(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
 	auto n = GetI18NCategory(I18NCat::NETWORKING);
 	System_InputBoxGetString(GetRequesterToken(), n->T("Enter Quick Chat 1"), g_Config.sQuickChat0, false, [](const std::string &value, int) {
 		g_Config.sQuickChat0 = value;
 	});
-#endif
 	return UI::EVENT_DONE;
 }
 
 UI::EventReturn GameSettingsScreen::OnChangeQuickChat1(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
 	auto n = GetI18NCategory(I18NCat::NETWORKING);
 	System_InputBoxGetString(GetRequesterToken(), n->T("Enter Quick Chat 2"), g_Config.sQuickChat1, false, [](const std::string &value, int) {
 		g_Config.sQuickChat1 = value;
 	});
-#endif
 	return UI::EVENT_DONE;
 }
 
 UI::EventReturn GameSettingsScreen::OnChangeQuickChat2(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
 	auto n = GetI18NCategory(I18NCat::NETWORKING);
 	System_InputBoxGetString(GetRequesterToken(), n->T("Enter Quick Chat 3"), g_Config.sQuickChat2, false, [](const std::string &value, int) {
 		g_Config.sQuickChat2 = value;
 	});
-#endif
 	return UI::EVENT_DONE;
 }
 
 UI::EventReturn GameSettingsScreen::OnChangeQuickChat3(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
 	auto n = GetI18NCategory(I18NCat::NETWORKING);
 	System_InputBoxGetString(GetRequesterToken(), n->T("Enter Quick Chat 4"), g_Config.sQuickChat3, false, [](const std::string &value, int) {
 		g_Config.sQuickChat3 = value;
 	});
-#endif
 	return UI::EVENT_DONE;
 }
 
 UI::EventReturn GameSettingsScreen::OnChangeQuickChat4(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
 	auto n = GetI18NCategory(I18NCat::NETWORKING);
 	System_InputBoxGetString(GetRequesterToken(), n->T("Enter Quick Chat 5"), g_Config.sQuickChat4, false, [](const std::string &value, int) {
 		g_Config.sQuickChat4 = value;
 	});
-#endif
-	return UI::EVENT_DONE;
-}
-
-UI::EventReturn GameSettingsScreen::OnChangeNickname(UI::EventParams &e) {
-#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
-	auto n = GetI18NCategory(I18NCat::NETWORKING);
-	System_InputBoxGetString(GetRequesterToken(), n->T("Enter a new PSP nickname"), g_Config.sNickName, false, [](const std::string &value, int) {
-		g_Config.sNickName = StripSpaces(value);
-	});
-#endif
 	return UI::EVENT_DONE;
 }
 
diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h
index cc31d7c3ea..4032a143e6 100644
--- a/UI/GameSettingsScreen.h
+++ b/UI/GameSettingsScreen.h
@@ -89,7 +89,6 @@ private:
 	UI::EventReturn OnChangeQuickChat2(UI::EventParams &e);
 	UI::EventReturn OnChangeQuickChat3(UI::EventParams &e);
 	UI::EventReturn OnChangeQuickChat4(UI::EventParams &e);
-	UI::EventReturn OnChangeNickname(UI::EventParams &e);
 	UI::EventReturn OnChangeproAdhocServerAddress(UI::EventParams &e);
 	UI::EventReturn OnChangeBackground(UI::EventParams &e);
 	UI::EventReturn OnFullscreenChange(UI::EventParams &e);