Submarine bug investigation

Antal1987

Warlord
Joined
Sep 4, 2013
Messages
160
Location
Karelia, Russia
I'm investigating the submarine bug using very simple scenario:

Simulation scene:


Every time I skip turn, the Galeon tries to move througn the submarine.
However, if I replace the submarine with the carrier, it doesn't happen.

I started to figure out why this is happening

I found out that every time the Game computes possible movements for AI unit it builds a movement graph. The graph contains information about current unit tile neighbours. For Naval or Air unit radius (the depth) of graph searching (in affinite coordinates) is 7. For Land unit it is 9.

Graph searhing are implemented in Trade Net class:

Code:
#pragma pack(push, 1)
struct class_Trade_Net
{
  int vtable;
  int Map_Width;
  int Current_Unit_X;
  int Current_Unit_Y;
  class_Unit *Current_Unit;
  int Current_Unit_CivID;
  int Flags;
  int *Data1;
  struct_Trade_Net_Distance_Info *Data2;
  struct_Trade_Net_Distance_Info *Data3;
  int V1;
  struct_Trade_Net_Distance_Info *Data4;
  struct_Trade_Net_Distance_Info *Data5;
  int V2;
  int Matrix[262144];
};
#pragma pack(pop)

The address of Trade_Net in C3C v1.22: 0x00B72848
Matrix is array of int[512][512] - it containt info about Cities in Trade Net. That's why max cities count is 512 (0x200)
The address of Trade_Net.Matrix: 00B72880

I don't know yet what are those Data2, Data3, Data4 and Data5. But for naval unit Trade Net uses Data2 as Global Tiles Distance Info (Data_R1) and Data3 as Current Depth Distance Info (Data_R2).

In certain conditions (I don't known them) Data_R1 = Data4; Data_R2 = Data5;

Here is the structure of Distance Info Item:

Code:
#pragma pack(push, 1)
struct struct_Trade_Net_Distance_Info
{
  __int16 field_0;
  char Remained_Moves;
  char Direction;
  __int16 field_4;
  __int16 Moves;
  __int16 field_8;
  __int16 field_A;
  __int16 Tile_Index;
  __int16 Radius_Neighbour_Index;
};
#pragma pack(pop)

I suppose Trade Net uses kind of breadth-first search (BFS) algorithm (see)

BFS calculates distances and weights (masses) of vertices level by level. A level is a group of vertices with the same distance. So in C3C it calculates for each Tile in Radius optimal direction to the graph root (Current Unit Tile).

Here are directions values used by Trade Net BFS. Lets call it Graph Direction. Because there are Movement Directions.


Movement Directions:
Code:
		8		  
	7		1	
6		0		2
	5		3	
		4

And here is part of result given by Trade Net BFS when it finished. (Sorry about that handwritten image). The image corresponds to simulation scene.
Source - Current Unit (the Galeon)
Subm - the Submarine
Target 1 and Target 2 are the points to which the game calculates routes.
W - weight, means movement cost
D - Direction to root


Well, there is a function which returns optimal movement direction from origin position (source) to target. It also returns movement cost.
Code:
int __thiscall class_Trade_Net::Get_Optimal_Direction(class_Trade_Net *this, int X, int Y, int Target_X, int Target_Y, class_Unit *Unit, int CivID, int Flags, int *p_Moves)

X, Y - Origin Position
Target_X, Target_Y - target position
Unit - Current Unit (the Galeon)
CivID - Current Unit Civ ID
Flags - ???
p_Moves - pointer to variable used to store movement cost.

Returns movement direction
It's address: 0x005802A0.
I do not share the code here, because it's too big and it has many different calls to relevant functions which are big too.

Let's look at the simulation scene.
The Galeon has coordinated [0x1C, 0x46]
The Submarine has coordinated [0x1D, 0x47]

class_Trade_Net::Get_Optimal_Direction(Trade_Net, 0x1C, 0x46, 0x1D, 0x47, Galeon, Galeon.Civ.ID, Flags, &Moves) == 3
class_Trade_Net::Get_Optimal_Direction(Trade_Net, 0x1C, 0x46, 0x1E, 0x48, Galeon, Galeon.Civ.ID, Flags, &Moves) == 3

But if I replace the submarine with the carrier
class_Trade_Net::Get_Optimal_Direction(Trade_Net, 0x1C, 0x46, 0x1D, 0x47, Galeon, Galeon.Civ.ID, Flags, &Moves) == 0
class_Trade_Net::Get_Optimal_Direction(Trade_Net, 0x1C, 0x46, 0x1E, 0x48, Galeon, Galeon.Civ.ID, Flags, &Moves) == 0

If the game takes 0 as optimal direction it doesn't use the corresponding tile into future graph calculations. So because of Get_Optimal_Direction returning 0 (when the carrier is near by the galeon), the galeon doesn't move anywhere, it just skips turn.
But if it's the submarine near by, the galeon "thinks": "the road is open, let's move out"...

I've manage to found a root of that behaviour.
There is a function, which checks whether some unit occipies a tile.
int __stdcall f_Get_Tile_OccupantID(int X, int Y, int CivID, bool Flag)
it's address: 0x0056D740
X, Y - coordinates of tile being checked
CivID - Id of civ which requests the game of checking the tile.
In this case, CivID = Galeon.Civ.ID
Flag - ??? (does't affect on result)

it returns -1 if there are no visible units in [X, Y]
If in [X, Y] there is at leat 1 visible unit of some Civ, it returns ID of that Civ.
It returns 0 if a unit in [X, Y] has Hidden Nationality flag.

Here are some tests:
f_Get_Tile_OccupantID(Submarine.X, Submarine.Y, Galeon.Civ.ID) == -1
f_Get_Tile_OccupantID(Submarine.X, Submarine.Y, Submarine.Civ.ID) == 1 (Player Civ)
f_Get_Tile_OccupantID(Carrier.X, Carrier.Y, Galeon.Civ.ID) == 1 (And here is the root of misbehavoiur)
f_Get_Tile_OccupantID(Galeon.X, Galeon.Y, Galeon.Civ.ID) == 3 (Inca Civ)

Here is the code:
Code:
int __stdcall f_Get_Tile_OccupantID(int X, int Y, int CivID, bool Flag)
{
  int v4; // ebx@0
  int v5; // edi@1
  int v6; // esi@1
  __int16 v7; // bp@1
  unsigned __int16 v8; // ax@1
  class_Tile *v9; // eax@1
  class_Tile *v10; // eax@2
  signed int _Unit_Civ_ID; // esi@2
  class_Tile *v12; // eax@3
  class_Tile *v13; // eax@4
  int _Civ_ID_Hidden; // ebp@4
  class_Tile *v15; // eax@5
  int v16; // eax@5
  int _Tile_UnitID; // eax@5
  class_Unit *_Tile_Unit; // esi@6
  struct_Base_List_Item *v19; // eax@14
  class_Tile *v20; // eax@20
  int v21; // eax@20
  int i; // eax@20
  void *v23; // eax@24
  class_Unit *v24; // esi@25
  class_Tile *v25; // eax@35
  int result; // eax@36
  signed int v27; // [sp+Ch] [bp-4h]@4

  v5 = Y;
  v6 = X;
  v7 = X >> 1;
  v8 = (X >> 1) + Y * (BIC_Data.Map.Width >> 1);
  LOWORD(X) = X >> 1;
  v9 = class_Map::GetTile(&BIC_Data.Map, v8);
  if ( class_Tile::Check_City(v9) )
  {
    v10 = class_Map::GetTile(&BIC_Data.Map, (v7 + v5 * (BIC_Data.Map.Width >> 1)));
    _Unit_Civ_ID = v10->vtable->m69_get_Tile_City_CivID(v10);
  }
  else
  {
    v12 = class_Map::GetTile(&BIC_Data.Map, (v7 + v5 * (BIC_Data.Map.Width >> 1)));
    if ( v12->vtable->m26_Check_Tile_Building(v12) )
    {
      v13 = class_Map::GetTile(&BIC_Data.Map, (v7 + v5 * (BIC_Data.Map.Width >> 1)));
      v27 = v13->vtable->m70_Get_Tile_Building_OwnerID(v13);
      _Civ_ID_Hidden = -1;
      if ( !class_Map::f18_Check_XY_Valid(&BIC_Data.Map, v6, v5)
        || (v15 = f_Get_Map_Tile_by_XY(v6, v5),
            v16 = v15->vtable->m40_get_TileUnit_ID(v15),
            _Tile_UnitID = class_TileUnits::Get_Unit_ID(&Tile_Units, v16, &Y),
            _Tile_UnitID == -1) )
        goto LABEL_40;
      do
      {
        _Tile_Unit = class_Base_List::Get_Item(&Units, _Tile_UnitID);
        if ( _Tile_Unit && (!Flag || CivID == -1 || class_Unit::Check_Unit_Visibility(_Tile_Unit, CivID, 1)) )
        {
          _Civ_ID_Hidden = class_Unit::Check_Hidden_For_Civ(_Tile_Unit, CivID);
          if ( _Civ_ID_Hidden )
            break;
        }
        if ( Tile_Units.Base.Items && Y >= 0 && Y <= Tile_Units.Base.LastIndex )
        {
          v19 = &Tile_Units.Base.Items[Y];
          Y = v19->V;
          _Tile_UnitID = v19->Object;
        }
        else
        {
          _Tile_UnitID = Tile_Units.DefaultValue;
          Y = -1;
        }
      }
      while ( _Tile_UnitID != -1 );
      if ( _Civ_ID_Hidden != -1 )
        _Unit_Civ_ID = _Civ_ID_Hidden;
      else
LABEL_40:
        _Unit_Civ_ID = v27;
      v7 = X;
    }
    else
    {
      Y = -1;
      if ( class_Map::f18_Check_XY_Valid(&BIC_Data.Map, v6, v5) )
      {
        v20 = f_Get_Map_Tile_by_XY(v6, v5);
        v21 = v20->vtable->m40_get_TileUnit_ID(v20);
        for ( i = class_TileUnits::Get_Unit_ID(&Tile_Units, v21, &X);
              i != -1;
              i = class_TileUnits::Get_Unit_ID(&Tile_Units, X, &X) )
        {
          if ( Units.Items )
          {
            if ( i >= 0 )
            {
              if ( i <= Units.LastIndex )
              {
                v23 = Units.Items[i].Object;
                if ( v23 )
                {
                  v24 = (v23 - 28);
                  if ( v23 != 28 )
                  {
                    if ( !Flag || CivID == -1 || class_Unit::Check_Unit_Visibility(v24, CivID, 1) )
                    {
                      Y = class_Unit::Check_Hidden_For_Civ(v24, CivID);
                      if ( Y )
                        break;
                    }
                  }
                }
              }
            }
          }
        }
      }
      _Unit_Civ_ID = Y;
    }
  }
  if ( _Unit_Civ_ID == -1
    && (v25 = class_Map::GetTile(&BIC_Data.Map, (v7 + v5 * (BIC_Data.Map.Width >> 1))),
        LOBYTE(v4) = (CivID == -1) - 1,
        v25->vtable->m7_Check_Barbarian_Camp(v25, CivID & v4)) )
    result = 0;
  else
    result = _Unit_Civ_ID;
  return result;
}

Function class_Unit::Check_Unit_Visibility(class_Unit * this, int CivID) returns 0 - if unit is invisible for CivID, and 1 - otherwise.
class_Unit::Check_Unit_Visibility(Carrier, Galeon.Civ.ID) == 1
class_Unit::Check_Unit_Visibility(Submarine, Galeon.Civ.ID) == 0

Therefore, the first solution can be changing the code to make f_Get_Tile_OccupantID return Submarine.Civ.ID instead of -1. That will make the Galeon skip turn instead of triyng pass the tile where the submarine is situated.


P.S. Do not quote the entire post. Shrink it. :)
 
I'm impressed! Does the code you currently have decifered explain why the human player gets a warning when trying to move a unit into a tile occupied by an invisible rival unit? Or does these code segments only apply to the AI behaviour?

Unless I'm mistaking, this bug was fixed by Firaxis (in Play the World?) and re-introduced in a later patch/expansion pack (Conquest?). You probably knew that already, just mentioned it in case you didn't.

PS. Lovely to finally be able to play Civ3 without pollution. Thanks!
 
Is there C3C build newer than 1.22?

No, there isn´t any newer version I´m aware of.

Now I have access to the CDs needed to start some old Versions of Civ 3. In a short test-scenario I created for Civ 3 Vanilla (where the bug was fixed by Firaxis, as I posted in another of your threads) the AI surface ship always run around my sub. It never stopped before it, it never moved over my sub, it always bypassed the sub when possible. When this bypassing wasn´t possible, the AI surface ship didn´t move.

I soon will have a look on the behaviour of subs in PtW.
 
No, there isn´t any newer version I´m aware of.

Now I have access to the CDs needed to start some old Versions of Civ 3. In a short test-scenario I created for Civ 3 Vanilla (where the bug was fixed by Firaxis, as I posted in another of your threads) the AI surface ship always run around my sub. It never stopped before it, it never moved over my sub, it always bypassed the sub when possible. When this bypassing wasn´t possible, the AI surface ship didn´t move.

I soon will have a look on the behaviour of subs in PtW.

What is Civ3 Vanilla? I don't clear understand. Was it released by Firaxis? And what version does it have?

Unfortunately, viewing another exe-file requires it full decompiling and analysis. May be there are some code coincidences, but it's locations I suppose are certainly different.
 
'Vanilla' means the first version of the game without extensions (Play the World and Conquests)

Yes :yup:, it is the original version of Civ 3 and its patches. Later versions are the expansions PtW and C3C.
I only reported the behaviour of the sub in the original version of Civ 3 without expansions to show how the surface ship did move.

Antal1987, my humble theory about the submarine bug without your profound knowldege about the mechanism of C3C is the following:

In C3C the AI knows the position of every unit on the map. This would make submarines obsolete, as this kind of unit would loose its special advantage -the invisibility.

Therefore may be there is a programming inside C3C that lets the AI forget the knowledge about the position of the units, if these units are submarines - but such a programming would only make sense, if the civs with the sub and the surface ship are in war. If the civs with the submarine and the surface ship are in peace, the AI must know the position of the other ship otherwise a collision would trigger war between these civs.

So my guess is, that the trigger that lets forget the AI the position of ships, for submarines is always in the wrong "war-position" while it should normally be in "peace-position" (meaning the AI knows the position of the sub) and this position should only switch to the "war-position" (meaning the AI is now forgetting the position of subs) when both civs are at war.

Of course this is only a guess, without your brilliant knowledge, that gives me the impression that it comes from another highly sophisticated universe. :)
 
According to the results of f_Get_Tile_OccupantID AI doesn't know about invisible units. It's the movement functions that make troubles.

When f_Get_Tile_OccupantID says the tile is "free" AI takes a decision to move through that tile. The problem is that somewhere in a movement function AI controller incorrectly gives information to AI. So it makes the AI responding to what it shouldn't respond to.

Now I have access to the CDs needed to start some old Versions of Civ 3. In a short test-scenario I created for Civ 3 Vanilla (where the bug was fixed by Firaxis, as I posted in another of your threads) the AI surface ship always run around my sub. It never stopped before it, it never moved over my sub, it always bypassed the sub when possible. When this bypassing wasn´t possible, the AI surface ship didn´t move.

A Unit class has a field for storing AI action state:
Code:
Unit State
1 - Fortifying (Fortified ???)
2 - Build Mines
3 - Irrigate
4 - Build Forest
5 - Build Road
6 - Build Railroad
7 - Plant Forest
8 - Clean Forest
10 - Clear Pollution
11 - Build Airfield
12 - Build Radar Tower
13 - Build Outpost
14 - Build Barricade
15 - Intercept
16 - Go To
17 - Road To Tile
18 - Railroad To Tile
19 - Build Colony
20 - Auto Irrigate
21 - Build Trade Routes
22 - Auto Clear Forest
23 - Auto Clear Swamp
24 - Auto Clear Pollution
25 - Auto Save City Tiles
26 - Explore
31 - Auto Bombard
32 - Auto Air Bombard

Unit state is used when the unit gives control of it's actions to the game. If in origin Civ3 a surface ship can bypass a sub, then that unit have to make 2 consequent moves. I suppose it takes some kind of that unit state to make AI controller to move out sufrace ship from the sub's tile.
 
Top Bottom