はじめに
Minecraft 1.4.7からアップデートしないままに11年が過ぎた. 最初は1.5.xのテクスチャ周りの変更に追従しきれないmodの対応待ちであったが, MojangがMinecraftをMicrosoftに明け渡したこと(私はこれを開発終了宣言だと見做している), またアップデートしたところで大して面白くもない要素を追加し続けるだけだったこと, 何より面白い要素は随時1.4.7環境に自力で取り込んでしまっていたこと等から, 5年を過ぎたあたりでもうMinecraftというソフトウェアを完全に自力でメンテナンスし続けることに決めていた.
とはいえここ3年程は(転職等も相俟って)プレイ時間も漸減しており, 今の私にとってのMinecraftとは開発フレームワークの一種である. 事実, 本業で触れることがないタイプの大規模な1開発の総指揮を取り, またその開発に携わることが今の最大の開発目的なのである.
さて, 個人で開発し続けるだけならば特に問題ないわけだが, 今の私はとある事情からマルチサーバーの利用者というステークホルダー, そして数人の協力者を得た状況にある. このため, 既にその維持管理が切実な問題として顕れている. 本記事はその一環として, Minecraft 1.4.7をJava 21で動作させるまでの道のりを軽く要約しておくものである.
留意事項
Minecraftや周辺のMod本体に深く手を入れる必要があるため, パッチの公開とコード本体の公開がほぼ同義になるような状況である. このため, 本記事に書かれる内容に関してコードを公開する予定はなく, また一般にバイナリを公開する予定もない. あくまで本記事は通過点を自分のために要約する備忘録としての役割を担うものである.
加えて, コードをそのまま共有するとEULAに抵触するような箇所も存在しており, パッチのみを共有するとしても労力的に面倒である. したがって, 開発に協力できるという申し出があったとしても, 基本的に全て拒否させていただくこととなる. 公開している範囲でPRを送ってもらう / Issueを立ててもらう分には構わない.
歴史的事情の整理
Minecraft 1.4.7はJava 5を基準とした2ソフトウェアであり, そのままではJava 9以降で利用不可能なレガシーソフトウェアだ. Minecraft Forge #534 のリリース時点でJava 6までは利用可能な扱いであった3が, 実際にはこのパラメータもJava 7までは書き換えて開発・利用していた記憶がある4.
大きな転機はJava 9のリリース時に訪れた. Java 9はProject Jigsawと呼ばれるモジュールシステムを導入したバージョンであり, クラスパスシステムは事実上非推奨となった. Minecraft ForgeやModLoaderはModファイルをクラスパスに追加するという形でModのロードシステムを実現しており, クラスパスシステムを通じない形への移行とは即ち方式全体を改訂することを迫られるものであった. 加えてLWJGLのJNI APIも非互換となり, Java 9では本体を含めてまともに動作しないコードだらけとなった.
加えて, 開発者視点ではJava 8の時点で既に問題が発生していた. MCPに利用されていた難読化・難読化解除用の変換ソフトウェア(Retroguard)はInvokedynamic命令へのサポートが不十分であり5, Java 8を用いたコードを通した際に難読化不可能な場合があった. Minecraft Forgeで利用されていたライブラリであるObjectweb ASM 4.xはそもそもJava 8のバイトコードに対応しておらず, 仮にRetroguard問題を解決できたところで, ユーザー環境にロードさせることは叶わなかった6. 開発環境そのものの構築にも支障が出るようになっており, 事実私が重い腰を上げてJava 8対応を試みた際には長々とパッチを書く羽目になった7.
Javaはその後も次々変更されていき, いつしかJava 17やJava 18といった番号にまでなった. しかしながら, Java 8のサポートはまだ続く. Sun MicrosystemsもといOracleもJava 8からJava 9の間に崖が存在することは認知しており, Java 8はExtended Supportを設定したLTSとしてサポート期間を伸ばしていたのである. そうやって伸びた期間は最終的に2030年12月まで. 一方Java 11のサポート期間は当初2026年9月8, Java 17に至っては2029年9月である.
ところが, 昨年登場したLTSであるJava 21のExtended Support期限は2031年9月である. Java 8よりも長く使えるJavaがとうとう登場したということで, いい加減に移植するかという本腰が入った. これが本記事に繋がる開発が開始された経緯である.
Java 8までの流れ
Java 21の話が登場するよりも以前, 2015年頃からJava 8を使えないかという試行錯誤は実施していた. 本節はまずその点について記す.
上にも書いた通り, まずは兎にも角にもRetroguardをなんとかせねばならない. ただ, ASMの問題を考慮すると安易に持ち込めるものでもない. そこでまずはluontola/retrolambdaを適用してJava 7基準のクラスファイルに統一することで事なきを得た. メモリ効率等は多少犠牲になるものの, この案そのものはかなり良いworkaroundであった.
ただretrolambdaはAPIの変換まではしない. よって我々はAPI的にはJava 7に縛られたままであった. デフォルトメソッドも基本的に命令書き換えで対応するため, たとえば適当なMinecraft本体のインターフェイスにdefault methodをcoremodで追加し, 透過的に全てのMinecraftクラスから当該メソッドを利用可能であるかのように思わせることも不可能である. static methodも同様. Stream APIに限ってはstefan-zobel/streamsupportを利用して解決できたが, それ以外にはこのような制限があった.
これらを解消するため, Retroguard本体の改良を思い立ち, これを実施した. ローレイヤーな世界はもともと私の本職エリアでもあり, 仕様とやりたいことが決まっていれば特に難しい要素はなかった.
ASMの問題があるためretrolambdaの利用は変わらず継続していたが, もともとreobfuscate前の(Minecraft本体を含めた全ての)クラスをretrolambdaにかけていたところ, 必要最低限のクラスのみがretrolambdaに渡るようになったため, ビルド時間の高速化には寄与した.
このような体制でしばらく動いていたところ, 2021年3月に https://files.minecraftforge.net/fmllibs/
が廃止された. 各種ライブラリのダウンロード元がこのURLであったため, 1.4.7を含めた古いバージョンのMinecraft + Minecraft Forge環境におけるライブラリダウンロード処理は最早動作しない. 手動で配置すれば動作するものの, 我々にとってはこれ幸いとばかりにこの部分に手を加える口実になった. ASMは5.2にアップデートされ, 他のライブラリはMavenからダウンロードするようになる. retrolambdaも外せた. これによってJava 8対応は大まかに完了となる.
問題点の洗い出し
実のところ, Java 14, Java 17, Java 20とそれぞれのバージョンで何度かアップグレードにトライしていた. その際にある程度問題点は判明していたので, 以下に列挙しておく.
- 先述のRetroguard問題
- Java 8には対応したものの, それ以降には未対応
- 先述のASM問題
- クラスパス問題を解決するためにはクラスローダ周りに手を入れなければならないが, RelaunchClassLoader周りに手を入れるにはどうしてもjar書き換えが必要になってしまう
- この辺りに手を入れようとして頑張っていた時期の副産物が r3qu13m/PythonModLoader である
- LWJGL 2からLWJGL 3への対応に苦戦する
- 2.6.6 LWJGL3 migration にあるような形でmigrateすればよい
- それに関しては何も難しくはないのだが, いかんせん手を入れなければいけないコードが多すぎる
- GLFWへの移行が最大の問題
- 仮にMinecraft本体をGLFW対応させられたところで, 100個近くある常用Mod全ての検査をしなければならない
- かといってLWJGL2を使い続けようとすればJNIのバージョン問題を踏む
- EnumHelperを始めとした黒魔術の存在
- Javaはリフレクションによって様々なコードのオブジェクト指向性が剥奪可能であることは周知の事実
- private finalなメソッドを外から勝手に書き換えるようなことも可能であるが, その中でもEnumに関しては特別な立場にあり, 通常の手続きではfinal外しに失敗する
- でも
sun.reflect.ConstructorAccessor
やsun.reflect.ReflectionFactory
といったInternal APIを利用すれば書き換えられた. これを用いて動的に列挙子を追加していたのがnet.minecraftforge.common.EnumHelper
- ところが, Java 9以降, 特にJava 17以降においてはInternal APIの隠蔽度が上がり, 従来Enumに対して適用できたfinal外しの手法はほぼ塞がれてしまっていた
- しかしこれを解消するためにはかなりの量の書き換えを必要としてしまう
- mcp自体の古さ
対応作業
以上の問題を時系列でどう解消していったかをメモしておく.
MCP / Retroguard
まずはMCPをGradleベースに書き直すこととした.
最新版のMinecraftではFabricかForgeGradleのほぼ二択になるようで, Forge-basedに考えたいのでFabricは論外. ForgeGradleもレガシー版を開発している人間を発見できた(RetroGradle/ForgeGradle).
ただし, MinecraftForgeに対して私はもう信用を置いていない. 便利なAPIであり, 大変世話になっているのは事実そうなのだが, そもそも彼ら彼女らの見ているものは最新版であって, 1.4.7という化石のようなバージョンのサポートは次々に切っていくフェーズにある. また, LexManosとcpwで揉めてNeoForgedに分裂した事件は今でも記憶に新しい9し, https://files.minecraftforge.net/fmllibs/
の件でも依存先にするにはちと困るなあという印象を持っていた. 頼らないので良いのであれば頼らずに済ませたいというのが本音である. なお, 難読化マップに関してはSu5eD/MCPConfigに存在する10ものの, よくよく掘っていくとこれもSu5eD氏のMavenサーバーを利用せねばならない形になっていたために除外. そもそもとして, Forge本体はおろかMinecraft本体にかなり手を加えなければならない状況においてForgeGradleは使いづらい.
他にも検討はしたが, 大まかには上の通りの理由からForgeGradleを検討対象から外した. すると候補はほぼ存在しなくなり, SDK全体の再開発が必須となった. ついでなのでRetroguardをJava 20まで対応させる作業も実施し, これを踏まえてGradleによるプロジェクト管理に全面移行. GitHub ActionsによるCIも回しやすくなり, 管理も楽になった.
新MCPは難読化マップのライセンスの問題から公開していないが, RetroguardはGPLv2ライセンスであったためMeiServer/Retroguardにて公開している. 公式にもプルリクエストを送ったがrejectされた. その理由は「もうこれはメンテナンスしていないし, ForgeAutoRenamingToolなんかの代替があるからそっちを使ってくれ」ということであった11. 他にもバグを発見次第修正している.
LWJGL
コンパイルターゲットをJava 8に変更し, javahコマンドが消滅したところの修正を入れたところ, Java 21でも使えるLWJGL2バイナリをうまく作成できた. そのため特にそれ以外はいじっていない.
なお, 私がLinux環境しか使わないせいでWindows環境等での利用可能性は不明である. AArch64環境下での実行を念頭に置くといずれどこかでgdbと向かい合う日々となりそうな予感がある. ただし, この点に関しては r58Playz/lwjgl2-m1 に完成品が存在するため, これを / これにマージするという方向性でも考えたほうがよいだろう.
ASM / Guava
ASMは9.5まではアップデートできている. 9.7へのアップデートは特に変更なしに済むものと思われるが, 未検証. Guavaも33までアップデートすることを考えているが, 現状MCPC+のみに適用できている状況である.
クラスパス問題
cpw/modlauncherでは動くという話だったのでそれを見に行く. クラスパス探索を自前でやってしまえば特に問題ないという話のようだったので, 適当にjava.class.path
よりクラスパス情報を引っ張ってくるだけのコードを書けば事足りた.
diff --git a/src/cpw/mods/fml/relauncher/FMLRelauncher.java b/src/cpw/mods/fml/relauncher/FMLRelauncher.java index eb8ea45..d587b2f 100644 --- a/src/cpw/mods/fml/relauncher/FMLRelauncher.java +++ b/src/cpw/mods/fml/relauncher/FMLRelauncher.java @@ -3,7 +3,12 @@ import java.applet.Applet; import java.io.File; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.swing.JDialog; import javax.swing.JOptionPane; @@ -39,9 +44,16 @@ static FMLRelauncher instance() { } private FMLRelauncher() { - URLClassLoader ucl = (URLClassLoader) getClass().getClassLoader(); + String classpath = System.getProperty("java.class.path"); + List<URL> classPathValues = Arrays.stream(classpath.split(File.pathSeparator)).map(x -> { + try { + return new File(x).toURI().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList()); - classLoader = new RelaunchClassLoader(ucl.getURLs()); + classLoader = new RelaunchClassLoader(classPathValues.toArray(new URL[0])); }
EnumHelper
結論から言うと, sun.misc.Unsafe
を用いることで解決可能である. getDeclaredFields0
辺りの実行に --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
を必要とするため, 本機構のためにランチャーはオプションについて配慮せねばならず, またJava8以前 / Java9以降を同一コードで実現するのは難しい可能性がある. この点に関しては今後の検討課題である.
diff --git a/src/net/minecraftforge/common/EnumHelper.java b/src/net/minecraftforge/common/EnumHelper.java index 8efe3aa..608c51c 100644 --- a/src/net/minecraftforge/common/EnumHelper.java +++ b/src/net/minecraftforge/common/EnumHelper.java @@ -3,11 +3,16 @@ */ package net.minecraftforge.common; +import sun.misc.Unsafe; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; @@ -137,6 +142,8 @@ private static void setup() { return; } try { + /* + FIXME: these methods are not able to use on Java 20 final Method getReflectionFactory = Class.forName("sun.reflect.ReflectionFactory") .getDeclaredMethod("getReflectionFactory", new Class[0]); EnumHelper.reflectionFactory = getReflectionFactory.invoke(null, new Object[0]); @@ -148,40 +155,62 @@ private static void setup() { .getDeclaredMethod("newFieldAccessor", Field.class, Boolean.TYPE); EnumHelper.fieldAccessorSet = Class.forName("sun.reflect.FieldAccessor").getDeclaredMethod("set", Object.class, Object.class); + */ } catch (final Exception e) { e.printStackTrace(); } EnumHelper.isSetup = true; } - private static Object getConstructorAccessor(final Class<?> enumClass, final Class<?>[] additionalParameterTypes) - throws Exception { + private static MethodHandle getConstructorAccessor(final Class<?> enumClass, + final Class<?>[] additionalParameterTypes) throws Exception { final Class[] parameterTypes = new Class[additionalParameterTypes.length + 2]; parameterTypes[0] = String.class; parameterTypes[1] = Integer.TYPE; System.arraycopy(additionalParameterTypes, 0, parameterTypes, 2, additionalParameterTypes.length); - return EnumHelper.newConstructorAccessor.invoke(EnumHelper.reflectionFactory, - enumClass.getDeclaredConstructor(parameterTypes)); + Constructor<?> c = enumClass.getDeclaredConstructor(parameterTypes); + c.setAccessible(true); + return MethodHandles.lookup().unreflectConstructor(c); } private static <T extends Enum<?>> T makeEnum(final Class<T> enumClass, final String value, final int ordinal, - final Class<?>[] additionalTypes, final Object[] additionalValues) throws Exception { + final Class<?>[] additionalTypes, final Object[] additionalValues) throws Throwable { final Object[] parms = new Object[additionalValues.length + 2]; parms[0] = value; parms[1] = ordinal; System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length); - return (T) (Enum) enumClass.cast(EnumHelper.newInstance - .invoke(EnumHelper.getConstructorAccessor(enumClass, additionalTypes), new Object[] { parms })); + return enumClass.cast(getConstructorAccessor(enumClass, additionalTypes).invokeWithArguments(parms)); } public static void setFailsafeFieldValue(final Field field, final Object target, final Object value) throws Exception { field.setAccessible(true); - final Field modifiersField = Field.class.getDeclaredField("modifiers"); + // https://stackoverflow.com/questions/74723932/java-17-reflection-issue/74727966#74727966 + Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + getDeclaredFields0.setAccessible(true); + Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false); + Field modifiersField = null; + for (Field each : fields) { + if ("modifiers".equals(each.getName())) { + modifiersField = each; + break; + } + } modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & 0xFFFFFFEF); - final Object fieldAccessor = EnumHelper.newFieldAccessor.invoke(EnumHelper.reflectionFactory, field, false); - EnumHelper.fieldAccessorSet.invoke(fieldAccessor, target, value); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + final Unsafe unsafe = (Unsafe) unsafeField.get(null); + + if (target == null) { + final Object staticFieldBase = unsafe.staticFieldBase(field); + final long staticFieldOffset = unsafe.staticFieldOffset(field); + unsafe.putObject(staticFieldBase, staticFieldOffset, value); + } else { + final long offset = unsafe.objectFieldOffset(field); + unsafe.putObject(target, offset, value); + } } private static void blankField(final Class<?> enumClass, final String fieldName) throws Exception { @@ -225,7 +254,7 @@ public static <T extends Enum<?>> T addEnum(final Class<T> enumType, final Strin if (!EnumHelper.isSetup) { EnumHelper.setup(); } - AccessibleObject valuesField = null; + Field valuesField = null; final Field[] fields = enumType.getDeclaredFields(); final int flags = 4122; final String valueType = String.format("[L%s;", enumType.getName().replace('.', '/')); @@ -243,11 +272,11 @@ public static <T extends Enum<?>> T addEnum(final Class<T> enumType, final Strin final ArrayList<Enum> values = new ArrayList<>(Arrays.asList(previousValues)); final T newValue = EnumHelper.makeEnum(enumType, enumName, values.size(), paramTypes, paramValues); values.add(newValue); - EnumHelper.setFailsafeFieldValue((Field) valuesField, null, - values.toArray((Enum[]) Array.newInstance(enumType, 0))); - EnumHelper.cleanEnumCache(enumType); + + setFailsafeFieldValue((Field) valuesField, null, values.toArray((T[]) Array.newInstance(enumType, 0))); + cleanEnumCache(enumType); return newValue; - } catch (final Exception e) { + } catch (final Throwable e) { e.printStackTrace(); throw new RuntimeException(e.getMessage(), e); }
その他
細々とした仕様変更に纏わるバグをいくつも修正する必要があった. 例えばRedPower2は(Integer[]) Arrays.asList(1, 2, 3).toArray()
に相当する処理を入れていたためクラッシュする12し, rptweaksはASMのバージョンアップに伴いvisitMethodInsnの引数追加に追従する必要があった. いずれもJava 8 compatibleに修正可能である.
結果
2024/07/31時点で, MCPC+サーバー・プラグインを含めたほぼフルセットの環境が動作している. ただし未だステージング環境であり, クラッシュバグや, 落とされた後ワールドに再度入れなくなるタイプのバグも複数報告されている. coremodの変更は幸い少なく済み, OptifineやShadersModといったJAR書き換え系Modをcoremod化すれば残るはバグ修正のみ. 既存環境用のビルドはJava 8環境で動作しているCIがチェックしてくれるので, ユーザー側に影響することはない. しばらくは同時並行でJava 8 compatibleなコードを中心にマージしていく作業が続きそうだ.
コミュニティ的な貢献はRetroguardぐらいしか存在しない状況であるが, いずれ公開できるものは公開していきたいと考えている. ただ, 私のスタンスとして「Minecraft 1.4.7からアップデートしないために書く」という立場からの公開になるため, 世間とのギャップは多かれ少なかれ存在するであろう. これに伴う認識の相違から始まる謎のサポートが発生することを考えると少し微妙な気分になるところである. もうそれは本業でやっているし, 趣味なんだから使う人間を突き放すぐらいしてもいいのではないか?
終わりに
雑な紹介にはなったが, これが最新の1.4.7である. これを地味と捉えるか変なやつだと捉えるか, はたまたバカだと捉えるか, その辺りは読者諸賢にお任せする. 私にとってマイクラとは面白い開発プラットフォームであり, そのサンドボックス性はゲームから飛び出しコードになっても変わっていないなと思っている次第である.
ここには書いていないが, プロジェクト遂行に際して必要な補助ツールとしてVite + Vue3 + Vercelなツールを書いていたり, self-hosted runnerの環境構築用にDockerを利用していたり, デプロイパイプラインのためにいくらかサービス開発をしたりと実に様々な技術が投入されてきている. そしてそれらは全て開発の土台となるべくして投入した. 実際にユーザーの触る部分は従来通りのModding風景と変わっておらず, 体験はそのまま変わらないようにできている. 地味だがこのことが何より喜ばしい.
ゲーム開発と言うとやはりユーザーの手に触れる箇所ばかりが取り沙汰されがちだが, 10年後を見据えて開発していくためには, SDKの更新・最新プレイ環境への追従といったソフトウェア的なインフラ整備, GitHub Projectsを用いたカンバンの導入, 或いはそこまで行かずとも「とりあえずissueを登録してもらう(登録できるようにしておく)」というような 文化的なインフラ の整備, そして何より技術的なチャレンジ要素によって飽きずに居られること等, 土台作りにも時間を十分に割く必要があると考える.
Java 21対応は実のところメインラインではなく, 私個人の独断でやっているプロジェクトである. しかしブランチ切り替え一個で済ませられる程度には環境切り替えも容易であり, 現状EnumHelperを除いてはどちらの環境でも上手に動くようなコードを書くことにも成功している. このまま実運用まで持っていきたいところだ.
- 2024/07/31現在私の管理下に存在するコードはおおよそ100万行と少し存在する(拡張子にjavaをもつ8349ファイルそれぞれの行数の総和). 開発者が数人しかいないコミュニティとしてはまあまあな量だと経験上感じる量.↩
- 当時のMinecraft.jarのクラスをjavapコマンドで眺めると49.0というバージョンを確認できる. これはJava 5でコンパイルされたクラスファイルであることを意味する.↩
- mcp 7.26aのデフォルト設定は1.6である↩
- 私が初めてMinecraftに触れた際にはJava 7が最新版であり, 寧ろLWJGLの更新と共に最新版の利用が推奨されていた.↩
-
今でこそラムダ式やString concatenationのイメージが強いInvokedynamicであるが, もともとはJRubyのようなJVM系統の言語におけるJIT性能の向上が目的であったため, サポートがおざなりであったのだろうと推察される. 以下に示すスライドにより詳しい事情が記載されているため, 読んでおくことを推奨する.
↩ - アップデートすればいいじゃん, という向きもあろう. Minecraft Forgeが当時採用していたライブラリダウンローダはcoremodですら書き換え不可能な初期段階に利用されており, いわゆるjar書き換え必須な状態だったのである. coremod側の対応状況もASM4に偏っている状況を踏まえると, アップデートは事実上不可能だと結論する他なかったのである.↩
- Java8においてForgeの完全動作のために書いたパッチ類 - https://r3qu13m.hatenablog.com/entry/2018/01/08/061936 なおこの時点でASMを5.2にアップデートしようと試行しているが, Retroguard問題を解消できていなかったので, このパッチだけでは動作しない.↩
- 2023年に顧客要望で更新され, 2032年1月となった.↩
- What is happening? - https://neoforged.net/news/theproject/↩
- これはMCP本体の規約(再配布の禁止等)に抵触するのではないかとSu5eD氏に直接問い合わせたところ, Discordでcpw / LexManosに許可を取ったという話であった. LexManosもcpwもMCP Teamsの人間として当該マップファイルの開発に参加していたわけではなかったと記憶していたが...?↩
- https://github.com/ModCoderPack/Retroguard/pull/3#issuecomment-1730469067 だったらArchiveしてくれ...↩
-
(coll) Arrays.asList(x).toArray().getClass() should be Object[].class - https://bugs.openjdk.org/browse/JDK-6260652 により, Java9以降の
Arrays.asList
はObjectの配列を返すようになった. Java8以前では単にcloneを返していたことから先述のコードはvalidであったわけだが, この変更によって純粋なObject[]
からのキャスト試行となり, キャスト失敗からの例外発生と続く.toArray(new Integer[0])
にすれば解決する.↩