Exemple #1
0
        private void OnBackgroundInteractiveData(byte[] data)
        {
            // WARNING: Only invoke applet state changes after an explicit finalization
            // request from the game, this is because the inline keyboard is expected to
            // keep running in the background sending data by itself.

            using (MemoryStream stream = new MemoryStream(data))
                using (BinaryReader reader = new BinaryReader(stream))
                {
                    var request = (InlineKeyboardRequest)reader.ReadUInt32();

                    long remaining;

                    // Always show the keyboard if the state is 'Ready'.
                    bool showKeyboard = _state == SoftwareKeyboardState.Ready;

                    switch (request)
                    {
                    case InlineKeyboardRequest.Unknown0: // Unknown request sent by some games after calc
                        _interactiveSession.Push(InlineResponses.Default());
                        break;

                    case InlineKeyboardRequest.UseChangedStringV2:
                        // Not used because we only send the entire string after confirmation.
                        _interactiveSession.Push(InlineResponses.Default());
                        break;

                    case InlineKeyboardRequest.UseMovedCursorV2:
                        // Not used because we only send the entire string after confirmation.
                        _interactiveSession.Push(InlineResponses.Default());
                        break;

                    case InlineKeyboardRequest.SetCustomizeDic:
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>())
                        {
                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!");
                        }
                        else
                        {
                            var keyboardDictData = reader.ReadBytes((int)remaining);
                            _keyboardDict = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData);
                        }
                        _interactiveSession.Push(InlineResponses.Default());
                        break;

                    case InlineKeyboardRequest.Calc:
                        // Put the keyboard in a Ready state, this will force showing
                        _state    = SoftwareKeyboardState.Ready;
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardCalc>())
                        {
                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!");
                        }
                        else
                        {
                            var keyboardCalcData = reader.ReadBytes((int)remaining);
                            _keyboardCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData);

                            if (_keyboardCalc.Utf8Mode == 0x1)
                            {
                                _encoding = Encoding.UTF8;
                            }

                            // Force showing the keyboard regardless of the state, an unwanted
                            // input dialog may show, but it is better than a soft lock.
                            if (_keyboardCalc.Appear.ShouldBeHidden == 0)
                            {
                                showKeyboard = true;
                            }
                        }
                        // Send an initialization finished signal.
                        _interactiveSession.Push(InlineResponses.FinishedInitialize());
                        // Start a task with the GUI handler to get user's input.
                        new Task(() =>
                        {
                            bool submit      = true;
                            string inputText = (!string.IsNullOrWhiteSpace(_keyboardCalc.InputText) ? _keyboardCalc.InputText : DefaultText);

                            // Call the configured GUI handler to get user's input.
                            if (!showKeyboard)
                            {
                                // Submit the default text to avoid soft locking if the keyboard was ignored by
                                // accident. It's better to change the name than being locked out of the game.
                                submit    = true;
                                inputText = DefaultText;

                                Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown");
                            }
                            else if (_device.UiHandler == null)
                            {
                                Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
                            }
                            else
                            {
                                var args = new SoftwareKeyboardUiArgs
                                {
                                    HeaderText      = "", // The inline keyboard lacks these texts
                                    SubtitleText    = "",
                                    GuideText       = "",
                                    SubmitText      = (!string.IsNullOrWhiteSpace(_keyboardCalc.Appear.OkText) ? _keyboardCalc.Appear.OkText : "OK"),
                                    StringLengthMin = 0,
                                    StringLengthMax = 100,
                                    InitialText     = inputText
                                };

                                submit = _device.UiHandler.DisplayInputDialog(args, out inputText);
                            }

                            if (submit)
                            {
                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK...");

                                if (_encoding == Encoding.UTF8)
                                {
                                    _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(inputText));
                                }
                                else
                                {
                                    _interactiveSession.Push(InlineResponses.DecidedEnter(inputText));
                                }
                            }
                            else
                            {
                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel...");
                                _interactiveSession.Push(InlineResponses.DecidedCancel());
                            }

                            // TODO: Why is this necessary? Does the software expect a constant stream of responses?
                            Thread.Sleep(500);

                            Logger.Debug?.Print(LogClass.ServiceAm, "Resetting state of the keyboard...");
                            _interactiveSession.Push(InlineResponses.Default());
                        }).Start();
                        break;

                    case InlineKeyboardRequest.Finalize:
                        // The game wants to close the keyboard applet and will wait for a state change.
                        _state = SoftwareKeyboardState.Uninitialized;
                        AppletStateChanged?.Invoke(this, null);
                        break;

                    default:
                        // We shouldn't be able to get here through standard swkbd execution.
                        Logger.Error?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_state}!");
                        _interactiveSession.Push(InlineResponses.Default());
                        break;
                    }
                }
        }
        private void OnBackgroundInteractiveData(byte[] data)
        {
            // WARNING: Only invoke applet state changes after an explicit finalization
            // request from the game, this is because the inline keyboard is expected to
            // keep running in the background sending data by itself.

            using (MemoryStream stream = new MemoryStream(data))
                using (BinaryReader reader = new BinaryReader(stream))
                {
                    InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32();
                    InlineKeyboardState   state   = GetInlineState();
                    long remaining;

                    Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}");

                    switch (request)
                    {
                    case InlineKeyboardRequest.UseChangedStringV2:
                        Logger.Stub?.Print(LogClass.ServiceAm, "Keyboard response ChangedStringV2");
                        break;

                    case InlineKeyboardRequest.UseMovedCursorV2:
                        Logger.Stub?.Print(LogClass.ServiceAm, "Keyboard response MovedCursorV2");
                        break;

                    case InlineKeyboardRequest.SetUserWordInfo:
                        // Read the user word info data.
                        remaining = stream.Length - stream.Position;
                        if (remaining < sizeof(int))
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes");
                        }
                        else
                        {
                            int wordsCount = reader.ReadInt32();
                            int wordSize   = Marshal.SizeOf <SoftwareKeyboardUserWord>();
                            remaining = stream.Length - stream.Position;

                            if (wordsCount > MaxUserWords)
                            {
                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}");
                            }
                            else if (wordsCount * wordSize != remaining)
                            {
                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words");
                            }
                            else
                            {
                                _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount];

                                for (int word = 0; word < wordsCount; word++)
                                {
                                    byte[] wordData = reader.ReadBytes(wordSize);
                                    _keyboardBackgroundUserWords[word] = ReadStruct <SoftwareKeyboardUserWord>(wordData);
                                }
                            }
                        }
                        _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state));
                        break;

                    case InlineKeyboardRequest.SetCustomizeDic:
                        // Read the custom dic data.
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardCustomizeDic>())
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
                        }
                        else
                        {
                            var keyboardDicData = reader.ReadBytes((int)remaining);
                            _keyboardBackgroundDic = ReadStruct <SoftwareKeyboardCustomizeDic>(keyboardDicData);
                        }
                        break;

                    case InlineKeyboardRequest.SetCustomizedDictionaries:
                        // Read the custom dictionaries data.
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>())
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
                        }
                        else
                        {
                            var keyboardDictData = reader.ReadBytes((int)remaining);
                            _keyboardBackgroundDictSet = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData);
                        }
                        break;

                    case InlineKeyboardRequest.Calc:
                        // The Calc request tells the Applet to enter the main input handling loop, which will end
                        // with either a text being submitted or a cancel request from the user.

                        // Read the Calc data.
                        SoftwareKeyboardCalc newCalc;
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardCalc>())
                        {
                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes");
                            newCalc = new SoftwareKeyboardCalc();
                        }
                        else
                        {
                            var keyboardCalcData = reader.ReadBytes((int)remaining);
                            newCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData);
                        }

                        // Make the state transition.
                        if (state < InlineKeyboardState.Ready)
                        {
                            // This field consistently is -1 when the calc is not meant to be shown.
                            if (newCalc.Appear.Padding1 == -1)
                            {
                                state = InlineKeyboardState.Initialized;

                                Logger.Debug?.Print(LogClass.ServiceAm, $"Calc during state {state} is probably a dummy");
                            }
                            else
                            {
                                // Set the new calc
                                _keyboardBackgroundCalc = newCalc;

                                // Check if the application expects UTF8 encoding instead of UTF16.
                                if (_keyboardBackgroundCalc.UseUtf8)
                                {
                                    _encoding = Encoding.UTF8;
                                }

                                string newText        = _keyboardBackgroundCalc.InputText;
                                uint   cursorPosition = (uint)_keyboardBackgroundCalc.CursorPos;
                                _dynamicTextInputHandler?.SetText(newText);

                                state = InlineKeyboardState.Ready;
                                PushChangedString(newText, cursorPosition, state);
                            }

                            SetInlineState(state);
                        }
                        else if (state == InlineKeyboardState.Complete)
                        {
                            state = InlineKeyboardState.Initialized;
                        }

                        // Send the response to the Calc
                        _interactiveSession.Push(InlineResponses.Default(state));
                        break;

                    case InlineKeyboardRequest.Finalize:
                        // Destroy the dynamic text input handler
                        if (_dynamicTextInputHandler != null)
                        {
                            _dynamicTextInputHandler.TextChanged -= DynamicTextChanged;
                            _dynamicTextInputHandler.Dispose();
                        }
                        // The calling application wants to close the keyboard applet and will wait for a state change.
                        SetInlineState(InlineKeyboardState.Uninitialized);
                        AppletStateChanged?.Invoke(this, null);
                        break;

                    default:
                        // We shouldn't be able to get here through standard swkbd execution.
                        Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {state}");
                        _interactiveSession.Push(InlineResponses.Default(state));
                        break;
                    }
                }
        }
        private void OnBackgroundInteractiveData(byte[] data)
        {
            // WARNING: Only invoke applet state changes after an explicit finalization
            // request from the game, this is because the inline keyboard is expected to
            // keep running in the background sending data by itself.

            using (MemoryStream stream = new MemoryStream(data))
                using (BinaryReader reader = new BinaryReader(stream))
                {
                    InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32();
                    InlineKeyboardState   state   = GetInlineState();
                    long remaining;

                    Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}");

                    switch (request)
                    {
                    case InlineKeyboardRequest.UseChangedStringV2:
                        _useChangedStringV2 = true;
                        break;

                    case InlineKeyboardRequest.UseMovedCursorV2:
                        // Not used because we only reply with the final string.
                        break;

                    case InlineKeyboardRequest.SetUserWordInfo:
                        // Read the user word info data.
                        remaining = stream.Length - stream.Position;
                        if (remaining < sizeof(int))
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes");
                        }
                        else
                        {
                            int wordsCount = reader.ReadInt32();
                            int wordSize   = Marshal.SizeOf <SoftwareKeyboardUserWord>();
                            remaining = stream.Length - stream.Position;

                            if (wordsCount > MaxUserWords)
                            {
                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}");
                            }
                            else if (wordsCount * wordSize != remaining)
                            {
                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words");
                            }
                            else
                            {
                                _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount];

                                for (int word = 0; word < wordsCount; word++)
                                {
                                    byte[] wordData = reader.ReadBytes(wordSize);
                                    _keyboardBackgroundUserWords[word] = ReadStruct <SoftwareKeyboardUserWord>(wordData);
                                }
                            }
                        }
                        _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state));
                        break;

                    case InlineKeyboardRequest.SetCustomizeDic:
                        // Read the custom dic data.
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardCustomizeDic>())
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
                        }
                        else
                        {
                            var keyboardDicData = reader.ReadBytes((int)remaining);
                            _keyboardBackgroundDic = ReadStruct <SoftwareKeyboardCustomizeDic>(keyboardDicData);
                        }
                        _interactiveSession.Push(InlineResponses.UnsetCustomizeDic(state));
                        break;

                    case InlineKeyboardRequest.SetCustomizedDictionaries:
                        // Read the custom dictionaries data.
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardDictSet>())
                        {
                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
                        }
                        else
                        {
                            var keyboardDictData = reader.ReadBytes((int)remaining);
                            _keyboardBackgroundDictSet = ReadStruct <SoftwareKeyboardDictSet>(keyboardDictData);
                        }
                        _interactiveSession.Push(InlineResponses.UnsetCustomizedDictionaries(state));
                        break;

                    case InlineKeyboardRequest.Calc:
                        // The Calc request tells the Applet to enter the main input handling loop, which will end
                        // with either a text being submitted or a cancel request from the user.

                        // NOTE: Some Calc requests happen early in the application and are not meant to be shown. This possibly
                        // happens because the game has complete control over when the inline keyboard is drawn, but here it
                        // would cause a dialog to pop in the emulator, which is inconvenient. An algorithm is applied to
                        // decide whether it is a dummy Calc or not, but regardless of the result, the dummy Calc appears to
                        // never happen twice, so the keyboard will always show if it has already been shown before.
                        bool shouldShowKeyboard = _alreadyShown;
                        _alreadyShown = true;

                        // Read the Calc data.
                        remaining = stream.Length - stream.Position;
                        if (remaining != Marshal.SizeOf <SoftwareKeyboardCalc>())
                        {
                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes");
                        }
                        else
                        {
                            var keyboardCalcData = reader.ReadBytes((int)remaining);
                            _keyboardBackgroundCalc = ReadStruct <SoftwareKeyboardCalc>(keyboardCalcData);

                            // Check if the application expects UTF8 encoding instead of UTF16.
                            if (_keyboardBackgroundCalc.UseUtf8)
                            {
                                _encoding = Encoding.UTF8;
                            }

                            // Force showing the keyboard regardless of the state, an unwanted
                            // input dialog may show, but it is better than a soft lock.
                            if (_keyboardBackgroundCalc.Appear.ShouldBeHidden == 0)
                            {
                                shouldShowKeyboard = true;
                            }
                        }
                        // Send an initialization finished signal.
                        state = InlineKeyboardState.Ready;
                        SetInlineState(state);
                        _interactiveSession.Push(InlineResponses.FinishedInitialize(state));
                        // Start a task with the GUI handler to get user's input.
                        new Task(() => { GetInputTextAndSend(shouldShowKeyboard, state); }).Start();
                        break;

                    case InlineKeyboardRequest.Finalize:
                        // The calling application wants to close the keyboard applet and will wait for a state change.
                        _backgroundState = InlineKeyboardState.Uninitialized;
                        AppletStateChanged?.Invoke(this, null);
                        break;

                    default:
                        // We shouldn't be able to get here through standard swkbd execution.
                        Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}");
                        _interactiveSession.Push(InlineResponses.Default(state));
                        break;
                    }
                }
        }