diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..aaf2809
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+# http://editorconfig.org/
+root = yes
+
+[*]
+indent_size = 2
+indent_style = space
+end_of_line = lf
+charset = utf-8
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8fb9836
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/config.yml
+/*.json
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..acb1f3d
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,34 @@
+### 必須項目 (Required items)
+
+
+* ServerOS:
+
+
+* PHP version:
+
+
+* Minecraft version:
+
+
+* Texter version:
+
+
+* issue(s):
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a8c0e68
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 yuko fuyutsuki
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 29a7ef8..b32d784 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,121 @@
-# Texter
-This is a PocketMine-MP plugin that can add, remove and update FloatingTextParticle.
+[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fuyutsuki/Texter/blob/master/LICENSE)
+[![Github All Releases](https://img.shields.io/github/downloads/fuyutsuki/Texter/total.svg)](https://github.com/fuyutsuki/Texter/releases)
+
+## Overview
+Select Language: [English](#eng), [日本語](#jpn)
+
+***
+
+## English
+
+## Notice
+This plugin is **fork** of *yuko fuyutsuki`s* plugin developed by *p-NLab*.
+It is distributed under MIT license.
+
+
+
+## Texter
+Texter is plugin that displays and deletes FloatingTextPerticle supported to multi-world.
+Latest: ver **2.2.4** _Papilio dehaanii(カラスアゲハ)_
+
+### Supporting
+- [x] Multi-language (eng, jpn)
+- [x] Multi-world display
+- [x] Minecraft:PE v1.1.x
+- [x] Minecraft(Bedrock) v1.2.0.x
+
+### Commands
+#### General command
+| \ |command|argument|alias|
+|:--:|:--:|:--:|:--:|
+|Add text|`/txt add`|`
[text]`|`/txt a`|
+|Remove text|`/txt remove`|``|`/txt r`|
+|Update text|`/txt update`|` `|`/txt u`|
+|Help|`/txt or /txt help`|`none`|`/txt ?`|
+
+#### Management command
+| \ |command|argument|alias|
+|:--:|:--:|:--:|:--:|
+|Remove all|`/txtadm allremove`|`none`|`/tadm ar`|
+|Remove texts/user|`/txtadm userremove`|``|`/tadm ur`|
+|Info|`/txtadm info`|`none`|`/tadm i`|
+|Help|`/txtadm or /txtadm help`|`none`|`/tadm ?`|
+
+**Please use `#` for line breaks.**
+
+### json notation
+```json
+anythingUniqueValue: {
+ "WORLD" : "worldName",
+ "Xvec" : 128,
+ "Yvec" : 90,
+ "Zvec" : 128,
+ "TITLE" : "title",
+ "TEXT" : "1st Line#2nd Line..."
+}
+```
+It is output as follows.
+
+
+***
+
+## 日本語
+
+## お知らせ
+このプラグインは *p-NLab* が開発する、*yuko fuyutsuki* さんのプラグインの**フォーク**です。
+MITライセンスの下で配布されています。
+
+
+
+## Texter
+TexterはFloatingTextPerticleを複数ワールドに渡り表示、削除ができるプラグインです。
+最新バージョン: **2.2.4** _Papilio dehaanii(カラスアゲハ)_
+
+### 対応状況
+- [x] 複数言語 (eng, jpn)
+- [x] 複数ワールドの表示
+- [x] Minecraft:PE v1.1.x
+- [x] Minecraft(Bedrockエンジン) v1.2.x
+
+### コマンド
+#### 一般用コマンド
+| \ |コマンド|引数|エイリアス|
+|:--:|:--:|:--:|:--:|
+|浮き文字追加|`/txt add`|`<タイトル> [テキスト]`|`/txt a`|
+|浮き文字削除|`/txt remove`|``|`/txt r`|
+|浮き文字更新|`/txt update`|`<タイトル, テキスト> <メッセージ>`|`/txt u`|
+|ヘルプ|`/txt or /txt help`|`無し`|`/txt ?`|
+
+#### 管理用コマンド
+| \ |コマンド|引数|エイリアス|
+|:--:|:--:|:--:|:--:|
+|浮き文字すべて削除|`/txtadm allremove`|`none`|`/tadm ar`|
+|ユーザーの浮き文字を削除|`/txtadm userremove`|``|`/tadm ur`|
+|浮き文字の各種情報を見る|`/txtadm info`|`none`|`/tadm i`|
+|ヘルプ|`/txtadm or /txtadm help`|`none`|`/tadm ?`|
+
+**改行の際には `#` を使用してください。**
+
+### json記法
+```json
+{
+ "一意な文字列": {
+ "WORLD" : "world",
+ "Xvec" : 128,
+ "Yvec" : 90,
+ "Zvec" : 128,
+ "TITLE" : "title",
+ "TEXT" : "1st Line#2nd Line"
+ }
+}
+```
+
+こう書くことで以下のように出力されます。
+
diff --git a/assets/Texter.png b/assets/Texter.png
new file mode 100644
index 0000000..e37e83a
Binary files /dev/null and b/assets/Texter.png differ
diff --git a/plugin.yml b/plugin.yml
new file mode 100644
index 0000000..5ed2d15
--- /dev/null
+++ b/plugin.yml
@@ -0,0 +1,35 @@
+name: Texter
+main: Texter\Main
+version: 2.2.4
+api:
+ - 3.0.0
+ - 3.0.0-ALPHA8
+ - 3.0.0-ALPHA9 #pmmp/PocketMine-MP
+
+author: yuko_fuyutsuki
+authors:
+# SpecialThanks(敬称略)
+- mfmfnek0
+- onebone
+- ogiwara
+- Shootsta_ERU0531
+- Toganon
+- ShadowArt
+- Marron0421
+website: https://twitter.com/_riu_m
+
+permissions:
+ texter.*:
+ default: op
+ description: "permission that allows player to use texter"
+ children:
+ texter.command.*:
+ default: op
+ description: "permission that allows player to use texter"
+ children:
+ texter.command.txt:
+ default: true # もしopのみ実行させたい場合は op と記述してください
+ description: "permission that allows player to use /txt"
+ texter.command.txtadm:
+ default: op
+ description: "permission that allows player to use /txtadm"
diff --git a/resources/config.yml b/resources/config.yml
new file mode 100644
index 0000000..7c1d121
--- /dev/null
+++ b/resources/config.yml
@@ -0,0 +1,38 @@
+configVersion: 22
+
+# 日本語 ガイド
+# eng guide
+
+# Texter 設定ファイル
+# Texter config file
+
+# 言語を設定してください (eng or jpn)
+# set language (eng or jpn)
+language: jpn
+
+# タイムゾーンを設定してください
+# set timezone
+timezone: Asia/Tokyo
+
+# もし起動時にアップデートの通知が必要ならtrue, 必要ないならfalseにしてください
+# When set to true, update notification is displayed at startup
+checkUpdate: true
+
+# コマンドを使用不可にしたい場合はfalseにしてください
+# If you want to disable the command, prease set it to false
+canUseCommands: true
+
+# 文字数の制限をする場合は文字数を, 不要なら -1 にしてください
+# To limit the number of characters, set it to that, if unnecessary, set it to -1.
+limit: 50
+
+# 改行数の制限をする場合は改行回数を、不要なら -1 にしてください
+# To limit the number of line feeds, set it to that, if unnecessary, set it to -1.
+feed: 3
+
+# テキストを設置不可能なワールドを設定する場合はワールド名を指定してください
+# Please specify the world name when setting the world where text can not be set up
+world:
+ # example
+ # - world
+ # - world-2
diff --git a/resources/crfts.json b/resources/crfts.json
new file mode 100644
index 0000000..2d29f6f
--- /dev/null
+++ b/resources/crfts.json
@@ -0,0 +1,26 @@
+{
+ "0":{
+ "WORLD": "world",
+ "Xvec": 128,
+ "Yvec": 90,
+ "Zvec": 128,
+ "TITLE": "タイトル",
+ "TEXT": "テキスト(\nを入れると改行)"
+ },
+ "1":{
+ "WORLD": "world",
+ "Xvec": 128,
+ "Yvec": 90,
+ "Zvec": 130,
+ "TITLE": "タイトル",
+ "TEXT": "テキスト(\nを入れると改行)"
+ },
+ "2":{
+ "WORLD": "world",
+ "Xvec": 128,
+ "Yvec": 90,
+ "Zvec": 132,
+ "TITLE": "タイトル",
+ "TEXT": "テキスト(\nを入れると改行)"
+ }
+}
diff --git a/resources/fts.json b/resources/fts.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/resources/fts.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/src/Texter/EventListener.php b/src/Texter/EventListener.php
new file mode 100644
index 0000000..a4b8fe0
--- /dev/null
+++ b/src/Texter/EventListener.php
@@ -0,0 +1,83 @@
+main = $main;
+ $this->api = $main->getApi();
+ }
+
+ public function onJoin(PlayerJoinEvent $e){
+ $p = $e->getPlayer();
+ $lev = $p->getLevel();
+ $crfts = $this->api->getCrftsByLevel($lev);
+ if (!empty($crfts)) {
+ foreach ($crfts as $crft) {
+ $crft->sendToPlayer($p, CRFT::SEND_TYPE_ADD);
+ }
+ }
+ $fts = $this->api->getFtsByLevel($lev);
+ if (!empty($fts)) {
+ foreach ($fts as $ft) {
+ $ft->sendToPlayer($p, FT::SEND_TYPE_ADD);
+ }
+ }
+ }
+
+ public function onLevelChange(EntityLevelChangeEvent $e){
+ $p = $e->getEntity();
+ if ($p instanceof Player) {
+ $lev = $p->getLevel();
+ $crfts = $this->api->getCrftsByLevel($lev);
+ if (!empty($crfts)) {
+ foreach ($crfts as $crft) {
+ $crft->sendToPlayer($p, CRFT::SEND_TYPE_REMOVE);
+ }
+ }
+ $fts = $this->api->getFtsByLevel($lev);
+ if (!empty($fts)) {
+ foreach ($fts as $ft) {
+ $ft->sendToPlayer($p, FT::SEND_TYPE_REMOVE);
+ }
+ }
+ switch (strtolower($this->main->getServer()->getName())) {
+ case 'pocketmine-mp':
+ $task = new WorldGetTask($this->main, $p);
+ $this->main->getServer()->getScheduler()->scheduleDelayedTask($task, 20);
+ break;
+
+ // NOTE: Confirmed
+ case 'genisyspro':
+ case 'leveryl':
+ default:
+ $task = new WorldGetTaskOld($this->main, $p);
+ $this->main->getServer()->getScheduler()->scheduleDelayedTask($task, 20);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Texter/Main.php b/src/Texter/Main.php
new file mode 100644
index 0000000..9c02f07
--- /dev/null
+++ b/src/Texter/Main.php
@@ -0,0 +1,330 @@
+
+ *
+ * Released under the "MIT license".
+ * You should have received a copy of the MIT license
+ * along with this program. If not,
+ * < https://opensource.org/licenses/mit-license >.
+ *
+ * ---------------------------------------------------------------------
+ * ## 日本語
+ *
+ * TexterはPocketMine-MP向けのFloatingTextPerticleを表示するプラグインです。
+ * Copyright (c) 2017 yuko fuyutsuki < https://github.com/fuyutsuki >
+ *
+ * このソフトウェアは"MITライセンス"下で配布されています。
+ * あなたはこのプログラムと共にMITライセンスのコピーを受け取ったはずです。
+ * 受け取っていない場合、下記のURLからご覧ください。
+ * < https://opensource.org/licenses/mit-license >
+ */
+
+namespace Texter;
+
+# Pocketmine
+use pocketmine\Player;
+use pocketmine\Server;
+use pocketmine\command\{
+ Command,
+ CommandSender};
+use pocketmine\entity\Entity;
+use pocketmine\event\{
+ Listener,
+ entity\EntityLevelChangeEvent,
+ player\PlayerJoinEvent};
+use pocketmine\item\Item;
+use pocketmine\level\{
+ Level,
+ Position};
+use pocketmine\math\Vector3;
+use pocketmine\plugin\PluginBase;
+use pocketmine\utils\TextFormat as TF;
+
+# Texter
+use Texter\EventListener;
+use Texter\TexterApi;
+use Texter\commands\{
+ TxtCommand,
+ TxtCommandOld,
+ TxtAdmCommand,
+ TxtAdmCommandOld};
+use Texter\language\Lang;
+use Texter\text\{
+ CantRemoveFloatingText as CRFT,
+ FloatingText as FT};
+use Texter\task\{
+ CheckUpdateTask,
+ WorldGetTask};
+use Texter\utils\TunedConfig as Config;
+
+define("DS", DIRECTORY_SEPARATOR);
+
+class Main extends PluginBase {
+
+ const NAME = "Texter";
+ const VERSION = "v2.2.4";
+ const CODENAME = "Papilio dehaanii(カラスアゲハ)";
+
+ const FILE_CONFIG = "config.yml";
+ const FILE_CRFTP = "crftps.json";// for old format
+ const FILE_CRFT = "crfts.json";
+ const FILE_FTP = "ftps.json";// for old format
+ const FILE_FT = "fts.json";
+
+ const CONFIG_VERSION = 22;
+
+ /** @var bool $devmode */
+ public $devmode = false;
+ /** @var string $dir */
+ public $dir = "";
+ /** @var Config $config */
+ private $config = null;
+ /** @var TexterApi $api */
+ private $api = null;
+ /** @var Lang $language */
+ private $language = null;
+ /** @var array $crfts */
+ private $crfts = [];
+ /** @var array $fts */
+ private $fts = [];
+
+ /****************************************************************************/
+ /* Public functions */
+
+ /**
+ * TexterApiを取得します
+ * @return TexterApi $this->api
+ */
+ public function getApi(): TexterApi{
+ return $this->api;
+ }
+
+ /**
+ * 文字数制限のための文字数を取得します
+ * @return int
+ */
+ public function getCharaLimit(): int{
+ return (int)$this->config->get("limit");
+ }
+
+ /**
+ * 改行数制限のための改行数を設定します
+ * @return int
+ */
+ public function getFeedLimit(): int{
+ return (int)$this->config->get("feed");
+ }
+
+ /**
+ * ワールド制限のためのワールド名を配列で取得します
+ * @return array
+ */
+ public function getWorldLimit(): array{
+ $worlds = $this->config->get("world");
+ if ($worlds !== false) {
+ return array_flip($worlds);
+ }else {
+ return [];
+ }
+ }
+
+ /****************************************************************************/
+ /* PMMP Api */
+
+ public function onLoad(){
+ $this->loadFiles();
+ $this->initApi();
+ $this->registerCommands();
+ $this->checkUpdate();
+ $this->setTimezone();
+ }
+
+ public function onEnable(){
+ $this->prepareTexts();
+ $listener = new EventListener($this);
+ $this->getServer()->getPluginManager()->registerEvents($listener, $this);
+ $this->getLogger()->info(TF::GREEN.self::NAME." ".self::VERSION." - ".TF::BLUE."\"".self::CODENAME."\" ".TF::GREEN.$this->language->transrateString("on.enable"));
+ }
+
+ /****************************************************************************/
+ /* Private functions */
+
+ private function loadFiles(){
+ $this->dir = $this->getDataFolder();
+ //
+ if(!file_exists($this->dir)){
+ mkdir($this->dir);
+ }
+ if(!file_exists($this->dir.self::FILE_CONFIG)){
+ file_put_contents($this->dir.self::FILE_CONFIG, $this->getResource(self::FILE_CONFIG));
+ }
+ // config.yml
+ $this->config = new Config($this->dir.self::FILE_CONFIG, Config::YAML);
+ // Lang
+ $lang = $this->config->get("language");
+ if ($lang !== false) {
+ $this->language = new Lang($this, $lang);
+ $this->getLogger()->info(TF::GREEN.$this->language->transrateString("lang.registered", ["{lang}"], [$lang]));
+ }else {
+ $this->getLogger()->error("Invalid language settings. If you have any questions, please contact the issue.");
+ }
+ if(!file_exists($this->dir.self::FILE_CRFT)){
+ if (!file_exists($this->dir.self::FILE_CRFTP)) {
+ file_put_contents($this->dir.self::FILE_CRFT, $this->getResource(self::FILE_CRFT));
+ }else {
+ $tmpOld = new Config($this->dir.self::FILE_CRFTP, Config::JSON);
+ $tmpOldData = $tmpOld->getAll();
+ file_put_contents($this->dir.self::FILE_CRFT, []);
+ $tmpNew = new Config($this->dir.self::FILE_CRFT, Config::JSON);
+ $tmpNew->setAll($tmpOldData);
+ $tmpNew->save();
+ unlink($this->dir.self::FILE_CRFTP);
+ $this->getLogger()->info(TF::GREEN.$this->language->transrateString("transfer.crftp"));
+ }
+ }
+ if(!file_exists($this->dir.self::FILE_FT)){
+ if (!file_exists($this->dir.self::FILE_FTP)) {
+ file_put_contents($this->dir.self::FILE_FT, $this->getResource(self::FILE_FT));
+ }else {
+ $tmpOld = new Config($this->dir.self::FILE_FTP, Config::JSON);
+ $tmpOldData = $tmpOld->getAll();
+ file_put_contents($this->dir.self::FILE_FT, []);
+ $tmpNew = new Config($this->dir.self::FILE_FT, Config::JSON);
+ $tmpNew->setAll($tmpOldData);
+ $tmpNew->save();
+ unlink($this->dir.self::FILE_FTP);
+ $this->getLogger()->info(TF::GREEN.$this->language->transrateString("transfer.ftp"));
+ }
+ }
+ // crfts.json
+ $crft_config = new Config($this->dir.self::FILE_CRFT, Config::JSON);
+ $this->crfts = $crft_config->getAll();
+ // fts.json
+ $ft_config = new Config($this->dir.self::FILE_FT, Config::JSON);
+ $this->fts = $ft_config->getAll();
+ // CheckConfigVersion
+ if (!$this->config->exists("configVersion") ||
+ $this->config->get("configVersion") < self::CONFIG_VERSION) {
+ $this->getLogger()->notice($this->language->transrateString("config.update", ["{newer}"], [self::CONFIG_VERSION]));
+ }
+ }
+
+ private function initApi(){
+ $this->api = new TexterApi($this);
+ }
+
+ private function registerCommands(){
+ if ((bool)$this->config->get("canUseCommands")) {
+ $map = $this->getServer()->getCommandMap();
+ switch (strtolower($this->getServer()->getName())) {
+ case 'pocketmine-mp':
+ $commands = [
+ new TxtCommand($this),
+ new TxtAdmCommand($this)
+ ];
+ break;
+
+ // NOTE: Confirmed
+ case 'genisyspro':
+ case 'leveryl':
+ default:
+ $commands = [
+ new TxtCommandOld($this),
+ new TxtAdmCommandOld($this)
+ ];
+ break;
+ }
+ $map->registerAll(self::NAME, $commands);
+ $this->getLogger()->info(TF::GREEN.$this->language->transrateString("commands.registered"));
+ }else {
+ $this->getLogger()->info(TF::RED.$this->language->transrateString("commands.unavailable"));
+ }
+ }
+
+ private function checkUpdate(){
+ if ((bool)$this->config->get("checkUpdate")) {
+ try {
+ $async = new CheckUpdateTask();
+ $this->getServer()->getScheduler()->scheduleAsyncTask($async);
+ } catch (\Exception $e) {
+ $this->getLogger()->warning($e->getMessage());
+ }
+ }
+ if (strpos(self::VERSION, "-") !== false) {
+ $this->getLogger()->notice($this->language->transrateString("version.pre"));
+ $this->devmode = true;
+ }
+ }
+
+ public function versionCompare(array $data){
+ $curver = str_replace("v", "", self::VERSION);
+ $newver = str_replace("v", "", $data[0]["name"]);
+ if ($this->getDescription()->getVersion() !== $curver) {
+ $this->getLogger()->warning($this->messages->get("version.warning"));
+ }
+ if (version_compare($newver, $curver, "=")) {
+ $this->getLogger()->notice($this->language->transrateString("update.unnecessary", ["{curver}"], [$curver]));
+ }elseif (version_compare($newver, $curver, ">")){
+ $this->getLogger()->notice($this->language->transrateString("update.available.1", ["{newver}", "{curver}"], [$newver, $curver]));
+ $this->getLogger()->notice($this->language->transrateString("update.available.2"));
+ $this->getLogger()->notice($this->language->transrateString("update.available.3", ["{url}"], [$data[0]["html_url"]]));
+ }
+ }
+
+ private function setTimezone(){
+ $timezone = $this->config->get("timezone");
+ if ($timezone !== false) {
+ date_default_timezone_set($timezone);
+ $this->getLogger()->info(TF::GREEN.$this->language->transrateString("timezone", ["{zone}"], [$timezone]));
+ }
+ }
+
+ private function prepareTexts(){
+ if (!empty($this->crfts)) {
+ foreach ($this->crfts as $value) {
+ $title = isset($value["TITLE"]) ? str_replace("#", "\n", $value["TITLE"]) : "";
+ $text = isset($value["TEXT"]) ? str_replace("#", "\n", $value["TEXT"]) : "";
+ if (is_null($value["WORLD"]) || $value["WORLD"] === "default"){
+ $value["WORLD"] = $this->getServer()->getDefaultLevel()->getName();
+ }
+ //
+ if ($this->getServer()->loadLevel($value["WORLD"])) {
+ $level = $this->getServer()->getLevelByName($value["WORLD"]);
+ $crft = new CRFT($level, $value["Xvec"], $value["Yvec"], $value["Zvec"], $title, $text);
+ if ($crft->failed) {
+ $message = $this->language->transrateString("txt.failed");
+ $this->getLogger()->notice($message);
+ }
+ }else {
+ $message = $this->language->transrateString("world.not.exists", ["{world}"], [$value["WORLD"]]);
+ $this->getLogger()->notice($message);
+ }
+ }
+ }
+ if (!empty($this->fts)) {
+ foreach ($this->fts as $value) {
+ $title = isset($value["TITLE"]) ? str_replace("#", "\n", $value["TITLE"]) : "";
+ $text = isset($value["TEXT"]) ? str_replace("#", "\n", $value["TEXT"]) : "";
+ if (is_null($value["WORLD"]) || $value["WORLD"] === "default"){
+ $value["WORLD"] = $this->getServer()->getDefaultLevel()->getName();
+ }
+ //
+ if ($this->getServer()->loadLevel($value["WORLD"])) {
+ $level = $this->getServer()->getLevelByName($value["WORLD"]);
+ $ft = new FT($level, $value["Xvec"], $value["Yvec"], $value["Zvec"], $title, $text, strtolower($value["OWNER"]));
+ if ($ft->failed) {
+ $message = $this->language->transrateString("txt.failed");
+ $this->getLogger()->notice($message);
+ }
+ }else {
+ $message = $this->language->transrateString("world.not.exists", ["{world}"], [$value["WORLD"]]);
+ $this->getLogger()->notice($message);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Texter/TexterApi.php b/src/Texter/TexterApi.php
new file mode 100644
index 0000000..2dded79
--- /dev/null
+++ b/src/Texter/TexterApi.php
@@ -0,0 +1,319 @@
+
+ *
+ * Released under the "MIT license".
+ * You should have received a copy of the MIT license
+ * along with this program. If not, see
+ * < https://opensource.org/licenses/mit-license >.
+ *
+ * ---------------------------------------------------------------------
+ * ## 日本語
+ *
+ * TexterはPocketMine-MP向けのFloatingTextPerticleを表示するプラグインです。
+ * Copyright (c) 2017 yuko fuyutsuki < https://github.com/fuyutsuki >
+ *
+ * このソフトウェアは"MITライセンス"下で配布されています。
+ * あなたはこのプログラムと共にMITライセンスのコピーを受け取ったはずです。
+ * 受け取っていない場合、下記のURLからご覧ください。
+ * < https://opensource.org/licenses/mit-license >
+ */
+
+namespace Texter;
+
+# Pocketmine
+use pocketmine\Player;
+use pocketmine\entity\Entity;
+use pocketmine\item\Item;
+use pocketmine\level\{
+ Level,
+ Position};
+use pocketmine\math\Vector3;
+use pocketmine\utils\{
+ TextFormat as TF,
+ UUID};
+
+# Texter
+use Texter\Main;
+use Texter\language\Lang;
+use Texter\text\{
+ CantRemoveFloatingText as CRFT,
+ FloatingText as FT};
+use Texter\utils\TunedConfig as Config;
+
+/**
+ * TexterApi
+ */
+class TexterAPI{
+
+ /** @var TexterAPI */
+ private static $instance = null;
+ /** @var Config $crft_config */
+ private $crft_config = null;
+ /** @var Config $ft_config */
+ private $ft_config = null;
+ /** @var array $crfts[$levelName][] = $pk */
+ private $crfts = [];
+ /** @var array $ft[$levelName][] = $pk */
+ private $fts = [];
+
+ public function __construct(Main $main){
+ self::$instance = $this;
+ $this->main = $main;
+ $this->language = Lang::getInstance();
+ $this->crft_config = new Config($main->getDataFolder().Main::FILE_CRFT, Config::JSON);
+ $this->crft_config->setAll([]);
+ $this->ft_config = new Config($main->getDataFolder().Main::FILE_FT, Config::JSON);
+ $this->ft_config->setAll([]);
+ }
+
+ /****************************************************************************/
+ /* get/set(情報取得/変更) 関連 */
+ /**
+ * インスタンスを取得
+ * @return TexterApi
+ */
+ public static function getInstance(): TexterApi{
+ return self::$instance;
+ }
+
+ /**
+ * 現在使用中の言語を取得します
+ * @return string "eng"|"jpn"
+ */
+ public function getLanguage(): string{
+ return $this->language->getLang();
+ }
+
+ /**
+ * テキストをファイルに保存します
+ * @param CRFT $text
+ * @param bool $new 新規かどうか
+ * @return bool
+ */
+ public function saveCrft(CRFT $text, bool $new = false): bool{
+ $levelName = $text->level->getName();
+ $key = $levelName . $text->z . $text->x . $text->y;
+ if ($new) { // 新規
+ if ($this->crft_config->exists($key)) { // 既設
+ return false;
+ }else {
+ $this->crfts[$levelName][$text->eid] = $text;
+ }
+ }
+ $data = [
+ "WORLD" => $levelName,
+ "Xvec" => sprintf('%0.1f', $text->x),
+ "Yvec" => sprintf('%0.1f', $text->y),
+ "Zvec" => sprintf('%0.1f', $text->z),
+ "TITLE" => $text->title,
+ "TEXT" => $text->text
+ ];
+ $this->crft_config->set($key, $data);
+ $this->crft_config->save();
+ return true;
+ }
+
+ /**
+ * テキストをファイルに保存します
+ * @param FT $text
+ * @param bool $new 新規かどうか
+ * @return bool
+ */
+ public function saveFt(FT $text, bool $new = false): bool{
+ $levelName = $text->level->getName();
+ $key = $levelName . $text->z . $text->x . $text->y;
+ if ($new) { // 新規
+ if ($this->ft_config->exists($key)) { // 既設
+ return false;
+ }else {
+ $this->fts[$levelName][$text->eid] = $text;
+ }
+ }
+ $data = [
+ "WORLD" => $levelName,
+ "Xvec" => sprintf('%0.1f', $text->x),
+ "Yvec" => sprintf('%0.1f', $text->y),
+ "Zvec" => sprintf('%0.1f', $text->z),
+ "TITLE" => $text->title,
+ "TEXT" => $text->text,
+ "OWNER" => $text->owner
+ ];
+ $this->ft_config->set($key, $data);
+ $this->ft_config->save();
+ return true;
+ }
+
+ /**
+ * テキストをファイルから消去します
+ * @param FT $text
+ * @return bool true
+ */
+ public function removeText(FT $text): bool{
+ $levelName = $text->level->getName();
+ $key = $levelName . $text->z . $text->x . $text->y;
+ $this->ft_config->remove($key);
+ $this->ft_config->save();
+ unset($this->fts[$levelName][$text->eid]);
+ return true;
+ }
+
+ /**
+ * すべてのcrftを返します
+ * @return array $this->crfts
+ */
+ public function getCrfts(): array{
+ return $this->crfts;
+ }
+
+ /**
+ * 指定されたワールドのすべてのcrftを返します
+ * @param Level $level
+ * @return array
+ */
+ public function getCrftsByLevel(Level $level): array{
+ $levelName = $level->getName();
+ if (!isset($this->crfts[$levelName])) {
+ return [];
+ }else {
+ return $this->crfts[$levelName];
+ }
+ }
+
+ /**
+ * 指定されたワールドのすべてのcrftを返します
+ * @param string $levelName
+ * @return array
+ */
+ public function getCrftsByLevelName(string $levelName): array{
+ if (!isset($this->crfts[$levelName])) {
+ return [];
+ }else {
+ return $this->crfts[$levelName];
+ }
+ }
+
+ /**
+ * 指定されたワールド, eidのcrftを取得します
+ * @param string $levelName
+ * @param int $entityId
+ * @return null|CRFT
+ */
+ public function getCrft(string $levelName, int $entityId){
+ if (!isset($this->crfts[$levelName][$entityId])) {
+ return null;
+ }else{
+ return $this->crfts[$levelName][$entityId];
+ }
+ }
+
+ /**
+ * crftの個数を返します
+ * @return int
+ */
+ public function getCrftsCount(): int{
+ $cc = 0;
+ $crfts = $this->getCrfts();
+ if ($crfts !== false) {
+ foreach ($crfts as $levCrfts) {
+ foreach ($levCrfts as $crft) {
+ ++$cc;
+ }
+ }
+ }
+ return $cc;
+ }
+
+ /**
+ * すべてのftを返します
+ * @return array $this->fts
+ */
+ public function getFts(): array{
+ return $this->fts;
+ }
+
+ /**
+ * 指定されたワールドのすべてのftを返します
+ * @param Level $level
+ * @return array
+ */
+ public function getFtsByLevel(Level $level): array{
+ $levelName = $level->getName();
+ if (!isset($this->fts[$levelName])) {
+ return [];
+ }else {
+ return $this->fts[$levelName];
+ }
+ }
+
+ /**
+ * 指定されたワールドのすべてのftを返します
+ * @param string $levelName
+ * @return array
+ */
+ public function getFtsByLevelName(string $levelName): array{
+ if (!isset($this->fts[$levelName])) {
+ return [];
+ }else {
+ return $this->fts[$levelName];
+ }
+ }
+
+ /**
+ * 指定されたユーザー名所有のftをすべて取得します
+ * @param string $name
+ * @return array
+ */
+ public function getFtsByName(string $name): array{
+ $name = strtolower($name);
+ $fts = $this->getFts();
+ if (empty($fts)) {
+ return [];
+ }else {
+ $return = [];
+ foreach ($fts as $levFts) {
+ foreach ($levFts as $ft) {
+ if ($ft->owner === $name) {
+ $return[] = $ft;
+ }
+ }
+ }
+ return $return;
+ }
+ }
+
+ /**
+ * 指定されたワールド,eidのftを取得します
+ * @param string $levelName
+ * @param int $entityId
+ * @return null|FT
+ */
+ public function getFt(string $levelName, int $entityId){
+ if (!isset($this->fts[$levelName][$entityId])) {
+ return null;
+ }else{
+ return $this->fts[$levelName][$entityId];
+ }
+ }
+
+ /**
+ * ftの個数を返します
+ * @return int
+ */
+ public function getFtsCount(): int{
+ $fc = 0;
+ $fts = $this->getFts();
+ if ($fts !== false) {
+ foreach ($fts as $levFts) {
+ foreach ($levFts as $fts) {
+ ++$fc;
+ }
+ }
+ }
+ return $fc;
+ }
+}
diff --git a/src/Texter/commands/TxtAdmCommand.php b/src/Texter/commands/TxtAdmCommand.php
new file mode 100644
index 0000000..160875b
--- /dev/null
+++ b/src/Texter/commands/TxtAdmCommand.php
@@ -0,0 +1,113 @@
+main = $main;
+ $this->api = $main->getAPI();
+ $this->lang = Lang::getInstance();
+ parent::__construct("txtadm", $this->lang->transrateString("command.description.txtadm"), "/txtadm ", ["tadm"]);//登録
+ //
+ $this->setPermission("texter.command.txtadm");
+ //
+ $this->help = $this->lang->transrateString("command.txt.usage")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.allremove")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.userremove")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.info");
+ }
+
+ public function execute(CommandSender $sender, string $label, array $args){
+ if (!$this->main->isEnabled()) return false;
+ if (!$this->testPermission($sender)) return false;
+ if (isset($args[0])) {
+ $name = $sender->getName();
+ $lev = $sender->level;
+ $levn = $lev->getName();
+ switch (strtolower($args[0])) { // subCommand
+ case 'allremove':
+ case 'ar':
+ $fts = $this->api->getFts();
+ $count = $this->api->getFtsCount();
+ if ($fts === 0) {
+ $message = $this->lang->transrateString("command.txtadm.notexists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ foreach ($fts as $levFts) {
+ foreach ($levFts as $ft) {
+ $ft->remove();
+ }
+ }
+ $message = $this->lang->transrateString("command.txtadm.allremove", ["{count}"], [$count]);
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'userremove':
+ case 'ur':
+ if (isset($args[1])) { // username
+ $fts = $this->api->getFtsByName($args[1]);
+ if (empty($fts)) {
+ $message = $this->lang->transrateString("txt.user.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txtadm.userremove", ["{user}"], [$args[1]]);
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txtadm.usage.ur");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'info':
+ case 'i':
+ $crfts = $this->api->getCrftsCount();
+ $fts = $this->api->getFtsCount();
+ $message = TF::AQUA . Lang::PREFIX . "\n";
+ $message .= TF::AQUA . "crfts: " . TF::GOLD . $crfts . "\n";
+ $message .= TF::AQUA . "fts: " . TF::GOLD . $fts . "\n";
+ $message .= TF::GRAY . Main::NAME . " " . Main::VERSION . " - " . Main::CODENAME;
+ $sender->sendMessage($message);
+ break;
+
+ case 'help':
+ case 'h':
+ case '?':
+ default:
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ break;
+
+ case 'test':
+ if ($this->main->devmode) {
+ for ($i=1; $i<51; $i++) {
+ $ft = new FT($lev, $s->x+$i, $s->y, $s->z, "test", "§$i", $name);
+ }
+ }
+ break;
+ }
+ }else {
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ }
+ return true;
+ }
+}
diff --git a/src/Texter/commands/TxtAdmCommandOld.php b/src/Texter/commands/TxtAdmCommandOld.php
new file mode 100644
index 0000000..e5ec67d
--- /dev/null
+++ b/src/Texter/commands/TxtAdmCommandOld.php
@@ -0,0 +1,113 @@
+main = $main;
+ $this->api = $main->getAPI();
+ $this->lang = Lang::getInstance();
+ parent::__construct("txtadm", $this->lang->transrateString("command.description.txtadm"), "/txtadm ", ["tadm"]);//登録
+ //
+ $this->setPermission("texter.command.txtadm");
+ //
+ $this->help = $this->lang->transrateString("command.txt.usage")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.allremove")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.userremove")."\n";
+ $this->help .= $this->lang->transrateString("command.txtadm.usage.info");
+ }
+
+ public function execute(CommandSender $sender, $label, array $args){
+ if (!$this->main->isEnabled()) return false;
+ if (!$this->testPermission($sender)) return false;
+ if (isset($args[0])) {
+ $name = $sender->getName();
+ $lev = $sender->level;
+ $levn = $lev->getName();
+ switch (strtolower($args[0])) { // subCommand
+ case 'allremove':
+ case 'ar':
+ $fts = $this->api->getFts();
+ $count = $this->api->getFtsCount();
+ if ($fts === 0) {
+ $message = $this->lang->transrateString("command.txtadm.notexists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ foreach ($fts as $levFts) {
+ foreach ($levFts as $ft) {
+ $ft->remove();
+ }
+ }
+ $message = $this->lang->transrateString("command.txtadm.allremove", ["{count}"], [$count]);
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'userremove':
+ case 'ur':
+ if (isset($args[1])) { // username
+ $fts = $this->api->getFtsByName($args[1]);
+ if (empty($fts)) {
+ $message = $this->lang->transrateString("txt.user.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txtadm.userremove", ["{user}"], [$args[1]]);
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txtadm.usage.ur");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'info':
+ case 'i':
+ $crfts = $this->api->getCrftsCount();
+ $fts = $this->api->getFtsCount();
+ $message = TF::AQUA . Lang::PREFIX . "\n";
+ $message .= TF::AQUA . "crfts: " . TF::GOLD . $crfts . "\n";
+ $message .= TF::AQUA . "fts: " . TF::GOLD . $fts . "\n";
+ $message .= TF::GRAY . Main::NAME . " " . Main::VERSION . " - " . Main::CODENAME;
+ $sender->sendMessage($message);
+ break;
+
+ case 'help':
+ case 'h':
+ case '?':
+ default:
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ break;
+
+ case 'test':
+ if ($this->main->devmode) {
+ for ($i=1; $i<51; $i++) {
+ $ft = new FT($lev, $s->x+$i, $s->y, $s->z, "test", "§$i", $name);
+ }
+ }
+ break;
+ }
+ }else {
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ }
+ return true;
+ }
+}
diff --git a/src/Texter/commands/TxtCommand.php b/src/Texter/commands/TxtCommand.php
new file mode 100644
index 0000000..73b3c26
--- /dev/null
+++ b/src/Texter/commands/TxtCommand.php
@@ -0,0 +1,216 @@
+main = $main;
+ $this->api = $main->getAPI();
+ $this->lang = Lang::getInstance();
+ parent::__construct("txt", $this->lang->transrateString("command.description.txt"), "/txt ");//登録
+ //
+ $this->setPermission("texter.command.txt");
+ //
+ $this->help = $this->lang->transrateString("command.txt.usage")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.add")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.remove")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.update")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.indent");
+ //
+ $this->lim = $this->main->getCharaLimit();
+ $this->feed = $this->main->getFeedLimit();
+ $this->world = $this->main->getWorldLimit();
+ }
+
+ public function execute(CommandSender $sender, string $label, array $args){
+ if (!$this->main->isEnabled()) return false;
+ if (!$this->testPermission($sender)) return false;
+ if ($sender instanceof Player) {
+ $lev = $sender->level;
+ $levn = $lev->getName();
+ if (array_key_exists($levn, $this->world)) {
+ $message = $this->lang->transrateString("command.txt.world");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ if (isset($args[0])) {
+ switch (strtolower($args[0])) { // subCommand
+ case 'add':
+ case 'a':
+ if (!empty($args[1])) { // Title
+ $title = str_replace("#", "\n", $args[1]);
+ if (!empty($args[2])) { // Text
+ $texts = array_slice($args, 2);
+ $text = str_replace("#", "\n", implode(" ", $texts));
+ }else {
+ $text = "";
+ }
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($title.$text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ $title = TF::clean($title);
+ $text = TF::clean($text);
+ }
+ $x = sprintf('%0.1f', $sender->x);
+ $y = sprintf('%0.1f', $sender->y + 1);
+ $z = sprintf('%0.1f', $sender->z);
+ $name = $sender->getName();
+ $ft = new FT($lev, $x, $y, $z, $title, $text, $name);
+ if ($ft->failed) {
+ $message = $this->lang->transrateString("txt.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txt.set");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.add");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'remove':
+ case 'r':
+ if (isset($args[1])) { // entityId
+ $eid = (int)$args[1];
+ $ft = $this->api->getFt($levn, $eid);
+ if ($ft !== null) {
+ if ($ft->canEditFt($sender)) {
+ $ft->remove();
+ $message = $this->lang->transrateString("command.txt.remove");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txt.permission");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("txt.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.remove");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'update':
+ case 'u':
+ if (isset($args[1]) && isset($args[2]) && !empty($args[3])) {
+ // eid && title" or "text" && contents
+ $eid = (int)$args[1];
+ $ft = $this->api->getFt($levn, $eid);
+ if ($ft !== null) {
+ if ($ft->canEditFt($sender)) {
+ switch (strtolower($args[2])) {
+ case 'title':
+ $title = str_replace("#", "\n", $args[3]);
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($title.$ft->text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ $title = TF::clean($title);
+ }
+ $ft->setTitle($args[3]);
+ $message = $this->lang->transrateString("command.txt.updated");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+
+ case 'text':
+ if (!empty($args[3])) {
+ $texts = array_slice($args, 3);
+ $text = str_replace("#", "\n", implode(" ", $texts));
+ }else {
+ $text = "";
+ }
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($ft->title.$text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ $text = TF::clean($text);
+ }
+ $ft->setText($text);
+ $message = $this->lang->transrateString("command.txt.updated");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+
+ default:
+ $message = $this->lang->transrateString("command.txt.usage.update");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.permission");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("txt.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.update");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'help':
+ case 'h':
+ case '?':
+ default:
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ break;
+ }
+ }else {
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ }
+ }
+ }else {
+ $message = $this->lang->transrateString("command.console");
+ $this->main->getLogger()->info(TF::RED . $message);
+ }
+ return true;
+ }
+
+ /**
+ * テキストのルールに違反していないか確かめます
+ * @param string $text
+ * @return string|bool
+ */
+ private function checkTextLimit(string $text){
+ if ($this->lim > -1 && mb_strlen($text, "UTF-8") > $this->lim) {
+ $message = $this->lang->transrateString("command.txt.limit", ["{limit}"], [$this->lim]);
+ return $message;
+ }else {
+ if ($this->feed > -1 && mb_substr_count($text, "\n" , "UTF-8") > $this->feed) {
+ $message = $this->lang->transrateString("command.txt.feed", ["{feed}"], [$this->feed]);
+ return $message;
+ }else {
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/Texter/commands/TxtCommandOld.php b/src/Texter/commands/TxtCommandOld.php
new file mode 100644
index 0000000..64f45a4
--- /dev/null
+++ b/src/Texter/commands/TxtCommandOld.php
@@ -0,0 +1,217 @@
+main = $main;
+ $this->api = $main->getAPI();
+ $this->lang = Lang::getInstance();
+ parent::__construct("txt", $this->lang->transrateString("command.description.txt"), "/txt ");//登録
+ //
+ $this->setPermission("texter.command.txt");
+ //
+ $this->help = $this->lang->transrateString("command.txt.usage")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.add")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.remove")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.update")."\n";
+ $this->help .= $this->lang->transrateString("command.txt.usage.indent");
+ //
+ $this->lim = $this->main->getCharaLimit();
+ $this->feed = $this->main->getFeedLimit();
+ $this->world = $this->main->getWorldLimit();
+ }
+
+ public function execute(CommandSender $sender, $label, array $args){
+ if (!$this->main->isEnabled()) return false;
+ if (!$this->testPermission($sender)) return false;
+ if ($sender instanceof Player) {
+ $lev = $sender->level;
+ $levn = $lev->getName();
+ if (array_key_exists($levn, $this->world)) {
+ $message = $this->lang->transrateString("command.txt.world");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ if (isset($args[0])) {
+ switch (strtolower($args[0])) { // subCommand
+ case 'add':
+ case 'a':
+ if (!empty($args[1])) { // Title
+ $title = str_replace("#", "\n", $args[1]);
+
+ if (!empty($args[2])) { // Text
+ $texts = array_slice($args, 2);
+ $text = str_replace("#", "\n", implode(" ", $texts));
+ }else {
+ $text = "";
+ }
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($title.$text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ $title = TF::clean($title);
+ $text = TF::clean($text);
+ }
+ $x = sprintf('%0.1f', $sender->x);
+ $y = sprintf('%0.1f', $sender->y + 1);
+ $z = sprintf('%0.1f', $sender->z);
+ $name = $sender->getName();
+ $ft = new FT($lev, $x, $y, $z, $title, $text, $name);
+ if ($ft->failed) {
+ $message = $this->lang->transrateString("txt.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txt.set");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.add");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'remove':
+ case 'r':
+ if (isset($args[1])) { // entityId
+ $eid = (int)$args[1];
+ $ft = $this->api->getFt($levn, $eid);
+ if ($ft !== null) {
+ if ($ft->canEditFt($sender)) {
+ $ft->remove();
+ $message = $this->lang->transrateString("command.txt.remove");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }else {
+ $message = $this->lang->transrateString("command.txt.permission");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("txt.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.remove");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'update':
+ case 'u':
+ if (isset($args[1]) && isset($args[2]) && !empty($args[3])) {
+ // eid && title" or "text" && contents
+ $eid = (int)$args[1];
+ $ft = $this->api->getFt($levn, $eid);
+ if ($ft !== null) {
+ if ($ft->canEditFt($sender)) {
+ switch (strtolower($args[2])) {
+ case 'title':
+ $title = str_replace("#", "\n", $args[3]);
+ $title = TF::clean($title);
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($title.$ft->text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ }
+ $ft->setTitle($args[3]);
+ $message = $this->lang->transrateString("command.txt.updated");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+
+ case 'text':
+ if (!empty($args[3])) {
+ $texts = array_slice($args, 3);
+ $text = str_replace("#", "\n", implode(" ", $texts));
+ $text = TF::clean($text);
+ }else {
+ $text = "";
+ }
+ if (!$sender->isOp()) {
+ $message = $this->checkTextLimit($ft->title.$text);
+ if ($message !== true) {
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ return true;
+ }
+ }
+ $ft->setText($text);
+ $message = $this->lang->transrateString("command.txt.updated");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+
+ default:
+ $message = $this->lang->transrateString("command.txt.usage.update");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ break;
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.permission");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("txt.doesn`t.exists");
+ $sender->sendMessage(TF::RED . Lang::PREFIX . $message);
+ }
+ }else {
+ $message = $this->lang->transrateString("command.txt.usage.update");
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $message);
+ }
+ break;
+
+ case 'help':
+ case 'h':
+ case '?':
+ default:
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ break;
+ }
+ }else {
+ $sender->sendMessage(TF::AQUA . Lang::PREFIX . $this->help);
+ }
+ }
+ }else {
+ $message = $this->lang->transrateString("command.console");
+ $this->main->getLogger()->info(TF::RED . $message);
+ }
+ return true;
+ }
+
+ /**
+ * テキストのルールに違反していないか確かめます
+ * @param string $text
+ * @return string|bool
+ */
+ private function checkTextLimit(string $text){
+ if ($this->lim > -1 && mb_strlen($text, "UTF-8") > $this->lim) {
+ $message = $this->lang->transrateString("command.txt.limit", ["{limit}"], [$this->lim]);
+ return $message;
+ }else {
+ if ($this->feed > -1 && mb_substr_count($text, "\n" , "UTF-8") > $this->feed) {
+ $message = $this->lang->transrateString("command.txt.feed", ["{feed}"], [$this->feed]);
+ return $message;
+ }else {
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/Texter/language/Lang.php b/src/Texter/language/Lang.php
new file mode 100644
index 0000000..8b3071c
--- /dev/null
+++ b/src/Texter/language/Lang.php
@@ -0,0 +1,94 @@
+main = $main;
+ $this->dir = $main->getDataFolder();
+ $this->setLang($lang);
+ }
+
+ /**
+ * 静的にインスタンス取得
+ * @return Lang
+ */
+ public static function getInstance(): Lang{
+ return self::$instance;
+ }
+
+ /**
+ * 現在使用中の言語を取得
+ * @return string $this->language
+ */
+ public function getLang(): string{
+ return $this->language;
+ }
+
+ /**
+ * 使用する言語を取得
+ * @param string $lang
+ * @return string $this->language
+ */
+ public function setLang(string $lang): string{
+ switch (strtolower($lang)) {
+ case self::ENG:
+ $this->language = self::ENG;
+ break;
+
+ case self::JPN:
+ $this->language = self::JPN;
+ break;
+
+ default:
+ $this->language = self::ENG;
+ break;
+ }
+ $this->lang = new Config(__DIR__.DS.$this->language.".json", Config::JSON);
+ return $this->language;
+ }
+
+ /**
+ * 翻訳
+ * @param string $key
+ * @param array $search = []
+ * @param array $replace = []
+ * @return string
+ */
+ public function transrateString(string $key, array $search = [], array $replace = []): string{
+ $result = $this->lang->get($key);
+ if ($result !== false) {
+ $result = str_replace($search, $replace, $result);
+ return $result;
+ }else {
+ return $key;
+ }
+ }
+}
diff --git a/src/Texter/language/eng.json b/src/Texter/language/eng.json
new file mode 100644
index 0000000..2192f65
--- /dev/null
+++ b/src/Texter/language/eng.json
@@ -0,0 +1,57 @@
+{
+ "on.enable": "is Enabled.",
+ "on.disable": "is Disabled.",
+ "lang.registered": "Language was set to {lang}.",
+ "config.update": "config.yml version {newer} => Please delete config.yml once.",
+ "transfer.crftp": "Successful file transfer from crftps.json to crfts.json.",
+ "transfer.ftp": "Successful file transfer from ftps.json to fts.json.",
+
+ "commands.registered": "Command was set to enabled.",
+ "commands.unavailable": "Command was set to disabled.",
+
+ "version.pre": "This version is under development. Operation may be unstable.",
+ "version.warning": "Detected version change, you may not be able to receive support.",
+
+ "update.unnecessary": "Newest version: v{curver} / No update required.",
+ "update.available.1": "Newest version: v{newver} / Current version: v{curver}",
+ "update.available.2": "Update is available.",
+ "update.available.3": "URL: {url}",
+
+ "timezone": "Timezone was set to {zone}.",
+
+ "world.not.exists": "The listed world ({world}) does not exist.",
+ "txt.type.invalid": "The type of the specified text is invalid.",
+
+ "command.description.txt": "Operate floating texts.",
+ "command.description.txtadm": "Manage floating texts.",
+
+ "command.txt.usage": "Usage:",
+ "command.txt.usage.add": "/txt a(dd) [message]",
+ "command.txt.usage.remove": "/txt r(emove) ",
+ "command.txt.usage.update": "/txt u(pdate) ",
+ "command.txt.usage.indent": "When breaking a line, insert §6#§b",
+
+ "command.txt.set": "Placed floating text.",
+ "command.txt.world": "You can`t manipulate floating letters in this world.",
+ "command.txt.limit": "The number of characters is limited by {limit}.",
+ "command.txt.feed": "Line feeds are limited to {feed} times.",
+ "command.txt.limit": "The number of characters is limited by {limit}.",
+ "command.txt.remove": "Removed floating text.",
+ "command.txt.updated": "Updated floating text.",
+ "command.txt.permission": "You don`t have permission to edit the specified floating text.",
+
+ "command.txtadm.usage.allremove": "/t(xt)adm a(ll)r(emove)",
+ "command.txtadm.usage.userremove": "/t(xt)adm u(ser)r(emove) ",
+ "command.txtadm.usage.info": "/txtadm info",
+
+ "command.txtadm.allremove": "{count} floating texts were deleted.",
+ "command.txtadm.userremove": "All floating texts of {user} have been deleted.",
+ "command.txtadm.notexists": "Any floating texts doesn`t exists.",
+
+ "command.console": "Please use from within game.",
+
+ "txt.failed": "Failed to generate floating text.",
+ "txt.exists": "Floating text already exists at the specified coordinates.",
+ "txt.doesn`t.exists": "The specified ID floating text does not exist.",
+ "txt.user.doesn`t.exists": "The float text of the specified user does not exist."
+}
diff --git a/src/Texter/language/jpn.json b/src/Texter/language/jpn.json
new file mode 100644
index 0000000..8cf0800
--- /dev/null
+++ b/src/Texter/language/jpn.json
@@ -0,0 +1,56 @@
+{
+ "on.enable": "が読み込まれました。",
+ "on.disable": "が無効化されました。",
+ "lang.registered": "言語は{lang} に設定されました。",
+ "config.update": "config.yml {newer} => 一度config.ymlを削除してください。",
+ "transfer.crftp": "crftps.json から crfts.json へのファイル移行に成功しました。",
+ "transfer.ftp": "ftps.json から fts.json へのファイル移行に成功しました。",
+
+ "commands.registered": "コマンドは使用可能に設定されました。",
+ "commands.unavailable": "コマンドは使用不能に設定されました。",
+
+ "version.pre": "このバージョンは開発中のものです。動作が不安定な場合があります。",
+ "version.warning": "バージョンの変更を検知しました。バグが発生した場合、製作者は一切の責任を負いません。",
+
+ "update.unnecessary": "最新バージョン: v{curver} / アップデートは必要ありません",
+ "update.available.1": "最新バージョン: v{newver} / 現在のバージョン: v{curver}",
+ "update.available.2": "アップデートが利用可能です。",
+ "update.available.3": "URL: {url}",
+
+ "timezone": "タイムゾーンは {zone} に設定されました。",
+
+ "world.not.exists": "記載されたワールド名 {world} は存在しません。",
+ "txt.type.invalid": "指定されたテキストの型は無効です。",
+
+ "command.description.txt": "浮き文字を操作します。",
+ "command.description.txtadm": "浮き文字の管理をします。",
+
+ "command.txt.usage": "使用方法:",
+ "command.txt.usage.add": "/txt a(dd) <タイトル> <メッセージ>",
+ "command.txt.usage.remove": "/txt r(emove) ",
+ "command.txt.usage.update": "/txt u(pdate) <テキスト>",
+ "command.txt.usage.indent": "改行したい場合は§6#§bを挿入",
+
+ "command.txt.set": "浮き文字を設置しました。",
+ "command.txt.world": "このワールドでは浮き文字を操作できません。",
+ "command.txt.limit": "{limit} までに文字数が制限されています。",
+ "command.txt.feed": "改行は {feed} 回までに制限されています。",
+ "command.txt.remove": "浮き文字を削除しました。",
+ "command.txt.updated": "浮き文字を更新しました。",
+ "command.txt.permission": "指定された浮き文字を扱う権限がありません。",
+
+ "command.txtadm.usage.allremove": "/t(xt)adm a(ll)r(emove)",
+ "command.txtadm.usage.userremove": "/t(xt)adm u(ser)r(emove) <ユーザー名>",
+ "command.txtadm.usage.info": "/txtadm info",
+
+ "command.txtadm.allremove": "{count} 個の浮き文字を削除しました。",
+ "command.txtadm.userremove": "{user} の浮き文字をすべて削除しました。",
+ "command.txtadm.notexists": "浮き文字が一つもありません。",
+
+ "command.console": "ゲーム内から使用してください。",
+
+ "txt.failed": "浮き文字の生成に失敗しました。",
+ "txt.exists": "指定された座標に既に浮き文字が存在します。",
+ "txt.doesn`t.exists": "指定されたIDの浮き文字が存在しません。",
+ "txt.user.doesn`t.exists": "指定されたユーザーの浮き文字は存在しません。"
+}
diff --git a/src/Texter/task/CheckUpdateTask.php b/src/Texter/task/CheckUpdateTask.php
new file mode 100644
index 0000000..d1d7eda
--- /dev/null
+++ b/src/Texter/task/CheckUpdateTask.php
@@ -0,0 +1,38 @@
+ "https://api.github.com/repos/fuyutsuki/Texter/releases",
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_USERAGENT => "getGitHubAPI",
+ CURLOPT_SSL_VERIFYPEER => false
+ ]);
+ $json = curl_exec($curl);
+
+ $errorno = curl_errno($curl);
+ if ($errorno) {
+ $error = curl_error($curl);
+ throw new \Exception($error);
+ }
+ curl_close($curl);
+ $data = json_decode($json, true);
+ $this->setResult($data);
+ }
+
+ public function onCompletion(Server $server){
+ $main = $server->getPluginManager()->getPlugin("Texter");
+ $main->versionCompare($this->getResult());
+ }
+}
diff --git a/src/Texter/task/WorldGetTask.php b/src/Texter/task/WorldGetTask.php
new file mode 100644
index 0000000..36be56e
--- /dev/null
+++ b/src/Texter/task/WorldGetTask.php
@@ -0,0 +1,43 @@
+api = $main->getApi();
+ $this->p = $p;
+ }
+
+ public function onRun(int $tick){
+ $p = $this->p;
+ $lev = $p->getLevel();
+ //
+ $crfts = $this->api->getCrftsByLevel($lev);
+ if (!empty($crfts)) {
+ foreach ($crfts as $crft) {
+ $crft->sendToPlayer($p, CRFT::SEND_TYPE_ADD);
+ }
+ }
+ $fts = $this->api->getFtsByLevel($lev);
+ if (!empty($fts)) {
+ foreach ($fts as $ft) {
+ $ft->sendToPlayer($p, FT::SEND_TYPE_ADD);
+ }
+ }
+ }
+}
diff --git a/src/Texter/task/WorldGetTaskOld.php b/src/Texter/task/WorldGetTaskOld.php
new file mode 100644
index 0000000..ac5d860
--- /dev/null
+++ b/src/Texter/task/WorldGetTaskOld.php
@@ -0,0 +1,43 @@
+api = $main->getApi();
+ $this->p = $p;
+ }
+
+ public function onRun($tick){
+ $p = $this->p;
+ $lev = $p->getLevel();
+ //
+ $crfts = $this->api->getCrftsByLevel($lev);
+ if (!empty($crfts)) {
+ foreach ($crfts as $crft) {
+ $crft->sendToPlayer($p, CRFT::SEND_TYPE_ADD);
+ }
+ }
+ $fts = $this->api->getFtsByLevel($lev);
+ if (!empty($fts)) {
+ foreach ($fts as $ft) {
+ $ft->sendToPlayer($p, FT::SEND_TYPE_ADD);
+ }
+ }
+ }
+}
diff --git a/src/Texter/text/CantRemoveFloatingText.php b/src/Texter/text/CantRemoveFloatingText.php
new file mode 100644
index 0000000..0871234
--- /dev/null
+++ b/src/Texter/text/CantRemoveFloatingText.php
@@ -0,0 +1,9 @@
+level = $level;
+ $this->x = $x;
+ $this->y = $y;
+ $this->z = $z;
+ $this->title = $title;
+ $this->text = $text;
+ $this->owner = strtolower($owner);
+ $this->eid = Entity::$entityCount++;
+ $this->api = TexterApi::getInstance();
+ if ($this->api->saveFt($this, true)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ }else {
+ $this->failed = true;
+ }
+ }
+
+ /**
+ * X座標を変更します
+ * @Override
+ * @param int|float $x
+ * @return bool
+ */
+ public function setX($x): bool{
+ if (is_numeric($x)) {
+ $tmpX = $this->x;
+ $this->x = $x;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->x = $tmpX;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Y座標を変更します
+ * @Override
+ * @param int|float $y
+ * @return bool
+ */
+ public function setY($y): bool{
+ if (is_numeric($y)) {
+ $tmpY = $this->y;
+ $this->y = $y;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->y = $tmpY;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Z座標を変更します
+ * @Override
+ * @param int|float $z
+ * @return bool
+ */
+ public function setZ($z): bool{
+ if (is_numeric($z)) {
+ $tmpZ = $this->z;
+ $this->z = $z;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->z = $tmpZ;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Levelを変更します
+ * @Override
+ * @param Level $level
+ * @return bool
+ */
+ public function setLevel(Level $level): bool{
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ $tmpLev = $this->level;
+ $this->level = $level;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->level = $tmpLev;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return false;
+ }
+ }
+
+ /**
+ * Levelを変更します
+ * @Override
+ * @param string $levelName
+ * @return bool
+ */
+ public function setLevelByName(string $levelName): bool{
+ $level = Server::getInstance()->getLevelByName($levelName);
+ if ($level !== null) {
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ $tmpLev = $this->level;
+ $this->level = $level;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->level = $tmpLev;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 座標を変更します
+ * @Override
+ * @param Vector3 $pos
+ * @return bool true
+ */
+ public function setCoord(Vector3 $pos): bool{
+ $tmpX = $this->x;
+ $tmpY = $this->y;
+ $tmpZ = $this->z;
+ $this->x = $pos->x;
+ $this->y = $pos->y;
+ $this->z = $pos->z;
+ if ($this->api->saveFt($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->x = $tmpX;
+ $this->y = $tmpY;
+ $this->z = $tmpZ;
+ return false;
+ }
+ }
+
+ /**
+ * タイトルを変更します, # で改行です.
+ * @Override
+ * @param string $title
+ * @return bool true
+ */
+ public function setTitle(string $title): bool{
+ $this->title = str_replace("#", "\n", $title);
+ $this->api->saveFt($this);
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * テキストを変更します, # で改行です
+ * @Override
+ * @param string $text
+ * @return bool true
+ */
+ public function setText(string $text): bool{
+ $this->text = str_replace("#", "\n", $text);
+ $this->api->saveFt($this);
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * プレイヤーに送信します
+ * @Override
+ * @param Player $player
+ * @param int $type
+ * @return bool
+ */
+ public function sendToPlayer(Player $player, int $type): bool{
+ switch ($type) {
+ case self::SEND_TYPE_ADD:
+ $pk = $this->getAsAddPacket();
+ if ($this->canEditFt($player)) {
+ $pk->metadata[4][1] = TF::GRAY . "[" . $this->eid . "] " . TF::WHITE . $pk->metadata[4][1];
+ }
+ $player->dataPacket($pk);
+ break;
+
+ case self::SEND_TYPE_REMOVE:
+ $pk = $this->getAsRemovePacket();
+ $player->dataPacket($pk);
+ break;
+
+ default:
+ return false;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * テキストを更新します
+ * @Override
+ * @param int $type
+ * @return bool true
+ */
+ public function sendToLevel(int $type): bool{
+ switch ($type) {
+ case self::SEND_TYPE_ADD:
+ $pk = $this->getAsAddPacket();
+ $players = $this->level->getPlayers();
+ foreach ($players as $player) {
+ if ($this->canEditFt($player)) {
+ $pk->metadata[4][1] = TF::GRAY . "[" . $this->eid . "] " . TF::WHITE . $pk->metadata[4][1];
+ }
+ $player->dataPacket($pk);
+ }
+ break;
+
+ case self::SEND_TYPE_REMOVE:
+ $this->api->removeText($this);
+ $pk = $this->getAsRemovePacket();
+ $players = $this->level->getPlayers();
+ foreach ($players as $player) {
+ $player->dataPacket($pk);
+ }
+ break;
+
+ default:
+ return false;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * 所有者を取得します
+ * @return string $this->owner
+ */
+ public function getOwner(): string{
+ return $this->owner;
+ }
+
+ /**
+ * 所有者を変更します
+ * @param string $owner
+ * @return bool true
+ */
+ public function setOwner(string $owner): bool{
+ $this->owner = strtolower($owner);
+ $this->api->saveFt($this);
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * テキストを操作できるか確認します
+ * @param Player $player
+ * @return bool
+ */
+ public function canEditFt(Player $player): bool{
+ $name = strtolower($player->getName());
+ if ($player->isOp() || $this->owner === $name) {
+ return true;
+ }else {
+ return false;
+ }
+ }
+}
diff --git a/src/Texter/text/Text.php b/src/Texter/text/Text.php
new file mode 100644
index 0000000..f556dca
--- /dev/null
+++ b/src/Texter/text/Text.php
@@ -0,0 +1,454 @@
+sendTo***() */
+ const SEND_TYPE_ADD = 0;
+ const SEND_TYPE_REMOVE = 1;
+
+ /** @var TexterApi */
+ protected $api = null;
+ /** @var float $x */
+ public $x = 0.0;
+ /** @var float $y */
+ public $y = 0.0;
+ /** @var float $z */
+ public $z = 0.0;
+ /** @var Level $level */
+ public $level = null;
+ /** @var string $title */
+ public $title = "";
+ /** @var string $text */
+ public $text = "";
+ /** @var bool $invisible */
+ public $invisible = false;
+ /** @var int $eid */
+ public $eid = 0;
+ /** @var bool $failed */
+ public $failed = false;
+
+ /**
+ * コンストラクタ
+ * @param Level $level
+ * @param int|float $x = 0
+ * @param int|float $y = 0
+ * @param int|float $z = 0
+ * @param Vector3 $pos
+ * @param string $title = ""
+ * @param string $text = ""
+ */
+ public function __construct(Level $level, $x = 0, $y = 0, $z = 0, string $title = "", string $text = ""){
+ $this->level = $level;
+ $this->x = $x;
+ $this->y = $y;
+ $this->z = $z;
+ $this->title = $title;
+ $this->text = $text;
+ $this->eid = Entity::$entityCount++;
+ $this->api = TexterApi::getInstance();
+ if ($this->api->saveCrft($this, true)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ }else {
+ $this->failed = true;
+ }
+ }
+
+ /**
+ * X座標を取得します
+ * @return int|float $this->x
+ */
+ public function getX(){
+ return $this->x;
+ }
+
+ /**
+ * X座標を変更します
+ * @param int|float $x
+ * @return bool
+ */
+ public function setX($x): bool{
+ if (is_numeric($x)) {
+ $tmpX = $this->x;
+ $this->x = $x;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->x = $tmpX;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Y座標を取得します
+ * @return int|float $this->y
+ */
+ public function getY(){
+ return $this->y;
+ }
+
+ /**
+ * Y座標を変更します
+ * @param int|float $y
+ * @return bool
+ */
+ public function setY($y): bool{
+ if (is_numeric($y)) {
+ $tmpY = $this->y;
+ $this->y = $y;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->y = $tmpY;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Z座標を取得します
+ * @return int|float $this->z
+ */
+ public function getZ(){
+ return $this->z;
+ }
+
+ /**
+ * Z座標を変更します
+ * @param int|float $z
+ * @return bool
+ */
+ public function setZ($z): bool{
+ if (is_numeric($z)) {
+ $tmpZ = $this->z;
+ $this->z = $z;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->z = $tmpZ;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Levelを取得します
+ * @return Level $this->level
+ */
+ public function getLevel(): Level{
+ return $this->level;
+ }
+
+ /**
+ * Levelを変更します
+ * @param Level $level
+ * @return bool
+ */
+ public function setLevel(Level $level): bool{
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ $tmpLev = $this->level;
+ $this->level = $level;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->level = $tmpLev;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return false;
+ }
+ }
+
+ /**
+ * Levelを変更します
+ * @param string $levelName
+ * @return bool
+ */
+ public function setLevelByName(string $levelName): bool{
+ $level = Server::getInstance()->getLevelByName($levelName);
+ if ($level !== null) {
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ $tmpLev = $this->level;
+ $this->level = $level;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->level = $tmpLev;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 座標をVector3オブジェクトとして取得します
+ * @return Vector3 $this->pos
+ */
+ public function getAsVector3(){
+ return new Vector3($this->x, $this->y, $this->z);
+ }
+
+ /**
+ * 座標をPositionオブジェクトとして取得します
+ * @return Position
+ */
+ public function getAsPosition(){
+ return new Position($this->x, $this->y, $this->z, $this->level);
+ }
+
+ /**
+ * 座標を変更します
+ * @param Vector3 $pos
+ * @return bool true
+ */
+ public function setCoord(Vector3 $pos): bool{
+ $tmpX = $this->x;
+ $tmpY = $this->y;
+ $tmpZ = $this->z;
+ $this->x = $pos->x;
+ $this->y = $pos->y;
+ $this->z = $pos->z;
+ if ($this->api->saveCrft($this)) {
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }else {
+ $this->x = $tmpX;
+ $this->y = $tmpY;
+ $this->z = $tmpZ;
+ return false;
+ }
+ }
+
+ /**
+ * タイトルを取得します
+ * @return string $this->title
+ */
+ public function getTitle(): string{
+ return $this->title;
+ }
+
+ /**
+ * タイトルを変更します, # で改行です.
+ * @param string $title
+ * @return bool true
+ */
+ public function setTitle(string $title): bool{
+ $this->title = str_replace("#", "\n", $title);
+ $this->api->saveCrft($this);
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * テキストを取得します
+ * @return string $this->text
+ */
+ public function getText(): string{
+ return $this->text;
+ }
+
+ /**
+ * テキストを変更します, # で改行です
+ * @param string $text
+ * @return bool true
+ */
+ public function setText(string $text): bool{
+ $this->text = str_replace("#", "\n", $text);
+ $this->api->saveCrft($this);
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * 不可視かどうか取得します
+ * @return bool
+ */
+ public function isInvisible(): bool{
+ return $this->invisible;
+ }
+
+ /**
+ * 不可視かどうか変更します
+ * @param bool $bool
+ * @return bool true
+ */
+ public function setInvisible(bool $bool): bool{
+ $this->invisible = $bool;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * エンティティIDを取得します
+ * @return int $this->eid
+ */
+ public function getEntityId(): int{
+ return $this->eid;
+ }
+
+ /**
+ * エンティティIDを変更します
+ * @param int $eid
+ * @return bool true
+ */
+ public function setEntityId(int $eid): bool{
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ $this->eid = $eid;
+ $this->sendToLevel(self::SEND_TYPE_ADD);
+ return true;
+ }
+
+ /**
+ * AddPlayerPacketとして取得します
+ * @return AddPlayerPacket $pk
+ */
+ public function getAsAddPacket(): AddPlayerPacket{
+ $pk = new AddPlayerPacket();
+ $pk->uuid = UUID::fromRandom();
+ $pk->username = "text";
+ $pk->eid = $this->eid;// for GenisysPro
+ $pk->entityUniqueId = $this->eid;
+ $pk->entityRuntimeId = $this->eid;// ...huh?
+ $x = (float)sprintf('%0.1f', $this->x);
+ $y = (float)sprintf('%0.1f', $this->y);
+ $z = (float)sprintf('%0.1f', $this->z);
+ $pk->x = $x;
+ $pk->y = $y;
+ $pk->z = $z;
+ $pk->position = new Vector3($x, $y, $z);// for 1.2~
+ $pk->motion = new Vector3();
+ $pk->item = Item::get(Item::AIR);
+ $flags = 0;
+ $flags |= 1 << Entity::DATA_FLAG_CAN_SHOW_NAMETAG;
+ $flags |= 1 << Entity::DATA_FLAG_ALWAYS_SHOW_NAMETAG;
+ $flags |= 1 << Entity::DATA_FLAG_IMMOBILE;
+ if ($this->invisible) {
+ $flags |= 1 << Entity::DATA_FLAG_INVISIBLE;
+ }
+ $pk->metadata = [
+ Entity::DATA_FLAGS => [Entity::DATA_TYPE_LONG, $flags],
+ Entity::DATA_NAMETAG => [Entity::DATA_TYPE_STRING, $this->title . TF::RESET . TF::WHITE . ($this->text !== "" ? "\n" . $this->text : "")],
+ Entity::DATA_SCALE => [Entity::DATA_TYPE_FLOAT, 0]
+ ];
+ return $pk;
+ }
+
+ /**
+ * RemoveEntityPacketとして取得します
+ * @return RemoveEntityPacket $pk
+ */
+ public function getAsRemovePacket(): RemoveEntityPacket{
+ $pk = new RemoveEntityPacket();
+ $pk->eid = $this->eid;
+ $pk->entityUniqueId = $this->eid;
+ return $pk;
+ }
+
+ /**
+ * プレイヤーに送信します
+ * @param Player $player
+ * @param int $type
+ * @return bool
+ */
+ public function sendToPlayer(Player $player, int $type): bool{
+ switch ($type) {
+ case self::SEND_TYPE_ADD:
+ $pk = $this->getAsAddPacket();
+ $player->dataPacket($pk);
+ break;
+
+ case self::SEND_TYPE_REMOVE:
+ $pk = $this->getAsRemovePacket();
+ $player->dataPacket($pk);
+ break;
+
+ default:
+ return false;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * ワールドに送信します
+ * @param int $type
+ * @return bool true
+ */
+ public function sendToLevel(int $type): bool{
+ switch ($type) {
+ case self::SEND_TYPE_ADD:
+ $pk = $this->getAsAddPacket();
+ $players = $this->level->getPlayers();
+ foreach ($players as $player) {
+ $player->dataPacket($pk);
+ }
+ break;
+
+ case self::SEND_TYPE_REMOVE:
+ $this->api->removeText($this);
+ $pk = $this->getAsRemovePacket();
+ $players = $this->level->getPlayers();
+ foreach ($players as $player) {
+ $player->dataPacket($pk);
+ }
+ break;
+
+ default:
+ return false;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * @link $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ */
+ public function remove(){
+ $this->sendToLevel(self::SEND_TYPE_REMOVE);
+ }
+
+ /**
+ * 処理に失敗したかを取得します
+ * @return bool
+ */
+ public function getFailed(): bool{
+ return $this->failed;
+ }
+
+ /**
+ * 処理に失敗したかどうかを変更します
+ * @param bool $value
+ * @return bool true
+ */
+ public function setFailed(bool $value): bool{
+ $this->failed = $value;
+ return true;
+ }
+}
diff --git a/src/Texter/utils/TunedConfig.php b/src/Texter/utils/TunedConfig.php
new file mode 100644
index 0000000..712fa68
--- /dev/null
+++ b/src/Texter/utils/TunedConfig.php
@@ -0,0 +1,462 @@
+ self::PROPERTIES,
+ "cnf" => self::CNF,
+ "conf" => self::CNF,
+ "config" => self::CNF,
+ "json" => self::JSON,
+ "js" => self::JSON,
+ "yml" => self::YAML,
+ "yaml" => self::YAML,
+ //"export" => self::EXPORT,
+ //"xport" => self::EXPORT,
+ "sl" => self::SERIALIZED,
+ "serialize" => self::SERIALIZED,
+ "txt" => self::ENUM,
+ "list" => self::ENUM,
+ "enum" => self::ENUM,
+ ];
+
+ /**
+ * @param string $file Path of the file to be loaded
+ * @param int $type Config type to load, -1 by default (detect)
+ * @param array $default Array with the default values that will be written to the file if it did not exist
+ * @param null &$correct Sets correct to true if everything has been loaded correctly
+ */
+ public function __construct($file, $type = self::DETECT, $default = [], &$correct = null){
+ $this->load($file, $type, $default);
+ $correct = $this->correct;
+ }
+
+ /**
+ * Removes all the changes in memory and loads the file again
+ */
+ public function reload(){
+ $this->config = [];
+ $this->nestedCache = [];
+ $this->correct = false;
+ $this->load($this->file, $this->type);
+ }
+
+ /**
+ * @param $str
+ *
+ * @return mixed
+ */
+ public static function fixYAMLIndexes($str){
+ return preg_replace("#^([ ]*)([a-zA-Z_]{1}[ ]*)\\:$#m", "$1\"$2\":", $str);
+ }
+
+ /**
+ * @param $file
+ * @param int $type
+ * @param array $default
+ *
+ * @return bool
+ */
+ public function load($file, $type = self::DETECT, $default = []){
+ $this->correct = true;
+ $this->type = (int) $type;
+ $this->file = $file;
+ if(!is_array($default)){
+ $default = [];
+ }
+ if(!file_exists($file)){
+ $this->config = $default;
+ $this->save();
+ }else{
+ if($this->type === self::DETECT){
+ $extension = explode(".", basename($this->file));
+ $extension = strtolower(trim(array_pop($extension)));
+ if(isset(self::$formats[$extension])){
+ $this->type = self::$formats[$extension];
+ }else{
+ $this->correct = false;
+ }
+ }
+ if($this->correct === true){
+ $content = file_get_contents($this->file);
+ switch($this->type){
+ case self::PROPERTIES:
+ case self::CNF:
+ $this->parseProperties($content);
+ break;
+ case self::JSON:
+ $this->config = json_decode($content, true);
+ break;
+ case self::YAML:
+ $content = self::fixYAMLIndexes($content);
+ $this->config = yaml_parse($content);
+ break;
+ case self::SERIALIZED:
+ $this->config = unserialize($content);
+ break;
+ case self::ENUM:
+ $this->parseList($content);
+ break;
+ default:
+ $this->correct = false;
+
+ return false;
+ }
+ if(!is_array($this->config)){
+ $this->config = $default;
+ }
+ if($this->fillDefaults($default, $this->config) > 0){
+ $this->save();
+ }
+ }else{
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function check(){
+ return $this->correct === true;
+ }
+
+ /**
+ * @param bool $async
+ *
+ * @return boolean
+ */
+ public function save($async = false){
+ if($this->correct === true){
+ try{
+ $content = null;
+ switch($this->type){
+ case self::PROPERTIES:
+ case self::CNF:
+ $content = $this->writeProperties();
+ break;
+ case self::JSON:
+ $content = json_encode($this->config, JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+ break;
+ case self::YAML:
+ $content = yaml_emit($this->config, YAML_UTF8_ENCODING);
+ break;
+ case self::SERIALIZED:
+ $content = serialize($this->config);
+ break;
+ case self::ENUM:
+ $content = implode("\r\n", array_keys($this->config));
+ break;
+ }
+
+ if($async){
+ Server::getInstance()->getScheduler()->scheduleAsyncTask(new FileWriteTask($this->file, $content));
+ }else{
+ file_put_contents($this->file, $content);
+ }
+ }catch(\Throwable $e){
+ $logger = Server::getInstance()->getLogger();
+ $logger->critical("Could not save Config " . $this->file . ": " . $e->getMessage());
+ if(\pocketmine\DEBUG > 1 and $logger instanceof MainLogger){
+ $logger->logException($e);
+ }
+ }
+
+ return true;
+ }else{
+ return false;
+ }
+ }
+
+ /**
+ * @param $k
+ *
+ * @return boolean|mixed
+ */
+ public function __get($k){
+ return $this->get($k);
+ }
+
+ /**
+ * @param $k
+ * @param $v
+ */
+ public function __set($k, $v){
+ $this->set($k, $v);
+ }
+
+ /**
+ * @param $k
+ *
+ * @return boolean
+ */
+ public function __isset($k){
+ return $this->exists($k);
+ }
+
+ /**
+ * @param $k
+ */
+ public function __unset($k){
+ $this->remove($k);
+ }
+
+ /**
+ * @param $key
+ * @param $value
+ */
+ public function setNested($key, $value){
+ $vars = explode(".", $key);
+ $base = array_shift($vars);
+
+ if(!isset($this->config[$base])){
+ $this->config[$base] = [];
+ }
+
+ $base =& $this->config[$base];
+
+ while(count($vars) > 0){
+ $baseKey = array_shift($vars);
+ if(!isset($base[$baseKey])){
+ $base[$baseKey] = [];
+ }
+ $base =& $base[$baseKey];
+ }
+
+ $base = $value;
+ $this->nestedCache[$key] = $value;
+ }
+
+ /**
+ * @param $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getNested($key, $default = null){
+ if(isset($this->nestedCache[$key])){
+ return $this->nestedCache[$key];
+ }
+
+ $vars = explode(".", $key);
+ $base = array_shift($vars);
+ if(isset($this->config[$base])){
+ $base = $this->config[$base];
+ }else{
+ return $default;
+ }
+
+ while(count($vars) > 0){
+ $baseKey = array_shift($vars);
+ if(is_array($base) and isset($base[$baseKey])){
+ $base = $base[$baseKey];
+ }else{
+ return $default;
+ }
+ }
+
+ return $this->nestedCache[$key] = $base;
+ }
+
+ /**
+ * @param $k
+ * @param mixed $default
+ *
+ * @return boolean|mixed
+ */
+ public function get($k, $default = false){
+ return ($this->correct and isset($this->config[$k])) ? $this->config[$k] : $default;
+ }
+
+ /**
+ * @param string $k key to be set
+ * @param mixed $v value to set key
+ */
+ public function set($k, $v = true){
+ $this->config[$k] = $v;
+ foreach($this->nestedCache as $nestedKey => $nvalue){
+ if(substr($nestedKey, 0, strlen($k) + 1) === ($k . ".")){
+ unset($this->nestedCache[$nestedKey]);
+ }
+ }
+ }
+
+ /**
+ * @param array $v
+ */
+ public function setAll($v){
+ $this->config = $v;
+ }
+
+ /**
+ * @param $k
+ * @param bool $lowercase If set, searches Config in single-case / lowercase.
+ *
+ * @return boolean
+ */
+ public function exists($k, $lowercase = false){
+ if($lowercase === true){
+ $k = strtolower($k); //Convert requested key to lower
+ $array = array_change_key_case($this->config, CASE_LOWER); //Change all keys in array to lower
+ return isset($array[$k]); //Find $k in modified array
+ }else{
+ return isset($this->config[$k]);
+ }
+ }
+
+ /**
+ * @param $k
+ */
+ public function remove($k){
+ unset($this->config[$k]);
+ }
+
+ /**
+ * @param bool $keys
+ *
+ * @return array
+ */
+ public function getAll($keys = false){
+ return ($keys === true ? array_keys($this->config) : $this->config);
+ }
+
+ /**
+ * @param array $defaults
+ */
+ public function setDefaults(array $defaults){
+ $this->fillDefaults($defaults, $this->config);
+ }
+
+ /**
+ * @param $default
+ * @param $data
+ *
+ * @return integer
+ */
+ private function fillDefaults($default, &$data){
+ $changed = 0;
+ foreach($default as $k => $v){
+ if(is_array($v)){
+ if(!isset($data[$k]) or !is_array($data[$k])){
+ $data[$k] = [];
+ }
+ $changed += $this->fillDefaults($v, $data[$k]);
+ }elseif(!isset($data[$k])){
+ $data[$k] = $v;
+ ++$changed;
+ }
+ }
+
+ return $changed;
+ }
+
+ /**
+ * @param $content
+ */
+ private function parseList($content){
+ foreach(explode("\n", trim(str_replace("\r\n", "\n", $content))) as $v){
+ $v = trim($v);
+ if($v == ""){
+ continue;
+ }
+ $this->config[$v] = true;
+ }
+ }
+
+ /**
+ * @return string
+ */
+ private function writeProperties(){
+ $content = "#Properties Config file\r\n#" . date("D M j H:i:s T Y") . "\r\n";
+ foreach($this->config as $k => $v){
+ if(is_bool($v) === true){
+ $v = $v === true ? "on" : "off";
+ }elseif(is_array($v)){
+ $v = implode(";", $v);
+ }
+ $content .= $k . "=" . $v . "\r\n";
+ }
+
+ return $content;
+ }
+
+ /**
+ * @param $content
+ */
+ private function parseProperties($content){
+ if(preg_match_all('/([a-zA-Z0-9\-_\.]*)=([^\r\n]*)/u', $content, $matches) > 0){ //false or 0 matches
+ foreach($matches[1] as $i => $k){
+ $v = trim($matches[2][$i]);
+ switch(strtolower($v)){
+ case "on":
+ case "true":
+ case "yes":
+ $v = true;
+ break;
+ case "off":
+ case "false":
+ case "no":
+ $v = false;
+ break;
+ }
+ if(isset($this->config[$k])){
+ MainLogger::getLogger()->debug("[Config] Repeated property " . $k . " on file " . $this->file);
+ }
+ $this->config[$k] = $v;
+ }
+ }
+ }
+
+}