Logo Pending


Space Quest 4 – Roger Wilco and the Failed Attempt at Being Cute

While playing Space Quest 4 – Roger Wilco and The Time Rippers, you travel back and forth between several different time periods, each designated by a sequel number. The introduction and ending are set in SQ4, the main plot in SQ12 – Vohaul’s Revenge II, and from there you travel to SQ1 – The Sarien Encounter, SQ10 – Latex Babes of Estros, and in an easter egg, SQ3 – The Pirates of Pestulon.

While in the SQ1 era, you revisit Ulence Flats. Literally, you arrive just after the original Roger is done there and leaves. In the SCI remake of SQ1, you actually see the time pod from SQ4 arrive right behind you when you leave the area, but SQ4 came out before the SQ1 remake so you revisit the AGI version instead. Sort of. The resolution’s all wrong, but who cares?

One thing you’ll quickly notice is that instead of the usual BorderWindow frames, this part of the game uses a custom frame meant to evoke AGI’s. It is, however, immediately clear (at least to me) that they fucked it up.

That’s one unsightly black border! Also, the window is light gray instead of white but that’s not the issue here.

If you look closely and remember what I wrote about window style bits before, you might recognize that it’s drawing a white light gray window with a red border, and then a nwTRANSPARENT window on top. What does the actual code say? Here’s Sq1Window, which is used in lieu of BorderWindow in all Ulence Flats rooms:

(class Sq1Window of SysWindow
  (properties
    underBits 0
    pUnderBits 0
    bordWid 3
  )
 
  (method (dispose)
    (SetPort 0)
    (Graph grRESTORE_BOX underBits)
    (Graph grRESTORE_BOX pUnderBits)
    (Graph grREDRAW_BOX lsTop lsLeft lsBottom lsRight)
    (super dispose:)
  )
 
  (method (open &tmp temp0 temp1)
    (SetPort 0)
    (= color gColor)
    (= back gBack)
    (= temp1 1)
    (if (!= priority -1) (= temp1 (| temp1 $0002)))
    (= lsTop (- top bordWid))
    (= lsLeft (- left bordWid))
    (= lsRight (+ right bordWid))
    (= lsBottom (+ bottom bordWid))
    (= underBits (Graph grSAVE_BOX lsTop lsLeft lsBottom lsRight 1))
    (if (!= priority -1)
      (= pUnderBits (Graph grSAVE_BOX lsTop lsLeft lsBottom lsRight 2))
    )
    ; Draw the background
    (Graph grFILL_BOX lsTop lsLeft lsBottom lsRight temp1 back priority)
    ; Draw the border
    (Graph grDRAW_LINE (+ lsTop 1) (+ lsLeft 1) (+ lsTop 1) (- lsRight 2) global131 priority)
    (Graph grDRAW_LINE (- lsBottom 2) (+ lsLeft 1) (- lsBottom 2) (- lsRight 2) global131 priority)
    (Graph grDRAW_LINE (+ lsTop 1) (+ lsLeft 1) (- lsBottom 2) (+ lsLeft 1) global131 priority)
    (Graph grDRAW_LINE (+ lsTop 1) (- lsRight 2) (- lsBottom 2) (- lsRight 2) global131 priority)
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
    ; Open a logical window for the contents to be drawn into
    (= type 129)
    (super open:)
  )
)

And here’s the trick: not only does it open a logical window after drawing it, but it opens one with the wrong style.

Fix idea #1. Open the logical window before drawing it.

(method (open &tmp temp0 temp1)
  (= type 129)
  (super open:)
  (SetPort 0)
  (= color gColor)
  ;...
)

That ain’t it. Not only does it put the contents of the window in the corner of the screen (because we’re using SetPort wrong) but when the window closes, the frame appears behind it:

No, no. The way I solved it was like this:

(method (open &tmp port temp1)
  ; temp0 was unused so we're taking it for proper SetPorting.
  (= color gColor)
  (= back gBack)
  ; Set our type to ONLY wCustom, not wCustom|wNoSave, and open.
  (= type 128)
  (super open:)
  ; Nothing will have appeared because wCustom don't draw anything, but a port has been set up!
  ; Switch to drawing on the whole screen but also *save the window's port*.
  (= port (SetPort 0))
 
  (= temp1 1)
  ; ...
  (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
 
  ; Reset to the window's port.
  (SetPort port)
)

And that fixes everything!

No extra black border, no misplaced contents, and no leftovers! If you have the CD-ROM edition, you may be able to drop this file in the game’s directory and rename it to 706.scr.

Now, eagle-eyed viewers might notice that in the very broken screenshot, the text was drawn on a white background, while the window itself is light gray. This is because each graphical port in the system tracks its own color settings, among other things. A logical window is a kind of port, as is the screen as a whole. Since the broken version called (SetPort 0) after (super open:), its contents were drawn on the screen port, not the window’s. And the screen port, by default, has a white background color.

Shoutouts to the Space Quest Historian for putting me on this track with his playthrough video. And as such, see ya on the Chronostream, time jockeh!

[ , , ] 2 Comments on Space Quest 4 – Roger Wilco and the Failed Attempt at Being Cute

Frame sizes

And I don’t mean graphical windows.

I’ll admit it, I’m only passingly familiar with the Sierra PMachine’s internal workings. I know much more about the SC script code than the PMachine bytecode it compiles to. Specifically, I know it’s a stack machine with a single accumulator and that literally everything is a 16-bit value but while researching that thing from last time I finally figured out how sends work.

Let’s go through some examples of various things you might do in SCI code.

Let’s say we have a local variable that we want to set to a particular value: (= aLocal 42). Simple enough, right? That translates to ldi 42, sal aLocal. Well, technically the disassembler has no notion of what the local variables are named so that’d actually come out as sal local0 or whatever our local’s place in the list is but the instructions are clear: set the accumulator to a value, then store the accumulator in a local variable. A global variable is much the same but would use sag instead.

Here’s a slightly more complicated one:

(if aLocal
	(Printf "lol")
)
_:
	lal aLocal
	bnt notTrue
	push1 
	lofss strLol
	calle 921 1 2 //Printf, script 921 export 1.
notTrue:
	//rest of the script

See what’s going on there? First we use lal to load a local var’s value to the accumulator. Branch to another spot if that value is not truthy (non-zero), skipping over the Printf. Then we first push the amount of arguments given (just the one), and load the offset of our string to the stack. Then we call an external function by number. With one 16-bit argument on the stack taking two bytes, we look back that far plus another two to reach the 1, which is our argument count, so that Printf can later tell how many arguments it was given. Likewise, (Printf "lol" 42) would become

_:
	push2 
	lofss strLol
	pushi 42
	calle 921 1 4

We pass a frame size, as they’re called, of four, so we look back that many bytes plus two in the stack, passing an argc of two, a pointer to our string, and the number 42 to Printf.

How about a little trickery? (Printf "lol" (+ aLocal 1)) perhaps?

_:
	push2 
	lofss strLol
	lsl aLocal
	ldi 1 
	add 
	push 
	calle 921 1 4

So first we load a local to the stack, not the accumulator, then load the immediate value 1 to the accumulator (there is no accumulator version of push1 after all). The add command takes the value on top of the stack and adds it to the accumulator, leaving the result in the accumulator. Then push takes the accumulator and puts it on the stack, thus giving a stack of two arguments, the offset to “lol”, and the value of aLocal plus one. We look back four bytes, plus another two, and we know what to tell Printf.

And that’s how Kawa learned what a frame size actually is.

[ ] Leave a Comment

Selectors and different ways to push them

Earlier today, some 19 hours ago at the time of writing, Eric Oakford opened an issue on the SCI Companion GitHub repo. Eric is working on a big decompilation project, taking mostly demo versions of SCI games and trying to wrangle them into a recompilable state.

In the issue he’d opened, Eric described how the demo version of Leisure Suit Larry 3 didn’t decompile right, while the actual LSL3 worked fine. Here’s an example of the problem:

One of the first things a Room instance would normally do in its init method is to call (super init:), letting the Room class itself do its setup before anything specific to that room is done, like setting up actors, scripts, features, and walk polygons. In PMachine byte code that statement looks like this:

39 57       pushi 87    // the "init" selector
76          push0       // init takes no parameters
57 36 04    super Rm 4  // four bytes (two words) worth of stack to send

Now, the SCI PMachine is a stack machine, with two parts that are important to know about: the stack (because duh) and the accumulator. You can load numbers onto the accumulator, push them directly onto the stack, push the accumulator onto the stack, duplicate the top item, etcetera. Every value on that stack is a 16-bit number, as is the accumulator. Pointers? Just 16-bit numbers. Characters returned by StrAt? 16-bit numbers, even if it’s just ASCII codes. Properties and methods to invoke in a send? Yup.

There’s a separate table, vocabulary 997, that lists the name of every selector — every method and property of a class or instance. And that’s where it says that selector #87 is init.

Noting that superself, and send are all three sides of the same weirdly-shaped coin, the decompiler can tell that there should be a (super ...) command in the output, four bytes of stack space back. Since it’s not actually running anything it has no actual stack, but it can look back to find two push operations that’ll fit the bill just as well. It can tell that the first value should be a selector, so whatever is being pushed is taken to be one, which is correct — 87 is the init selector. Then the next value is the amount of arguments given to init, which is zero.

But everything went wrong in the demo. This is how rm200, the overlook with the binoculars and memorial plaque, starts in the demo:

35 57       ldi 87
36          push 
39 00       pushi 0
57 36 04    super Rm 4

The actual values on the stack stay the same — 87 0 — but the way they’re put on there is subtly different, and that tripped SCI Companion up.

Instead of (super init:), it decompiled the above as (super species?).

In fact, all selectors in code blocks were species. Six hours ago at the time of writing, I figured out the problem.

Now, the SCI Companion code is super hairy and I really couldn’t do it justice by just including snippets here but the gist of it?

Every operation may have a couple operands. One method in the decompiler returns what the first operand for a given operation in the byte code may be. For pushi, that’d be the immediate value to push. For push0push1, and push2, that’d be zero, one, and two. For ldi it’s exactly the same as pushi, just that the operand is to be put in the accumulator, not the stack. By default, this method just assumes zero.

Notice how push has no operands at all? Why would it? It pushes the accumulator’s value, after all. The only difference between pushi 87 and ldi 87, push is that in the latter case, the accumulator is also 87. The accumulator doesn’t matter to a send, only the contents of the stack. And pushi 0 is just push0 with one extra byte. And that makes these two snippets effectively the same with regards to actual execution in an SCI interpreter.

So what happens when the decompiler sees the LSL3 demo’s scripts, is that it looks back for two pushes, as it should. It finds the first, push, which should be a selector. But the helper method that returns the value being pushed can’t return any operand — there are no operands here! So it returns the default, zero. Some confusion about it possibly being a variable later, it decides that it must be species. And then it does this for all the sends in the demo, because they all push their values the same way.

The fix came to me when I saw the case for the dup operation in that very same method that’s supposed to return the value that’d be pushed. It too takes no operands, yet does return a value that’s only zero if it should be. Turns out it scans back a bit, looking at the previous operation in the bytecode stream, and steals its value by calling the same helper method again, but aimed at the previous operation. The fix then is to make push also steal its predecessor’s value. I did decide to special-case things for now, though. It’ll only do the stealy thing if it’s an ldipush pair, like in the LSL3 demo.

But it does work.

 

Addendum: You might wonder why the incorrect decompilation was (super species?) with a question mark instead of a colon. The decompiler and interpreter alike can tell which selectors on a given class are supposed to be properties, and which are methods. When invoking a method or setting a property, the standard is to use a colon, like in (theSong number: 4 play:), which is a property set followed by an arg-less method, and to use a question mark for property gets, like in (= theX (gEgo x?)). And since species is a property and there was no argument, it was taken to be a property get.

[ , ] 1 Comment on Selectors and different ways to push them

Noses are a waste of bandwidth

<Maxane> But then again, rather not ;-p
<Kawa> NOSES ARE A WASTE OF BANDWIDTH

So yeah. That’s me referencing some old IRC thing moments before I decided to write this post, but it got me thinking. There’s the joke that adding a nose to your classic :) smileys is a waste of bandwidth, but how does it stack up against modern emoji?

This is just a short observation, mind you. Done in minutes.

Display Characters UTF8 bytes
:) 2 3A 29
:-) 3 3A 2D 29
🙂 1* F0 9F 99 82

There’s really not much to it, innit?

* depending on how you define ’em.

[ ] Leave a Comment

The Universal Translator

The Universal Translator, as seen in Star Trek, regularly raises questions.

For example, why do the users not hear double voices? But more commonly, how do characters keep saying untranslated klingon words? How can they keep saying p’takh? How can Worf call his embarrassing pimple a gorch without the UT biting him in his klingon ass and making him call it a pimple?

@RikerGoogling has repeated the question a couple times, why the UT doesn’t seem to work on Klingonese. I keep replying, it’s intentional on the speaker’s part. But let’s me go into way too much detail.

How the UT determines intent, I don’t know. It doesn’t matter. Maybe something else makes it skip those words,  but think about it: how would you explain something about a particular language when every word you say ends up rendered in English, or whatever the listener speaks? It’s not just Klingonese either — Picard would sometimes curse in French, right there on the bridge. Likewise, the senior crew singing “For He’s a Jolly Good Fellow” in Klingonese? They meant for it to remain the way they said it, so it was. Even if their manual translation was a bit rough.

So what about the lack of overlapping sounds? Well, I don’t care what you think about Discovery, but one particular scene stood out to me.

You know those channel logos in the corner of the TV screen? Or for you viewers of certain Japanese imports, the (much more obvious) clock? Eventually, you kinda stop seeing them. They’re still there, but you don’t actively register them like you do the rest of whatever it is you’re watching. Or in a cheap dub where the original voice tracks are faintly heard underneath the louder dub track. Background noise, this mild headache I’ve had since before I care to remember, it all just gets tuned out. And that brings me to that one scene in Discovery.

Michael Burnham sneaks onto the klingon vessel and eventually takes out her communicator, enabling its UT feature. We hear the communicator repeat what the klingons say, in English, with a slight delay and text-to-speech quality. Revealing herself, Burnham addresses the leader in English, only for the communicator to echo her words in Klingonese. And shortly into the conversation, the delay disappears and in fact, the klingon leader himself starts speaking fluent English.

I think that’s meant to represent Burnham, but perhaps mostly the viewers, getting used to the UT’s effect. Eventually, you don’t even hear the original lines any more, and your mind just ignores the delay.

There’s only one counter-argument that comes to mind: people without universal translators who listen to people with. Ferengi speaking their own language to 20th century humans, who (once the devices are fixed) can understand the ferengi perfectly well? All the above can’t explain that.

But at least the thing where they keep speaking untranslated klingon is covered.

[ ] Leave a Comment

No start!

Back in February there was this whole deal on Twitter about the Konami Code, when the person who introduced the thing passed away, and I wondered if I was an asshole for being bothered by all of these people including the Konami PR guys posting the Code… with a start at the end.

Because the Code, as Kazuhisa Hashimoto originally introduced it, never included the start button. And I brought receipts! So here’s a quick rehash of what I wrote the day after.

This is part of the 6502 code for Contra, where it determines how many lives you should start the game with. As you can see the amount of lives is stored at address $32 and is set to either 2 or 29 depending on the value of address $24, where it tracks if the Code had been entered.

This is a view of the actual memory of the NES, or at least the relevant part of it. The right half had been cut off for size in the original tweets, but none of the values we’re looking at are on that side anyway. Focus is on $24, the Code flag, which is unset.

Entering the Code flips $24 to 1 when you press A. The only thing the start button does is… start the game. That includes running the code above to initialize the lives counter.

“But Kawa, what about Gradius?” you might ask. Well, you go start up Gradius and enter the code, and tell me what you see. You begin the game, press start to pause, enter the code, and press start to unpause. Don’t enter those button presses too quickly or you might not catch on and fool yourself! Turns out the options and such appear the moment you press A.

Here’s the RAM for Gradius during a pause. $33 tracks how far along the Code entry you are and ranges from zero up to nine. When it’s zero, the next button expected is up. When it’s nine, the next expected input is A. You finish, $33 equals ten, and your loadout changes on the spot:

Also the tracker is reset to zero so you can enter it again. Now, in Gradius you can freely make a mistake and try again because the tracker just resets to zero when you mistype. In Contra, the tracker ($3F so it’s not visible in those screenshots) is set to 255 as a “lock” value when you mess up and you don’t get to try again.

But yeah, no start in the Konami Code. Your tattoo is now ruined.

↑↑↓↓←→←→ⒷⒶ

[ , , , ] Leave a Comment

How to SCI – Old vs New

To make an SCI game today, you can just grab a copy of SCI Companion and use that to import graphics and music, do the scripting, text… basically everything but drawing bitmap-based backgrounds. You get a copy of the interpreter matching the template you chose, the system scripts are all set up for you, and you can just hit Compile, watch it work, and it’ll even automatically run the game for you in DOSBox, perhaps even starting you off in a specific room.

But dear lord do we have it easy nowadays.

In the old days, making an SCI game involved several separate utilities, many of them interface-less command line tools, and a particular network setup. That is, the tools expect to be invoked from a specific hard drive letter, as they are provided from one point of the network. There’s another where the system programmers keep the latest builds of the interpreter and system scripts, and the team for a given game has a batch script file to pull the latest into that game’s working directory. Writing the actual script code is roughly the same as it is now, but instead of a dedicated script editor they mostly used Brief. To test their changes, the programmers had to invoke SC, the Script Compiler. Given that Brief was apparently pretty extensible, this could probably be done from there.

While we mostly work directly on RESOURCE.### files, Sierra’s games were developed what I call loose-leaf style. Each type of resource was stored in its own folder, and a “wherefile” specified where each of them could be found — they were basically just RESOURCE.CFG by another name, really. And that name was literally WHERE. Turns out you can specify which configuration file you want the interpreter to use.

view=../view
sound=../sound
pic=../pic
font=../system
cursor=../system
videoDrv=/system/ega320.drv
soundDrv=/system/std.drv
kbdDrv=/system/ibmkbd.drv
joydrv=/system/joystick.drv

To make the game, they didn’t use makefiles. They used batch scripts that invoked SC and compiled the .sc source files to .scr files in the SCRIPT directory, and copied over the script resources from SYSTEM.

Given how relative paths work, running another particular batch file would run the interpreter from one directory while in another, from which point the paths given above can be considered valid. Since they didn’t have the “start at the room specified in this file” feature that SCI Companion’s template game adds, we get the game-specific debug modes that ask for a room number on startup, as extensively documented elsewhere.

And then, when the game is considered fit to ship, they build a list of which resources go on which disk, pass that to yet another command-line tool, which goes through all that and produces the RESOURCE.### files. Copy the result and there you go.

That list does not need to include all resources though. Indeed, as there are things that are included in the game data but left unused, there are some things that never got on a release disk in the first place. Let’s just say some things in the Larry source assets are even raunchier than you’d expect.

[ , ] Leave a Comment

On object lists, meta-tiles, and Mario

A fun fact about most 2D Super Mario platform games is that they all share a common way of storing their level data. A common paradigm as it were. Only the Game Boy games don’t.

If ROM-based games load so fast compared to disk-based games, why does Super Mario Bros 1 make you wait on a mostly-black screen before you get to play a given level? Why does Super Mario World? Surely it’s doing more than just sitting idly?

The answer? Besides graphics in the case of later games, it’s converting the level map from one format to another. From a list of objects to a tile map, to be precise. That brick-block-brick-block-brick line we all know and love from SMB1‘s world 1-1 for example? Five tiles, but only three objects. First, a brick object set to five tiles wide. Then two separate question block objects that overlap the five bricks. On load, these objects are rendered into a tile map.

(Now, SMB1 didn’t have the space to hold an entire converted level in memory and only had a screen or two at once, which is why you can’t backtrack. So in SMB1‘s case, it does in fact sit idly. Thanks to NovaSquirrel for mentioning that.)

While the NES has 8×8 pixel tiles, the map this object list is rendered to has 16×16 pixel tiles. It is what some would call a meta tile map, where each entry itself refers to a different data structure that says “meta tile 2 has this color palette and is built from these four tiles”. That’s the map format a great many games of the era and later use. When an area is about to scroll into view, that tile map is then quickly converted to VRAM-native tiles. And that’s how you can have a set of three coins be defined as one object, yet pick each coin up separately, or have a strip of bricks that you can individually break. And since that alters the big tile map in memory, if you were to backtrack (even though you can’t do that in SMB1, as mentioned, but you can in the later games) those coins and bricks would not reappear.

Sprite objects come in a separate list, usually after the level geometry, and at least for the “classic” games they are subdivided into pages, about a screen wide. They’re only instantiated when their page is just off-screen, and they’re not marked as properly dead. Which is why if you knock out a koopa trooper but leave him there, go about a screen away then double back, the trooper will be back in his starting position and perfectly fine. Your leaving that page made him despawn without marking as properly dead.

Now, the Super Mario Land games… they do what they want. SML1 for example subdivides levels into screens, which are lists of strips that can be reused at will. I think the screens themselves can also be reused. And that is then converted to a regular tile map. The original Legend of Zelda used a similar strip-based layout.

I think I remember Super Mario Land 2 used straight-up 16×16 pixel tile maps for its level geometry. Both of these methods are still better than storing several screens worth of tile map in its native size.

Using straight-up tile maps of any resolution is of course a common technique used by many games. As a rule, the larger your levels can be the larger you want your meta tiles to be. Sonic the Hedgehog has positively huge meta tiles, themselves defined in terms of smaller tiles, since your average speed almost requires levels be large to accommodate. And it makes constructing those loops easy as a bonus. Most NES games tend to have 16×16 pixel meta tiles though, because of the attribute map being that size.

[ , ] Leave a Comment

Ball Road

The dictionary for AGI and SCI games’ text parser input is stored in alphabetical order. This allows a prefix-based compression:

  • another
  • any
  • appear
  • appearance
  • apple
  • at
  • attack

Though the formats for the two engines’ dictionaries are completely different, they share this one aspect. Each of these words is then assigned a group number which is then used to store the said specs. I’ve written about that before. The thing is that when you decompile a game script, you can’t tell which synonym from a given group was originally used. And that’s why when you decompile Leisure Suit Larry 2 and look in the scripts regarding really any female character you’ll see them being called bimbos.

(if (or (Said 'call/bimbo,agent') (Said 'get,buy/ticket'))

That is of course because “bimbo” is in the same group (#42) as “woman” and “lady”, but alphabetically comes before them. “Agent” is in its own group (#50) together with various other jobs. You can call this particular woman either by gender or by profession. You can even call her a KGB agent and the game will allow it. By that same token, “call” is in the same group as the “talk” you’d expect to see here (#11).

But the decompiler has little to no idea of these things.

I have recently acquired the full source code for Larry 2, and that shows a slightly different, more sensible word choice:

(if (or (Said 'talk/girl, clerk') (Said 'get, buy/ticket'))

That is of course because these are the actual word groups being used here:

11 42 50
talk
speak
converse
call
woman
girl
lady
stewardess
blond
chick
blonde
slut
broad
bimbo
maid
receptionist
secretary
mother
mama
momma
mom
clerk
waiter
waitress
bartender
storekeeper
shopkeeper
agent
kgb
kgbishna
custom
attendant
shark

You can see how alphabetical order would mess that up.

And by that same token I can now securely say that the debug cheat code in Larry 3 is not in fact “ascot backdrop”.

“Backdrop” in Larry 3 is in group #1063 together with “put”, “drop”, “release”, “set”, “stash”, and various other “put something here” verbs. You know by now how the smart fella who discovered the debug cheat may have gone about it, and how “backdrop” would be the first word in that group. The canonical phrase however, is

Ascot Place

Because of course if I have the Larry 2 code why wouldn’t I have Larry 3 as well?

(cond
  ((Said 'ascot/place')
    (^= debugging TRUE)
    (if debugging
      (Print "Hi, Al!")
    else
      (Print "\"Goodbye.\"")
    )
  )

The question is… why is this the debug phrase?

And the answer? It’s a callback to Larry 2:

And just like that this post’s title makes a little sense.

[ , , , ] Leave a Comment

More pronoun problems in Ranma fanfics

(Edited from a Twitter rant in ten parts.)

One thing I find linguistically interesting about Ranma ½ fan fiction, especially most of the more recent works, is that they make a big fucking deal of Ranma’s pronouns. Mind you, these stories are set in Japan, starring Japanese characters, speaking Japanese. It’s all just rendered in English because Internet.

Third-person pronouns in English have genders. He/she and such, you know the ones. Since these stories are written in English, you’ll often find characters refer to Ranma with one pronoun or the other. No problem there, the original manga and anime do it too. But there’s a twist.

There are lots of stories about Ranma being transgender, especially in recent years as far as I’ve seen. Which is totally understandable, really. That’s not the problem. Write about transgender Ranma all you want. The problem, at least to me, is when characters start mentioning how other characters use this or that pronoun to refer to Ranma.

(This of course applies not just to Ranma but to any other character who shares the same curse. Let’s keep it simple, though.)

Worse, for the purpose of this rant, is this one story where Ranma joins a support group for LGBTQ people and the members all introduce themselves and state their pronouns. See, if these are Japanese characters (they are) speaking Japanese (this is implied), and my research is correct (I can only hope), that is literally not a thing they could do.

Where in English it is the third person pronouns that are gendered (he/him, she/her), Japanese has them in the first person. Ano hito, yatsu, and koitsu, those are all gender-neutral. Boku, watashi, and atai, are all gendered. And that’s just a small sample of first person pronouns.

So the very first time someone like Ranma opens his pie hole and speaks of himself, he’ll use whatever pronoun he wants. That’d be ore, a very manly one, as in “ore wa otoko da,” “I’m a guy.” It’s when Ranma uses a feminine pronoun that the eyebrows rise. Mind the phrasing there!

I was reminded of the Twitter rant this post is adapted from by another fanfic I read last night, where Genma caught himself thinking about his recently-cursed child with female pronouns. As in, the English third-person ones. It didn’t do much to damage the scene or anything but I felt mildly distracted by the idea that a Japanese man would think in English terms.

There are in fact fanfics, written in English, where Ranma will say something and maybe there’s something about the phrasing in English, and another character remarks that Ranma used a feminine pronoun, perhaps even saying the pronoun itself in Japanese, in the middle of a story otherwise written in English. Just as an example: “Ranma used atashi just now instead of ore, and he’s not trying to trick Ryōga. Something’s going on here.” Something like that. It’s quite interesting how you might handle this difference.

The episode Am I Pretty comes to mind, where Ranma’s entire way of talking changes right along with his first-person pronoun. I only watched it in Japanese, but I’d imagine the dub just only has his way of talking change. If you’ve seen it dubbed, feel free to let me know how they handled it in the comments.

Suffice it to say, as weird as pronouns can get in one language, it gets so much weirder when there’s two in play.

[ , , ] Leave a Comment