AGIWiki/Memory and Script

From ScummVM :: Wiki
< AGIWiki
Revision as of 22:59, 11 February 2018 by Sact (talk | contribs) (corrected syntax -> source)
Jump to navigation Jump to search
AGIWiki


Plagiarized from AGI Development Site , originally written by Nick Sonneveld.

Memory Allocation

Whenever a resource is loaded, the interpreter needs a spot in memory to store it. This memory area is called the “heap”. The interpreter also sets aside a room.0 portion for data that is shared between rooms.

Standard items on the heap include:

  • Room.0 Data
  • View Resources
  • Pictures Resources
  • Sound Resources
  • Logic Resources
  • Animated Objects background (special)

AGI treats loaded resources like a stack. When you load and discards resources in AGI, you have to be careful to ensure that resources are discarded in the reverse order that they were loaded. A resource discarded in the wrong place may unintentionally discard other resources that were loaded after it.

There are also linked lists containing all the loaded objects (separate ones for view, pic, sound, logic), with pointers to the memory heap. These lists are used by the interpreter to locate resources on the heap. So if things are discarded too early, the linked list will contain pointers referring to overwritten data. The linked lists are also like stacks too. If you discard a view object, all other view objects loaded afterward are discarded from the list too.

Each animated object stores the original screen buffer data it is covering in a blitlist. It uses this to restores the original screen data as it moves across the screen. This data is constantly updated so the interpreter handles this differently from other memory on the heap. Whenever a resource is added to the heap, the interpreter first saves all blit information back to the screen buffer and destroys the blitlist. Once the resource is saved to the heap, the blitlist is reconstructed and views are restored back to the screen buffer. Because of this process, the blitlist is always at the end of the heap.

In conclusion, the resource data is stored on the heap and the linked list of resources helps the interpreter locate resources on the heap and to determine if they're already loaded.

Room 0

Room.0 data isn't stored by the save-games which is one of the reasons why save-games can't be shared between heavily modified games. If you change the menu layout and load a previously saved game, the menu layout will have changed also.

room.0 items:

  • dir files
  • Animated Objects Array
  • logic.0
  • Stack
  • Script
  • Menu Data
  • Menus logic (usually logic.0)

The data is separated using a special pointer, the room.0 pointer, which points at the end of the *room.0 data. Whenever room.0 data is added, the pointer is changed to point at the end of the new (data. Commands that affect room.0 pointer

  • script.size()
  • new.room() (if script.size has not been called first)
  • submit.menu()
Warning
If a command that affects the room.0 pointer is called within a sub-logic, then that sub-logic will be kept in memory as well. This happens in the Manhunter games since it configures the menus in a separate logic.

Whenever a new room is created, the heap is reset using the room.0 pointer, which means everything is erased except the room.0 data. Certain commands called in logic can set the room.0 pointer which means any resources loaded before it (like views or sounds) will be "trapped" and will take up memory for the rest of the game. Care has to be taken so that these commands are only called in logic.0 and only once.

This room.0 feature isn't as useful as it appears. While data is kept in the room.0 heap section for the duration of the game, the linked list that helps the interpreter locate this data will not point to it after new.room() is called. The only data that those lists will keep is a pointer to logic.0. Everything else will be erased from the list.

Swapping view for ego

If you want to discard the view resource for the ego, things get tricky because the default template script tries to load the ego's view as the first thing before each room. If you try and discard that view, it will discard all the rest of the room data as well! You might have to create a special case where the first ego view isn't discarded. Or modify the logic.0 new room code so it loads the ego view last.

So what you have before you change views is probably:

mem = GAME->LOGIC.0->EGO_VIEW->LOGIC.ROOM->SOUND->VIEW->VIEW->NEXT

logic = LOGIC.0->LOGIC.ROOM

pic = 0  // always discarding pics hopefully

view = EGO_VIEW->VIEW->VIEW

sound = SOUND

The reason why ego view is at the start is because logic.0 always loads it for a new room. The Next is the pointer to the next position in the memory heap.

so when you load up the new view, the memory layout will look like this:

mem = GAME->LOGIC.0->EGO_VIEW->LOGIC.ROOM->SOUND->VIEW->VIEW->*MAN_VIEW*->NEXT

logic = LOGIC.0->LOGIC.ROOM

pic = 0  // always discarding pics hopefully

view = EGO_VIEW->VIEW->VIEW->*MAN_VIEW*

sound = SOUND

A new view has been loaded for the ego, so you decide to remove the old one. but this is where the algorithm of the agi memory gets screwy.. When agi removes an item from the view list, it just sets the next pointer of the *PREV* item to zero... since you're removing ego_view, the first item in the list, it will remove all the others too.

However, these views are still in memory, they just aren't in the view list any more. So if you load up that view again, you will have it in memory twice!

view = 0

that's not all.. to remove it from memory, it just sets the "next available space" pointer to point to EGO_VIEW, so when you load a new resource, it will overwrite the old resource.. this works fine.. but if you set the next available space to ego_view, new resources will write over ALL the other resources loaded in memory..

so the memory map looks like this now:

mem = GAME->LOGIC.0->*NEXT*->EGO_VIEW->LOGIC.ROOM->SOUND->VIEW->VIEW->MAN_VIEW

So most of the items are still in memory, but when you load up another resource, it will load over them, AND the view list is destroyed, so the interpreter won't know about any of the loaded views..

The full (corrupted) memory layout:

mem = GAME->LOGIC.0->*NEXT*->EGO_VIEW->LOGIC.ROOM->SOUND->VIEW->VIEW->MAN_VIEW

logic = LOGIC.0->LOGIC.ROOM

pic = 0  // always discarding pics hopefully

view = 0

sound = SOUND

That's why you might get a "view not loaded" and possibly other weird stuff happening...

The main thing to remember is to discard in the same order you load.. even if it means having an ego view and a man view in memory at the same time. If you want to be able to load and discard the view object as many times as possible, then you have to worry about the AGI script buffer as well

Memory Commands

void mem.info();

A handy memory statistics command strictly for debugging purposes.

Statistics shown:

heapsize - Total heap size. Includes rm.0 data and unallocated heap.
now - Memory allocated on heap at the moment.
max - Maximum amount of memory that has been allocated for the duration of the game.
rm.0, etc - Amount of heap taken up by room.0 data.
max script - Maximum script size for the duration of the game. Multiply by 2 to get size in bytes. Use to find the optimum script size.

Script Buffer

The script buffer stores an ordered list of commands such as loading/discarding resources, drawing/overlaying pictures and adding views to the background (add.to.pic()) which are later saved in a save game. Because the memory layout is so dependent on order (as explained above), the script is necessary to ensure this order is conserved between save games. These commands are later executed from a restored game to ensure the interpreter is in the exact same state as it was when the game was saved. The script buffer is cleared when new.room() is called so the script is room-centric.

The default size of the script buffer is 50 entries. Each entry is 2 bytes long so the default buffer size is 100 bytes. You can increase the size of the buffer by using the command script.size(). Certain commands can increase script usage unless script writing has been disabled by setting flag 7 (script_blocked in template #defines.txt). Care must be taken so that the game does not overflow the script buffer as this has been a cause of many problems. The interpreter keeps a record of the largest script size used in the game, which can be accessed from the command show.mem().

Different commands can use differing number of script entries:

Script Entry Sizes

Action Size
Load/Discard Resource
1
Draw/Overlay Picture
1
add.to.pic()
4

Script Item Types

There are nine resource activities that are tracked in the AGI script:

Script Activity #
AGI Command
0
load.logic
1
load.view
2
load.pic
3
load.sound
4
draw.pic
5
add.to.pic*
6
discard.pic
7
discard.view
8
overlay.pic

*add.to.pic actually uses 4 script table entries (total of eight bytes) as follows:

byte 0: script activity number
byte 1: not used
byte 2: view number
byte 3: loop number
byte 4: cel number
byte 5: x position
byte 6: y position
byte 7: priority

Loading/Discarding Any Number of Times

Peter Kelly's experience with scripts:

"I was working on a part of a game where the player walks through a door and a small room appears on the side of the screen. I was originally using overlay.pic to draw the room (and erase it again afterward when the player walked out of it), but I noticed that if the player walked in and out of the room a few times the game would crash (with a "script buffer overflow" error). It turned out that every time the load.pic, overlay.pic and discard.pic commands were executed, the script size was increased and it eventually got too big. Instead, I decided to simply use a view for the room instead of overlaying a picture."

What we want to do is to be able to load and discard resources as many times as possible in one room. (say, music for a jukebox or changing views for an animated obj) It might not be possible to have all resources loaded at once due to space reasons.

Warning
Be careful when swapping resources for the ego. Most templates load the ego view resource before loading the room logic. So if the ego view is discarded, it will discard all other resources loaded by the room logic as well. Instead of modifying logic.0, you can adjust your code to remember the view number that was first loaded (ego_view_no or v16) and not discard it.

There are two solutions: disable script writing or saving the script position

Disable Script Writing

By setting flag 7, you can prevent the interpreter from writing to the script. There will be no record at all, and if the player saves the game, the saved-game will not have a record either. The flag should be reset afterward to ensure proper script saving later.

You have to be careful with this script block however. If a view is loaded but not written to the script and then the view is associated with an animated object, the save game won't reflect this. If a game is saved and restored, it goes through a process where the resources are loaded up from the script and then views are reassigned back to the animated objects. Strange things will occur if a view isn't loaded but assigned to an object.

Unless your logic script checks for the restore_game flag, it won't load this resource later either. The new_room portion of scripts are only ran the first time the room is entered, not when a game is restored.

One solution to this is to modify logic.0 and change it so the animated view is erased (erase()), save the game, and draw it back (draw()). Then check for the restore game flag (flag 12), load the appropriate view resources that were missing from the script, set.view and then draw the object again.

Another solution is to only write to the script before a save game but eventually the saved game's script will fill up with consecutive saves.

However, script blocking is useful for small bits of code that don't allow the player to save or restore in between:

#define script_blocked f7
 
 if(v27 == 1){
 	set(script_blocked);
 	load.view(7);
 
 	// Code which will use view 7
 	// no saving or restoring allowed here
 
 	discard.view(7);
 	reset(script_blocked);
 }

Save Script Position

Sierra must have realised how difficult it was, so they implemented two new commands push.script() and pop.script(). They can store and restore the position of the script pointer.

The script works like this (you wouldn't use this in a game):

 load.view(10);	// script contains view 10
 push.script();
 load.view(20);	// script contains view 10, 20
 pop.script();	// script contains view 10

To use in a real game, you need to initialise push.script first. This is similar to the first example in blocking script writing (but overkill):

 if (new_room)
 {
 	load.view(blah);
 	....
 	push.script();
 }
 
 if(v27 == 1){
 	load.view(7);		// script contains load view 7
 
 	// Code which will use view 7
 
 	discard.view(7);	// script contains load view 7, discard view 7
 	pop.script();		// script doesn't contain either
 }

Here is my version of code to swap the ego's view and still letting the player save the game:

In the room logic (will be needed in all rooms):

 if (new_room)
 {
 	...
 }
 
 /* view switching logic */
 if (new_room)
 	{ load.logics(70); }
 call(70);
 /* view switching logic */
 
 ...

Logic 70 - View switching logic. It can be given any number, just make sure it's the same in the calling logic.

#include "defines.txt"
 
 #define vLarryDress 10
 #define vRobotDress 11
 #define vPoliceDress 12
 #define vGirlDress 13
 
 #define clothesFirst v62     /* don't discard this. already loaded by logic.0*/
 #define towear v63   /* set this variable to change view */
 
 if (new_room)
 {
 	push.script();	/* save script position */
 	clothesFirst = ego_view_no;  /* ego_view_no only set on new room */
 	towear = 255;
 }
 
 if (said("wear","larry")) {towear = vLarryDress;}
 if (said("wear","robot")) {towear = vRobotDress;}
 if (said("wear","police")) {towear = vPoliceDress;}
 if (said("wear","girl")) {towear = vGirlDress;}
 
 if (towear != 255)
 {
 	if (ego_view_no == towear)
 		{ print("You're already wearing that dress!"); }
 	else
 	{
 		erase(ego);	/* we need to erase ego while changing views */
 
 		/* if not loaded by logic.0 */
 		if (ego_view_no != clothesFirst)
 			{ discard.view.v( ego_view_no); } /* remove previous */
 
 		pop.script();   // redo script from start
 
 		/* load new clothes view */
 		load.view.v(towear);
 		set.view.v(ego, towear);
 		ego_view_no = towear;
 
 		draw(ego);      /* give back ego */
 	}
 	towear = 255;
 }
 
 return();

This is a better solution, because the script will have the correct views loaded when a game is restored and the script size will no increase each time the game is saved.

Script Commands

void script.size(size);

num size;'

Sets the size of the script buffer by allocating size amount of entries. Each entry is two bytes long. This command should only be executed once during the whole game. The script is cleared after it has been executed so make sure you use it before any resources are loaded (excluding logic.0). The room.0 pointer will be set so anything loaded previously will stay in memory.

Default size is 50 entries.

void push.script();

Saves the position in the script. Despite its name, it is not a stack. If it is called again, it will overwrite the old value. Must be called before pop.script() is called.

Previously known as unknown.171(). Overwritten by Brian's AGI Mouse patch.

void pop.script();

Restore the position in the script. Does not destroy the value stored and can be called multiple times. Ensure push.script() has been called before or it will read in garbage.

Previously known as unknown.172().

These two commands are only available in AGI versions 2.915 and above.

Despite their names, the push.script and pop.script functions are not stack functions. Pushing the script will save the current script table position. If push.script is called multiple times, it will overwrite the stored script position each time it is called.

Popping the script will restore the script table position. After calling pop.script , the next script entry will be made at the restored position. It is important that push.script be called BEFORE pop.script. Popping the script position without first pushing it could result in unpredictable results since the memory location where the script position is stored may contain an unknown value.

Because AGI relies on the script entries to correctly save and restore games, it is important to exercise caution when manipulating scripts.

Script Flags

Flags 7 (script_buffer_blocked) - If set, it prevents the interpreter from writing to the script buffer. Remember to reset flag once resource has been loaded so other resources can be loaded normally.

See Also