diff --git a/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm b/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm
new file mode 100644
index 0000000000..9ff619a52a
--- /dev/null
+++ b/Content.Tests/DMProject/Tests/Text/StringInterpolation9.dm
@@ -0,0 +1,39 @@
+/datum/thing
+ var/name = "thing"
+
+/datum/Thing
+ var/name = "Thing"
+
+/datum/proper_thing
+ var/name = "\proper thing"
+
+/datum/plural_things
+ var/name = "things"
+ var/gender = PLURAL
+
+/proc/RunTest()
+ // Lowercase \a on datums
+ ASSERT("\a [new /datum/thing]" == "a thing")
+ ASSERT("\a [new /datum/Thing]" == "Thing")
+ ASSERT("\a [new /datum/proper_thing]" == "thing")
+ ASSERT("\a [new /datum/plural_things]" == "some things")
+
+ // Uppercase \A on datums
+ ASSERT("\A [new /datum/thing]" == "A thing")
+ ASSERT("\A [new /datum/Thing]" == "Thing")
+ ASSERT("\A [new /datum/proper_thing]" == "thing")
+ ASSERT("\A [new /datum/plural_things]" == "Some things")
+
+ // Lowercase \a on strings
+ ASSERT("\a ["thing"]" == "a thing")
+ ASSERT("\a ["Thing"]" == "Thing")
+ ASSERT("\a ["\proper thing"]" == "thing")
+
+ // Uppercase \A on strings
+ ASSERT("\A ["thing"]" == "A thing")
+ ASSERT("\A ["Thing"]" == "Thing")
+ ASSERT("\A ["\proper thing"]" == "thing")
+
+ // Invalid \a
+ ASSERT("\a [123]" == "")
+ ASSERT("\A [123]" == "")
\ No newline at end of file
diff --git a/OpenDreamRuntime/Objects/DreamObject.cs b/OpenDreamRuntime/Objects/DreamObject.cs
index 305ec32b0e..3d054eb781 100644
--- a/OpenDreamRuntime/Objects/DreamObject.cs
+++ b/OpenDreamRuntime/Objects/DreamObject.cs
@@ -276,8 +276,6 @@ public static bool StringIsProper(string str) {
return true;
case StringFormatEncoder.FormatSuffix.Improper:
return false;
- default:
- break;
}
}
@@ -312,9 +310,7 @@ public string GetDisplayName(StringFormatEncoder.FormatSuffix? suffix = null) {
if (this is DreamObjectClient client)
return client.Connection.Session!.Name;
- if (!TryGetVariable("name", out DreamValue nameVar) || !nameVar.TryGetValueAsString(out string? name))
- return ObjectDefinition.Type.ToString();
-
+ var name = GetRawName();
bool isProper = StringIsProper(name);
name = StringFormatEncoder.RemoveFormatting(name); // TODO: Care about other formatting macros for obj names beyond \proper & \improper
if(!isProper) {
@@ -335,9 +331,18 @@ public string GetDisplayName(StringFormatEncoder.FormatSuffix? suffix = null) {
/// Similar to except it just returns the name as plaintext, with formatting removed. No article or anything.
///
public string GetNameUnformatted() {
+ return StringFormatEncoder.RemoveFormatting(GetRawName());
+ }
+
+ ///
+ /// Returns the name of this object with no formatting evaluated
+ ///
+ ///
+ public string GetRawName() {
if (!TryGetVariable("name", out DreamValue nameVar) || !nameVar.TryGetValueAsString(out string? name))
- return ObjectDefinition?.Type.ToString() ?? String.Empty;
- return StringFormatEncoder.RemoveFormatting(name);
+ return ObjectDefinition.Type.ToString();
+
+ return name;
}
#endregion Name Helpers
diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
index f9b8cab3ca..0043d5c807 100644
--- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
+++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
@@ -372,26 +372,28 @@ public static ProcStatus FormatString(DMProcState state) {
nextInterpIndex++;
continue;
}
- case StringFormatEncoder.FormatSuffix.StringifyNoArticle:
- {
- if (interps[nextInterpIndex].TryGetValueAsDreamObject(out var dreamObject) && dreamObject != null) {
+ case StringFormatEncoder.FormatSuffix.StringifyNoArticle: {
+ if (interps[nextInterpIndex].TryGetValueAsDreamObject(out var dreamObject)) {
formattedString.Append(dreamObject.GetNameUnformatted());
+ } else if (interps[nextInterpIndex].TryGetValueAsString(out var interpStr)) {
+ formattedString.Append(StringFormatEncoder.RemoveFormatting(interpStr));
}
// NOTE probably should put this above the TryGetAsDreamObject function and continue if formatting has occured
if(postPrefix != null) { // Cursed Hack
- switch(postPrefix) {
+ switch (postPrefix) {
case StringFormatEncoder.FormatSuffix.LowerRoman:
ToRoman(ref formattedString, interps, nextInterpIndex, false);
break;
case StringFormatEncoder.FormatSuffix.UpperRoman:
ToRoman(ref formattedString, interps, nextInterpIndex, true);
break;
- default: break;
}
+
postPrefix = null;
}
- //Things that aren't objects just print nothing in this case
+
+ //Things that aren't objects or strings just print nothing in this case
prevInterpIndex = nextInterpIndex;
nextInterpIndex++;
continue;
@@ -414,33 +416,40 @@ public static ProcStatus FormatString(DMProcState state) {
continue;
}
case StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle:
- case StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle:
- {
- bool wasCapital = formatType == StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle; // saves some wordiness with the ternaries below
- if (interps[nextInterpIndex].TryGetValueAsDreamObject(out var dreamObject) && dreamObject != null)
- {
- bool hasName = dreamObject.TryGetVariable("name", out var objectName);
- string nameStr = objectName.Stringify();
- if (!hasName) continue; // datums that lack a name var don't use articles
- if (DreamObject.StringIsProper(nameStr)) continue; // Proper nouns don't need articles, I guess.
-
- if (dreamObject.TryGetVariable("gender", out var gender)) // Aayy babe whats ya pronouns
- {
- if (gender.TryGetValueAsString(out var str) && str == "plural") // NOTE: In Byond, this part does not work if var/gender is not a native property of this object.
- {
- formattedString.Append(wasCapital ? "Some" : "some");
- continue;
- }
- }
- if (DreamObject.StringStartsWithVowel(nameStr))
- {
- formattedString.Append(wasCapital ? "An " : "an ");
- continue;
+ case StringFormatEncoder.FormatSuffix.LowerIndefiniteArticle: {
+ var interpValue = interps[nextInterpIndex];
+ string displayName;
+ bool isPlural = false;
+
+ if (interpValue.TryGetValueAsDreamObject(out var dreamObject)) {
+ displayName = dreamObject.GetRawName();
+
+ // Aayy babe whats ya pronouns
+ if (dreamObject.TryGetVariable("gender", out var gender) &&
+ gender.TryGetValueAsString(out var genderStr)) {
+ // NOTE: In Byond, this part does not work if var/gender is not a native property of this object.
+ isPlural = (genderStr == "plural");
}
- formattedString.Append(wasCapital ? "A " : "a ");
- continue;
+ } else if (interpValue.TryGetValueAsString(out var interpStr)) {
+ displayName = interpStr;
+ } else {
+ break;
}
- continue;
+
+ if (DreamObject.StringIsProper(displayName))
+ break; // Proper nouns don't need articles, I guess.
+
+ // saves some wordiness with the ternaries below
+ bool wasCapital = formatType == StringFormatEncoder.FormatSuffix.UpperIndefiniteArticle;
+
+ if (isPlural)
+ formattedString.Append(wasCapital ? "Some " : "some ");
+ else if (DreamObject.StringStartsWithVowel(displayName))
+ formattedString.Append(wasCapital ? "An " : "an ");
+ else
+ formattedString.Append(wasCapital ? "A " : "a ");
+
+ break;
}
//Suffix macros
case StringFormatEncoder.FormatSuffix.UpperSubjectPronoun: