nancygold's Journal
 
[Most Recent Entries] [Calendar View] [Friends View]

Tuesday, May 21st, 2024

    Time Event
    1:39a
    The Most Insane Example of OOP So Far
    One of the Westwood contractor published a compiler for a C like language, AESOP, he made for the games
    https://archive.org/details/eye-of-the-beholder-ii-engine-westwood-studios-1991-source-code.-7z

    The unusual thing about it is merging the concept of an object file (in the compilation unit sense) and the object (in the OOP class sense). Basically, each source file (SOP) is an object, and the dynamic linker links such object in the program, and then invokes the main object. Kinda Java before Java. Such system allowed Westwood to port the game to all contemporary 16bit and 32bit systems.

    Instead of methods, these objects had "messages". So each time a new instance of a class required, it copied he compilation unit's data. There was also a support for polypmorphism, where unprocessed messages were passed to a parent object. He also introduced `loop` keyword and `break(argument)`. Really makes you think how much better and simpler could have been C++, if Straustrup wasn't that demented and pretentious.

    The language had preprocessor, which included the same header files and the native C code, keeping constants in sync. Similarly to how I use the same pre-processor for my NewC and for the Symta itself.

    Typical AESOP's main object looked like:
    #depend "arc.exe"       // for test purposes, recompile when ARC.EXE changes
    #depend "$obsolete"
    
    #define PAGE1 0         // default window numbers for display and staging
    #define PAGE2 1         // screens
    
    #define X_SIZE 40
    #define Y_SIZE 25
    
    #define X_M (X_SIZE-2)
    #define Y_M (Y_SIZE-2)
    
    #define CEL_FRONTIER -1
    #define CEL_VIRGIN   0
    #define CEL_N        1
    #define CEL_E        2
    #define CEL_S        4
    #define CEL_W        8
    #define CEL_BORDER   100
    
    object "amaze"
    {
       long p,c,d;
       word fizzle;
       word i,t,f,x,y,mx,my,nx,ny,dir;
       private word dx[4],dy[4],cel[4],adj[4];
       private byte maze[Y_SIZE][X_SIZE];
    
       procedure make_frontier
          {
          long tx,ty;
    
          for (dir=0;dir<4;dir++)
             {
             x = tx+dx[dir];
             y = ty+dy[dir];
    
             if (maze[y][x] == CEL_VIRGIN)
                maze[y][x] = CEL_FRONTIER;
             }
          }
    
       message "create"
          {
          init_graphics();
          init_sound("Ad Lib FM driver","Voice file #2");
    
          set_palette("Maze palette");
    
          for (y=0;y<Y_SIZE;y++)
             for (x=0;x<X_SIZE;x++)
                if (x==0 or y==0 or x==X_SIZE-1 or y==Y_SIZE-1)
                   maze[y][x] = CEL_BORDER;
                else
                   maze[y][x] = CEL_VIRGIN;
    
          dx[0] =  0; dy[0] = -1;
          dx[1] =  1; dy[1] =  0;
          dx[2] =  0; dy[2] =  1;
          dx[3] = -1; dy[3] =  0;
    
          cel[0] = CEL_N; cel[1] = CEL_E;
          cel[2] = CEL_S; cel[3] = CEL_W;
    
          adj[0] = CEL_S; adj[1] = CEL_W;
          adj[2] = CEL_N; adj[3] = CEL_E;
    
          play_sequence("Couple Days Off");
    
          for (y=0;y<Y_SIZE;y++)
             for (x=0;x<X_SIZE;x++)
                draw_cell(X_SIZE,Y_SIZE,x,y,maze[3],32+x/10+y/6);
    
          dir = rnd(0,3);
          mx  = rnd(2,X_M-1); nx = mx + dx[dir];
          my  = rnd(2,Y_M-1); ny = my + dy[dir];
    
          maze[my][mx] += cel[dir];
          maze[ny][nx] += adj[dir];
    
          tx = mx; ty = my; make_frontier;
          tx = nx; ty = ny; make_frontier;
    
          x = mx;
          y = my;
    
          loop
             {
             mx = x;
             my = y;
          
             loop
                {
                dir = rnd(0,3);
    
                nx = mx+dx[dir];
                ny = my+dy[dir];
    
                if (maze[ny][nx] == CEL_FRONTIER)
                   break;
                }
    
             maze[my][mx] += cel[dir];
             maze[ny][nx] = adj[dir];
    
             draw_cell(X_SIZE,Y_SIZE,mx,my,maze,32+x/10+y/6);
    
             for (dir=f=0;dir<4;dir++)
                {
                x = nx+dx[dir];
                y = ny+dy[dir];
    
                if ((maze[y][x] == CEL_FRONTIER) or
                   (maze[y][x] == CEL_VIRGIN))
                      {
                      f = 1;
                      maze[y][x] = CEL_FRONTIER;
                      }
                }
    
             x = nx;
             y = ny;
    
             if (f) continue;
    
             i = 0;
             for (my=0;my<Y_SIZE;my++)
                for (mx=0;mx<X_SIZE;mx++)
                   if (maze[my][mx] == CEL_FRONTIER)
                      {
                      i = 1;
                      break(2);
                      }
    
             if (!i) break;
    
             loop
                {
                dir = rnd(0,3);
    
                x = mx+dx[dir];
                y = my+dy[dir];
    
                if ((maze[y][x] >= CEL_N) and
                   (maze[y][x] <  CEL_BORDER))
                   break;
                }
             }
    
          while (not inkey())
             {
             d = read_palette(32);
             for (p=33;p<=48;p++)
                {
                if (not (p % 4)) wait_vertical_retrace();
    
                c = read_palette(p);
                write_palette(p-1,c);
                }
             write_palette(48,d);
             }
          getkey();
    
          shutdown_sound();
          shutdown_graphics();
    
          fizzle = create_object("fizzle",1,2,3);
          send_message(fizzle,"snowstorm");
          destroy_object(fizzle);
          }
    }
    


    Current Mood: amused
    12:42p
    Hashtable for Components
    The main pitfall is that hashtable elements order depends on the entity ids, so if the same sub-system run on two different computers in a lock-step mode, but the entities have different ids (i.e. server doesn't need a visualization system and the associated entities, like camera). Now if two entities get processed in different order (say two units trying to move into the same cell), we have a racing condition, despite the lock-step. There are several possible solutions:
    1. Reserving id range for the shared entities.
    2. Implementing a hashtable, which can list elements in the order they got added, basically a hashtable which indices into a table where entities can have any order.
    3. Allow hybrid components, where some tables are B tree based.
    4. Sorting by id every time we want to process entities without race conditions.
    5. Implement a movement resolution system, independent of order. I.e. the first system runs, and second system could be checking if any two of the moved entities share a cell id, and resolve the collision.

    The 1st is the fastest, but the 5th is the most robust, since we explicitly acknowledge the possibility of collisions and move their logic into a system separate from movement. I.e. we can allow a unit with higher momentum to squash or displace the smaller one.

    The UUID generation schemas is a huge topic in itself:
    https://www.youtube.com/watch?v=a-K2C3sf1_Q

    Usually it is solved by having enough bits and introducing an id space layout, which includes author, made_by, type tag and the serial generation method. Kind of like a bar code plus serial on a product.

    Current Mood: contemplative
    3:32p
    More Amazing Stuff
    To cite DoD:
    https://www.dataorienteddesign.com/dodbook/node4.html
    Cyclomatic complexity concerns itself only with flow control. The formula, summarised for our purposes, is one (1) plus the number of conditionals present in the system being analysed. That means for any system it starts at one, and for each if, while, for, and do-while, we add one. We also add one per path in a switch statement excluding the default case if present.

    Under the hood, if we consider how a virtual call works, that is, a lookup in a function pointer table followed by a branch into the class method, we can see that a virtual call is effectively just as complex as a switch statement. Counting the flow control statements is more difficult in a virtual call because to know the complexity value, you have to know the number of possible methods that can fulfil the request. In the case of a virtual call, you have to count the number of overrides to a base virtual call. If the base is pure-virtual, then you may subtract one from the complexity.


    TLDR: if you take away OOP with virtual methods, the reatards will use one big switch:
    https://github.com/SheridanR/god-of-thunder/blob/master/src/1_OBJECT.C
    void pick_up_object(int p){
    int r,x,y,s;
    
    switch(object_map[p]){
          case 1:           //red jewel
               if(thor_info.jewels>=999){
                 cannot_carry_more();
                 return;
               }
               add_jewels(10);
               break;
          case 2:           //blue jewel
               if(thor_info.jewels>=999){
                 cannot_carry_more();
                 return;
               }
               add_jewels(1);
               break;
          case 3:           //red potion
               if(thor_info.magic>=150){
                 cannot_carry_more();
                 return;
               }
               add_magic(10);
               break;
          case 4:           //blue potion
               if(thor_info.magic>=150){
                 cannot_carry_more();
                 return;
               }
               add_magic(3);
               break;
          case 5:          //good apple
               if(thor->health>=150){
               cannot_carry_more();
                 return;
               }
               play_sound(GULP,0);
               s=1;
               add_health(5);
               break;
          case 6:           //bad apple
               play_sound(OW,0);
               s=1;
               add_health(-10);
               break;
          case 7:           //key (reset on exit)
    //           if(scrn.reset) r=0;
               add_keys(1);
               break;
          case 8:          //treasure
               if(thor_info.jewels>=999){
                 cannot_carry_more();
                 return;
               }
               add_jewels(50);
               break;
          case 9:          //trophy
               add_score(100);
               break;
          case 10:         //crown
               add_score(1000);
               break;
          case 12:
          case 13:
          case 14:
          case 15:
          case 16:
          case 17:
          case 18:
          case 19:
          case 20:
          case 21:
          case 22:
          case 23:
          case 24:
          case 25:
          case 26:
               if(object_map[p]==13 && HERMIT_HAS_DOLL) return;
               thor->num_moves=1;
               hammer->num_moves=2;
               actor[2].used=0;
               shield_on=0;
               tornado_used=0;
               thor_info.inventory|=64;
               thor_info.item=7;
               thor_info.object=object_map[p]-11;
               display_item();
               thor_info.object_name=object_names[thor_info.object-1];
               odin_speaks((object_map[p]-12)+501,object_map[p]-1);
               break;
          case 27:
          case 28:
          case 29:
          case 30:
          case 31:
          case 32:
               hourglass_flag=0;
               thunder_flag=0;
               shield_on=0;
               lightning_used=0;
               tornado_used=0;
               hammer->num_moves=2;
               thor->num_moves=1;
               actor[2].used=0;
               s=1 << (object_map[p]-27);
               thor_info.inventory |= s;
               odin_speaks((object_map[p]-27)+516,object_map[p]-1);
               s=1;
               thor_info.item=object_map[p]-26;
               display_item();
               add_magic(150);
               fill_score(5);
               break;
    }
    x=p%20;
    y=p/20;
    
    ox=x*16;
    oy=y*16;
    of=1;
    xfput(ox,oy,PAGE2,(char far *) (bg_pics+(scrn.bg_color*262)));
    xfput(ox,oy,PAGE2,(char far *) (bg_pics+(scrn.icon[y][x]*262)));
    xcopyd2d(ox,oy,ox+16,oy+16,ox,oy,PAGE2,draw_page,320,320);
    //xcopyd2d(ox,oy,ox+16,oy+16,ox,oy,PAGE2,draw_page,320,320);
    
    r=1;
    s=0;
    if(!s) play_sound(YAH,0);
    object_map[p]=0;
    if(r){   //reset so it doesn't reappear on reentry to screen
      if(object_index[p]<30) scrn.static_obj[object_index[p]]=0;
      object_index[p]=0;
    }
    }
    


    Current Mood: amused
    Current Music: Bauhaus - Double Dare
    4:31p
    Why do I read 80ies and 90ies code?
    Several reasons:
    1. Back then people had to be smart to fit stuff into the limited amount of memory and make it run fast, and yet there were enough resources to employ more advanced algorithms (compared to say Atari 2600). Since I work on a programming language runtime, I'm looking towards the sources of inspiration.
    2. Some of the code I check, like the Ultima Underworld and Eye of the Beholder engines, were made by the best programmers of their time. So the code is just fun to read. Same reason I enjoyed reading Lambda Papers.
    3. The code bases were small and well structured, since only a few people worked on them. Compared to modern monstrosities. So you can go through in an hour.
    4. No C++/Java/Rust/JS with its templates and getters/setters boilerplate.

    There are enough of modern projects to read, like https://github.com/N64Recomp/N64Recomp

    But really, back in 70ies and 80ies there were several such recompiler projects, serving role in moving software to newer hardware in a safe way. Everything new is really nicely forgotten old. Even ChatGPT is just a Markov's language model from 19th century.

    Current Mood: contemplative
    8:03p
    List of games with debug symbols
    https://www.retroreversing.com/games/symbols

    available as separate downloads, if you're too lazy to dig the isos
    http://debugging.games/
    https://dg.getrektby.us/

    These can be opened in Ghidra and dumped to C/C++.
    Soul Reaver and Driver 2 (a GTA3 before GTA3) was already reverse engineered.
    Final Fantasy 5 is worth checking I guess.
    Croc 2 included some really advanced engine for its time, and it was also used for other games.

    The "Torneko no Daibouken 2 - Fushigi no Dungeon (Japan)" there is actually the Chocobo Dungeon engine.

    I personally want to reverse the PoPoRoGue, because I has a nice isometric engine.

    Current Mood: contemplative

    << Previous Day 2024/05/21
    [Calendar]
    Next Day >>

About LJ.Rossia.org