Подключаем MiniMessage на Spigot 1.8.8

MrDrag0nXYT

Вопросы по форуму решаю **ТОЛЬКО** на форуме
Модератор
Пользователь
Сообщения
2 783
Решения
87
Веб-сайт
drakoshaslv.ru
MiniMessage уже давно стал чуть ли не стандартом форматирования текста. И не удивительно: удобночитаемый формат с кучей фишек - что ещё нужно для счастья? Но поскольку очень много игроков и серверов (а в следствие ещё и разработчиков) сидят на старых версиях типа 1.16.5. И поэтому у некоторых возникают трудности с его подключением. Настало время исправить эту ситуацию и сгородить свой костылесипед!

Вам необходимо зарегистрироваться для просмотра изображений-вложений

По факту это будет почти пересказ , которую почему-то далеко не все смогли прочитать. И так, начнём с теоретического описания задачи.

Дано:

Сервер на Spigot 1.8.8
Сборщик Gradle (чтобы зашейдить библиотеку в JAR файл с помощью Gradle Shadow Plugin; для ценителей Maven имеется аналогичный Maven Shade Plugin)​



А теперь приступим:


1. Настраиваем файл сборки (build.gradle или build.gradle.kts) - подключаем Gradle Shadow Plugin если такового нет, библиотеки и настраиваем шейдинг библиотек.
ВАЖНО: проверяйте последние версии библиотек самостоятельно!

Подключаем сам плагин:
Код:
plugins {
    id 'java'
    id 'com.gradleup.shadow' version '8.3.9'
}

И библиотеки:
Код:
repositories {
    mavenCentral()
    maven {
        name = "spigotmc-repo"
        url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/"
    }
    maven {
        name = "sonatype"
        url = "https://oss.sonatype.org/content/groups/public/"
    }
}

dependencies {
    compileOnly "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"

    implementation "net.kyori:adventure-text-minimessage:4.24.0"
    implementation "net.kyori:adventure-platform-bukkit:4.4.1"
}

Настраиваем Gradle Shade Plugin:
Код:
shadowJar {
    archiveClassifier.set('')

    mergeServiceFiles()
    configurations = [project.configurations.runtimeClasspath]
}

tasks.build {
    dependsOn shadowJar
}

2. Добавляем в Main-класс объект BukkitAudiences

Java:
public final class MiniMessageTest extends JavaPlugin {

    // сам объект
    private BukkitAudiences adventure;

    // метод-геттер
    public @NonNull BukkitAudiences getAdventure() {
        if (this.adventure == null) {
            throw new IllegalStateException("Tried to access Adventure when the plugin was disabled!");
        }
        return this.adventure;
    }

    @Override
    public void onEnable() {
        // инициализация объекта
        this.adventure = BukkitAudiences.create(this);
     
        // ...
    }

    @Override
    public void onDisable() {
        if (this.adventure != null) {
            this.adventure.close();
            this.adventure = null;
        }
     
        // ...
    }
}

3. Теперь создадим класс, например, для слушателя события входа и попробуем отправить игроку сообщение в MiniMessage

Java:
public class JoinListener implements Listener {
    private final MiniMessageTest plugin;

    // синглтоны зло кстати
    public JoinListener(MiniMessageTest plugin) {
        this.plugin = plugin;
    }

    // а ещё по хорошему надо игнорировать отменённые события
    @EventHandler(ignoreCancelled = true)
    void onJoin(PlayerJoinEvent event) {
        BukkitAudiences bukkitAudiences = plugin.getAdventure();
        Player player = event.getPlayer();

        // этот кусок кода вообще нужно вытащить в конфиг, но это как-нибудь потом
        String message = "<white>Привет, <red><player_name></red>! <rainbow>Добро пожаловать на сервер!";
        Component component = MiniMessage.miniMessage().deserialize(
            message,
            Placeholder.unparsed("player_name", player.getName()) // заменялка плейсхолдера в тексте
        );

        // передаём получателя и отправляем сообщение в component
        bukkitAudiences.sender(player).sendMessage(component);
    }
}

И подключим слушатель JoinListener в Main-классе:

Java:
@Override
public void onEnable() {
    this.adventure = BukkitAudiences.create(this);
    // регистрируем наш обработчик входа
    this.getServer().getPluginManager().registerEvents(new JoinListener(this), this);
}

4. Собираем наш плагин кнопкой сборки в IDE или командой
Bash:
./gradlew build

Готово!

Проверка


Теперь возвращаемся к нашему Spigot версии 1.8.8, закидываем плагин в папку plugins и запускаем сервер. После чего заходим на него и видим следующий результат:

Вам необходимо зарегистрироваться для просмотра изображений-вложений

Наш плагин отлично функционирует, вставляет свои плейсхолдеры и форматирует текст! Градиент на 1.8, к сожалению, не отображается во всей своей красе - поддержку шестнадцатиричных цветов добавили только в 1.16.

Мораль


Вот так с помощью нехитрых приспособлений буханку белого (или чёрного) хлеба можно превратить в троллейбус... но зачем?!

Вам необходимо зарегистрироваться для просмотра изображений-вложений

А я надеюсь что теперь никто не скажет что MiniMessage это сложно и приглянется к данному прекрасному формату
 
Последнее редактирование:
com.github.johnrengelman.shadow
Гайд хороший, но этот пакет устарел, плагин обновляется уже в другом:

TextReplacementConfig textReplacementConfig = TextReplacementConfig.builder() .match(playerNamePattern) .replacement(player.getDisplayName()) .build();
Это неправильный способ реализации плейсхолдеров с MiniMessage. Вот правильный:
Java:
           // этот кусок кода вообще нужно вытащить в конфиг, но это как-нибудь потом
        String message = "<white>Привет, <red><player_name></red>! <rainbow>Добро пожаловать на сервер!";
        Component component = MiniMessage.miniMessage().deserialize(
            message,
            Placeholder.unparsed("player_name", player.getName()) // заменялка плейсхолдера в тексте
        );

        // передаём получателя и отправляем сообщение в component
        bukkitAudiences.sender(player).sendMessage(component);
 
Это неправильный способ реализации плейсхолдеров с MiniMessage. Вот правильный:
Но тогда перед каждой отправкой придётся парсить компонент что хуже чем один раз его получить и заменять данные в нём через replaceText, разве нет?
 
Но тогда перед каждой отправкой придётся парсить компонент что хуже чем один раз его получить и заменять данные в нём через replaceText, разве нет?
Парсинг быстрый. Всё равно замена тоже требует сканирования всего текста и создания копии объектов, да и для запуска самой замены нужно создавать кучу объектов (как минимум билдер конфига и сам конфиг)
 
Парсинг быстрый. Всё равно замена тоже требует сканирования всего текста и создания копии объектов, да и для запуска самой замены нужно создавать кучу объектов (как минимум билдер конфига и сам конфиг)
Хорошо, спасибо большое за уточнение
 
БЫСТРЫЙ!?!?!??!??!??!??!?!??!??!??

Мне кажется нам стоит заняться jmh тестами
1. Я никогда не видел, чтобы парсинг был виден в профайлере, т.е. он никогда не был боттлнеком. А я написал десятки плагинов с ММ
2. Это то, как нужно пользоваться ММ, эта функция создана специально для плейсхолдеров:

Если упарываться микрооптимизациями и везде следовать результатами микробенчмарков, надо вообще парсить компоненты в готовые пакеты отправки сообщения при запуске, и реплейсом заменять там нужные плейсхолдеры перед отправкой. Вот только выигрыш от этого будет только в голове, на практике сам факт отправки пакета в любом случае гораздо весомее любого парсинга в рантайме
 
Мне кажется нам стоит заняться jmh тестами
Что-ж, я занялся. Вот код теста:

Код:
@State(Scope.Benchmark)
open class MiniMessageBenchmark {

    @Benchmark
    fun deserialize(blackhole: Blackhole, executionPlan: ExecutionPlan) {
        val component = if (executionPlan.parseCustomPlaceholder!!) {
            MiniMessage.miniMessage().deserialize(
                executionPlan.rawString!!,
                Placeholder.unparsed("placeholder", "placeholder-value")
            )
        } else {
            MiniMessage.miniMessage().deserialize(executionPlan.rawString!!)
        }

        blackhole.consume(component)
    }

    @State(Scope.Benchmark)
    open class ExecutionPlan {

        @Param(
            "Привет! <red>Это small_rawString</red> <placeholder>!",
            "<rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>"
        )
        var rawString: String? = null

        @Param("true", "false")
        var parseCustomPlaceholder: Boolean? = null
    }
}

Вот настройки JMH:
Код:
jmh {
    benchmarkMode = listOf("AverageTime")
    warmupIterations = 5
    iterations = 5
    jmhTimeout = "10s"
    fork = 1
    timeUnit = "ms"
}

А вот результаты:

Код:
Benchmark                         (parseCustomPlaceholder)                                                                                             (rawString)  Mode  Cnt  Score    Error  Units
MiniMessageBenchmark.deserialize                      true                                                   Привет! <red>Это small_rawString</red> <placeholder>!  avgt    5  0.004 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                      true  <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>  avgt    5  0.019 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                     false                                                   Привет! <red>Это small_rawString</red> <placeholder>!  avgt    5  0.004 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                     false  <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>  avgt    5  0.018 ±  0.001  ms/op

И вот более подробный вывод:
Код:
> Task :jmh
# JMH version: 1.36
# VM version: JDK 21.0.5, OpenJDK 64-Bit Server VM, 21.0.5+11-LTS
# VM invoker: C:\Users\baroness-pc\scoop\persist\gradle\.gradle\jdks\eclipse_adoptium-21-amd64-windows.2\bin\java.exe
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=C:\Users\baroness-pc\Personal\IdeaProjects\Nightfang\build\tmp\jmh -Duser.country=US -Duser.language=en -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 s per iteration, ***WARNING: The timeout might be too low!***
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize
# Parameters: (parseCustomPlaceholder = true, rawString = Привет! <red>Это small_rawString</red> <placeholder>!)

# Run progress: 0.00% complete, ETA 00:06:40
# Fork: 1 of 1
# Warmup Iteration   1: 0.005 ms/op
# Warmup Iteration   2: 0.004 ms/op
# Warmup Iteration   3: 0.004 ms/op
# Warmup Iteration   4: 0.004 ms/op
# Warmup Iteration   5: 0.004 ms/op
Iteration   1: 0.004 ms/op
Iteration   2: 0.004 ms/op
Iteration   3: 0.004 ms/op
Iteration   4: 0.004 ms/op
Iteration   5: 0.004 ms/op


Result "io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize":
  0.004 ±(99.9%) 0.001 ms/op [Average]
  (min, avg, max) = (0.004, 0.004, 0.004), stdev = 0.001
  CI (99.9%): [0.004, 0.004] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.5, OpenJDK 64-Bit Server VM, 21.0.5+11-LTS
# VM invoker: C:\Users\baroness-pc\scoop\persist\gradle\.gradle\jdks\eclipse_adoptium-21-amd64-windows.2\bin\java.exe
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=C:\Users\baroness-pc\Personal\IdeaProjects\Nightfang\build\tmp\jmh -Duser.country=US -Duser.language=en -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 s per iteration, ***WARNING: The timeout might be too low!***
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize
# Parameters: (parseCustomPlaceholder = true, rawString = <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>)

# Run progress: 25.00% complete, ETA 00:05:01
# Fork: 1 of 1
# Warmup Iteration   1: 0.020 ms/op
# Warmup Iteration   2: 0.019 ms/op
# Warmup Iteration   3: 0.019 ms/op
# Warmup Iteration   4: 0.019 ms/op
# Warmup Iteration   5: 0.019 ms/op
Iteration   1: 0.019 ms/op
Iteration   2: 0.019 ms/op
Iteration   3: 0.019 ms/op
Iteration   4: 0.019 ms/op
Iteration   5: 0.019 ms/op


Result "io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize":
  0.019 ±(99.9%) 0.001 ms/op [Average]
  (min, avg, max) = (0.019, 0.019, 0.019), stdev = 0.001
  CI (99.9%): [0.019, 0.019] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.5, OpenJDK 64-Bit Server VM, 21.0.5+11-LTS
# VM invoker: C:\Users\baroness-pc\scoop\persist\gradle\.gradle\jdks\eclipse_adoptium-21-amd64-windows.2\bin\java.exe
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=C:\Users\baroness-pc\Personal\IdeaProjects\Nightfang\build\tmp\jmh -Duser.country=US -Duser.language=en -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 s per iteration, ***WARNING: The timeout might be too low!***
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize
# Parameters: (parseCustomPlaceholder = false, rawString = Привет! <red>Это small_rawString</red> <placeholder>!)

# Run progress: 50.00% complete, ETA 00:03:20
# Fork: 1 of 1
# Warmup Iteration   1: 0.005 ms/op
# Warmup Iteration   2: 0.004 ms/op
# Warmup Iteration   3: 0.004 ms/op
# Warmup Iteration   4: 0.004 ms/op
# Warmup Iteration   5: 0.004 ms/op
Iteration   1: 0.004 ms/op
Iteration   2: 0.004 ms/op
Iteration   3: 0.004 ms/op
Iteration   4: 0.004 ms/op
Iteration   5: 0.004 ms/op


Result "io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize":
  0.004 ±(99.9%) 0.001 ms/op [Average]
  (min, avg, max) = (0.004, 0.004, 0.004), stdev = 0.001
  CI (99.9%): [0.004, 0.004] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.5, OpenJDK 64-Bit Server VM, 21.0.5+11-LTS
# VM invoker: C:\Users\baroness-pc\scoop\persist\gradle\.gradle\jdks\eclipse_adoptium-21-amd64-windows.2\bin\java.exe
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=C:\Users\baroness-pc\Personal\IdeaProjects\Nightfang\build\tmp\jmh -Duser.country=US -Duser.language=en -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 s per iteration, ***WARNING: The timeout might be too low!***
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize
# Parameters: (parseCustomPlaceholder = false, rawString = <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>)

# Run progress: 75.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration   1: 0.019 ms/op
# Warmup Iteration   2: 0.018 ms/op
# Warmup Iteration   3: 0.018 ms/op
# Warmup Iteration   4: 0.018 ms/op
# Warmup Iteration   5: 0.018 ms/op
Iteration   1: 0.018 ms/op
Iteration   2: 0.018 ms/op
Iteration   3: 0.018 ms/op
Iteration   4: 0.018 ms/op
Iteration   5: 0.018 ms/op


Result "io.github.blackbaroness.nightfang.jmh.MiniMessageBenchmark.deserialize":
  0.018 ±(99.9%) 0.001 ms/op [Average]
  (min, avg, max) = (0.018, 0.018, 0.018), stdev = 0.001
  CI (99.9%): [0.018, 0.018] (assumes normal distribution)


# Run complete. Total time: 00:06:41

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
extra caution when trusting the results, look into the generated code to check the benchmark still
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
different JVMs are already problematic, the performance difference caused by different Blackhole
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.

Benchmark                         (parseCustomPlaceholder)                                                                                             (rawString)  Mode  Cnt  Score    Error  Units
MiniMessageBenchmark.deserialize                      true                                                   Привет! <red>Это small_rawString</red> <placeholder>!  avgt    5  0.004 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                      true  <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>  avgt    5  0.019 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                     false                                                   Привет! <red>Это small_rawString</red> <placeholder>!  avgt    5  0.004 ±  0.001  ms/op
MiniMessageBenchmark.deserialize                     false  <rainbow>Привет! <red>Это large_rawString</red>! Он заметно длиннее и тут больше стилей! <placeholder>  avgt    5  0.018 ±  0.001  ms/op

Какие выводы мы можем сделать?

  1. Кастомные плейсхолдеры (Placeholder.unparsed) не оказывают заметного влияния на скорость MiniMessage.deserialize
  2. MiniMessage.deserialize занимает 0.004 ms для простых сообщений и 0.018 ms для сложных (допустим даже 0.1ms для особо сложных сообщений, которые довольно редки). Среднее время выполнения получилось 0.011 ms.
  3. Учитывая скорость выполнения MiniMessage.deserialize, чтобы потратить 2% пропуской способности сервера (1ms), нужно распарсить порядка 100 сообщений за один тик, а эта ситуация крайне редка и в таких случаях уже программисту очевидно, что надо компонент кешировать
 
У меня сделано чутка по другому - сначала из конфига парсится конкретный компонент, потом оно записывается в обычную строку, далее идет в FPS-формат и ждет своего момента
Ну и так-же бонусом идет предварительная вставка на "свои места" всех констант, чтобы потом к ним не возвращаться

И в момент когда надо - получает модель данных, тащит из нее что нужно и сразу подставляет на нужные позиции
Остается только завернуть в пакет и отправить

(В частных случаях, когда там кроме констант ничего нет, система может сразу создать пакет, к-ый останется только отправить, но это уже другая история)

Работает невероятно быстро



Можете так-же добавить в бенчмарк и проверить
 
Последнее редактирование:
Противоречиво, да и это не adventure. Отправлять сырые строки в современном майнкрафте это уже диковато
Поясняю. Все эти "Красивые текстовые компоненты" отпраляются клиенту как обычная строка. То что эта строка представляет собой какой-то json это уже не имеет значения
Так что достаточно просто подставить в нужные места "полученной строки" свои подстановки и отправить
Тк доступ к данным - только на get - получать данные так же можно асинхронно (как и отправлять пакет)

Не вижу в этом ничего дикого и противоречивого
 
Последнее редактирование:
Поясняю. Все эти "Красивые текстовые компоненты" отпраляются клиенту как обычная строка. То что эта строка представляет собой какой-то json это уже не имеет значения
Так что достаточно просто подставить в нужные места "полученной строки" свои подстановки и отправить
Тк доступ к данным - только на get - получать данные так же можно асинхронно (как и отправлять пакет)

Не вижу в этом ничего дикого и противоречивого
Компонент (который сериализуется позднее в жсон строку) это абстракция, которая обеспечивает удобный и безопасный доступ к функциям, которые поддерживаются спецификацией этого жсона

Библиотека, что ты показал, не поддерживает вообще никаких функций форматирования и является просто переусложненным (и откровенно неряшливо написанным) String.replace. Может быть, кому-то и нужна супер быстрая замена символов в тексте, но это не относится к теме обсуждения, так что упоминать здесь это не очень корректно. Это имело бы смысл только в случае, если бы тобой был написан какой-нибудь адаптер для Adventure, однако, в текущем контексте это совершенно бесполезно

Кроме того, мой бенчмарк занимался парсингом строки. Он вставлял значения и создавал дерево из стилизованных компонентов. То, что ты предложил, нельзя применить в этом бенчмарке. Единственное теоретическое использование - распарсить MM, превратить в жсон строку, а позже заменять там кусочки через LSFPS-Format, что как-бы... не нужно - выхлоп минимальный (мы уже увидели, что парсинг ММ быстрый), но при этом нужно подключать какую-то стороннюю библиотеку и городить кучу бойлерплейта из строка -> компонент -> строка -> готовая строка

UPD: Это бесполезно даже для логов, потому что в логировании бутылочным горлышком обычно является сам факт вывода строки, а не, скажем, форматирование SLF4J
 
Последнее редактирование:
является просто переусложненным (и откровенно неряшливо написанным) String.replace.
Посмотрим, во сколько тысяч раз это будет быстрее String#replace() ... когда текст будет хотя-бы в 200 символов и 4-5 подстановок ...

Единственное теоретическое использование - распарсить MM, превратить в жсон строку, а позже заменять там кусочки через LSFPS-Format, что как-бы
Я прямо об этом и написал. Не теоретический а практический. Сделать всю возможную работу заранее, чтобы потом не пришлось это повторять
Кроме того, мой бенчмарк занимался парсингом строки. Он вставлял значения и создавал дерево из стилизованных компонентов. То, что ты предложил, нельзя применить в этом бенчмарке.
И то верно, потому что у меня прямо подразумевается, что все это будет выполнено 1 раз и заранее

городить кучу бойлерплейта из строка -> компонент -> строка -> готовая строка
Дописал 1 статик метод на создание Formatter как нужно тебе
Унаследовал Formatter и дописал свою "выходную логику"
Дописать свой провайдер для подстановок - хоть от PAPI, хоть свои данные, хоть и то и другое
Даж встроенный и быстрый рефлектор для доступа к полям (правда устаревший) есть
И погнали

Хоть Spigot, хоть Forge, хоть не весть что
 
Последнее редактирование:
MiniMessage.deserialize занимает 0.004 ms для простых сообщений и 0.018 ms для сложных (допустим даже 0.1ms для особо сложных сообщений, которые довольно редки). Среднее время выполнения получилось 0.011 ms.
Ты меня знаешь...
Мне важны наносекунды :whistle:
 
Ты меня знаешь...
Мне важны наносекунды
offtop
Могу дать принцип работы некоторых ключевых оптимизаций EC за 90% от всех продаж твоего ядра
Производительность подскочит в разы
 
Могу дать принцип работы некоторых ключевых оптимизаций EC за 90% от всех продаж твоего ядра
Производительность подскочит в разы
offtop Главный вопрос - а нужна ли тебе эта 0 целых хрен десятых рублей от отсутствующих продаж (y)
 
offtop Главный вопрос - а нужна ли тебе эта 0 целых хрен десятых рублей от отсутствующих продаж (y)
offtop
Уфф печально конечно
Собственна вот и главная причина почему ES никогда не будет с поддержкой плагинов - тупо никому оно нахрен не надо
Даже forge не особо берут - в основном предлагают выкуп исходников за копейки, когда только 1 принцип работы 1 конкретной оптимизации (без реализации) будет стоить не меньше ...
 
Последнее редактирование:
Уфф печально конечно
Собственна вот и главная причина почему ES никогда не будет с поддержкой плагинов - тупо неликвид /
offtop Так и живём.
Людям проще поставить пурпур чем покупать ядро которое выдержит в 1.5 раза больше онлайна
 
Назад
Сверху Снизу