00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,708 / 1,880
Exp Rank:
38,121
Vote Power:
5.50 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,485
Blams:
50
Saves:
377
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,733
Supporter:
3y 10m 14d

NewGrounds.io API in Godot

Posted by 3p0ch - September 9th, 2021


Note: This NewGrounds API implementation was written for Godot 3.x. If you're using Godot 4, that version of the implementation is available at

https://3p0ch.newgrounds.com/news/post/1374957

but as of this writing (July 2023) Godot 4 web exports are not compatible with Apple (macOS and iOS) devices so I would recommend sticking with Godot 3 until that's confirmed to be fixed.


Just copy/paste the code below and save it in your Godot project as an AutoLoad script named ngio.gd and see the instructions near the top.


extends Node

var app_id = "" # Put your app ID from NewGrounds here
var aes_key = "" # Put your AES-128/Base64 encryption key 
var show_cloudsave_results = true # Whether to show API call results in Developer Tools - Console
#                                   You might want to turn that false in the final published version

# To add this NewGrounds.io helper script to your Godot project as an AutoLoad:
#   Save this file as ngio.gd anywhere in your project
#   In the top menu bar: Project -> Project Settings... -> AutoLoad tab
#   Click the folder button next to Path and choose this file (ngio.gd)
#   To the right of the Node Name box (it can say Ngio) click the Add button
# It should be added to your AutoLoad list and you're good to go

# Make sure you've added your App ID, and your
# AES-128/Base64 encryption key, to the lines near the top

# To make calls to the NewGrounds.io API from your scripts, use the command
#   Ngio.request(component, parameters)
# For example, to send a score to a leaderboard
#   Ngio.request("ScoreBoard.postScore", {"id": your_scoreboard_id, "value": the_score})
# Or to unlock a medal
#   Ngio.request("Medal.unlock", {"id": your_medal_id})
# For details on the components, see http://www.newgrounds.io/help/components/

# If you want to handle results from a component call, you can do it by
# making the script calling ngio.gd set up a funcref for the function that
# will handle the results, like so ...
# func _ready():
# 	Ngio.request("Medal.getList", {}, funcref(self, "handle_results"))
# 
# func handle_results(results):
# 	print ("Results: ", results) # results will show in your browser under Developer Tools in the Console tab)

# Cloud saving is more complicated and for a full description see my news post
# https://3p0ch.newgrounds.com/news/post/1294903
# But here's a quick reference
#   Ngio.cloud_save(data_to_be_saved, slot_number, funcref(self, "_on_cloudsave_complete"))
#     slot_number is optional and default is slot 1
#     the funcref is optional and in practice you probably don't want to use it
#
#   Ngio.cloud_load(funcref(self, "_on_cloud_loaded"), slot_number)
#     slot_number is optional and default is slot 1
#     the function that the funcref points to should be something like
#   func _on_cloud_loaded(loaded_data):
#     if loaded_data == null:
#       there was no data in that save slot or load attempt failed
#     elif loaded_data is TheDataStructureYouExpect: (likely a Dictionary)
#       handle the loaded data
#
#   Ngio.cloud_load_all(funcref(self, "_on_cloud_loaded"))
#     the function that funcref points to should handle each save slot as it comes in
#   func _on_cloud_loaded(slot_number, loaded_data):
#
#   To clear the data from a slot
#   Ngio.request("CloudSave.clearSlot", {"id": slot_number})

var newgroundsio_request
var session_id
var gateway_uri = "https://newgrounds.io/gateway_v3.php"

# Ping the server occasionally to keep the session_id from timing out
var ping_cooldown_max:float = 300.0
var ping_cooldown:float = ping_cooldown_max
func _physics_process(delta):
	ping_cooldown -= delta
	if ping_cooldown < 0.0:
		request("Gateway.ping", {})
		ping_cooldown = ping_cooldown_max

func request(component, parameters, func_ref = null, on_load_function = null):
	if OS.has_feature('JavaScript'):
		newgroundsio_request = HTTPRequest.new()
		add_child(newgroundsio_request)
		if func_ref and not on_load_function:
			newgroundsio_request.connect("request_completed", self, "_request_completed", [func_ref])
		if func_ref and on_load_function:
			newgroundsio_request.connect("request_completed", self, "_request_completed", [func_ref, on_load_function])
		
		var call_parameters = {
			"component": component,
			"parameters": parameters,
		}
		
		var secure_encoding = str(JavaScript.eval(
			"""
				/*
				CryptoJS v3.1.2
				code.google.com/p/crypto-js
				(c) 2009-2013 by Jeff Mott. All rights reserved.
				code.google.com/p/crypto-js/wiki/License
				*/
				var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
				r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k<a;k++)c[j+k>>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535<e.length)for(k=0;k<a;k+=4)c[j+k>>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
				32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e<a;e+=4)c.push(4294967296*u.random()|0);return new r.init(c,a)}}),w=d.enc={},v=w.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++){var k=c[j>>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j+=2)e[j>>>3]|=parseInt(a.substr(j,
				2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j<a;j++)e.push(String.fromCharCode(c[j>>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j<c;j++)e[j>>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}},
				q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q<a;q+=k)this._doProcessBlock(e,q);q=e.splice(0,a);c.sigBytes-=j}return new r.init(q,j)},clone:function(){var a=t.clone.call(this);
				a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:t.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,e){return(new a.init(e)).finalize(b)}},_createHmacHelper:function(a){return function(b,e){return(new n.HMAC.init(a,
				e)).finalize(b)}}});var n=d.algo={};return d}(Math);
				(function(){var u=CryptoJS,p=u.lib.WordArray;u.enc.Base64={stringify:function(d){var l=d.words,p=d.sigBytes,t=this._map;d.clamp();d=[];for(var r=0;r<p;r+=3)for(var w=(l[r>>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v<p;v++)d.push(t.charAt(w>>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w<
				l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();
				(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<<j|b>>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<<j|b>>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<<j|b>>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<<j|b>>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])},
				_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]),
				f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f,
				m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m,
				E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/
				4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math);
				(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length<q;){n&&s.update(n);var n=s.update(d).finalize(r);s.reset();for(var a=1;a<p;a++)n=s.finalize(n),s.reset();b.concat(n)}b.sigBytes=4*q;return b}});u.EvpKDF=function(d,l,p){return s.create(p).compute(d,
				l)}})();
				CryptoJS.lib.Cipher||function(u){var p=CryptoJS,d=p.lib,l=d.Base,s=d.WordArray,t=d.BufferedBlockAlgorithm,r=p.enc.Base64,w=p.algo.EvpKDF,v=d.Cipher=t.extend({cfg:l.extend(),createEncryptor:function(e,a){return this.create(this._ENC_XFORM_MODE,e,a)},createDecryptor:function(e,a){return this.create(this._DEC_XFORM_MODE,e,a)},init:function(e,a,b){this.cfg=this.cfg.extend(b);this._xformMode=e;this._key=a;this.reset()},reset:function(){t.reset.call(this);this._doReset()},process:function(e){this._append(e);return this._process()},
				finalize:function(e){e&&this._append(e);return this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(e){return{encrypt:function(b,k,d){return("string"==typeof k?c:a).encrypt(e,b,k,d)},decrypt:function(b,k,d){return("string"==typeof k?c:a).decrypt(e,b,k,d)}}}});d.StreamCipher=v.extend({_doFinalize:function(){return this._process(!0)},blockSize:1});var b=p.mode={},x=function(e,a,b){var c=this._iv;c?this._iv=u:c=this._prevBlock;for(var d=0;d<b;d++)e[a+d]^=
				c[d]},q=(d.BlockCipherMode=l.extend({createEncryptor:function(e,a){return this.Encryptor.create(e,a)},createDecryptor:function(e,a){return this.Decryptor.create(e,a)},init:function(e,a){this._cipher=e;this._iv=a}})).extend();q.Encryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize;x.call(this,e,a,c);b.encryptBlock(e,a);this._prevBlock=e.slice(a,a+c)}});q.Decryptor=q.extend({processBlock:function(e,a){var b=this._cipher,c=b.blockSize,d=e.slice(a,a+c);b.decryptBlock(e,a);x.call(this,
				e,a,c);this._prevBlock=d}});b=b.CBC=q;q=(p.pad={}).Pkcs7={pad:function(a,b){for(var c=4*b,c=c-a.sigBytes%c,d=c<<24|c<<16|c<<8|c,l=[],n=0;n<c;n+=4)l.push(d);c=s.create(l,c);a.concat(c)},unpad:function(a){a.sigBytes-=a.words[a.sigBytes-1>>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a,
				this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684,
				1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})},
				decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d,
				b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}();
				(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8,
				16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j<a;j++)if(j<d)e[j]=c[j];else{var k=e[j-1];j%d?6<d&&4==j%d&&(k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;d<a;d++)j=a-d,k=d%4?e[j]:e[j-4],c[d]=4>d||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>>
				8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r<m;r++)var q=d[g>>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t=
				d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})();
			""" +
			"var iv = CryptoJS.lib.WordArray.random(16); " +
			"var encrypted = CryptoJS.AES.encrypt('" + JSON.print(call_parameters) + "', CryptoJS.enc.Base64.parse('" + aes_key + "'), { iv: iv }); " +
			"CryptoJS.enc.Base64.stringify(iv.concat(encrypted.ciphertext));"
			, true))
		
		var input_parameters = {
			"app_id": app_id,
			"session_id": str(JavaScript.eval(
				'var urlParams = new URLSearchParams(window.location.search);' +
				'urlParams.get("ngio_session_id");'
				, true)),
			"call": {
				"secure": secure_encoding,
			}
		}
		
		newgroundsio_request.request(
			gateway_uri,
			["Content-Type: application/x-www-form-urlencoded"],
			true,
			HTTPClient.METHOD_POST,
			"input=" + JSON.print(input_parameters).percent_encode()
		)

func _request_completed(result, response_code, headers, body, func_ref, on_load_function = null):
	if on_load_function:
		func_ref.call_func(parse_json(body.get_string_from_ascii()), on_load_function)
	else:
		func_ref.call_func(parse_json(body.get_string_from_ascii()))


func cloud_save(data:Dictionary, slot:int = 1, func_ref:FuncRef = funcref(self, "_show_cloud_save_results")):
	var stringified_data = to_json(data)
	stringified_data = stringified_data.replace('"', '<<<doublequote>>>')
	stringified_data = stringified_data.replace("'", '<<<singlequote>>>')
	request("CloudSave.setData", {"data": stringified_data, "id": slot}, func_ref)

func _show_cloud_save_results(results):
	if show_cloudsave_results:
		print ("Results from cloud save request: ", results)


func cloud_load(on_load_function:FuncRef, slot:int = 1):
	if show_cloudsave_results:
		print("Sending cloud load request for slot ", slot)
	request("CloudSave.loadSlot", {"id": slot}, funcref(self, "_handle_single_cloud_load_results"), on_load_function)

func _handle_single_cloud_load_results(results, on_load_function:FuncRef):
	var load_http_request = HTTPRequest.new()
	if show_cloudsave_results:
		print("Results from single slot cloud load request (not the actual data yet): ", results)
	if not results["success"]:
		on_load_function.call_func(null)
		return
	if results["result"]["data"]["slot"]["url"] == null:
		on_load_function.call_func(null)
		return
	add_child(load_http_request)
	load_http_request.connect("request_completed", self, "_handle_single_cloud_load_data", [on_load_function])
	load_http_request.request(results["result"]["data"]["slot"]["url"])

func _handle_single_cloud_load_data(result, response_code, headers, body, on_load_function:FuncRef):
	var stringified_data:String = body.get_string_from_ascii()
	stringified_data = stringified_data.replace('<<<doublequote>>>', '"')
	stringified_data = stringified_data.replace('<<<singlequote>>>', "'")
	if show_cloudsave_results:
		print("Loaded object: ", parse_json(stringified_data))
	on_load_function.call_func(parse_json(stringified_data))


func cloud_load_all(on_load_function:FuncRef):
	if show_cloudsave_results:
		print("Sending cloud load request for all slots")
	request("CloudSave.loadSlots", {}, funcref(self, "_handle_multiple_cloud_load_results"), on_load_function)

func _handle_multiple_cloud_load_results(results, on_load_function:FuncRef):
	var load_http_request
	if show_cloudsave_results:
		print("Results from all slot cloud load request (not the actual data yet): ", results)
	if not results["success"]:
		on_load_function.call_func(null)
		return
	for slot in results["result"]["data"]["slots"]:
		if slot["url"] == null:
			on_load_function.call_func(slot["id"], null)
		else:
			load_http_request = HTTPRequest.new()
			add_child(load_http_request)
			load_http_request.connect("request_completed", self, "_handle_multiple_cloud_load_data", [on_load_function, slot["id"]])
			load_http_request.request(slot["url"])

func _handle_multiple_cloud_load_data(result, response_code, headers, body, on_load_function:FuncRef, slot_number):
	var stringified_data:String = body.get_string_from_ascii()
	stringified_data = stringified_data.replace('<<<doublequote>>>', '"')
	stringified_data = stringified_data.replace('<<<singlequote>>>', "'")
	if show_cloudsave_results:
		print("Loaded from slot #", slot_number, ": ", parse_json(stringified_data))
	on_load_function.call_func(slot_number, parse_json(stringified_data))


Tags:

15

Comments

Great work! One of the 3 alternatives that actually functions properly.
Having it be as simple as an autoload is a GODSEND, thank you so much!

Yeah yeah based very cool

Been hoping to port one of my Android games to browser to post here, to Newgrounds. Thanks for this!

This is awesome and I've been using it on most of my games, so thank you! <3

I've taken the liberty of adding a few extra functions though to call Gateway.ping, to keep the session alive. What do you think? Sorry about the (lack of) formatting!

var session_request
var session_timer

func _ready():
if OS.has_feature('JavaScript'):
session_timer = Timer.new()
add_child(session_timer)
session_timer.one_shot = false
session_timer.autostart = false
session_timer.connect("timeout", self, "_on_session_timer_timeout")
session_timer.start(30)

func _on_session_timer_timeout():
session_request = HTTPRequest.new()
add_child(session_request)
session_request.request(
gateway_uri,
["Content-Type: application/x-www-form-urlencoded"],
true,
HTTPClient.METHOD_POST,
"input=" + JSON.print({
"app_id": app_id,
"session_id": str(JavaScript.eval(
'var urlParams = new URLSearchParams(window.location.search);' +
'urlParams.get("ngio_session_id");'
, true)),
"execute": {
"component": "Gateway.ping",
}
}).percent_encode()
)

Hmm, I wasn't even aware there would be any need to ping the server to keep the session alive. I looked at the docs again and in the section on NewGrounds Passport (https://www.newgrounds.io/help/passport/) it talks about sessions being "remembered" or not, where if it's remembered then it's good for 30 days and if it's not remembered then it's good for a few hours.

I tested it out and yeah, even if when I log in I have the "Keep me signed in" option checked, if I call App.checkSession then the Results object has remember:False. So that could potentially foul things up if the player goes AFK for a day and then comes back and keeps playing.

I agree it's probably better to just ping the server periodically instead of calling App.startSession – I always thought it was obnoxious whenever I play a game and it asked me to log in even though I was already logged in to my NewGrounds account when I opened the game's page, but I guess that's probably what they're doing and why. I still think it's unnecessarily obnoxious though :p so yeah ping the server. But the ping rate could probably be decreased and instead of every 30 seconds do a ping every hour (3600 seconds) or so.

But first, I just uploaded a test project that has a button to check the session ID and made sure it works, so I'm going to go take a catnap and come back sometime tomorrow to confirm if it really does forget the session ID, and if so then add a pinger.

Incidentally, while I was checking the newgrounds.io site I saw that @jefvel made an addon for Godot 4 that looks really sophisticated and automates stuff like keeping track of medals and scores that are achieved while not logged in and syncs them when the user does log in. It looks like it's only for Godot 4 and not Godot 3 though, which wouldn't be my preference because AFAIK the Apple incompatibility issue with Godot web exports is still there for the foreseeable future (plz correct me if I'm wrong about that though).

@3p0ch this is comes off the back of a discussion in the newgrounds.io discord (you should join! :) ) where @psychogoldfish said sessions timeout after 30 minutes of inactivity, which I was definitely seeing - some of my harder medals weren’t unlocking for people! The 30 second timer came from the unity implantation of this.

And yeah, AFAIK, web export is largely broken for Godot 4. Not that I’ve tried it myself! I’ve been quite happy with Godot 3.5.

@blit-blat @3p0ch Hello! Yeah, I've been meaning to write up a news post here about that addon. But as you say, Godot 4 is not really viable for the web at the moment. I wrote it mostly in hopes that web support will improve in the future. (There was a PR for fixing the Apple issues though, which I think has been merged but not yet released. Keeping my fingers crossed!)

I could try to port it to Godot 3 at some point, not sure how complicated it would be since I've only used Godot 4.

I think you should ping the server at least every half hour according to somewhere (don't remember where I read it), so maybe every 5 minutes just to be sure?

I noticed the code uses some inline JS for AES encryption, so I modified it a bit. It uses Godot's own AES stuff, this way you can also run it on desktop. It also frees the request node on completion (otherwise every request will remain in the tree): https://hastebin.skyra.pw/wiqixituwo.php

This is amazing, thanks so much. You are, indeed, based.