mmo-dev team

whiteo

cvaba community
Хроники
  1. Prelude
  2. Harbingers of War
  3. Age of Splendor
  4. Rise of Darkness
  5. Scions of Destiny
  6. Oath of Blood
  7. Interlude
  8. The 1st Throne: The Kamael
  9. The 1st Throne: Hellbound
  10. The 2nd Throne: Gracia
  11. The 2nd Throne: Freya
  12. Chaotic Throne: High Five
  13. Goddess of Destruction Awakening
  14. Goddess of Destruction Harmony
  15. Goddess of Destruction Tauti
  16. Goddess of Destruction Glory Days
  17. Goddess of Destruction Lindvior
  18. Valliance / Epeisodion / Raiders
  19. Ertheia / Dimensional Strangers
  20. Infinite Odyssey
  21. Helios
  22. Grand Cursade
  23. Salvation
  24. Fafurion
  25. Shadow of the Kamael
  26. Prelude Of War
  27. Homunculus
  28. Return Of The Queen Ant
  29. Master Class
Всем привет, в моей прошлой теме тыц, мы успели пообсуждать кто, что и как пишет.
В итоге я решил что пора бы уже делать что-то, что можно было бы и людям показать.
Посему предлагаю организовать команду, чтобы совместно сделать качественный продукт.
Конечно, не все захотят тратить время, а кому-то денег подавай, но тут каждому свое. Если хотите внести вклад — милости просим.
Сразу скажу: Java, Go
Для начала я выкатил в репозиторий только логин-сервер. Но даже по нему уже можно посмотреть, насколько я упоролся.
Если ты такой же упоротый — ты тот, кто нужен команде mmo-dev team!
З.Ы. Зачатки гейм-сервера залью чуть позже.

github:

P.S. Пост будет дополнятся.
 
Последнее редактирование:
Кто в здравом уме будет писать сервер с нуля на java?
Надо было тебе писать на Java... Единомышленников бы набрал)
закладыванием нормальной архитектуры
а какая нормальная архитектура (не идеальная./лучшая) а нормальная?
 
Та самая которую тут эмулируют джавой или в данном случае Go. Думаешь надмозги из NCSoft с их миллионами и бюджетами не распланировали какая архитектура и какой язык нужен для сервера? Они точно не монетку там кидали. В контексте же тематики данного форума а это эмуляция то джава вполне себе вытягивает все это непотребство, вот человек проверяет на Go а я в свое время видел вообще свистопляску на Питоне, там черт ногу сломает. Нет преград для патриотов своего дела. Вот и выясним а точно ли только джава или еще что то.
Сообщение объединено:
whiteo, это клиент или сервер?
Смотрю в книгу вижу фигу.
 
Джава вполне себе хорошо себя чувствует, взять опыт коропораций где нагрузка измеряется сотнями или тысячами rps - там везде java/kotlin
нцсофт вбухали кучу денег в свое монолитное решение и как я понял особо архитектуру они не поменяли, да и зачем, если она работает годами и приносит деньги. Поэтому их сервера сжирают ресурсы лопатами и масштабируются только вертикально, что в современном мире уже не нужно, возможно в последних хрониках они докрутили хотя бы базовую репликацию, но что-то мне кажется нет
 
Foxx, где-то мы это уже обсуждали, но повторюсь, даже если бы я выбрал java/c#/etc, это все так же никому было бы не нужно.

Logan22, выглядит страшно.
Hitokiri, может быть на этом веку и увидим.
Lacost,
 
а какая нормальная архитектура (не идеальная./лучшая) а нормальная?
На java? Ну уж точно не текущих сборок l2, которые никак не меняются десятилетиями.
Я то думал C, C++, Rust для высоконагруженных используют, а тут оказывается java/kotlin . А там где нагрузка измеряется сотнями ли тысячами начинается лишь байтоёбство и забыванием многих фич языка и по памяти все равно проигрывает, разогнанный JIT и выжранная память, поможет приблизиться к данным языкам по скорости работы в некоторых операциях, но какой ценой? А для более высокоуровневого есть C#, но уж не java
NCSoft по сути опередили своё время с архитектурой, и далеко не монолит они делали уже тогда у нас были разделены логин, гейм, нпц сервер, репортинг сервер, сервер петиций, CacheD и не только, так что под описание монолита никак не подвязать, это SOA(Service-Oriented Architecture).
Не поэтому, а другие причины, а переписывать всю архитектуру, весь код и современные подходы не имеет никакой выгоды, на 2026 такие ресурсы вообще не колоссальные, java со своим пожиранием памяти куда больше потратит ресурсов.
 
Я то думал C, C++, Rust для высоконагруженных используют, а тут оказывается java/kotlin
Та хоть на asm пиши. Факт остается что Java используется активно, а как поддерживать HL HA это уже задача девопсов
Да это похоже на SOA, но какая разница как это называется, если оно в простое жрет ресурсы, как монолит который разрабатывался годами? Делили бы дальше свои сервисы для масштабирования
 
kick, вряд ли они гнались за громкими названиями и/или абревиатурами, делали то что позволяло железо/софт тех лет.
отличную тему я создал, обсуждаем все, но только не сабж
 
Последнее редактирование:
ну да, хотя бы какой то план - я учусь строить =)
Сообщение объединено:
java со своим пожиранием памяти куда больше потратит ресурсов
честно говоря есть подозрение, что это устаревшая информация
Сообщение объединено:
обсуждаем все, но только не сабж
вроде все в теме - производство старой MMO новыми киянками
 
Активно с каждым годом эта активность падает.
На загрузку лишь ресурсы уходят не более.
Ну так да, а архитектура для тех лет просто замечательная и весьма технологичная.
ну да, хотя бы какой то план - я учусь строить =)
В java проектах л2 нет архитектуры, это лишь пример, как не надо делать. Для обучения, это колоссальный объем знаний, который нужен на сегодняшний день с пониманием всех инструментов, базовых принципов как всё работает, концепции хранения данных, масштабирование, кэирование, системы сообщений, часть знаний от бизнес-аналитики, так же как популярные гиганты решают определенные проблемы, с чем сталкиваются, доклады их. Многие показывают готовые архитектуры, схемы, уже брать за основу, от того же netflix не мало статей.
честно говоря есть подозрение, что это устаревшая информация
В чём?) Как то резко изменилась jvm?) Нет, сколько уже сборщиков мусора сменилась в jvm? 7 и до сих пор не могут заставить работать их нормально. У JVM нет контроля, там где он нужен, потому что она забирает ответственность на себя и решает за программиста где и что она будет оптимизировать. То что java сейчас это отстающая среда, пока комитет java примет новую фичу.
Java:
import java.util.UUID;

class Node {
    String id;
    Integer value;
    Node left;
    Node right;

    Node(int depth) {
        this.id = UUID.randomUUID().toString();
        this.value = depth;
       
        if (depth > 0) {
            this.left = new Node(depth - 1);
            this.right = new Node(depth - 1);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Node root = new Node(25);
    }
}
И вот будут чудеса и особенно с автоупаковкой int -> Integer и тоже самое, что даже не почувствуется.
C++:
#include <iostream>
#include <memory>
#include <string>

struct Node {
    char id[36];
    int value;
    std::unique_ptr<Node> left;
    std::unique_ptr<Node> right;

    Node(int depth) : value(depth) {
        int8_t dummy[] = "12345678-1234-1234-1234-123456789012";
        std::copy(std::begin(dummy), std::end(dummy), id);

        if (depth > 0) {
            left = std::make_unique<Node>(depth - 1);
            right = std::make_unique<Node>(depth - 1);
        }
    }
};

int main() {
    auto root = std::make_unique<Node>(25);
    return 0;
}
Про аллокации лямбд и не только думаю мне не надо, я могу даже дать задачу вот такую, что максимум на плюсах выжмет 30 мегабайт.
Дан массив из 1000000 объектов. Каждый объект представляет собой точку и содержит:
Координаты (x, y, z)
Флаг состояния (isActive)
Цель: пробежать по всему массиву и для всех активных точек (isActive == true) посчитать общую сумму их координат.
Типы данных: внутри объекта координаты должны быть объявлены как Double, а флаг, как Boolean (объектные типы-обертки).
Контейнер: сам массив должен быть реализован в виде связного списка (LinkedList), а проверка флага на активность должна проходить через Optional.
Логика вычислений: расчет суммы для каждой точки должен выполняться через вызов метода, в который передается динамическая лямбда. Лямбда обязана захватывать переменную текущего индекса элемента из внешнего цикла в качестве модификатора, причем сам индекс перед передачей должен быть приведен к объектному типу Integer.
Агрегация результата: итоговый результат общей суммы координат всех активных точек должен накапливаться и храниться в переменной объектного типа Double.
Java:
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

class Point {
    public Double x;
    public Double y;
    public Double z;
    public Boolean isActive;

    public Point(double x, double y, double z, boolean isActive) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.isActive = isActive;
    }
}

public class Main {
    private static double calculate(Point p, Function<Point, Double> formula) {
        return formula.apply(p);
    }

    public static void main(String[] args) {
        List<Point> points = new LinkedList<>();

        for (int i = 0; i < 1_000_000; i++) {
            points.add(new Point(i * 1.1, i * 1.2, i * 1.3, i % 2 == 0));
        }

        long startTime = System.nanoTime();
       
        Double totalSum = 0.0;

        int index = 0;
        for (Point p : points) {
            Integer stepId = index;
            final double modifier = (stepId % 2 == 0) ? 1.0 : 1.005;

            if (Optional.ofNullable(p.isActive).orElse(false)) {
                totalSum += calculate(p, pt -> (pt.x + pt.y + pt.z) * modifier);
            }
            index++;
        }

        long endTime = System.nanoTime();
        System.out.println("Результат: " + totalSum);
        System.out.println("Время выполнения: " + (endTime - startTime) / 1_000_000.0 + " мс");
    }
}

153 мегабайт на хранение миллиона точек это очень много, для плюсов это копейки и это специально так поставлены задачи и код, но это имеет место быть и как раз то что происходит потом на hot-path при захвате лямбы, начинается веселье.
А ещё бывают такие места где хоть убей тебе нужно удалить объект, а этого не сделать, потому что JVM решает за тебя это, а в C# всё решается
Код:
using System.Diagnostics;
using System.Runtime.InteropServices;

public struct Point
{
    public double X;
    public double Y;
    public double Z;
    public bool IsActive;
}

public static class Program
{
    public static void Main()
    {
        List<Point> points = new List<Point>(1_000_000);

        for (int i = 0; i < 1_000_000; i++)
        {
            points.Add(new Point { X = i * 1.1, Y = i * 1.2, Z = i * 1.3, IsActive = (i % 2 == 0) });
        }

        Stopwatch sw = Stopwatch.StartNew();

        double totalSum = 0.0;
        Span<Point> pointSpan = CollectionsMarshal.AsSpan(points);
        for (int i = 0; i < pointSpan.Length; i++)
        {
            ref readonly Point p = ref pointSpan[i];

            if (p.IsActive)
            {
                double modifier = (i % 2 == 0) ? 1.0 : 1.005;
                totalSum += (p.X + p.Y + p.Z) * modifier;
            }
        }

        sw.Stop();
        Console.WriteLine($"Результат: {totalSum}");
        Console.WriteLine($"Время выполнения в C#: {sw.Elapsed.TotalMilliseconds:F2} мс");
    }
}

Ну и плюсы:
C++:
#include <iostream>
#include <list>
#include <optional>
#include <chrono>

struct Point {
    double x;
    double y;
    double z;
    std::optional<bool> isActive;
};

template<typename F>
double calculate(const Point& p, F formula) {
    return formula(p);
}

int main() {
    std::list<Point> points;

    for (int i = 0; i < 1000000; ++i) {
        points.push_back({ i * 1.1, i * 1.2, i * 1.3, (i % 2 == 0) });
    }

    auto start = std::chrono::high_resolution_clock::now();
    std::optional<double> total_sum = 0.0;

    int index = 0;
    for (const auto& p : points) {
        int step_id = index;
        double modifier = (step_id % 2 == 0) ? 1.0 : 1.005;

        if (p.isActive.value_or(false)) {
            total_sum.value() += calculate(p, [&](const Point& pt) {
                return (pt.x + pt.y + pt.z) * modifier;
                });
        }
        index++;
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> duration = end - start;

    std::cout << "Результат: " << total_sum.value() << "\n";
    std::cout << "Время выполнения в C++: " << duration.count() << " мс\n";
    return 0;
}

Ну по памяти прекрасно видим на самом плохом примере, на каждом из языков можно лучше в разы сделать и без таких условий, а как пишут код и какие люди на собесы и все те же болячки присутствуют, конечно можно писать правильно и как надо, но некоторые даже не задумываются о особенностях или пишут, а потом ловят на проде приколы. Есть много особенностей в языке и чтобы начинать экономить память, так и написано
Потому что если начать тоже делать в си/плюсах/расте/шарпе будет все равно выигрывать по памяти. Я может быть и могу согласиться с выражением, что java больше не такая медленная как была раньше, да это так и есть и в некоторых моментах она даже может обойти плюсы, это так, но опять таки при каких условиях это достигается и из-за чего. Но по памяти все ещё актуально
 
whiteo, нужно соблюсти условия все

Код:
package main

import (
    "container/list"
    "fmt"
    "time"
)

type Point struct {
    X        *float64
    Y        *float64
    Z        *float64
    IsActive *bool
}

func NewPoint(x, y, z float64, isActive bool) *Point {
    return &Point{
        X:        &x,
        Y:        &y,
        Z:        &z,
        IsActive: &isActive,
    }
}

func calculate(p *Point, formula func(*Point) float64) float64 {
    return formula(p)
}

func orElseFalse(b *bool) bool {
    if b == nil {
        return false
    }
    return *b
}

func main() {
    points := list.New()

    for i := 0; i < 1000000; i++ {
        points.PushBack(NewPoint(
            float64(i)*1.1,
            float64(i)*1.2,
            float64(i)*1.3,
            i%2 == 0,
        ))
    }

    startTime := time.Now()

    var totalSum float64 = 0.0

    index := 0
    for e := points.Front(); e != nil; e = e.Next() {
        p := e.Value.(*Point)
        stepId := index
        
        var modifier float64
        if stepId%2 == 0 {
            modifier = 1.0
        } else {
            modifier = 1.005
        }

        if orElseFalse(p.IsActive) {
            totalSum += calculate(p, func(pt *Point) float64 {
                return (*pt.X + *pt.Y + *pt.Z) * modifier
            })
        }
        index++
    }

    endTime := time.Now()
    executionTime := float64(endTime.Sub(startTime).Nanoseconds()) / 1000000.0

    fmt.Printf("Результат: %f\n", totalSum)
    fmt.Printf("Время выполнения: %f мс\n", executionTime)
}
 
kick, ничего не понял, в C# читерить можно а в Go нет.
Есть цель показать результаты как рантайм в хреновых условиях вывезет эту катку, то нужно сделать равные условия для всех, а не выделять C#.
 
Да вообще не проблема, я могу не использовать структуры, дописать, чтобы проверка была аналогична условию, от неё ничего не изменится, все ограничения задачи и поставлены выполняются. Перебираю коллекция в цикле foreach, получаю полноценные объекты Point, работаю со свойствами-обёртками (double?, bool?), проверяю их через аналог Optional (GetValueOrDefault) и вызываю метод через лямбду с упаковкой индекса в int?

Вся форма кода осталась оригинальной и оригинальной джавовской задачей.
C#:
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;

public class Point
{
    public double? x;
    public double? y;
    public double? z;
    public bool? isActive;

    public void Reset(double x, double y, double z, bool isActive)
    {
        this.x = x;
        this.y = y;
        this.z = z;
        this.isActive = isActive;
    }
}

public class LinkedListNode
{
    public Point Item;
    public LinkedListNode Next;
}

public struct CustomEnumerator : IEnumerator<Point>
{
    private LinkedListNode _current;
    private bool _started;

    public CustomEnumerator(LinkedListNode head)
    {
        _current = head;
        _started = false;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool MoveNext()
    {
        if (!_started)
        {
            _started = true;
            return _current != null;
        }

        if (_current != null)
        {
            _current = _current.Next;
        }
        return _current != null;
    }

    public Point Current => _current != null ? _current.Item : null;

    object IEnumerator.Current => Current;
    public void Reset() => throw new NotSupportedException();
    public void Dispose() { }
}

public class VirtualLinkedList : IReadOnlyCollection<Point>
{
    private LinkedListNode _head;
    private LinkedListNode _tail;
    public int Count { get; private set; } = 0;

    private readonly LinkedListNode[] _nodePool = new LinkedListNode[1_000_000];
    private readonly Point[] _pointPool = new Point[1_000_000];

    public VirtualLinkedList()
    {
        for (int i = 0; i < 1_000_000; i++)
        {
            _nodePool[i] = new LinkedListNode();
            _pointPool[i] = new Point();
        }
    }

    public void AddLast(double x, double y, double z, bool isActive)
    {
        Point p = _pointPool[Count];
        p.Reset(x, y, z, isActive);

        LinkedListNode newNode = _nodePool[Count];
        newNode.Item = p;
        newNode.Next = null;

        if (_head == null)
        {
            _head = newNode;
        }
        else
        {
            _tail.Next = newNode;
        }
        _tail = newNode;
        Count++;
    }

    public CustomEnumerator GetEnumerator() => new CustomEnumerator(_head);
    IEnumerator<Point> IEnumerable<Point>.GetEnumerator() => new CustomEnumerator(_head);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public readonly struct CalculationContext
{
    public readonly double Modifier;
    public CalculationContext(double modifier) => Modifier = modifier;
}

public static class Program
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static double Calculate(Point p, in CalculationContext ctx, Func<Point, CalculationContext, double> formula)
    {
        return formula(p, ctx);
    }

    public static void Main()
    {
        VirtualLinkedList points = new VirtualLinkedList();
        for (int i = 0; i < 1_000_000; i++)
        {
            points.AddLast(i * 1.1, i * 1.2, i * 1.3, i % 2 == 0);
        }

        Stopwatch sw = Stopwatch.StartNew();

        double? totalSum = 0.0;
        double localSum = 0.0;

        int index = 0;
        Func<Point, CalculationContext, double> formula = (pt, ctx) => (pt.x.Value + pt.y.Value + pt.z.Value) * ctx.Modifier;

        foreach (Point p in points)
        {
            int? stepId = index;
            double modifier = (stepId % 2 == 0) ? 1.0 : 1.005;

            if (p.isActive.GetValueOrDefault(false))
            {
                var context = new CalculationContext(modifier);
                localSum += Calculate(p, in context, formula);
            }
            index++;
        }

        totalSum = localSum;
        sw.Stop();

        Console.WriteLine($"Результат: {totalSum}");
        Console.WriteLine($"Время выполнения в C#: {sw.Elapsed.TotalMilliseconds:F2} мс");
    }
}
Или мне убрать Enumerator?)
Никто не запрещал использовать в Go колоночное хранение и паттерн легковес, но ты же код не показал.
Я могу использовать структуры, упадёт до 62 мегабайт спокойно
 
Последнее редактирование:
В эту игру можно играть вдвоем))

Есть облачный API который принимает UUID и возвращает определенное число от 0 до 10.
Также есть синхронный клиент, который предоставляет публичный метод для работы с этим API.
Каждый вызов метода блокирует вызывающий его поток на время от 1 до 5 секунд. Асинхронной версии метода не существует(увы, легаси). Апи не применяет рейт лимит и может принять любое количество запросов одновременно.

Для каждого UUID также заранее создается уникальный, неизменяемый контекст безопасности, содержащий идентификатор запроса и токен доступа. Публичный метод клиента принимает только UUID, но контекст должен быть неявно доступен клиенту и всем его внутренним методам на любой глубине стека. Контекст должен быть привязан только на время конкретного вызова, автоматически исчезать после его завершения или исключения, а при вложенном вызове автоматически восстанавливать предыдущие значение. Если клиент получит некорректный контекст, то апи отклонит запрос. Сигнатуру метода клиента менять нельзя из-за политики безопасности. Клиент принимает только UUID.

Можно использовать любые доступные в стандартной среде исполнения потоки, пулы потоков, задачи, экзекуторы и планировщики и штатные механизмы динамического контекста. Массив с уникальными UUID и токенами создается до начала измерения. Измеряется время от отправки первого запроса до получения суммы результатов всех 10 000 000 запросов. Писать альтерантивный клиент или модифицировать работу существующего клиента запрещено из-за строгих стандартов безопасности.




Java:
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.LongAdder;

public class Main {
    static final int N = 10_000_000;
    static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();

    record RequestContext(UUID requestId, UUID token) {}

    static class Client {
        int query(UUID id) {
            var context = CONTEXT.get();
            if (!context.requestId().equals(id) || context.token() == null) {
                throw new SecurityException();
            }
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5001));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return Math.floorMod(id.hashCode(), 11);
        }
    }

    public static void main(String[] args) {
        var ids = new UUID[N];
        var contexts = new RequestContext[N];
        for (int i = 0; i < N; i++) {
            ids[i] = UUID.randomUUID();
            contexts[i] = new RequestContext(ids[i], UUID.randomUUID());
        }

        var client = new Client();
        var sum = new LongAdder();
        long start = System.nanoTime();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < N; i++) {
                var id = ids[i];
                var context = contexts[i];
                executor.submit(() -> ScopedValue.where(CONTEXT, context)
                        .run(() -> sum.add(client.query(id))));
            }
        }

        System.out.println(sum.sum());
        System.out.printf("%.3f s%n", (System.nanoTime() - start) / 1e9);
    }
}


Код:
java --version
openjdk 26.0.1 2026-04-21
OpenJDK Runtime Environment Temurin-26.0.1+8 (build 26.0.1+8)
OpenJDK 64-Bit Server VM Temurin-26.0.1+8 (build 26.0.1+8, mixed mode, sharing)

java .\Main.java
50012355
32,628 s
 
Если это легаси, то там и нету виртуальных потоков и там далеко не java 21
Ну да) Там шарпы. Или гоу, или плюсы, или раст, или питон, или пхп, или еще чет.

Лан, не легаси. Упростим: "Вайбкодил чувак, который прошел собес через ИИ и два месяца успешно прикидывался синьором. Тендер на написание нового клиента уже разместили, но откликов пока нет. А отчет нужен через 10 минут.".

И ваще, пиши код, а не отговорки. Уже давно бы написал нормально на богоподобном шарпе. Он вон яву ебет в хвост и в гриву по твоим тестам) Я думаю там делов на 5 минут + секунд 10 на выполнение кода
 
Последнее редактирование:
Тогда задача не подходит
Асинхронной версии метода не существует
с нет фреймворка 4.5 async/await существует стабильно)
гоурутины ещё в 2009 были, до предрелиза языка
В чём проблема на плюсах? С большой обновой стандарта языка 2011 тоже появились интересные фичи, данные копейки
в 2010-2015 можно писать аналог горутин спокойно
Ну с легаси согласен могут быть проблемы, но и пример использует Java 21+ который далеко не легаси , а там уже есть Swoole, FrankenPHP и RoadRunner, но никто не запрещает сделать на сях расширение и дергать сишное расширение.
 
Данный сайт использует cookie. Вы должны принять их для продолжения использования. Узнать больше…