ruffle-rs / ruffle

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

Angry Birds Refresh - ExternalInterface Login Failure #16942

Open ultra0000 opened 5 days ago

ultra0000 commented 5 days ago

Describe the bug

The authentication data fails to get sent through ExternalInterface, with the following error:

ERROR core/src/external.rs:310 Unhandled error in External Interface callback sendDiscordData: TypeError: Error #2007: Parameter closure must be non-null.

Expected behavior

The game should receive the data properly after authenticating with Discord.

Content Location

https://refresh.teamflashcord.com/

Affected platform

Browser's extension

Operating system

Windows 11

Browser

Microsoft Edge 126.0.2592.81

Additional information

Relevant JavaScript code:

var discordResult;
var gotResult = false;

function HandleDiscordResult(result) {
    discordResult = JSON.parse(result);
    gotResult = true;
}

function discordLogIn() {
    var urlToOpen = "/discordauth/login/";
    var popup = createPopupWindow(urlToOpen, "", 450, 600);

    var pollTimer = window.setInterval(function() {
        if (popup.closed !== false) {
            window.clearInterval(pollTimer);
            if (discordResult !== null && gotResult) {
                document.getElementById("AngryBirdsRefresh").sendDiscordData(discordResult);
                discordResult = null;
            }
            else {
                if (gotResult) {
                    document.getElementById("AngryBirdsRefresh").sendDiscordData({"error": "Result is null"});
                }
                else {
                    document.getElementById("AngryBirdsRefresh").sendDiscordData({"error": "Didn't Log In"});
                }
            }
        }
    }, 200);
}

Relevant ActionScript 3 code:

      private static function serverCallDiscord() : void
      {
         if (AngryBirdsBase.singleton.mCanvas.loaderInfo.parameters.launcherType != "AIRBIRD")
         {
            // we're not on airbird, proceed as normal
            ExternalInterfaceHandler.performCall("discordLogIn");
            ExternalInterfaceHandler.addCallback("sendDiscordData",onDiscordGotData);
         }
         ...
      }

      private static function onDiscordGotData(response:Object) : void
      {
         ExternalInterfaceHandler.removeCallback("sendDiscordData",onDiscordGotData);
         if (response.error == "Didn't Log In")
         {
            AngryBirdsBase.singleton.popupManager.openPopup(new ErrorPopup(ErrorPopup.DIDNT_LOG_IN,"Server Error: User didn't log in."));
         }
         else if (response.error == "Banned")
         {
            AngryBirdsBase.singleton.popupManager.openPopup(new ErrorPopup(ErrorPopup.BANNED,"User has been banned.", response.banReason, response.banTime));
         }
         else if (response.error == "Success")
         {
            sFacebookUserId = response.userId;
            sAccessToken = response.accessToken;
            sExpiresInSeconds = response.expiresInSeconds;
            serverCallLogIn();
         }
         else
         {
            AngryBirdsBase.singleton.popupManager.openPopup(new ErrorPopup(ErrorPopup.ERROR_GENERAL,"Unknown Server Error"));
         }
      }

(AIRBird is our Adobe AIR launcher, which uses a different way of authenticating)

ultra0000 commented 5 days ago

Relevant code from ExternalInterfaceHandler:

      public static function addCallback(externalMethod:String, callback:Function) : void
      {
         try
         {
            if(!externalMethods[externalMethod])
            {
               externalMethods[externalMethod] = new ExternalInterfaceMethod(externalMethod);
            }
            (externalMethods[externalMethod] as ExternalInterfaceMethod).addCallback(callback);
         }
         catch(e:Error)
         {
         }
      }

      public static function removeCallback(externalMethod:String, callback:Function) : void
      {
         var method:ExternalInterfaceMethod = externalMethods[externalMethod] as ExternalInterfaceMethod;
         if(method)
         {
            method.removeCallback(callback);
            if(method.callbackCount == 0)
            {
               method.dispose();
               delete externalMethods[externalMethod];
            }
         }
      }

      public static function performCall(call:String, ... params) : *
      {
         var logStr:String = "ExternalInterface call: " + call + "(" + params.join(", ") + ");";
         if(logStr.length > 300)
         {
            logStr = logStr.substr(0,300) + "[...]";
         }
         Log.log(logStr);
         if(ExternalInterface.available && EXTERNAL_INTERFACES_ENABLED)
         {
            try
            {
               params.unshift(call);
               return ExternalInterface.call.apply(null,params);
            }
            catch(e:Error)
            {
               Log.log("ExternalInterface call failed!\nCall was:" + call + "\nError data:" + e.toString());
            }
         }
         ...
      }
ultra0000 commented 5 days ago

ExternalInterfaceMethod:

package com.rovio.externalInterface
{
   import com.rovio.factory.Log;
   import flash.external.ExternalInterface;

   public class ExternalInterfaceMethod
   {

      public var externalMethodName:String = "";

      private var callbacks:Array = null;

      public function ExternalInterfaceMethod(methodName:String)
      {
         super();
         this.externalMethodName = methodName;
         if(ExternalInterface.available)
         {
            ExternalInterface.addCallback(this.externalMethodName,this.methodListener);
         }
      }

      public function methodListener(... args) : *
      {
         var logStr:* = null;
         var i:Number = NaN;
         var f:Function = null;
         logStr = "call through externalInterface! " + this.externalMethodName + "(";
         for(i = 0; i < args.length; i++)
         {
            logStr += args[i] + ",";
         }
         logStr += ")";
         Log.log(logStr);
         var returnValue:* = null;
         if(this.callbacks != null)
         {
            for each(f in this.callbacks)
            {
               returnValue = f.apply(null,args);
            }
         }
         return returnValue;
      }

      public function addCallback(callback:Function) : void
      {
         if(this.callbacks == null)
         {
            this.callbacks = new Array();
         }
         if(this.callbacks.indexOf(callback) == -1)
         {
            this.callbacks.push(callback);
         }
      }

      public function removeCallback(callback:Function) : void
      {
         if(this.callbacks && this.callbacks.indexOf(callback) != -1)
         {
            this.callbacks.splice(this.callbacks.indexOf(callback),1);
         }
      }

      public function get callbackCount() : int
      {
         if(!this.callbacks)
         {
            return 0;
         }
         return this.callbacks.length;
      }

      public function dispose() : void
      {
         if (ExternalInterface.available)
         {
            ExternalInterface.addCallback(this.externalMethodName,null);
         }
      }
   }
}
sleepycatcoding commented 5 days ago

According to ExternalInterface.addCallback docs, using null closure argument on an existing callback should remove it (I haven't tested this, so it might be wrong). We currently throw on null or undefined https://github.com/ruffle-rs/ruffle/blob/fd5ca6ae392b44a3bdf63bfd8407e4c05d17c8dd/core/src/avm2/globals/flash/external/external_interface.rs#L57

Fancy2209 commented 5 days ago

According to ExternalInterface.addCallback docs, using null closure argument on an existing callback should remove it (I haven't tested this, so it might be wrong). We currently throw on null or undefined

https://github.com/ruffle-rs/ruffle/blob/fd5ca6ae392b44a3bdf63bfd8407e4c05d17c8dd/core/src/avm2/globals/flash/external/external_interface.rs#L57

This game works on Flash and AIR, so the docs must be wrong.