/*
	This DeltaX Fusion extension UWP port is under MIT license.
*/

/*
	DeltaX.js
	RuntimeUWP (WUA)
	
	Date Format: (MM/DD/YYYY)
	Created by Mustafa Hamad on 12/31/2025. (GMT+2)
	Updated on 02/24/2026. (GMT+2)
	Copyright (c) 2025-2026 Mustafa Hamad. All rights reserved.
*/

/* global console, darkEdif, alert, CRunExtension, CServices, window */
/* jslint esversion: 6, sub: true */

// This is strict, but that can be assumed
// "use strict";

/**
 * String.prototype.replaceAll() polyfill
 * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
 * @author Chris Ferdinandi
 * @license MIT
 */
if (!String.prototype.replaceAll) {
	String.prototype.replaceAll = function(str, newStr) {
		// If a regex pattern
		if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
			return this.replace(str, newStr);
		}

		// If a string
		return this.replace(new RegExp(str, 'g'), newStr);
	};
}

// Global data, including sub-applications, just how God intended.
// Note: This will allow newer SDK versions in later SDKs to take over.
// We need this[] and window[] instead of direct, because HTML5 Final Project minifies and breaks the names otherwise
// In case UWP is minified later, we'll keep these HTML5 minify precautions in UWP
window['darkEdif'] = (window['darkEdif'] && window['darkEdif'].sdkVersion >= 19) ? window['darkEdif'] :
	new (/** @constructor */ function() {
	this.data = {};
	this.getGlobalData = function (key) {
		key = key.toLowerCase();
		if (key in this.data) {
			return this.data[key];
		}
		return null;
	};
	this.setGlobalData = function (key, value) {
		key = key.toLowerCase();
		this.data[key] = value;
	};

	this.getCurrentFusionEventNumber = function (ext) {
		return ext.rh.rhEvtProg.rhEventGroup.evgLine || -1;
	};
	this.sdkVersion = 19;
	this.checkSupportsSDKVersion = function (sdkVer) {
		if (sdkVer < 16 || sdkVer > 19) {
			throw "UWP DarkEdif SDK does not support SDK version " + this.sdkVersion;
		}
	};

	// minifier will rename notMinified, so we can detect minifier simply
	this.minified = false;
	if (!this.hasOwnProperty('minified')) {
		this['minified'] = true;
	}

	this.consoleLog = function (ext, str) {
		// Log if DebugMode not defined, or true
		if (ext == null || ext['DebugMode'] == null || ext['DebugMode']) {
			// Exts will be renamed in minified
			if (this['minified'] && ext != null && ext['DebugMode'] == true) {
				console.warn("DebugMode left true for an extension in minified mode. Did the ext developer not set it false before distributing?");
				ext['DebugMode'] = false;
			}

			const extName = (ext == null || this['minified']) ? "Unknown DarkEdif ext" :
				ext['ExtensionName'] || ext.constructor.name.replaceAll('CRun', '').replaceAll('_',' ');
			console.log(extName + " - " + str);
		}
	};
	// UWP does not have FinalizationRegistry
	this.finalizer = { register: function(desc) { } };

	this['Properties'] = function(ext, edPtrFile, extVersion) {
		// DarkEdif SDK stores offset of DarkEdif props away from start of EDITDATA inside private data.
		// eHeader is 20 bytes, so this should be 20+ bytes.
		if (ext.ho.privateData < 20) {
			throw "Not smart properties - eHeader missing?";
		}
		// DarkEdif SDK header read:
		// header uint32, hash uint32, hashtypes uint32, numprops uint16, pad uint16, sizeBytes uint32 (includes whole EDITDATA)
		// then checkbox list, one bit per checkbox, including non-checkbox properties
		// so skip numProps / 8 bytes
		// then moving to Data list:
		// size uint32 (includes whole Data), propType uint16, propNameSize uint8, propname u8 (lowercased), then data bytes

		let header = new Uint8Array(edPtrFile.readBuffer(4 + 4 + 4 + 2 + 2 + 4));
		if (String.fromCharCode.apply('', [header[3], header[2], header[1], header[0]]) != 'DAR1') {
			throw "Did you read this.ho.privateData bytes?";
		}

		let headerDV = new DataView(header.buffer);
		this.numProps = headerDV.getUint16(4 + 4 + 4, true); // Skip past hash and hashTypes
		this.sizeBytes = headerDV.getUint32(4 + 4 + 4 + 4, true); // skip past numProps and pad

		let editData = edPtrFile.readBuffer(this.sizeBytes - header.byteLength);
		this.chkboxes = editData.slice(0, Math.ceil(this.numProps / 8));
		let that = this;
		let GetPropertyIndex = function(chkIDOrName) {
			if (typeof chkIDOrName == 'number') {
				if (that.numProps <= chkIDOrName) {
					throw "Invalid property ID " + chkIDOrName + ", max ID is " + (that.numProps - 1) + ".";
				}
				return chkIDOrName;
			}
			const p = that.props.find(function(p) { return p.propName == chkIDOrName; });
			if (p == null) {
				throw "Invalid property name \"" + chkIDOrName + "\"";
			}
			return p.index;
		};
		this['IsPropChecked'] = function(chkIDOrName) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return 0;
			}
			return (that.chkboxes[Math.floor(idx / 8)] & (1 << idx % 8)) != 0;
		};
		this['GetPropertyStr'] = function(chkIDOrName) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return "";
			}
			const prop = that.props[idx];
			const textPropIDs = [
				5, // PROPTYPE_EDIT_STRING:
				22, // PROPTYPE_EDIT_MULTILINE:
				16, // PROPTYPE_FILENAME:
				19, // PROPTYPE_PICTUREFILENAME:
				26, // PROPTYPE_DIRECTORYNAME:
				7, // PROPTYPE_COMBOBOX:
				20, // PROPTYPE_COMBOBOXBTN:
				24 // PROPTYPE_ICONCOMBOBOX:
			];
			if (textPropIDs.indexOf(prop.propTypeID) != -1) {
				let t = that.textDecoder.decode(prop.propData);
				if (prop.propTypeID == 22) //PROPTYPE_EDIT_MULTILINE
					t = t.replaceAll('\r', ''); // CRLF to LF
				return t;
			}
			throw "Property " + prop.propName + " is not textual.";
		};
		this['GetPropertyNum'] = function(chkIDOrName) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return 0.0;
			}
			const prop = that.props[idx];
			const numPropIDsInteger = [
				6, // PROPTYPE_EDIT_NUMBER
				9, // PROPTYPE_COLOR
				11, // PROPTYPE_SLIDEREDIT
				12, // PROPTYPE_SPINEDIT
				13 // PROPTYPE_DIRCTRL
			];
			const numPropIDsFloat = [
				21, // PROPTYPE_EDIT_FLOAT
				27 // PROPTYPE_SPINEDITFLOAT
			];
			if (numPropIDsInteger.indexOf(prop.propTypeID) != -1) {
				return new DataView(prop.propData.buffer).getUint32(0, true);
			}
			if (numPropIDsFloat.indexOf(prop.propTypeID) != -1) {
				return new DataView(prop.propData.buffer).getFloat32(0, true);
			}
			throw "Property " + prop.propName + " is not numeric.";
		};
		this['GetPropertyImageID'] = function(chkIDOrName, imgID) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return -1;
			}
			const prop = that.props[idx];
			if (prop.propTypeID != 23) { // PROPTYPE_IMAGELIST
				throw "Property " + prop.propName + " is not an image list.";
			}
			
			if ((~~imgID != imgID) || imgID < 0) {
				throw "Image index " + imgID + " is invalid.";
			}
			const dv = new DataView(prop.propData.buffer);
			if (imgID >= dv.getUint16(0, true)) {
				return -1;
			}
			
			return imgID.getUint16(2 * (1 + idx), true)
		};
		this['GetPropertyNumImages'] = function(chkIDOrName, imgID) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return -1;
			}
			const prop = that.props[idx];
			if (prop.propTypeID != 23) { // PROPTYPE_IMAGELIST
				throw "Property " + prop.propName + " is not an image list.";
			}
			
			return new DataView(prop.propData.buffer).getUint16(0, true);
		};
		this['GetSizeProperty'] = function(chkIDOrName) {
			const idx = GetPropertyIndex(chkIDOrName);
			if (idx == -1) {
				return -1;
			}
			const prop = that.props[idx];
			if (prop.propTypeID != 8) { // PROPTYPE_SIZE
				throw "Property " + prop.propName + " is not an size property.";
			}
			
			const dv = new DataView(prop.propData.buffer);
			return { width: dv.getInt32(0, true), height: dv.getInt32(4, true) };
		};

		this.props = [];
		const data = editData.slice(this.chkboxes.length);
		const dataDV = new DataView(new Uint8Array(data).buffer);

		this.textDecoder = null;
		if (window['TextDecoder'] != null) {
			this.textDecoder = new window['TextDecoder']();
		}
		else {
			// one byte = one char - should suffice for basic ASCII property names
			this.textDecoder = {
				decode: function(txt) {
					return String.fromCharCode.apply("", txt);
				}
			};
		}

		for (let i = 0, pt = 0, propSize, propEnd; i < this.numProps; ++i) {
			propSize = dataDV.getUint32(pt, true);
			propEnd = pt + propSize;
			const propTypeID = dataDV.getUint16(pt + 4, true);
			const propNameLength = dataDV.getUint8(pt + 4 + 2);
			pt += 4 + 2 + 1;
			const propName = this.textDecoder.decode(new Uint8Array(data.slice(pt, pt + propNameLength)));
			pt += propNameLength;
			const propData = new Uint8Array(data.slice(pt, pt + propSize - (4 + 2 + 1 + propNameLength)));

			this.props.push({ index: i, propTypeID: propTypeID, propName: propName, propData: propData });
			pt = propEnd;
		}
	};
})();

// ... Condition IDs (MUST match Windows side!) ...

const CND = {
	// Variable DeltaTime
	IsDtEnabled: 0,
	// Fixed DeltaTime
	IsFixedDtEnabled: 1,
	OnFixedDtUpdate: 2,
	// Movement Timer
	IsMvtTimerEnabled: 3
};
const NUM_CONDITIONS = 4; // DON'T FORGET TO UPDATE AFTER EDITING CND!

// ... Actions IDs (MUST match Windows side!) ...
const ACT = {
	SetDTRatio: 0,
	SetDTTimeScale: 1,
	SetDTStepsQuality: 2,
	EnableDT: 3,
	DisableDT: 4,
	ToggleDT: 5,
	SetDTState: 6,
	ForceCalculateDT: 7,

	SetFixedDTRatio: 8,
	EnableFixedDT: 9,
	DisableFixedDT: 10,
	ToggleFixedDT: 11,
	SetFixedDTState: 12,
	ForceCalculateFixedDT: 13,

	SetMvtTimerBase: 14,
	SetMvtTimerStepsQuality: 15,
	EnableMvtTimer: 16,
	DisableMvtTimer: 17,
	ToggleMvtTimer: 18,
	SetMvtTimerState: 19,

	// Wizard action (Custom param type on C++). Not supported in as of yet...
	SetMvtTimerBaseViaWizard: 20
};

// ... Expression IDs (MUST match Windows side!) ...
const EXP = {
	GetDT: 0,
	GetDTRatio: 1,
	GetDTTimeScale: 2,
	GetDTNSteps: 3,
	GetDTStepsQuality: 4,
	GetDTState: 5,

	GetFixedDTRatio: 6,
	GetFixedDTState: 7,

	GetMvtTimerCoef: 8,
	GetMvtTimerBase: 9,
	GetMvtTimerBaseFromTimeScale: 10,
	GetTimeScaleFromMvtTimerBase: 11,
	GetMvtTimerNSteps: 12,
	GetMvtTimerStepsQuality: 13,
	GetMvtTimerState: 14,

	MSToFPS: 15,
	FPSToMS: 16
};

// Utilities matching C++: kMaxFPS = 1000
const kMaxFPS = 1000;
const kMsPerSecond = 1000.0;
const kMinFractionalMs = 0.0;
const kMaxFractionalMs = 0.999999;

function ClampInt(n, minV, maxV) {
	n = ~~n;
	if (n < minV) { return minV; }
	if (n > maxV) { return maxV; }
	return n;
}
function ClampFloat(n, minV, maxV) {
	n = +n;
	if (n < minV) { return minV; }
	if (n > maxV) { return maxV; }
	return n;
}

/** @constructor */
function CRunDeltaX() {
	/// <summary> Constructor of Fusion object. </summary>
	CRunExtension.call(this);

	// DarkEdif SDK exts should have these four variables defined.
	// We need this[] and window[] instead of direct because HTML5 Final Project minifies and breaks the names otherwise
	this['ExtensionVersion'] = 6; // Must match C++ Extension::Version
	this['SDKVersion'] = 19; // To match C++ version
	this['DebugMode'] = false;
	this['ExtensionName'] = 'DeltaX';

	// Can't find DarkEdif wrapper
	if (!window.hasOwnProperty('darkEdif')) {
		throw "a wobbly";
	}
	window['darkEdif'].checkSupportsSDKVersion(this.SDKVersion);
	
	// ... Custom properties ...

	// Runtime state (mirrors C++ defaults)
	this.dt = 0.0;
	this.dtNSteps = 1;
	this.dtOldTimer = 0;
	this.dtRatio = 16.67;
	this.dtTimeScale = 100;
	this.dtStepsQuality = 1;
	this.dtState = false;
	this.dtLastDeltaMs = 0.0;
	this.dtUseFusionTimerSubMsPrecision = true;
	this.dtLastHiResMs = 0.0;
	this.dtHasHiResBaseline = false;

	this.fixedOldTimer = 0;
	this.fixedStepInterval = 0.0;
	this.fixedStepCount = 0.0;
	this.fixedRatio = 16.67;
	this.fixedState = false;

	this.mvtTimerStepsQuality = 1;
	this.returnMvtTimerCoefAsOneIfDisabled = false;
	this.mvtFakePrecisionEnabled = false;
	this.mvtLastHiResMs = 0.0;
	this.mvtHasHiResBaseline = false;
	this.mvtLastTimerMs = 0;
	this.mvtCachedFakeCoef = 0.0;
	this.mvtLastCoefTimerMs = 0;
	this.mvtHasCachedFakeCoef = false;

	// Properties mirrored from C++ Extension.cpp behavior
	this.pushEvents = false;

	// ======================================================================================================
	// Actions
	// ======================================================================================================

	/*this.Action_ActionExample = function (ExampleParameter) {
		// nothing, as C++ does nothing
	};
	this.Action_SecondActionExample = function () {
		// nothing, as C++ does nothing
	};*/

	// ... Variable DT ...
	this.Action_SetDTRatio = function (Ratio) {
		this.dtRatio = Math.max(+Ratio || 0.0, 0.0);
	};
	this.Action_SetDTTimeScale = function (TimeScale) {
		this.dtTimeScale = Math.max(~~TimeScale, 0);
	};
	this.Action_SetDTStepsQuality = function (StepsQuality) {
		this.dtStepsQuality = Math.max(~~StepsQuality, 1);
	};
	this.Action_EnableDT = function () {
		this._setDTState(true);
	};
	this.Action_DisableDT = function () {
		this._setDTState(false);
	};
	this.Action_ToggleDT = function () {
		this._setDTState(!this.dtState);
	};
	this.Action_SetDTState = function (Enabled) {
		this._setDTState((~~Enabled) !== 0);
	};
	this.Action_ForceCalculateDT = function () {
		this._calculateDT();
	};

	// ... Fixed DT ...
	this.Action_SetFixedDTRatio = function (Ratio) {
		this.fixedRatio = Math.max(+Ratio || 0.0, 0.0);
	};
	this.Action_EnableFixedDT = function () {
		this._setFixedDTState(true);
	};
	this.Action_DisableFixedDT = function () {
		this._setFixedDTState(false);
	};
	this.Action_ToggleFixedDT = function () {
		this._setFixedDTState(!this.fixedState);
	};
	this.Action_SetFixedDTState = function (Enabled) {
		this._setFixedDTState((~~Enabled) !== 0);
	};
	this.Action_ForceCalculateFixedDT = function () {
		this._calculateFixedDT();
	};

	// ... Movement timer ...
	this.Action_SetMvtTimerBase = function (Base) {
		this._setMvtTimerBase(~~Base);
	};
	this.Action_SetMvtTimerStepsQuality = function (StepsQuality) {
		this.mvtTimerStepsQuality = Math.max(~~StepsQuality, 1);
	};
	this.Action_EnableMvtTimer = function () {
		this._setMvtTimerState(true);
	};
	this.Action_DisableMvtTimer = function () {
		this._setMvtTimerState(false);
	};
	this.Action_ToggleMvtTimer = function () {
		this._setMvtTimerState(!this._getMvtTimerState());
	};
	this.Action_SetMvtTimerState = function (Enabled) {
		this._setMvtTimerState((~~Enabled) !== 0);
	};
	this.Action_SetMvtTimerBaseViaWizard = function (/* extDataBuffer */) {
		// Not supported in HTML5 as of yet...
	};

	// ======================================================================================================
	// Conditions
	// ======================================================================================================

	/*this.Condition_AreTwoNumbersEqual = function (First, Second) {
		return First == Second;
	};*/

	this.Condition_IsDTEnabled = function () {
		return !!this.dtState;
	};
	this.Condition_IsFixedDTEnabled = function () {
		return !!this.fixedState;
	};
	this.Condition_OnFixedDTUpdate = function () {
		return true;
	};
	this.Condition_IsMvtTimerEnabled = function () {
		return !!this._getMvtTimerState();
	};

	// =============================
	// Expressions
	// =============================

	/*this.Expression_Add = function (First, Second) {
		return First + Second;
	};
	this.Expression_HelloWorld = function () {
		return "Hello world!";
	};*/

	this.Expression_GetDT = function () {
		return +this.dt;
	};
	this.Expression_GetDTRatio = function () {
		return +this.dtRatio;
	};
	this.Expression_GetDTTimeScale = function () {
		return ~~this.dtTimeScale;
	};
	this.Expression_GetDTNSteps = function () {
		return ~~this.dtNSteps;
	};
	this.Expression_GetDTStepsQuality = function () {
		return ~~this.dtStepsQuality;
	};
	this.Expression_GetDTState = function () {
		return this.dtState ? 1 : 0;
	};
	this.Expression_GetFixedDTRatio = function () {
		return +this.fixedRatio;
	};
	this.Expression_GetFixedDTState = function () {
		return this.fixedState ? 1 : 0;
	};
	this.Expression_GetMvtTimerCoef = function () {
		return +this._getMvtTimerCoefOutput();
	};
	this.Expression_GetMvtTimerBase = function () {
		return ~~this._getMvtTimerBase();
	};
	this.Expression_GetMvtTimerBaseFromTimeScale = function (DefaultMvt, TimeScale) {
		return ~~this._getMvtTimerBaseFromTimeScale(~~DefaultMvt, ~~TimeScale);
	};
	this.Expression_GetTimeScaleFromMvtTimerBase = function (DefaultMvt, Base) {
		return ~~this._getTimeScaleFromMvtTimerBase(~~DefaultMvt, ~~Base);
	};
	this.Expression_GetMvtTimerNSteps = function () {
		return ~~this._getMvtTimerNSteps();
	};
	this.Expression_GetMvtTimerStepsQuality = function () {
		return ~~this.mvtTimerStepsQuality;
	};
	this.Expression_GetMvtTimerState = function () {
		return this._getMvtTimerState() ? 1 : 0;
	};
	this.Expression_MSToFPS = function (MS) {
		return ~~this._msToFps(+MS);
	};
	this.Expression_FPSToMS = function (FPS) {
		return +this._fpsToMs(~~FPS);
	};

	// ======================================================================================================
	// Function arrays (ordered by ID)
	// ======================================================================================================

	this.$actionFuncs = [
		/*  0 */ //this.Action_ActionExample,
		/*  1 */ //this.Action_SecondActionExample
		/*  0 */ this.Action_SetDTRatio,
		/*  1 */ this.Action_SetDTTimeScale,
		/*  2 */ this.Action_SetDTStepsQuality,
		/*  3 */ this.Action_EnableDT,
		/*  4 */ this.Action_DisableDT,
		/*  5 */ this.Action_ToggleDT,
		/*  6 */ this.Action_SetDTState,
		/*  7 */ this.Action_ForceCalculateDT,
		/*  8 */ this.Action_SetFixedDTRatio,
		/*  9 */ this.Action_EnableFixedDT,
		/* 10 */ this.Action_DisableFixedDT,
		/* 11 */ this.Action_ToggleFixedDT,
		/* 12 */ this.Action_SetFixedDTState,
		/* 13 */ this.Action_ForceCalculateFixedDT,
		/* 14 */ this.Action_SetMvtTimerBase,
		/* 15 */ this.Action_SetMvtTimerStepsQuality,
		/* 16 */ this.Action_EnableMvtTimer,
		/* 17 */ this.Action_DisableMvtTimer,
		/* 18 */ this.Action_ToggleMvtTimer,
		/* 19 */ this.Action_SetMvtTimerState,

		/* 20 */ this.Action_SetMvtTimerBaseViaWizard
	];
	this.$conditionFuncs = [
		/*  0 */ //this.Condition_AreTwoNumbersEqual
		/*  0 */ this.Condition_IsDTEnabled,
		/*  1 */ this.Condition_IsFixedDTEnabled,
		/*  2 */ this.Condition_OnFixedDTUpdate,
		/*  3 */ this.Condition_IsMvtTimerEnabled
	];
	this.$expressionFuncs = [
		/*  0 */ //this.Expression_Add,
		/*  1 */ //this.Expression_HelloWorld
		/*  0 */ this.Expression_GetDT,
		/*  1 */ this.Expression_GetDTRatio,
		/*  2 */ this.Expression_GetDTTimeScale,
		/*  3 */ this.Expression_GetDTNSteps,
		/*  4 */ this.Expression_GetDTStepsQuality,
		/*  5 */ this.Expression_GetDTState,
		/*  6 */ this.Expression_GetFixedDTRatio,
		/*  7 */ this.Expression_GetFixedDTState,
		/*  8 */ this.Expression_GetMvtTimerCoef,
		/*  9 */ this.Expression_GetMvtTimerBase,
		/* 10 */ this.Expression_GetMvtTimerBaseFromTimeScale,
		/* 11 */ this.Expression_GetTimeScaleFromMvtTimerBase,
		/* 12 */ this.Expression_GetMvtTimerNSteps,
		/* 13 */ this.Expression_GetMvtTimerStepsQuality,
		/* 14 */ this.Expression_GetMvtTimerState,
		/* 15 */ this.Expression_MSToFPS,
		/* 16 */ this.Expression_FPSToMS
	];
}

//
CRunDeltaX.prototype = {
	/// <summary> Prototype definition </summary>
	/// <description> This class is a sub-class of CRunExtension, by the mean of the
	/// CServices.extend function which copies all the properties of
	/// the parent class to the new class when it is created.
	/// As all the necessary functions are defined in the parent class,
	/// you only need to keep the ones that you actually need in your code. </description>

	getNumberOfConditions: function() {
		/// <summary> Returns the number of conditions </summary>
		/// <returns type="Number" isInteger="true"> Warning, if this number is not correct, the application _will_ crash</returns>
		return NUM_CONDITIONS;
	},

	createRunObject: function(file, cob, version) {
		/// <summary> Creation of the Fusion extension. </summary>
		/// <param name="file"> A CFile object, pointing to the object's data zone </param>
		/// <param name="cob"> A CCreateObjectInfo containing infos about the created object</param>
		/// <param name="version"> version : the version number of the object, as defined in the C code </param>
		/// <returns type="Boolean"> Always false, as it is unused. </returns>

		// Use the "file" parameter to call the CFile object, and
		// gather the data of the object in the order as they were saved
		// (same order as the definition of the data in the EDITDATA structure
		// of the C code).
		// Please report to the documentation for more information on the CFile object

		if (this.ho == null) {
			throw "HeaderObject not defined when needed to be.";
		}

		// DarkEdif properties are accessible as on other platforms: IsPropChecked(), GetPropertyStr(), GetPropertyNum()
		let props = new darkEdif['Properties'](this, file, version);

		/*this.checkboxWithinFolder = props['IsPropChecked']("Checkbox within folder");
		this.editable6Text = props['GetPropertyStr']("Editable 6");*/

		// Mirror C++ property ingestion (Extension.cpp)

		// Advanced
		this.pushEvents = !!props['IsPropChecked']("Push Triggered Events");

		// Variable DT
		{
			const autoCalcDT = !!props['IsPropChecked']("Auto Calculate Variable DeltaTime");
			const subMsPrecision = !!props['IsPropChecked']("Use Fusion Timer Sub-Millisecond Precision");
			const basedOnFPS = !!props['IsPropChecked']("Set Variable DeltaTime Ratio based on FPS");
			const ratioProp = +props['GetPropertyNum']("Variable DeltaTime Ratio");
			const timeScaleProp = ~~props['GetPropertyNum']("Variable DeltaTime Time Scale Percentage");
			const qualityProp = ~~props['GetPropertyNum']("Variable DeltaTime Steps Quality");

			const ratioMs = basedOnFPS ? this._fpsToMs(~~ratioProp) : ratioProp;

			this.dtRatio = Math.max(+ratioMs || 0.0, 0.0);
			this.dtTimeScale = Math.max(~~timeScaleProp, 0);
			this.dtStepsQuality = Math.max(~~qualityProp, 1);
			this.dtUseFusionTimerSubMsPrecision = !!subMsPrecision;
			this._setDTState(!!autoCalcDT);
		}

		// Fixed DT
		{
			const autoCalcFixed = !!props['IsPropChecked']("Auto Calculate Fixed DeltaTime");
			const basedOnFPS = !!props['IsPropChecked']("Set Fixed DeltaTime Step Ratio based on FPS");
			const ratioProp = +props['GetPropertyNum']("Fixed DeltaTime Step Ratio");
			this.fixedRatio = Math.max(basedOnFPS ? this._fpsToMs(~~ratioProp) : ratioProp, 0.0);
			this._setFixedDTState(!!autoCalcFixed);
		}

		// Movement timer
		{
			const overrideMvt = !!props['IsPropChecked']("Override Movement Timer on Initialization");
			const enableMvt = !!props['IsPropChecked']("Enable Movement Timer");
			const fakePrecision = !!props['IsPropChecked']("Fake Movement Timer Coef Precision");
			const baseBasedOnMS = !!props['IsPropChecked']("Set Movement Timer Base based on Milliseconds");
			this.returnMvtTimerCoefAsOneIfDisabled = !!props['IsPropChecked']("Return MvtTimerCoef as 1.0 if Movement Timer is Disabled");
			const mvtBaseRatioProp = +props['GetPropertyNum']("Movement Timer Base");
			const timeScale = ~~props['GetPropertyNum']("Movement Timer Time Scale Percentage");
			const stepsQuality = ~~props['GetPropertyNum']("Movement Timer Steps Quality");

			this.mvtTimerStepsQuality = Math.max(~~stepsQuality, 1);
			this.mvtFakePrecisionEnabled = !!fakePrecision;

			if (overrideMvt) {
				this._setMvtTimerState(!!enableMvt);

				let mvtTimerBase = 0;
				if (baseBasedOnMS) {
					mvtTimerBase = this._msToFps(mvtBaseRatioProp);
				}
				else {
					mvtTimerBase = ~~mvtBaseRatioProp;
				}

				const baseFromScale = this._getMvtTimerBaseFromTimeScale(~~mvtTimerBase, ~~timeScale);
				this._setMvtTimerBase(~~baseFromScale);
			}
		}

		// Initialize timer baselines
		this.dtOldTimer = this._getTimer();
		this.fixedOldTimer = this._getTimer();

		// The return value is not used in this version of the runtime: always return false.
		return false;
	},

	handleRunObject: function() {
		/// <summary> This function is called at every loop of the game. You have to perform
		/// in it all the tasks necessary for your object to function. </summary>
		/// <returns type="Number"> One of two options:
		///							   0 : this function will be called during the next loop
		/// CRunExtension.REFLAG_ONESHOT : this function will not be called anymore,
		///								   unless this.reHandle() is called. </returns>

		// Mirror C++ Extension::Handle()
		if (this.dtState) {
			this._calculateDT();
		}
		if (this.fixedState) {
			this._calculateFixedDT();
		}

		return 0;
	},

	condition: function(num, cnd) {
		/// <summary> Called when a condition of this object is tested. </summary>
		/// <param name="num" type="Number" integer="true"> The number of the condition; 0+. </param>
		/// <param name="cnd" type="CCndExtension"> a CCndExtension object, allowing you to retreive the parameters
		//			of the condition. </param>
		/// <returns type="Boolean"> True if the condition is currently true. </returns>

		const func = this.$conditionFuncs[~~num];
		if (func == null) {
			throw "Unrecognised condition ID " + (~~num) + " passed to " + this['ExtensionName'] + ".";
		}

		// Note: New Direction parameter is not supported by this, add a workaround based on condition and parameter index;
		// SDL Joystick's source has an example.
		const args = new Array(func.length);
		for (let i = 0; i < args.length; ++i) {
			args[i] = cnd.getParamExpString(this.rh, i);
		}

		return func.apply(this, args);
	},
	action: function(num, act) {
		/// <summary> Called when an action of this object is executed </summary>
		/// <param name="num" type="Number"> The ID/number of the action, as defined by
		///		its array index. </param>
		/// <param name="act" type="CActExtension"> A CActExtension object, allowing you to
		///		retrieve the parameters of the action </param>

		const func = this.$actionFuncs[~~num];
		if (func == null) {
			throw "Unrecognised action ID " + (~~num) + " passed to " + this['ExtensionName'] + ".";
		}

		// Note: New Direction parameter is not supported by this, add a workaround based on action and parameter index;
		// SDL Joystick's source has an example.
		const args = new Array(func.length);
		for (let i = 0; i < args.length; ++i) {
			args[i] = act.getParamExpression(this.rh, i);
		}

		func.apply(this, args);
	},
	expression: function(num) {
		/// <summary> Called during the evaluation of an expression. </summary>
		/// <param name="num" type="Number"> The ID/number of the expression. </param>
		/// <returns> The result of the calculation, a number or a string </returns>

		// Note that it is important that your expression function asks for
		// each and every one of the parameters of the function, each time it is
		// called. The runtime will crash if you miss parameters.

		const func = this.$expressionFuncs[~~num];
		if (func == null) {
			throw "Unrecognised expression ID " + (~~num) + " passed to " + this['ExtensionName'] + ".";
		}

		const args = new Array(func.length);
		for (let i = 0; i < args.length; ++i) {
			args[i] = this.ho.getExpParam();
		}

		return func.apply(this, args);
	},

	// ... Custom functions ...

	// ... Variable DT ...

	_calculateDT: function() {
		const currentTimer = this._getTimer();
		let delta = 0.0;

		if (this.dtUseFusionTimerSubMsPrecision) {
			delta = this._calculateFusionDeltaMs(currentTimer, 'dtOldTimer', 'dtLastHiResMs', 'dtHasHiResBaseline');
		}
		else {
			delta = (currentTimer - (this.dtOldTimer >>> 0));
			if (delta < 0) { delta = 0; }
		}

		this.dtLastDeltaMs = +delta;

		let ratio = +this.dtRatio;
		if ((~~this.dtTimeScale) > 0) {
			ratio = (ratio * 100.0) / (+this.dtTimeScale);
		}
		else {
			ratio = 0.0;
		}

		let dt = 0.0;
		if (ratio !== 0.0) {
			dt = (+delta) / ratio;
		}
		else {
			dt = 0.0;
		}

		this.dt = +dt;
		this.dtOldTimer = currentTimer >>> 0;

		this.dtNSteps = ~~((+this.dtStepsQuality) * this.dt);
	},
	_setDTState: function(state) {
		state = !!state;
		if (this.dtState !== state) {
			this.dtOldTimer = this._getTimer();
			this.dtLastHiResMs = 0.0;
			this.dtHasHiResBaseline = false;

			if (!state) {
				// Mirrors C++: when disabling, set DT=1 and steps=1
				this.dt = 1.0;
				this.dtNSteps = 1;
			}
		}
		this.dtState = state;
	},

	// ... Fixed DT ...

	_calculateFixedDT: function() {
		// Mirrors C++ FixedDeltaTime with safety caps
		const currentTimer = this._getTimer();
		let delta = (currentTimer - (this.fixedOldTimer >>> 0));
		if (delta < 0) { delta = 0; }

		const ratio = +this.fixedRatio;
		let stepInterval = 0.0;
		if (ratio !== 0.0) {
			stepInterval = (+delta) / ratio;
		}
		else {
			stepInterval = 0.0;
		}

		// C++ caps: kMaxStepInterval=1000, kMaxStepsPerTick=1000, kMaxStepCount=1000
		if (stepInterval < 0.0 || stepInterval > 1000.0) {
			stepInterval = 0.0;
		}

		this.fixedStepInterval = +stepInterval;
		this.fixedOldTimer = currentTimer >>> 0;

		this.fixedStepCount += this.fixedStepInterval;

		if (this.fixedStepCount < 0.0) {
			this.fixedStepCount = 0.0;
		}
		else if (this.fixedStepCount > 1000.0) {
			this.fixedStepCount = 0.0;
		}

		const steps = ~~this.fixedStepCount;
		if (steps > 0 && steps < 1000) {
			for (let i = 0; i < steps; ++i) {
				this._onFixedDTUpdate();
			}
		}

		// Keep remainder
		this.fixedStepCount = this.fixedStepCount - Math.floor(this.fixedStepCount);
		if (this.fixedStepCount < 0.0) {
			this.fixedStepCount = 0.0;
		}
		else if (this.fixedStepCount >= 1.0) {
			this.fixedStepCount = 0.0;
		}
	},
	_setFixedDTState: function(state) {
		state = !!state;
		if (this.fixedState !== state) {
			this.fixedOldTimer = this._getTimer();
			if (!state) {
				this.fixedStepInterval = 0.0;
				this.fixedStepCount = 0.0;
			}
		}
		this.fixedState = state;
	},

	_onFixedDTUpdate: function() {
		this._sendEvent(CND.OnFixedDtUpdate);
	},
	_sendEvent: function(condId) {
		if (this.pushEvents) {
			this.ho.pushEvent(condId);
		} else {
			this.ho.generateEvent(condId);
		}
	},

	// ... Movement Timer ...

	_getTimer: function() {
		return this.ho.hoAdRunHeader.rhTimer;
	},
	_getMvtTimerCoef: function() {
		// If disabled and override enabled: return 1.0 (C++ behavior when flag set)
		if (this._getReturnMvtTimerCoefAsOneIfDisabled()) {
			return 1.0;
		}

		return this.ho.hoAdRunHeader.rh4MvtTimerCoef;
	},
	_getMvtTimerBase: function() {
		return ~~this.ho.hoAdRunHeader.rhFrame.m_dwMvtTimerBase;
	},
	_setMvtTimerBase: function(base) {
		base = Math.max(~~base, 0);
		this.ho.hoAdRunHeader.rhFrame.m_dwMvtTimerBase = base;
	},
	_getMvtTimerState: function() {
		return (this.ho.hoAdRunHeader.rhFrame.leFlags & CRunFrame.LEF_TIMEDMVTS) !== 0;
	},
	_setMvtTimerState: function(state) {
		if (state) {
			this.ho.hoAdRunHeader.rhFrame.leFlags |= CRunFrame.LEF_TIMEDMVTS;
		} else {
			this.ho.hoAdRunHeader.rhFrame.leFlags &= ~CRunFrame.LEF_TIMEDMVTS;
		}

		// Reset fake-precision baselines on state change
		this.mvtLastHiResMs = 0.0;
		this.mvtHasHiResBaseline = false;
		this.mvtLastTimerMs = this._getTimer();
		this.mvtHasCachedFakeCoef = false;
	},
	_getReturnMvtTimerCoefAsOneIfDisabled: function() {
		return !!this.returnMvtTimerCoefAsOneIfDisabled && !this._getMvtTimerState();
	},
	_getMvtTimerNSteps: function() {
		const coef = +this._getMvtTimerCoef();
		const steps = Math.max((+this.mvtTimerStepsQuality) * coef, 1.0);
		return ~~steps;
	},
	_getMvtTimerBaseFromTimeScale: function(defaultMvt, timeScale) {
		return ~~((+timeScale / 100.0) * (+defaultMvt));
	},
	_getTimeScaleFromMvtTimerBase: function(defaultMvt, base) {
		defaultMvt = ~~defaultMvt;
		base = ~~base;
		if (defaultMvt <= 0) { return 0; }
		return ~~((base / (+defaultMvt)) * 100.0);
	},
	_getMvtTimerCoefOutput: function() {
		const raw = +this._getMvtTimerCoef();
		if (!this.mvtFakePrecisionEnabled) {
			return raw;
		}

		if (this._getReturnMvtTimerCoefAsOneIfDisabled()) {
			return 1.0;
		}

		if (!this._getMvtTimerState()) {
			return raw;
		}

		const base = ~~this._getMvtTimerBase();
		if (base <= 0) {
			return raw;
		}

		const currentTimer = this._getTimer();
		if (this.mvtHasCachedFakeCoef && currentTimer === this.mvtLastCoefTimerMs) {
			return this.mvtCachedFakeCoef;
		}

		let deltaMs = 0.0;
		if (this.dtState) {
			deltaMs = +this.dtLastDeltaMs;
		}
		else {
			deltaMs = this._calculateFusionDeltaMs(currentTimer, 'mvtLastTimerMs', 'mvtLastHiResMs', 'mvtHasHiResBaseline');
		}

		const coef = ((+deltaMs) * (+base)) / kMsPerSecond;

		this.mvtCachedFakeCoef = +coef;
		this.mvtLastCoefTimerMs = currentTimer >>> 0;
		this.mvtHasCachedFakeCoef = true;

		return this.mvtCachedFakeCoef;
	},

	// ... Utilities ...

	_fpsToMs: function(fps) {
		fps = ~~fps;
		if (fps <= 0) { return 0.0; }
		return (+kMaxFPS) / (+fps);
	},
	_msToFps: function(ms) {
		ms = +ms;
		if (ms <= 0.0) { return 0; }
		return ~~((+kMaxFPS) / ms);
	},

	_getHighResTimeMs: function() {
		if (window['performance'] && window['performance'].now) {
			return +window['performance'].now();
		}
		return +Date.now();
	},

	_calculateFusionDeltaMs: function(currentTimer, lastTimerMsKey, lastHiResMsKey, hasBaselineKey) {
		const nowMs = this._getHighResTimeMs();

		if (!this[hasBaselineKey] || currentTimer < this[lastTimerMsKey]) {
			this[lastHiResMsKey] = nowMs;
			this[hasBaselineKey] = true;
			this[lastTimerMsKey] = currentTimer;
			return 0.0;
		}

		const hiResDeltaMs = (+nowMs) - (+this[lastHiResMsKey]);
		this[lastHiResMsKey] = nowMs;

		let fusionDeltaMs = 0;
		if (currentTimer >= this[lastTimerMsKey]) {
			fusionDeltaMs = currentTimer - this[lastTimerMsKey];
		}

		this[lastTimerMsKey] = currentTimer;

		let fractional = 0.0;
		if (fusionDeltaMs > 0) {
			fractional = hiResDeltaMs - (+fusionDeltaMs);
			if (fractional < 0.0) {
				fractional = 0.0;
			}
		}

		const deltaMs = (+fusionDeltaMs) + ClampFloat(fractional, kMinFractionalMs, kMaxFractionalMs);
		return +deltaMs;
	}

	// No comma for the last function : the Google compiler
	// we use for final projects does not accept it
};

CServices.extend(CRunExtension, CRunDeltaX);
