Adding a teleport to a SOF2 map: all the key-value pairs

The process of modding old games is mostly dead links. Some of these dead links are behind logins or paywalls. For an upcoming lanparty, we needed teleports in our map for Soldier of Fortune 2. After some googling and reading documentation, this post summarizes how we did it.

SOF2 runs the Quake 3 engine, meaning it can be modded using GTKradiant. Radiant’s file format is .map.

Here’s what we needed to add to our .map-file. Replace the two occurrences of 487 655 30, with the x y z coordinates of the target of your teleport. The other lines (after brush 0) are the six planes of the triggering area (the source of the teleport). It’s probably easier to move this area around using GTKradiant.

The three entities are numbered. The syntax may suggest that this is a comment, but I think it couldn’t hurt to make it a logical sequence with the rest of your file.

// entity 199
{
"classname" "target_teleporter"
"origin" "487 655 30"
"target" "1"
"targetname" "teleport"
}
// entity 200
{
"classname" "target_location"
"origin" "487 655 30"
"targetname" "1"
}
// entity 201
{
"classname" "trigger_multiple"
"target" "teleport"
// brush 0
{
( -728 504 56 ) ( -728 472 56 ) ( -728 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 504 56 ) ( -728 504 56 ) ( -728 504 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 472 56 ) ( -672 504 56 ) ( -672 504 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -728 472 56 ) ( -672 472 56 ) ( -672 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -728 472 56 ) ( -728 504 56 ) ( -672 504 56 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
( -672 504 48 ) ( -728 504 48 ) ( -728 472 48 ) tools/_trigger 0 0 0 0.125000 0.125000 0 7 0
}
}

Of course, it’s not necessary to edit the map in a text editor. You could add the same key-value pairs using the entities menu. This can be opened by pressing N in GTKradiant, while having an object selected in the interface.

For the source of the teleport, it’s important to use the _trigger texture. Give this the following key-value pairs:

"classname" "trigger_multiple"
"target" "teleport"

After compiling your map, with these lines added, you should have a working teleport.

Syncing a bokeh plot to a video

The Python library bokeh is great for plotting all kinds of data in the browser. Bokeh includes a serve command, which can host a document, having a per-user state in the associated Python code. When creating a visualisation, you can have callback functions in both Python and JavaScript code. I used this here to sync an HTML video-element to a bokeh line plot.

Accelerometer data synced to a dashcam video:

The HTML video element

<video src="bokeh-video-sync/static/20190912_041033_EF.mp4"
       height="530"
       id="frontcamera"></video>

The JavaScript callback:

/* Select the video with the id frontcamera */
var v = document.getElementById("frontcamera");

/* If the time of the video element updates, run the callback */
v.addEventListener("timeupdate", function () {
    /* The selector below assumes there are no other input fields in your
       code */
    inputs = document.getElementsByTagName('input');
    for (index = 0; index < inputs.length; ++index) {
        /* Update the input field with the new time of the video */
        inputs[index].value = v.currentTime;
        /* Trigger a change of the input field, to call the Python code */
        var event = new Event('change', {bubbles: true});
        inputs[index].dispatchEvent(event);
    }
}, true);

The Python code, with the update callback function:

import pandas

from bokeh.layouts import row
from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import curdoc, figure


def update(_, old, new):
    """The callback function we want to invoke if the time in the video
       advances."""
    # Our data source is in milliseconds, but the callback receives seconds,
    # therefore we multiply the input by 1000
    new, old = float(new) * 1000, float(old) * 1000
    subset = df[(df['ms_since_start'] > old) & (df['ms_since_start'] < new)]
    data.stream({
        'ms': list(subset['ms_since_start']),
        'z': list(subset['z_int']),
        'x': list(subset['x_int']),
        'y': list(subset['y_int']),
    })

# Read in the data using pandas
df = pandas.read_csv('./20190912_041033_EF.acc.csv')

# Create a new bokeh figure
p = figure(plot_width=1900, plot_height=400)

# Define the columns to plot later
data = ColumnDataSource({
    'ms': [],
    'z': [],
    'x': [],
    'y': [],
})

# Plot three lines, that listen to changes in the ColumnDataSource
p.line('ms', 'z', source=data, color='red')
p.line('ms', 'x', source=data, color='white')
p.line('ms', 'y', source=data, color='lightgreen')

# Add a TextInput() that we use to pass on the current time in the video
# This is hacky, and could potentially be done in a nicer way
current_time = TextInput()
 
# If the text field current_time changes, invoke the update callback function 
current_time.on_change('value', update)

# Add all elements to the bokeh document
curdoc().add_root(row(p))
curdoc().add_root(row(current_time))

Although I like this first try at syncing plots, there is still room for improvement. It would be nicer to have the video element call the Python function directly, instead of through an input field. This could be achieved by implementing the video element in bokeh.models. Currently, the line will not disappear when you rewind the video, or start it a second time. All these things can be achieved using the HTML5 media events. Maybe I’ll make a proper media player for bokeh one day…

[Drone footage] Warmond

Pay for the cheapest Netflix, get the most premium one

TLDR: In this post I show how to take advantage of Netflix delivering your new subscription before your payment starts. You could do this manually, but of course "it’s more fun to compute".

Netflix has an interesting upgrade flow: Once you upgrade, you get the upgraded plan for the remainder of the billing period. You only start paying your new fee at the start of the new period.

However, if you upgrade, and downgrade in the same billing period, you’ll get the upgraded plan for the remainder of the current billing period. At the start of the new billing period, you’ll be downgraded to your original plan again. Of course, there’s nothing stopping you from doing the same thing again. Therefore, if you repeat this every billing period, you can have the best plan for the lowest price.

This raises a question: “This can’t be intentional, can it?”. After I submitted a short bug report, Netflix replied that it is indeed intended behaviour:

We actually received a similar report previously about this one and [decided] that this is actually an intended functionality.

So… here’s the code, using Selenium:

import logging
from collections import namedtuple

from selenium import webdriver

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

Configuration = namedtuple('Configuration', ['username', 'password'])

config = Configuration(username='username_here',
                       password='password_here')
options = webdriver.ChromeOptions()
options.binary_location = "./headless-chromium"
browser = webdriver.Chrome(executable_path='./chromedriver',
                           chrome_options=options)
browser.implicitly_wait(time_to_wait=10)
browser.get('https://www.netflix.com/ChangePlan')
browser.find_element_by_id('id_userLoginId').send_keys(
    config.username
)
browser.find_element_by_id('id_password').send_keys(
    config.password
)
browser.find_element_by_css_selector(
    'button.btn.login-button.btn-submit.btn-small'
).click()
try:
    message = browser.find_element_by_css_selector(
        'div.ui-message-contents'
    ).text
    logging.info('Page contains infobox (probably stating that Netflix '
                 'has already been upgraded this month')
    logging.info(message)
    logging.info('Nothing to do left')
    quit(0)
except TimeoutError:
    # The upgrade has not been done this month yet, because there's no
    # infobox saying so
    current_plan = browser.find_element_by_css_selector(
        'li.selected > div > h2 > div > div > span.plan-name'
    ).text
    logging.info(f'Currently the {current_plan} is selected')
    plans = browser.find_elements_by_css_selector('span.plan-name')
    # Now we click the premium plan (the exact term here may be
    # language dependent)
    for plan in plans:
        if plan.text == 'Premium':
            plan.click()
    browser.find_element_by_css_selector(
        'button.btn.save-plan-button.btn-blue.btn-small'
    ).click()
    browser.find_element_by_css_selector(
        'button.btn.modal-action-button.btn-blue.btn-small'
    ).click()
    logging.info('Upgraded to Premium')
    # Now we downgrade to our original plan
    browser.get('https://www.netflix.com/ChangePlan')
    for plan in plans:
        if plan.text == current_plan:
            plan.click()
    browser.find_element_by_css_selector(
        'button.btn.save-plan-button.btn-blue.btn-small'
    ).click()
    browser.find_element_by_css_selector(
        'button.btn.modal-action-button.btn-blue.btn-small'
    ).click()
    logging.info("Downgraded to the original plan again")

Of course this trick has to be deployed to AWS Lambda. We can’t be bothered to do this each month 🙈. I am working on that using Selenium and serverless Chrome.

Disclaimer: This code may or may not do what you and I expect. Run it at your own risk. In the worst case, it may actually upgrade your account, without doing the downgrade.

[Drone footage] Kinderdijk in the snow