The VScript Book

Chapter 5.2: Improved Data Types - Arrays & Lists

Standard VScript arrays are functional, but primitive. They lack modern programming conveniences that you'd expect in any serious scripting environment. PCapture-Lib's Improved Data Types (IDT) module fixes this with two powerful classes: ArrayEx and List.

The Problem: Vanilla Arrays Are Limited

Consider this common scenario: you have an array of enemy health values and need to find which enemies have low health.

// Vanilla VScript: Manual iteration required
local healthValues = [100, 25, 80, 15, 90]
local lowHealthEnemies = []

foreach (health in healthValues) {
    if (health < 30) {
        lowHealthEnemies.append(health)
    }
}
// Result: [25, 15]

This works, but it's verbose and requires manual loop management every time you need to filter data. What if you also need to find unique values? Remove duplicates? Reduce to a sum? Each requires writing another custom loop.

The Solution: ArrayEx

ArrayEx is a drop-in replacement for standard arrays that adds functional programming methods you'd expect from modern languages.

Creating an ArrayEx

// Method 1: Direct construction
local arr = ArrayEx(1, 2, 3, 4, 5)

// Method 2: From existing array
local vanillaArray = [10, 20, 30]
local arr2 = ArrayEx.FromArray(vanillaArray)

// Method 3: Pre-sized with default values
local arr3 = ArrayEx.WithSize(100, 0) // 100 zeros

Functional Methods: The Game Changers

Filter: Create a new array with only matching elements.

local healthValues = ArrayEx(100, 25, 80, 15, 90)

// Clean, declarative filtering
local lowHealth = healthValues.filter(function(idx, val) {
    return val < 30
})
// Result: ArrayEx(25, 15)

Map: Transform each element.

local numbers = ArrayEx(1, 2, 3, 4, 5)

// Double each value
local doubled = numbers.map(function(val, idx) {
    return val * 2
})
// Result: ArrayEx(2, 4, 6, 8, 10)

Reduce: Collapse to a single value.

local scores = ArrayEx(100, 50, 75, 200)

// Calculate total score
local total = scores.reduce(function(accumulator, currentValue) {
    return accumulator + currentValue
}, 0)
// Result: 425

Unique: Remove duplicates.

local values = ArrayEx(1, 2, 2, 3, 4, 4, 5, 2)
local unique = values.unique()
// Result: ArrayEx(1, 2, 3, 4, 5)

Utility Methods

local arr = ArrayEx(10, 20, 30, 40, 50)

// Search for value or matching element
arr.search(30)                    // Returns: 2 (index)
arr.search(function(x) { return x > 35 })  // Returns: 3 (first match)

// Check if contains
arr.contains(20)                  // Returns: true

// Join to string
arr.join(", ")                    // Returns: "10, 20, 30, 40, 50"

// Safe get with default
arr.get(10, -1)                   // Returns: -1 (index 10 doesn't exist)

// Apply function in-place (modifies original)
arr.apply(function(val, idx) {
    return val * 2
})
// arr is now: ArrayEx(20, 40, 60, 80, 100)

Lists: When Order and Insertion Matter

While ArrayEx is perfect for most use cases, there are scenarios where you need constant-time insertion/removal at arbitrary positions. This is where List (a doubly-linked list) excels.

When to Use a List

  • Frequent insertions/deletions at the beginning or middle
  • You need to maintain order but rarely access by index
  • Building queues or stacks
  • ❌ Don't use if you need fast random access by index (use ArrayEx instead)

Creating and Using a List

// Create a list
local myList = List(1, 2, 3, 4, 5)

// Or from array
local arr = [10, 20, 30]
local myList2 = List.FromArray(arr)

// Iterate (use .iter() for efficiency)
foreach (value in myList.iter()) {
    printl(value)
}

// Insert at specific position
myList.insert(2, 99)  // Insert 99 at index 2
// List is now: [1, 2, 99, 3, 4, 5]

// Remove by index
myList.remove(0)  // Remove first element
// List is now: [2, 99, 3, 4, 5]

List Has the Same Functional Methods

local list = List(1, 2, 3, 4, 5)

// Filter, map, reduce work identically
local evens = list.filter(function(idx, val) { return val % 2 == 0 })
// Result: List(2, 4)

local doubled = list.map(function(val, idx) { return val * 2 })
// Result: List(2, 4, 6, 8, 10)

// Sort in-place (uses merge sort)
local unsorted = List(5, 2, 8, 1, 9)
unsorted.sort()
// Result: List(1, 2, 5, 8, 9)

⚠️ Memory Leak Warning: Lists use references between nodes. If you create circular references or don't properly clean up, the garbage collector may not free the memory. Always call .clear() when you're done with a list, or let it go out of scope naturally.

Performance Comparison

Operation Array ArrayEx List
Access by index ⚡ O(1) ⚡ O(1) 🐌 O(n)
Insert at end ⚡ O(1) ⚡ O(1) ⚡ O(1)
Insert at beginning 🐌 O(n) 🐌 O(n) ⚡ O(1)
Filter/Map/Reduce ❌ Manual ✅ Built-in ✅ Built-in

Practical Example: Enemy Manager

// Manage a group of enemies with ArrayEx
EnemyManager <- class {
    enemies = ArrayEx()
    
    function AddEnemy(health, damage) {
        enemies.append({ health = health, damage = damage })
    }
    
    function GetLowHealthEnemies() {
        return enemies.filter(function(idx, enemy) {
            return enemy.health < 30
        })
    }
    
    function GetTotalDamage() {
        return enemies.reduce(function(total, enemy) {
            return total + enemy.damage
        }, 0)
    }
    
    function HealAll(amount) {
        enemies.apply(function(enemy, idx) {
            enemy.health = min(100, enemy.health + amount)
            return enemy
        })
    }
}

// Usage
EnemyManager.AddEnemy(100, 25)
EnemyManager.AddEnemy(20, 15)
EnemyManager.AddEnemy(75, 30)

local weak = EnemyManager.GetLowHealthEnemies()
printl("Weak enemies: " + weak.len())  // 1

local totalDamage = EnemyManager.GetTotalDamage()
printl("Total damage potential: " + totalDamage)  // 70

EnemyManager.HealAll(10)

Conversion Between Types

local arr = ArrayEx(1, 2, 3, 4, 5)

// ArrayEx → List
local list = arr.tolist()

// List → ArrayEx
local backToArray = list.toarray()

// To vanilla array (for compatibility)
local vanillaArr = arr.arr

// To table (keys are array values, all values are null)
local table = arr.totable()
printl(2 in table)  // true

Best Practices

  • Use ArrayEx as your default collection type
  • Use List only when you need frequent middle insertions
  • Chain functional methods for readable data transformations
  • Call .clear() on Lists when done to avoid memory leaks
  • Remember: functional methods (filter/map) create new collections; .apply() modifies in-place
  • ❌ Don't use List if you primarily access by index