ruffle-rs / ruffle

A Flash Player emulator written in Rust
https://ruffle.rs
Other
15.54k stars 806 forks source link

Exception on processing URLLoader data (AS3) #11586

Open Mennez opened 1 year ago

Mennez commented 1 year ago

Describe the bug

We are trying to communicate with our server to submit high scores in our AS3 games. We noticed that the data returned from the server is not correctly processed. Consider the following two lines of code:

var loader:URLLoader = URLLoader(e.target);
var urlVars:URLVariables = loader.data;

In this case loader.data should contain an object with several variables. As the server has returned the following string:

"newPosition=1&totalPlayers=13&yourHighest=173744&worldRecord=193596&message=Your score has been submitted successfully!"

We expected loader.data to contain:

{
newPosition: '1',
totalPlayers: '13',
yourHighest: '173744',
worldRecord: '193596',
message: 'Your score has been submitted successfully!'
}

We trying to access the vars using the urlVars['message'] and we receive the following error in our console:

ERROR core/src/avm2/events.rs:419 Error dispatching event EventObject(EventObject { type: "complete", class: flash.events::Event, ptr: 0x1169d9d4 }) to handler FunctionObject(FunctionObject { ptr: 0xf05fdc }) : TypeError: Error #1009: Cannot access a property or method of a null object reference. (accessing field: message)

This exact code worked in the original AS3 processor and we have also noticed that it works perfectly for AS2 games. Is this a known issue or is it on the roadmap? This is the only critical issue currently preventing us from publishing our AS3 games to our website.

Expected behavior

Expect it to work exactly like it does with AS2.

Affected platform

Desktop app

Operating system

macOS Ventura 13.2.1

Browser

Google Chrome 114.0.5735.106

Additional information

No response

Aaron1011 commented 1 year ago

Can you share the Swf or source code (or at least the code that creates the URLLoader)?

Mennez commented 1 year ago

We did explicitly set 'scoreLoader.dataFormat = URLLoaderDataFormat.VARIABLES', to use the url vars. This did work perfectly for the old flash player, but does not work for the RUFFLE player in AS3 games. Hereby the class we use with the URLLoader:

package clashflash.site {

    import clashflash.site.SessionEvent;
    import clashflash.site.ClashDataEvent;

    import flash.events.*;
    import flash.utils.*;
    import flash.display.*;
    import flash.net.*;
    import flash.system.Security;
    import flash.events.IOErrorEvent;
    import flash.events.HTTPStatusEvent;

    public class SiteCommunicator extends EventDispatcher {

        // config vars
        private var millisecondsBeforeDie = 5000; // millisecond before cancel communication attempt
        private var statusText:String = "Verifying, please wait..."; // use getStatusText()

        // vars that we will get with receiving data
        private var flashToken:String;
        private var submissionURL:String;
        private var saveURL:String;
        public var newPosition:String = "";
        public var totalPlayers:String = "";
        public var yourHighest:String = "";
        public var worldRecord:String = "";
        public var errorMsg:String;

        // private vars
        private var domainLocation:String;
        private var loaderInf:LoaderInfo;
        private var documentURL:String;
        private var scoreLoader:URLLoader = new URLLoader();
        private var sessionLoader:URLLoader = new URLLoader();
        private var loginLoader:URLLoader = new URLLoader();
        private var profileLoader:URLLoader = new URLLoader();
        private var scoresLoader:URLLoader = new URLLoader();
        private var statusEvent:Event;
        private var localHost:Boolean = true;
        private var isMobile:Boolean = false;
        private var hasResponse:Boolean = false;
        private var gameIdCode:String;

        // dispatches after verify has been called and all goes well
        public static const STATUSTEXT_UPDATE:String = "SiteCommunicator.StatusTextUpdate";
        // dispatches after submitClashScore has been called and all goes well
        public static const SCORE_SUBMITTED:String = "SiteCommunicator.DoneSendingScore";

        public static const SESSION_CREATED:String = "SiteCommunicator.SessionCreated";
        public static const LOGGED_IN:String = "SiteCommunicator.LoggedIn";
        public static const PROFILE_RETRIEVED:String = "SiteCommunicator.ProfileRetrieved";
        public static const SCORES_LISTED:String = "SiteCommunicator.ScoresListed";
        public static const LOGIN_FAILED:String = "SiteCommunicator.LoginFailed";

        /*
         Constructor
         */
        public function SiteCommunicator(documentURL:String, gameIdCode:String, isMobile:Boolean = false):void {
            if (documentURL.indexOf('file://') === -1 && documentURL.indexOf('localhost') === -1) this.localHost = false;
            else this.documentURL = documentURL;
            this.gameIdCode = gameIdCode;
            scoreLoader.addEventListener(Event.COMPLETE, doneSendingAndReceiving);
            scoreLoader.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            scoreLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHTTPStatus);
            addEventListener(SiteCommunicator.SCORE_SUBMITTED, dispatchDoneSendingAndReceiving);
            // interpreter loader as Variables
            scoreLoader.dataFormat = URLLoaderDataFormat.VARIABLES;

            loginLoader.addEventListener(Event.COMPLETE, doneLoginRequest);
            loginLoader.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            loginLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHTTPStatus);

            sessionLoader.addEventListener(Event.COMPLETE, doneSessionRequest);
            sessionLoader.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            sessionLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHTTPStatus);

            profileLoader.addEventListener(Event.COMPLETE, doneProfileRequest);
            profileLoader.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            profileLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHTTPStatus);

            scoresLoader.addEventListener(Event.COMPLETE, doneScoresRequest);
            scoresLoader.addEventListener(IOErrorEvent.IO_ERROR, handleIOError);
            scoresLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHTTPStatus);
            this.isMobile = isMobile;
        }

        /*
         let outside classes get event names
         */
        public function getScoreSubmitted():String {    return SiteCommunicator.SCORE_SUBMITTED; }
        public function getStatusTextUpdate():String {  return SiteCommunicator.STATUSTEXT_UPDATE; }

        /*
         Verify with domain and get token and urls
         */
        public function verify():Boolean {
            if (this.localHost || isMobile || Security.pageDomain == null) return true;
            else if (Security.pageDomain.indexOf('clashflash.com') != -1) {
                return true;
            }
            return false;
        }

        /*
         get statusText
         */
        public function getStatusText():String {
            return this.statusText;
        }

        /*
         Dispatch status update event
         */
        public function statusTextUpdate(str:String):void {
            this.statusText = str;
            dispatchEvent(new Event(SiteCommunicator.STATUSTEXT_UPDATE, true));
        }

        public function login(usernameOrEmail:String, password:String):void {
            statusTextUpdate("Attempting to login on clashflash.com");
            hasResponse = false;
            var loginRequest:URLRequest = new URLRequest((this.localHost ? 'http://localhost:3000' : 'https://www.clashflash.com') + '/users/login/');
            loginRequest.method = URLRequestMethod.POST;
            var loginVariables:URLVariables = new URLVariables();
            var emailCheck:RegExp = /\w+@\w+\./;
            usernameOrEmail = usernameOrEmail.toLowerCase(); // always lower case
            if (emailCheck.test(usernameOrEmail)) loginVariables.email = usernameOrEmail;
            else loginVariables.username = usernameOrEmail;
            loginVariables.password = password;
            loginRequest.data = loginVariables;
            try {
                this.loginLoader.load(loginRequest);
            } catch (e:Error) {
                trace("Loader error during login request");
                statusTextUpdate("Unable to login on clashflash server");
            }
        }

        public function getSession(authToken:String):void {
            statusTextUpdate("Requesting game session");
            var sessionRequest:URLRequest = new URLRequest((this.localHost ? 'http://localhost:3000' : 'https://www.clashflash.com') + '/methods/game-sessions.session/');
            sessionRequest.method = URLRequestMethod.POST;
            var sessionVariables:URLVariables = new URLVariables();
            sessionVariables.gameId = gameIdCode;
            sessionRequest.data = sessionVariables;
            try {
                sessionRequest.requestHeaders = [
                    new URLRequestHeader("pragma", "no-cache"),
                    new URLRequestHeader("Authorization", "Bearer "+authToken)
                ];
                this.sessionLoader.load(sessionRequest);
            } catch (e:Error) {
                trace("Loader error during the requesting of a game session");
                statusTextUpdate("Unable to request a game session");
            }
        }

        public function getProfile(userId:String):void {
            statusTextUpdate("Requesting user profile");
            var request:URLRequest = new URLRequest((this.localHost ? 'http://localhost:3000' : 'https://www.clashflash.com') + '/methods/users.get.profile/');
            request.method = URLRequestMethod.POST;
            var variables:URLVariables = new URLVariables();
            variables.userId = userId;
            request.data = variables;
            try {
                request.requestHeaders = [
                    new URLRequestHeader("pragma", "no-cache")
                ];
                this.profileLoader.load(request);
            } catch (e:Error) {
                trace("Loader error during the requesting the user profile");
                statusTextUpdate("Unable to request user profile");
            }
        }

        public function getHighscores():void {
            statusTextUpdate("Requesting highscore listing");
            var request:URLRequest = new URLRequest((this.localHost ? 'http://localhost:3000' : 'https://www.clashflash.com') + '/methods/game-sessions.highscores/');
            request.method = URLRequestMethod.POST;
            var variables:URLVariables = new URLVariables();
            variables.gameId = gameIdCode;
            request.data = variables;
            try {
                request.requestHeaders = [
                    new URLRequestHeader("pragma", "no-cache")
                ];
                this.scoresLoader.load(request);
            } catch (e:Error) {
                trace("Loader error during the requesting the highscore listing");
                statusTextUpdate("Unable to request the highscore listing");
            }
        }

        /*
         submit score to server
         */
        public function submitClashScore(score:int, gameKey:int, userId:String, token:String):String {
            hasResponse = false;
            var statusMessage = "Submitting score to "+(this.localHost ? "localhost" : "clashflash.com")+", please wait...";
            statusTextUpdate(statusMessage);
            // send score to php
            var scoreRequest:URLRequest = new URLRequest((this.localHost ? 'http://localhost:3000' : 'https://www.clashflash.com') + '/methods/game-sessions.score/');
            var scoreVariables:URLVariables = new URLVariables();
            // initialize vars
            if (gameIdCode) {
                scoreVariables.score = score;
                scoreVariables.gameId = gameIdCode;
                scoreVariables.token = token;
                scoreVariables.userId = userId;
            } else scoreVariables.gameId = 'test';
            // prepare to post data
            scoreRequest.method = URLRequestMethod.POST;
            scoreRequest.data = scoreVariables;
            // send and load vars
            try {
                this.scoreLoader.load(scoreRequest);
            } catch (e:Error) {
                trace("Loader error during result send request");
                statusTextUpdate("Unable to communicate with server");
            }
            // scoreLoader has event listener on complete: function DoneSendingAndReceiving
            return statusMessage;
        }

        /*
         Done sending score
         */
        private function dispatchDoneSendingAndReceiving(e:Event):void {
            trace("done sending score and receiving data: (see below)");
            trace("New position: "+this.newPosition);
            trace("Total players: "+this.totalPlayers);
            trace("Your highest: "+this.yourHighest);
            trace("Message: "+this.errorMsg);
            trace("WorldRecord: "+this.worldRecord);
        }
        /*
         Call function after sending and receiving
         */
        private function doneSendingAndReceiving(e:Event):void {
            var loader:URLLoader = URLLoader(e.target);
            var urlVars:URLVariables = loader.data;
            if (urlVars["message"] != null) {
                this.errorMsg = urlVars["message"];
                statusTextUpdate(urlVars["message"]);
            }
            if (urlVars["newPosition"] != null) this.newPosition = urlVars["newPosition"];
            else this.newPosition = "";
            if (urlVars["totalPlayers"] != null) this.totalPlayers = urlVars["totalPlayers"];
            else this.totalPlayers = "";
            if (urlVars["yourHighest"] != null) this.yourHighest = urlVars["yourHighest"];
            else this.yourHighest = "";
            if (urlVars["worldRecord"] != null) this.worldRecord = urlVars["worldRecord"];
            else this.worldRecord = "";
            dispatchEvent(new Event(SiteCommunicator.SCORE_SUBMITTED, true));
        }

        private function doneSessionRequest(e:Event):void {
            statusTextUpdate("Game session started successfully");
            var loader:URLLoader = URLLoader(e.target);
            var result:Object = JSON.parse(loader.data);
            dispatchEvent(new SessionEvent(SESSION_CREATED, result.token, result.tokenExpires, result.key));
        }

        private function doneProfileRequest(e:Event):void {
            statusTextUpdate("Profile loaded");
            var loader:URLLoader = URLLoader(e.target);
            var result:Object = JSON.parse(loader.data);
            dispatchEvent(new ClashDataEvent(PROFILE_RETRIEVED, result));
        }

        private function doneScoresRequest(e:Event):void {
            statusTextUpdate("Highscores retrieved");
            var loader:URLLoader = URLLoader(e.target);
            var result:Object = JSON.parse(loader.data);
            dispatchEvent(new ClashDataEvent(SCORES_LISTED, result));
        }

        private function doneLoginRequest(e:Event):void {
            statusTextUpdate("Authentication completed");
            var loader:URLLoader = URLLoader(e.target);
            var result:Object = JSON.parse(loader.data);
            dispatchEvent(new SessionEvent(LOGGED_IN, result.token, result.tokenExpires, result.id));
        }

        private function handleHTTPStatus(e:HTTPStatusEvent):void {
            hasResponse = true;
            trace(e);
            if (e.status == 400) {
                dispatchEvent(new Event(LOGIN_FAILED, true));
                statusTextUpdate('Incorrect user or password');
            } else if (e.status != 200) statusTextUpdate('An unexpected error ('+e.status+') occurred');
        }

        private function handleIOError(e:IOErrorEvent):void {
            if (!hasResponse) statusTextUpdate('No connection');
        }
    }
}
Mennez commented 1 year ago

@Aaron1011 is this enough to reproduce the issue?

Mennez commented 1 year ago

Any update? Is this noted for one of missing features in the AS3 api?