Assuming that gameData.title is an array of pointers to char arrays
Then gameData.title[0] holds a pointer to a char array that exists "somewhere" in memory. Let's say the title is "Foobar" and this is where it is in memory:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
Let's say that the bytes, or chars, immediately before and after the title are unset and unused by the program at this very moment in time, and that the empty slots above for them means just that.
Now, let's call the pointer to a char array that is in gameData.title[0] for p1. It is pointing at the "Foobar" char array, like this:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+-----------+
|
[ ][ ][ ][ ][ ][ ][ ][p1][p2][p3][p4][ ]
The pointers p2-p4 are the rest of the char pointers in the gameData.title array.
Now, you have 2 more interesting pointers in your scenario. First, the roomData.title char pointer. Let's call it pA for simplicity. When you pass it to GameGetCurrentRoomTitle, it's unset and points at an undefined char in the memory space:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+------------+
|
[ ][pA][ ][ ][ ][ ][ ][p1][p2][p3][p4][ ]
|
+---------+
|
v
[ ][ ][ ][ ]
Second, the GameGetCurrentRoomTitle title char pointer parameter. Let's call it pB for simplicity. When you pass pA as an argument to GameGetCurrentRoomTitle, its value is copied to pB. Its value is not the contents of the memory it points at, but the address it points at. This means that pB will point at the same undefined char in the memory space as pA does:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+-------------+
|
[ ][pA][ ][ ][ ][pB][ ][p1][p2][p3][p4][ ]
| |
+---------+--+
|
v
[ ][ ][ ][ ]
Next, in the body of GameGetCurrentRoomTitle, you reassign pB (the local title char pointer) to point at the same memory location as p1 (the pointer to a char array that is in gameData.title[0]):
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+------+------+
| |
[ ][pA][ ][ ][ ][pB][ ][p1][p2][p3][p4][ ]
|
+---------+
|
v
[ ][ ][ ][ ]
And, when GameGetCurrentRoomTitle finally returns to its caller, pA is still pointing at the same undefined memory location.
The fix you miss is to pass a pointer to pA to GameGetCurrentRoomTitle instead of just pA itself, and instead of reassigning pB to point at the same memory location as p1, you should dereference pB and assign p1 to that memory location, which is the memory location where the value of the pA variable is stored. That way, when GameGetCurrentRoomTitle returns, pA will see the string you expected.
With "graphics", the new situation would be that pB doesn't point at a char array, but at a pointer to a char array. We know that pA is that pointer to a char array:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+-------------+
|
[ ][pA][ ][ ][ ][pB][ ][p1][p2][p3][p4][ ]
^ |
| |
+------------+
By assigning something to the dereferenced pB, you effectively assign to the memory location it points at, not to pB itself. So assigning p1 to the deferenced pB is the same as assigning it to pA in this scenario:
[ ][ ][ ][ ][F][o][o][b][a][r][\0][ ][ ][ ][ ]
^
|
+-----+-------------+
| |
[ ][pA][ ][ ][ ][pB][ ][p1][p2][p3][p4][ ]
And in code:
GameGetCurrentRoomTitle(&roomData.title); // pass pointer to 'pA'
int GameGetCurrentRoomTitle(char **title) // 'pB' is a pointer to pointer
{
*title = &gameData.title[0]; // assign 'p1' to dereferenced 'pB'
return strlen(*title); // measure dereferenced 'pB'
}