mapbase-source/source-sdk-2013

[VSCRIPT] Class defined in the script' scope is not accessible in another class in the same scpoe

VZhelev opened this issue · 2 comments

Description

I found a strange behavior of the execution context in a script.
The following example works fine when it is executed directly in squirrel 3.1, but it fails when it is executed in the game with the following message AN ERROR HAS OCCURRED [the index 'Point' does not exist]

Steps to reproduce

Steps to reproduce the strange behavior:

  1. Have a .nut script that has the following code:
class Point {
	x = null;
	y = null;

	constructor(_x, _y) {
		x = _x;
		y = _y;
	}
}

class Rectangle {
	topLeftPoint = null;
	bottomRightPoint =  null;

	constructor() {
		topLeftPoint = Point(0, 0);
		bottomRightPoint = Point(100, 100);
	}
}

rectangle <- Rectangle();
  1. Craete a map that references our script by logic_script entity and run the map.
  2. See the following error in the console AN ERROR HAS OCCURRED [the index 'Point' does not exist]

Expected behavior

The expected behavior is to run the map and the Rectangle class is successfully instantiated.

Additional context, that may help for better understanding the problem

After hours of reading, debugging and testing I reproduce the same error in pure squirrel (v3.1), and it looks like the core of the problem should be in the execution context.

  1. Reproduced in Squirrel v3.1 - Nested execution context
CustomScope <- {}
class CustomScope.Point {}
class CustomScope.Circle {
	position = null;
	constructor() {
		// Here the Point does not exists either in current environment (this) and root table(::).
		// It exists ony in the CustomScope table
		position = Point(0, 0);
	}
}
function CustomScope::Run() {
	// Here the Point and Circle exists, because the current environment(this) is the CustomScope.
	local point = Point(); 
	local circle = Circle();
}
CustomScope.Run();
  1. Reproduced in Squirrel v3.1 - Execute a file in nested execution context
## execution code
CustomScope <- {}
function CustomScope::Run() {
	dofile("file-to-include.nut"); // load, compile and execute a code in the CustomScpoe execution context
}
CustomScope.Run();
## file-to-include.nut
class Point {}
class Circle {
	position = null;
	constructor() {
		// Here the Point does not exists either in current environment (this) and root table(::).
		// It exists ony in the CustomScope table
		position = Point(0, 0);
	}
}
// Here the Point and Circle exists, because the current environment(this) is the CustomScope.
local point = Point(); 
local circle = Circle();
  1. Tested in Mapbase VScript - Workaround for the issue
// define the script's scope in the root table, in order to access it in any execution context
::MyScriptScope <- this;

class Point {
	x = null;
	y = null;

	constructor(_x, _y) {
		x = _x;
		y = _y;
	}
}

class Rectangle {
	topLeftPoint = null;
	bottomRightPoint =  null;

	constructor() {
                // access the Point class by accessing the MyScriptScope, which is defined in the root table
		topLeftPoint = ::MyScriptScope.Point(0, 0);
		bottomRightPoint = ::MyScriptScope.Point(100, 100);
	}
}

rectangle <- Rectangle();
printl(rectangle);

This is the expected behaviour. The scope your script is running is not a delegate to your classes for them to fallback to, but every context falls back to the root table (since Squirrel 3.0).

Scripts executed inside entities are placed in a table in the root table. Assume T is the entity scope, A, B, C and D are defined inside your script file.

::printl <- function(s) { print( s + "\n" ) }

::T <-
{
	A = {}
	B = { function fn() { printl( A ) } }

	C = class {}
	D = class { function fn() { printl( C ) } }
}

There are no delegations here. A and B are independent tables, C and D are independent classes. They can only access themselves and the root. T.B.fn() and T.D.fn() will fail because they don't see other variables inside T.

For tables to access their 'siblings' inside T, you need to define T as their delegate.

A <- {}
B <- { function fn() { printl( A ) } }.setdelegate( this )

Now B.fn() will work.

For classes, you will have to inherit, and access the base class with base.

C <- class {}
D <- class extends C { function fn() { printl( base ) } }

Class inheritance is more restrictive, so you can simply declare the variables they need to access using local. The solution to your problem becomes:

class Point {}
local Point = Point;

class Rectangle
{
	constructor()
	{
		printl( Point() )
	}
}

Or alternatively reference the other class in a member variable:

class Point {}

class Rectangle
{
	Point = Point;

	constructor()
	{
		printl( Point() )
	}
}

Mapbase's implementation is only about how the engine accesses the Squirrel VM. The language itself is not any different.

Thank you for the clarifications and examples.
They really helped me to understand better what's going on.