🤔 Para Refletir : "Mudar não é fraqueza, pelo contrário, é um sinônimo de superação" - Ricky O Bardo

RPG MAKER MV/MZ - Armazenamento em nuvem

Membro Membro
Desenvolvedor FullStack
Postagens
337
Bravecoins
69
Cloud Sync-1.jpg


Olá a todos! Como estão?

Ontem eu tive uma ideia de disponibilizar de forma gratuita uma API para sincronizar os saves dos jogos no RPG MAKER MV/MZ na nuvem.

API

1. Rotas

2 Estrutura de dados


1. Rotas

/api/data/save -> POST (Armazena os dados do jogo)
/api/data/update -> POST (Atualiza os dados do jogo)
/api/data/load -> POST (Obtém os dados do jogo)
/api/data/remove -> POST (Remove os dados do jogo)

2 Estrutura de dados

/api/data/save -> POST (Armazena os dados do jogo)
/api/data/update -> PUT (Atualiza os dados do jogo)
Json:
{
    "gameId": "ID DO JOGO",
    "gameToken": "TOKEN DO JOGO",
    "playerId": "ID DO JOGADOR",
    "saveNum": "NÚMERO DO SAVE",
    "compatibilityVersion": "NÚMERO DA VERSÃO COMPATÍVEL",
    "data": "[STRING] DOS DADOS"
}

/api/data/load -> POST (Obtém os dados do jogo)
Json:
{
    "gameId": "ID DO JOGO",
    "gameToken": "TOKEN DO JOGO",
    "playerId": "ID DO JOGADOR",
    "compatibilityVersion": "NÚMERO DA VERSÃO COMPATÍVEL",
    "saveNum": "NÚMERO DO SAVE",
    "data": "[STRING] DOS DADOS"
}

/api/data/remove -> POST (Remove os dados do jogo)
Json:
{
    "gameId": "ID DO JOGO",
    "gameToken": "TOKEN DO JOGO",
    "playerId": "ID DO JOGADOR",
    "compatibilityVersion": "NÚMERO DA VERSÃO COMPATÍVEL",
    "saveNum": "NÚMERO DO SAVE"
}

Obviamente a API não está sendo feita para aguentar milhões de jogos, mas mesmo assim ainda ajudará e muito a comunidade, visto que a API pode ser usada em outras engines também.

O que vocês acham?

 
Última edição:
Prezado, bom dia.

Caramba @Dr.Xamã , quando você aparece vem com tudo. Parabéns!
Acho uma ideia bastante interessante para a era em que vivemos. Colocar isso em prática reduz bastante dor de cabeça em ter que ficar fuçando manualmente os arquivos.

Tire-me uma dúvida: há alguma distinção entre a versão do jogo ou poderá carregar um salve de uma versão demonstrativa, por exemplo, em um jogo completo?


Abraços.
 
Prezado, bom dia.

Caramba @Dr.Xamã , quando você aparece vem com tudo. Parabéns!
Acho uma ideia bastante interessante para a era em que vivemos. Colocar isso em prática reduz bastante dor de cabeça em ter que ficar fuçando manualmente os arquivos.

Tire-me uma dúvida: há alguma distinção entre a versão do jogo ou poderá carregar um salve de uma versão demonstrativa, por exemplo, em um jogo completo?


Abraços.
Realmente não tinha imaginado essa questão. Terei que criar uma propriedade no save onde será indicado a versão de compatibilidade.

- Obrigado por levantar essa questão!
 
API Configurada e em funcionamento.

Nesse final de semana irei programar o script do RPG MAKER para se comunicar com a API.

OBS: tirei alguns prints do Insomnia (Programa que uso para testar APIs), Heroku e MongoDB (Atlas).
 

Anexos

  • gnome-shell-screenshot-67nt3m.png
    gnome-shell-screenshot-67nt3m.png
    95,5 KB · Visualizações: 57
  • gnome-shell-screenshot-d8ogj8.png
    gnome-shell-screenshot-d8ogj8.png
    241,1 KB · Visualizações: 60
  • gnome-shell-screenshot-4t36ip.png
    gnome-shell-screenshot-4t36ip.png
    190,4 KB · Visualizações: 55

Cloud Sync - Paralelismo​

Cloud Sync - Paralelismo.png


Integridade dos dados​

Para manter a integridade dos arquivos intacta um sistema de fila deve ser implementado, pois uma coisa muito comum é enviar requisições de save ao mesmo tempo e ter respostas em tempos diferentes, o RPG MAKER usa o arquivo global para salvar algumas informações que interferem nos saves registrados e isso causa muita dor de cabeça, pois as requisições devem ser encadeadas para manter um fluxo de consistência dos dados.

Heroku - Dynos​

Heroku é uma plataforma em nuvem baseada em sistemas de containers gerenciados, chamados de Dynos, possuidores de um ambiente de software plugável e configurável, preparados para rodar e depurar sistemas web em um número limitado de linguagens de programação.

Javascript:
//==================================================================================================
// GS001_CloudSync.js
//==================================================================================================
/*:
 * @plugindesc v1.00 - Cloud Sync Plugin
 *
 * @author GuilhermeSantos001
 *
 * @param GameID
 * @desc Exclusive ID for the game.
 * @type string
 * @default ???
 *
 * @param GameToken
 * @desc Secret token for access saves on the cloud.
 * @type string
 * @default ???
 *
 * @param CompatibilityVersion
 * @desc Compatibility version for the save on the cloud.
 * @type number
 * @default 1
 * @min 1
 *
 * @param Save Game Message 1
 * @desc Message to be displayed when the game is saved.
 * @type string
 * @default Saved on the cloud.
 *
 * @param Save Game Message 2
 * @desc Message to be displayed when the game is not saved.
 * @type string
 * @default Error on save on the cloud.
 *
 * @param Save Game Message 3
 * @desc Message to be displayed when the game is loaded.
 * @type string
 * @default Save game loaded from the cloud.
 *
 * @param Save Game Message 4
 * @desc Message to be displayed when the game is not loaded.
 * @type string
 * @default Error on load on the cloud.
 *
 * @param Save Game Message 5
 * @desc Message to be displayed when the game is updated.
 * @type string
 * @default Save game updated from the cloud.
 *
 * @param Save Game Message 6
 * @desc Message to be displayed when the game is not updated.
 * @type string
 * @default Error on updated on the cloud.
 *
 * @param Save Game Message 7
 * @desc Message to be displayed when the game is removed.
 * @type string
 * @default Save game removed from the cloud.
 *
 * @param Save Game Message 8
 * @desc Message to be displayed when the game is not removed.
 * @type string
 * @default Error on removed on the cloud.
 *
 * @help
 * ================================================================================
 *    Introduction
 * ================================================================================
 * Keep your game save files in the cloud.
 * ================================================================================
 *    Commands
 * ================================================================================
 * $gameTemp.cloudSyncSetPlayerId(playerId); - Set the player id.
 * -- default value: default
 *
 * $gameTemp.cloudSyncLoad(); - Load the game save from the cloud.
 * $gameTemp.cloudSyncFileRemove(saveNum); - Remove the game save from the cloud.
 *
 * -- saveNum:
 * -1 - Config file.
 * 0 - Global file.
 * 1 - Save file 1.
 * 2 - Save file 2.
 * 3 - Save file 3.
 * 4...
 * ================================================================================
 *    Github
 * ================================================================================
 * Keep the script up-to-date on Github.
 * https://github.com/GuilhermeSantos001/rpg-maker-cloud-sync
 */
(function () {
  "use strict";

  require('nw.gui').Window.get().showDevTools()

  //-----------------------------------------------------------------------------
  // Parameters
  //
  const params = PluginManager.parameters('GS001_CloudSync');
  const apiURI = 'https://rpg-maker-cloud-sync.herokuapp.com/api/v1';
  const apiAuthorization = '297820d4a89ba880c89cfd5b3c0cd294f8c90cc9a9e6c14c9684c04a592b68ad';
  const gameId = params['GameID'] || '???';
  const gameToken = params['GameToken'] || '???';
  const saveGameMessage1 = params['Save Game Message 1'] || 'Saved on the cloud.';
  const saveGameMessage2 = params['Save Game Message 2'] || 'Error on save on the cloud.';
  const saveGameMessage3 = params['Save Game Message 3'] || 'Save game loaded from the cloud.';
  const saveGameMessage4 = params['Save Game Message 4'] || 'Error on load on the cloud.';
  const saveGameMessage5 = params['Save Game Message 5'] || 'Save game updated from the cloud.';
  const saveGameMessage6 = params['Save Game Message 6'] || 'Error on updated on the cloud.';
  const saveGameMessage7 = params['Save Game Message 7'] || 'Save game removed from the cloud.';
  const saveGameMessage8 = params['Save Game Message 8'] || 'Error on removed on the cloud.';
  const compatibilityVersion = Number(params['CompatibilityVersion']) || 1;

  let playerId = 'default';

  let
    _CLOUD_SYNC_TEXT = '',
    _WINDOW_CLOUD_SYNC_IS_APPEND = false,
    _WINDOW_CLOUD_SYNC_IS_OPEN = false,
    _WINDOW_CLOUD_SYNC_IS_SHOW = false,
    _WINDOW_CLOUD_SYNC_IS_TIMEOUT = false,
    _WINDOW_CLOUD_SYNC_IS_TIMEOUT_MS = 3000;

  let
    _CLOUD_SYNC_LOADED = false,
    _CLOUD_SYNC_LOAD_DATA = [];

  const
    getHeadersForFetchPost = () => {
      var myHeaders = new Headers();

      myHeaders.append('Content-Type', 'application/json');
      myHeaders.append('Authorization', apiAuthorization);

      return myHeaders;
    },
    openWindowCloudSync = (text) => {
      _CLOUD_SYNC_TEXT = text;
      _WINDOW_CLOUD_SYNC_IS_OPEN = true;
    },
    getTypeSaveGame = (savefileId) => {
      switch (savefileId) {
        case -1: return 'config';
        case 0: return 'global';
        default: return 'file';
      }
    },
    saveGameResponseSuccess = (data) => {
      if (!data.success) {
        console.error('Cloud Sync Error:', data);
        openWindowCloudSync(saveGameMessage2);
      }

      openWindowCloudSync(saveGameMessage1);
    },
    saveGameResponseError = (error) => {
      console.error('Cloud Sync Error:', error);
      openWindowCloudSync(saveGameMessage2);
    },
    updateGameResponseSuccess = (data) => {
      if (!data.success) {
        console.error('Cloud Sync Error:', data);
        openWindowCloudSync(saveGameMessage6);
      }

      openWindowCloudSync(saveGameMessage5);
    },
    updateGameResponseError = (error) => {
      console.error('Cloud Sync Error:', error);
      openWindowCloudSync(saveGameMessage6);
    },
    loadGameResponseSuccess = (data) => {
      if (!data.success) {
        console.error('Cloud Sync Error:', data);
        openWindowCloudSync(saveGameMessage4);
      }

      _CLOUD_SYNC_LOADED = true;
      _CLOUD_SYNC_LOAD_DATA = data.data;

      _CLOUD_SYNC_LOAD_DATA.forEach(save => {
        const
          savefileId = save.saveNum,
          json = LZString.decompressFromBase64(save.data);

        if (save.playerId == playerId)
          StorageManager.save(savefileId, json);
      });

      if (_CLOUD_SYNC_LOAD_DATA.length > 0)
        openWindowCloudSync(saveGameMessage3);
    },
    loadGameResponseError = (error) => {
      console.error('Cloud Sync Error:', error);
      openWindowCloudSync(saveGameMessage4);
    },
    removeGameResponseSuccess = (data) => {
      if (!data.success) {
        console.error('Cloud Sync Error:', data);
        openWindowCloudSync(saveGameMessage8);
      }

      openWindowCloudSync(saveGameMessage7);
    },
    removeGameResponseError = (error) => {
      console.error('Cloud Sync Error:', error);
      openWindowCloudSync(saveGameMessage8);
    },
    saveGameFile = (gameId, gameToken, playerId, type, saveNum, data) => {
      fetch(`${apiURI}/savegame`, {
        method: 'POST', body: JSON.stringify({
          gameId,
          gameToken,
          playerId,
          type,
          saveNum,
          compatibilityVersion,
          data
        }), headers: getHeadersForFetchPost()
      })
        .then(response => response.json())
        .then((data) => saveGameResponseSuccess(data))
        .catch(error => saveGameResponseError(error));
    },
    updateGameFile = (id, data) => {
      fetch(`${apiURI}/savegame/${id}`, {
        method: 'PUT', body: JSON.stringify({
          data
        }), headers: getHeadersForFetchPost()
      })
        .then(response => response.json())
        .then((data) => updateGameResponseSuccess(data))
        .catch(error => updateGameResponseError(error));
    },
    loadGameFiles = (gameId, gameToken) => {
      fetch(`${apiURI}/savesgame/${gameId}/${gameToken}`, {
        method: 'GET', headers: getHeadersForFetchPost()
      })
        .then(response => response.json())
        .then((data) => loadGameResponseSuccess(data))
        .catch(error => loadGameResponseError(error));
    },
    removeGameFile = (id) => {
      fetch(`${apiURI}/savegame/${id}`, {
        method: 'DELETE', headers: getHeadersForFetchPost()
      })
        .then(response => response.json())
        .then((data) => removeGameResponseSuccess(data))
        .catch(error => removeGameResponseError(error));
    },
    saveLocalConfig = (data) => {
      if (StorageManager.isLocalMode()) {
        var fs = require('fs');
        var dirPath = StorageManager.localFileDirectoryPath();
        var filePath = StorageManager.localFileDirectoryPath() + "cloudsync.rpgsave";
        if (!fs.existsSync(dirPath)) {
          fs.mkdirSync(dirPath);
        }
        fs.writeFileSync(filePath, data);
      } else {
        var key = 'RPG CLOUD SYNC';
        localStorage.setItem(key, data);
      }
    },
    loadLocalConfig = () => {
      if (StorageManager.isLocalMode()) {
        var fs = require('fs');
        var filePath = StorageManager.localFileDirectoryPath() + "cloudsync.rpgsave";
        if (fs.existsSync(filePath)) {
          return fs.readFileSync(filePath, 'utf8');
        }
      } else {
        var key = 'RPG CLOUD SYNC';
        return localStorage.getItem(key);
      }

      return false;
    }

  //-----------------------------------------------------------------------------
  // Game_Temp
  //
  Game_Temp.prototype.cloudSyncSetPlayerId = (name) => {
    playerId = name;
    saveLocalConfig(LZString.compressToBase64(JSON.stringify({ playerId: name })));
  };
  Game_Temp.prototype.cloudSyncLoad = () => loadGameFiles(gameId, gameToken);
  Game_Temp.prototype.cloudSyncFileRemove = (saveNum) => {
    _CLOUD_SYNC_LOAD_DATA.forEach(save => {
      if (save.saveNum == saveNum) {
        StorageManager.remove(saveNum);
        removeGameFile(save.id);
      }
    })
  }

  //-----------------------------------------------------------------------------
  // StorageManager
  //
  const _storageManager_save = StorageManager.save;
  StorageManager.save = function (savefileId, json) {
    _storageManager_save.apply(this, arguments);

    var
      data = LZString.compressToBase64(json),
      type = getTypeSaveGame(savefileId);

    if (_CLOUD_SYNC_LOAD_DATA.length > 0) {
      const save = _CLOUD_SYNC_LOAD_DATA.find(save => save.compatibilityVersion == compatibilityVersion && save.type == type && save.saveNum == savefileId);

      if (save)
        return updateGameFile(save.id, data);
    }

    saveGameFile(gameId, gameToken, playerId, type, savefileId, data);
  };

  //-----------------------------------------------------------------------------
  // Scene_Base
  //
  const _scene_base_initialize = Scene_Base.prototype.initialize;
  Scene_Base.prototype.initialize = function () {
    _scene_base_initialize.apply(this, arguments);
    if (!_CLOUD_SYNC_LOADED) {
      const localConfig = loadLocalConfig();

      if (localConfig) {
        const { playerId: _playerID } = JSON.parse(LZString.decompressFromBase64(localConfig));
        playerId = _playerID;
      }

      loadGameFiles(gameId, gameToken);
    }
  };

  //-----------------------------------------------------------------------------
  // Scene_Map
  //
  const _sceneMap_update = Scene_Map.prototype.update;
  Scene_Map.prototype.update = function () {
    _sceneMap_update.apply(this, arguments);
    if (!this._cloudSyncWindow && _WINDOW_CLOUD_SYNC_IS_APPEND)
      _WINDOW_CLOUD_SYNC_IS_APPEND = false;

    if (_WINDOW_CLOUD_SYNC_IS_OPEN && !_WINDOW_CLOUD_SYNC_IS_TIMEOUT) {
      _WINDOW_CLOUD_SYNC_IS_TIMEOUT = true;
      let timeout = setTimeout(() => {
        _WINDOW_CLOUD_SYNC_IS_OPEN = false;
        _WINDOW_CLOUD_SYNC_IS_TIMEOUT = false;
        clearTimeout(timeout);
      }, _WINDOW_CLOUD_SYNC_IS_TIMEOUT_MS);
    }

    if (_WINDOW_CLOUD_SYNC_IS_APPEND) {
      if (_WINDOW_CLOUD_SYNC_IS_OPEN) {
        if (!_WINDOW_CLOUD_SYNC_IS_SHOW) {
          _WINDOW_CLOUD_SYNC_IS_SHOW = true;
          this._cloudSyncWindow.refresh();
          this._cloudSyncWindow.open();
        }
      } else {
        if (_WINDOW_CLOUD_SYNC_IS_SHOW) {
          _WINDOW_CLOUD_SYNC_IS_SHOW = false;
          this._cloudSyncWindow.close();
        }
      }
    }

    if (
      !_WINDOW_CLOUD_SYNC_IS_APPEND &&
      _WINDOW_CLOUD_SYNC_IS_OPEN
    ) {
      _WINDOW_CLOUD_SYNC_IS_APPEND = true;
      this._cloudSyncWindow = new Window_CloudSync(8);
      this._cloudSyncWindow.y = Graphics.boxHeight - (this._cloudSyncWindow.height + 8);
      this.addWindow(this._cloudSyncWindow);
    }
  };

  //-----------------------------------------------------------------------------
  // Window_CloudSync
  //
  function Window_CloudSync() {
    this.initialize.apply(this, arguments);
  }

  Window_CloudSync.prototype = Object.create(Window_Base.prototype);
  Window_CloudSync.prototype.constructor = Window_CloudSync;

  Window_CloudSync.prototype.initialize = function (x, y) {
    var width = this.windowWidth();
    var height = this.windowHeight();
    Window_Base.prototype.initialize.call(this, x, y, width, height);
    this.refresh();
  };

  Window_CloudSync.prototype.windowWidth = function () {
    return 400;
  };

  Window_CloudSync.prototype.windowHeight = function () {
    return this.fittingHeight(1);
  };

  Window_CloudSync.prototype.refresh = function () {
    var x = this.textPadding();
    var maxWidth = this.contents.width - this.textPadding() * 2;
    this.contents.clear();
    this.drawText(_CLOUD_SYNC_TEXT, x, 0, maxWidth, 'center');
  };
})();


 
Só senti falta de um esquema de autorização, o que me impede de sobrescrever/ler o save de um outro jogador?
Pelo que entendi da ideia aí, o token do jogo é só uma string fixa (não um JWT da vida ou algo similar), então em tese se eu souber essa string, o parâmetro playerId ali pode ser o que eu quiser.
 
Eae @Brandt, de boa? Como anda suas bruxarias?



Só senti falta de um esquema de autorização, o que me impede de sobrescrever/ler o save de um outro jogador?
Pelo que entendi da ideia aí, o token do jogo é só uma string fixa (não um JWT da vida ou algo similar), então em tese se eu souber essa string, o parâmetro playerId ali pode ser o que eu quiser.

JSON Web Token​

Se tratando do RPG MAKER MV/MZ poderia ser feito na versão web, pois poderiamos usar os cookies com segurança na mesma origem. Poderiamos até mesmo salvar os tokens em LocalStorage ou IndexedDB.

Como os dados são salvos e recuperados (Até agora, lembrando que estou desenvolvendo)​

Basicamente o gameId é usado como um identificador unico do jogo e o gameToken é a palavra secreta que o usuario cria, o playerId é para identificar a autoria do save game, ou seja se você não definir nenhum playerId o plugin assumi o valor default. Eu queria deixar de forma transparente o save e load, mas serei obrigado a criar uma propriedade playerPass que basicamente será a senha do arquivo, se não qualquer um usa o arquivo de outros.
 

Heroku encerrado -> AWS EC2 Adicionado

Após alguns testes e enquanto estava desenvolvendo adicionei e fiquei com o EC2 xD.

Basicamente só houve uma mudança de cloud, o funcionamento ainda é o mesmo.
 
Eae @Brandt, de boa? Como anda suas bruxarias?
Bão, e aí? \o
A maioria tá no porão esperando sobrar um tempo haha, algum dia alguma sai.



JSON Web Token​

Se tratando do RPG MAKER MV/MZ poderia ser feito na versão web, pois poderiamos usar os cookies com segurança na mesma origem. Poderiamos até mesmo salvar os tokens em LocalStorage ou IndexedDB.
Cookies são uma possibilidade, mas tem o problema de perder essa informação. Cada vez que o usuário criar uma sessão ele perde os dados de save dele?
E salvar o token é a parte tranquila mesmo, o problema é quem fornece/assina ele. Você pretende ter uma autoridade central que cuida de autenticação (e.g. um login que os usuários têm que fazer pra usar a feature de salvar na nuvem)? Ou talvez integrar com a Steam e tirar essa info de lá?
Acho que é um aspecto importante a se considerar na arquitetura, não tem muito como fazer o sistema sem.

Como os dados são salvos e recuperados (Até agora, lembrando que estou desenvolvendo)​

Basicamente o gameId é usado como um identificador unico do jogo e o gameToken é a palavra secreta que o usuario cria, o playerId é para identificar a autoria do save game, ou seja se você não definir nenhum playerId o plugin assumi o valor default. Eu queria deixar de forma transparente o save e load, mas serei obrigado a criar uma propriedade playerPass que basicamente será a senha do arquivo, se não qualquer um usa o arquivo de outros.
A ideia do playerPass funciona até certo ponto, mas teria que ser aplicada ao processo de salvar também (se não ainda dá pra "sequestrar" o save de outro usuário). Aí já começa a virar uma ideia primitiva de autenticação, que pode ser prático mesmo, mas eu ainda iria por algum caminho mais canônico (e.g. login com email/senha mesmo, e autorização com JWT assinado).

Uma vantagem do JWT aliás é que você pode colocar valores dentro dele (por isso é um JSON Web Token, é um objeto JSON lá dentro), e não é só uma string; dentro do JWT seria um bom lugar pra colocar esses dados de gameId e playerId. Não teria nem necessidade de um gameToken, inclusive, porque o próprio login pode cuidar de validar isso.
 
Aqui tá tranquilo, estou voltando aos poucos. Quero ver seus trabalhos, gosto da profundidade e os algoritmos q vc trabalha.



Realmente o playerPass é uma autenticação simples, o jwt é autenticado com a segurança de autoridade emissora do token, no caso a API possuí um secret que gera os tokens e usa para ler os dados dos mesmos, sendo assim se eu mudar o secret eu inválido qualquer token gerado anteriormente, muito bom em casos de invasão onde preciso de uma rápida solução paleativa.

A idéia de salvar as informações nós tokens é bacana, eu particularmente não trânsito nada em jwt, só um username para identificar de quem é o token na API. Depois de ver palestras de invasão e segurança, evito deixar informações moscando nós tokens kkkk

Irei criar um fluxograma de funcionamento de segurança, mas o jwt é quase certo xD
 
Cloud Sync - Authenticação.png


Registrar o jogo​

A partir da implementação desse mecanismo de autenticação, o desenvolvedor deverá registrar o jogo na API para usar o sistema.

Autenticação

O sistema contará com um mecanismo de autenticação por JSON Web Token tendo validade de 7 dias, após esse prazo o usuario deverá efetuar login novamente.

Multiplos Logins?​

Os multiplos logins podem ser implementados na versão web, onde o RPG MAKER armazena usando o LocalStorange, mas nas plataformas onde o save é local, por questões de integridade e funcionamento do maker esse tipo de abordagem é inviavel. Por enquanto o sistema ficará com apenas 1 login, futuramente se existir a necessidade as versões web terão a implementação.
 
Última edição:
Voltar
Topo