/*============================================================================= ChatGptCaller.js ---------------------------------------------------------------------------- (C)2023 Triacontane This software is released under the MIT License. http://opensource.org/licenses/mit-license.php ---------------------------------------------------------------------------- Version 1.0.0 2023/04/15 初版 ---------------------------------------------------------------------------- [Blog] : https://triacontane.blogspot.jp/ [Twitter]: https://twitter.com/triacontane/ [GitHub] : https://github.com/triacontane/ =============================================================================*/ /*: @target MZ @url https://github.com/triacontane/RPGMakerMV/tree/mz_master/ChatGptCaller.js @plugindesc ChatGPT calling plugin @author Triacontane @license MIT License @help English Help Translator: munokura This is an unofficial English translation of the plugin help, created to support global RPG Maker users. Feedback is welcome to improve translation quality (see: https://github.com/munokura/triacontane-MZ-plugins ). Original plugin by Triacontane. Please check the latest official version at: https://triacontane.blogspot.com ----- ChatGptCaller.js Calls ChatGPT and sends a question. The question is executed asynchronously, and the specified switch is turned ON when the result is returned. The answer is stored in the file [save/gptAnswer.rmmzsave] and remains even after the game ends. You can retrieve the answer using the control character \ans[id] or the following script: $gameTemp.findGptAnswer(id); The API key is entered as a parameter, so please be careful when publishing your game. The API is generally only called during development, and the saved answers are intended to be used after publication. This plugin requires the base plugin "PluginCommonBase.js." "PluginCommonBase.js" is located in the following folder under the RPG Maker MZ installation folder. dlc/BasicResources/plugins/official Terms of Use: You may modify and redistribute this plugin without permission from the author, and there are no restrictions on its use (commercial, 18+, etc.). This plugin is now yours. @param apiKey @text API Key @desc Please enter your ChatGPT API key. @param organizationId @text Organization ID @desc Enter your ChatGPT organization ID if necessary. @param responseTrigger @text Answer receiving trigger @desc Specify the switch ID that will be turned ON when a response is received. @type switch @default 0 @param responseSelfTrigger @text Self-triggered response reception @desc Please specify the self switch that will be turned ON when a reply is received. If not specified, the setting of the plugin parameter will be used. @type select @option none @value @option A @value A @option B @value B @option C @value C @option D @value D @param model @text Model @desc Please specify the ChatGPT model. @default gpt-3.5-turbo @param requestTemplate @text Request Template @desc This is a template for requests to add weapons or enemy characters. No editing is required. @type multiline_string @default Please come up with one %2 like %1. Please return the result in RPG Maker MZ database json format. @command CALL_GPT_QUESTION @text Question call @desc Ask ChatGPT a question and store the results in a file. @arg questionId @text Question ID @desc This is an arbitrary identifier that is unique for each question. Answer data is saved using this ID as a key. @default 1 @arg questionText @text Question @desc Please specify the question text. @type multiline_string @arg responseTrigger @text Answer receiving trigger @desc Specify the switch ID that will be turned ON when a response is received. If not specified, the setting in the plugin parameter will be used. @type switch @default 0 @arg responseSelfTrigger @text Self-triggered response reception @desc Please specify the self-switch that will be turned ON when a reply is received. If not specified, the setting of the plugin parameter will be used. @type select @option none @value @option A @value A @option B @value B @option C @value C @option D @value D @arg temperature @text Sampling Temperature @desc Please specify the sampling temperature. Please specify a value between 0 and 200. The higher the value, the more random the answers will be. @type number @default 70 @max 200 @arg wait @text Wait until completion @desc The event execution will wait until the API result is returned. @type boolean @default false */ /*:ja @plugindesc ChatGPT呼び出しプラグイン @target MZ @url https://github.com/triacontane/RPGMakerMV/tree/mz_master/ChatGptCaller.js @base PluginCommonBase @orderAfter PluginCommonBase @author トリアコンタン @param apiKey @text APIキー @desc ChatGPTのAPIキーを入力してください。 @default @param organizationId @text 組織ID @desc 必要に応じてChatGPTの組織IDを入力してください。 @default @param responseTrigger @text 回答受信トリガー @desc 回答を受信したときにONになるスイッチIDを指定してください。 @default 0 @type switch @param responseSelfTrigger @text 回答受信セルフトリガー @desc 回答を受信したときにONになるセルフスイッチを指定してください。指定が無い場合、プラグインパラメータの設定が使用されます。 @default @type select @option none @value @option A @value A @option B @value B @option C @value C @option D @value D @param model @text モデル @desc ChatGPTのモデルを指定してください。 @default gpt-3.5-turbo @param requestTemplate @text リクエストテンプレート @desc 武器や敵キャラの追加リクエストのテンプレートです。基本的には編集不要です。 @default %1ような%2を1つ考えてください。結果はRPGツクールMZのデータベースのjson形式で返してください。 @type multiline_string @command CALL_GPT_QUESTION @text 質問呼び出し @desc ChatGPTに質問をして結果をファイルに蓄積します。 @arg questionId @text 質問ID @desc 質問ごとに一意になる任意の識別子です。このIDをキーにして回答データが保存されます。 @default 1 @arg questionText @text 質問文 @desc 質問文を指定してください。 @default @type multiline_string @arg responseTrigger @text 回答受信トリガー @desc 回答を受信したときにONになるスイッチIDを指定してください。指定が無い場合、プラグインパラメータの設定が使用されます。 @default 0 @type switch @arg responseSelfTrigger @text 回答受信セルフトリガー @desc 回答を受信したときにONになるセルフスイッチを指定してください。指定が無い場合、プラグインパラメータの設定が使用されます。 @default @type select @option none @value @option A @value A @option B @value B @option C @value C @option D @value D @arg temperature @text サンプリング温度 @desc サンプリング温度を指定してください。0~200の範囲で指定してください。高い値ほど回答文のランダム性が増します。 @default 70 @type number @max 200 @arg wait @text 完了まで待機 @desc APIの結果が返ってくるまでイベントの実行を待機します。 @default false @type boolean @help ChatGptCaller.js ChatGPTを呼び出して質問文を送信します。 質問は非同期で実行され、結果が返ってきたときに指定スイッチがONになります。 回答はファイル[save/gptAnswer.rmmzsave]に蓄積されゲーム終了後も残ります。 制御文字 \ans[id] あるいは以下のスクリプトで回答を取得できます。 $gameTemp.findGptAnswer(id); APIキーをパラメータから入力するので、ゲームを公開する場合はご注意ください。 APIの呼び出しは原則制作時にのみ行い、公開後はファイル保存された回答を 使用することを想定しています。 このプラグインの利用にはベースプラグイン『PluginCommonBase.js』が必要です。 『PluginCommonBase.js』は、RPGツクールMZのインストールフォルダ配下の 以下のフォルダに格納されています。 dlc/BasicResources/plugins/official 利用規約: 作者に無断で改変、再配布が可能で、利用形態(商用、18禁利用等) についても制限はありません。 このプラグインはもうあなたのものです。 */ (() => { 'use strict'; const script = document.currentScript; const param = PluginManagerEx.createParameter(script); PluginManagerEx.registerCommand(script, 'CALL_GPT_QUESTION', function(args) { const responseTrigger = args.responseTrigger || param.responseTrigger; const responseSelfTrigger = args.responseSelfTrigger || param.responseSelfTrigger; this.callQuestion(args.questionId, args.questionText, responseTrigger, responseSelfTrigger, args.temperature); if (args.wait) { this.setWaitMode('gpt'); } }); const _Game_Interpreter_initialize = Game_Interpreter.prototype.initialize; Game_Interpreter.prototype.initialize = function(depth) { _Game_Interpreter_initialize.call(this, depth); this._requestCount = 0; }; const _Game_Interpreter_updateWaitMode = Game_Interpreter.prototype.updateWaitMode; Game_Interpreter.prototype.updateWaitMode = function() { if (this._waitMode === 'gpt') { return this._requestCount > 0; } return _Game_Interpreter_updateWaitMode.call(this); } Game_Interpreter.prototype.callQuestion = function(questionId, questionText, responseTrigger, responseSelfTrigger, temperature) { const mapId = this._mapId; const eventId = this._eventId; this.callGpt(questionText, temperature, content => { gptAnswerData.setAnswer(questionId, content); if (responseTrigger) { $gameSwitches.setValue(responseTrigger, true); } if (responseSelfTrigger) { const key = [mapId, eventId, responseSelfTrigger]; $gameSelfSwitches.setValue(key, true); } }); $gameSwitches.setValue(responseTrigger, false); }; Game_Interpreter.prototype.callGpt = function(message, temperature, callBack) { if (!param.apiKey) { return; } this._requestCount++; const client = new OpenAiClient('chat/completions', 'POST'); client.setQuestion(message, temperature); client.request().then(response => { const json = JSON.parse(response); callBack(json.choices[0].message.content); this._requestCount--; }).catch(error => { PluginManagerEx.throwError(error, script); }); }; class GptAnswerData { constructor() { this.initialize.apply(this, arguments); } initialize() { this._fileName = 'gptAnswer'; this._answerList = {}; this._loaded = false; } load() { StorageManager.loadObject(this._fileName).then(info => { this._answerList = info.answerList || {}; this._loaded = true; }).catch(() => { this._loaded = true; }); } save() { StorageManager.saveObject(this._fileName, { answerList: this._answerList }); } getAnswer(question) { return this._answerList[question]; } setAnswer(question, answer) { this._answerList[question] = answer; this.save(); } isLoaded() { return this._loaded; } } const gptAnswerData = new GptAnswerData(); gptAnswerData.load(); const _Scene_Boot_isReady = Scene_Boot.prototype.isReady; Scene_Boot.prototype.isReady = function() { return _Scene_Boot_isReady.apply(this, arguments) && gptAnswerData.isLoaded(); }; Game_Temp.prototype.findGptAnswer = function(questionId) { return gptAnswerData.getAnswer(questionId) || ''; } const _PluginManagerEx_convertEscapeCharactersEx = PluginManagerEx.convertEscapeCharactersEx; PluginManagerEx.convertEscapeCharactersEx = function(text) { text = _PluginManagerEx_convertEscapeCharactersEx.apply(this, arguments); text = text.replace(/\x1bANS\[(.+?)]/gi, (_, p1) => $gameTemp ? $gameTemp.findGptAnswer(p1) : '' ); return text; }; class OpenAiClient { constructor(endPoint, method) { this._xmlHttpRequest = new XMLHttpRequest(); this._originalUrl = 'https://api.openai.com/v1/' + endPoint; this._requestHeaders = {}; this._queryParams = []; this._bodyParams = {}; this._method = method === 'POST' ? 'POST' : 'GET'; this.createOpenAiHeader(); } createOpenAiHeader() { this.addRequestHeader('Authorization', `Bearer ${param.apiKey}`); if (param.organizationId) { this.addRequestHeader('OpenAI-Organization', param.organizationId); } this.addRequestHeader('Content-Type', 'application/json'); } getFullUrl() { const queryText = this.createQueryText(); return this._originalUrl + (queryText.length > 0 ? '?' + queryText : ''); } setQuestion(questionText, temperature) { this.setBodyParam({ "model": param.model, "messages": [{"role": "user", "content": questionText}], "temperature": temperature / 100 }) } request() { const xhr = this._xmlHttpRequest; xhr.open(this._method, this.getFullUrl()); this.setRequestHeaders(); xhr.send(JSON.stringify(this._bodyParams)); return this.getPromise(); } getPromise() { return new Promise(function(resolve, reject) { this._xmlHttpRequest.addEventListener('load', this.onLoad.bind(this, resolve, reject)); this._xmlHttpRequest.addEventListener('error', this.onError.bind(this, reject)); }.bind(this)); } addRequestHeader(name, value) { this._requestHeaders[name] = value; } addQuery(name, value) { this._queryParams.push({name: name, value: value}); } setBodyParam(paramObject) { this._bodyParams = paramObject; } createQueryText() { const paramArray = this._queryParams.map(function(paramHash) { return encodeURIComponent(paramHash.name) + '=' + encodeURIComponent(paramHash.value); }); return paramArray.join('&'); } setRequestHeaders() { const keys = Object.keys(this._requestHeaders); if (keys && keys.length > 0) { keys.forEach(function(paramName) { this._xmlHttpRequest.setRequestHeader(paramName, this._requestHeaders[paramName]); }, this); } } onLoad(resolve, reject) { if (this.isStatusOk()) { resolve(this.getResponse()); } else { reject(this.getResponse()); } } onError(reject) { reject(this.getResponse()); } getResponse() { return this._xmlHttpRequest.responseText; } isStatusOk() { return this._xmlHttpRequest.status < 400; } } })();