Что нового?

Обсуждение Разбор исходного кода улучшения снаряжения в PW

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6

Вступление​

Внимание​

Я ни в коем случае не поддерживаю нарушений пользовательского соглашения и правил сервера ComebackPW, а данное чтиво представлено исключительно в ознакомительных целях. Изложенная ниже информация не может быть использована для читерства, или извлечения любой другой выгодны нечестным путем на просторах идеального мира.

Предисловие​

Всем привет! Каждый игрок PW сам для себя определяет, что его держит в идеальном мире. Меня — ивент «Конкурс Ремесленников» и заточка снаряжения. Казалось бы, обе темы избитые и что еще про них еще можно написать? Ну, например, очередную заметку, разрушающую большинство мифов и суеверий, существующих вокруг этого китайского казино :) Ну а как сделать это таким образом, чтоб потенциально хоть кого-то переубедить? Для меня, как программиста, работающего в game-dev индустрии, наиболее достоверный способ — это изучить исходный код игры, что я, собственно, и сделал. Спойлер: я не узнал почти ничего нового, но лично мне было чертовски интересно! Надеюсь, будет и вам.

Откуда исходный код?​

Разумеется, у меня нет исходного кода ComebackPW. Но не думаю, что я кому-то открою Америку, если скажу, что любая фришка подымается на основе чистого клиент-сервера, которые можно найти в общем доступе на тематических форумах. Нужно понимать, что разработчики ComebackPW вполне могли изменить исходный код под свой проект, но я склонен думать, что это не так, по следующим причинам:
  • Трогать столь sensitive аспект игры довольно рискованно. Стандартная реализация (хоть и говнокод, простите), проверена десятилетиями и гарантирует отсутствие багов.
  • Зачем трогать то, что и так отлично работает? :LOL: Люди десятки лет придумывает мифы вокруг точки, так что цель «удерживать игрока» достигается отлично.
Я скачал рандомную версию сервера 1.4.6, но также проверил, что код одинаковый и на 1.3.6 (позднее узнал, что ComebackPW 1.4.6 поднят на базе 1.5.5, но актуальности предоставленной информации это не убавляет).

Нужны ли мне какие-то математические/программистские знания, чтоб понять данную тему?​

Надеюсь, нет. Я постарался написать ее так, чтоб она была понятна каждому. Однако, часть пояснений я скрыл под спойлеры, чтобы:
  • не нагружать деталями тех, кому они не нужны
  • не объяснять лишний раз то, что кому-то может быть и так понятно в силу его профессии/образования/увлечений
Например, так:
Это такой тип приложения, который базируется на непрерывном взаимодействие двух программ. Первая — клиентская часть, распространяется на устройства юзеров, в нашем случае посредством лаунчера. На стороне клиента редко выполняется какая-то существенная логика (в первую очередь в целях безопасности). Обычно, клиентская часть отвечает за интерфейс в широком смысле этого понятия. Вторая программа — серверная часть. Сервер запускается на серверах (прошу прощения за тавтологию) и должен быть активным все время, чтобы игроки могли подключиться к нему через свой клиент. Клиент-серверное взаимодействие происходит непрерывно посредством отправки/получения пакетов с данными. Информация, разумеется, передается в зашифрованном виде.

Разберём на актуальном примере: пока вы идете к НИП-у, чтоб начать процесс заточки, со стороны клиента отправляется информация о движении персонажа. Как только на экране прогрузился НИП — это сервер прислал клиенту эту информацию исходя из местоположения персонажа, а он просто отрисовал ее. То же самое со взаимодействием с НИП-ом, списком предлагаемых им заданий и т.д. Когда уже открыто окно точки и вы выбираете предмет и камень улучшения — за это отвечает клиент. Как только нажата кнопка «Улучшение», отправляется запрос на сервер, где происходит вся логика (которую мы и будем с вами разбирать), а с получением ответа с результатом отображается результат и обновляются айтемы в инверторе.
При написании заметки я не использовал Википедию и другие подобные ресурсы. Все определения и пояснения «от себя», так что я не претендую на их истинность в последней инстанции.

Код v.s. Данные (Code v.s. Data)​

Последнее, что я хотел бы пояснить перед тем, как переходить к тому, ради чего мы здесь сегодня собрались — объяснения концепции различия исходного кода и данных. Код должен был скомпилирован (в случае компилируемых языков программирования, каковым является C++, на котором написано PW).
Процесс перевода конструкций языка программирования, понятных для человека, в бинарный код (состоящий из нулей и единиц), понятный для процессора. В результате создается исполняемый файл (.exe в случае Windows), который используется для запуска приложения.
При необходимости обновить любую строчку кода выполнение приложение должно быть прервано, перекомпилировано, заново задеплоено (т.е. доставлено на целевую машину: компьютер игрока и сервер), и перезапущено. Для этого разработчики и проводят «Технические работы».

Дата является более широким понятием и может представлять собой что угодно: таблицы, xml-файлы, ассеты (картинки), базы данных и т.д. Особенность даты заключается в том, что исходный код способен подхватить обновления данных «на лету». Таким образом, игроку даже не придется переоткрывать клиент (передаю привет мастерам, разбирающим .pck 😄 НАПОМИНАЮ, изменение клиента игры (в том числе данных) справедливо карается баном).

Почему я счел необходимым это объяснить? Дело в том, что все item-ы, в том числе камни для повышения шансов заточки, являются данными, а значит шансы могут быть запросто «подкручены» без изменения исходного кода игры. Однако, как и прежде, я склонен верить, что на ComebackPW все шансы стандартные, тем более это ни один раз отмечалось администрацией сервера.

Чтение кода​

gplayer_imp::RefineItemAddon​

Процесс заточки инициируется этой функцией, вызов происходит при получение соответственного запроса с клиента. Она определена в файле player.cpp в классе gplayer_imp, что расшифровывается как «global player implementation». Данный класс реализует множество логики, которая может быть выполнена персонажем в PW. «Refine» — дословно с английского «улучшить», «Addon» — сокр. от «Addition», в переводе с английского «дополнение». Ну, в процессе точке действительно мы улучшаем дополнения к характеристике/-ам предмета, так что все логично.
C++:
bool
gplayer_imp::RefineItemAddon(size_t index, int item_type, int rt_index)
{
    if(index >= _inventory.Size()) return false;
    item & it = _inventory[index];
    if(it.type ==-1 || it.body == NULL || item_type != it.type )  return false;

    if(rt_index >= 0 &&  (size_t)rt_index  >= _inventory.Size()) return false;

    int material_need = 0xFFFFF;
    int refine_addon = world_manager::GetDataMan().get_item_refine_addon(item_type,material_need);
    if(refine_addon <=0 || material_need <= 0) return false;

    //检查幻仙石是否足够
    int material_id = world_manager::GetDataMan().get_refine_meterial_id();
    if(!_inventory.IsItemExist(material_id, material_need)) return false;

    //检查概率调整装置是否存在
    float adjust[4] = {0,0,0,0};
    float adjust2[12] = {0,0,0,0,0,0,0,0,0,0,0,0};
    int rt_id = -1;
    if(rt_index >= 0)
    {
        rt_id = _inventory[rt_index].type;
        if(rt_id <= 0) return false;
        DATA_TYPE dt2;
        const REFINE_TICKET_ESSENCE &ess= *(const REFINE_TICKET_ESSENCE*)world_manager::GetDataMan().get_data_ptr(rt_id, ID_SPACE_ESSENCE,dt2);
        if(dt2 != DT_REFINE_TICKET_ESSENCE || &ess == NULL)
        {
            return false;
        }

        //限制装备天人合一
        if(ess.binding_only && !(it.proc_type & item::ITEM_PROC_TYPE_BIND)) return false;
        //限制装备品阶上限
        if(ess.require_level_max && world_manager::GetDataMan().get_item_level(it.type) > ess.require_level_max) return false;

        float adj1 = ess.ext_succeed_prob;
        float adj2 = ess.ext_reserved_prob;
        if(adj1 < 0) adj1 = 0;
        if(adj2 < 0) adj2 = 0;
        if(adj1 > 1.0) adj1 = 1.0;
        if(adj2 > 1.0) adj2 = 1.0;
        if(adj1 != ess.ext_reserved_prob || adj2 != ess.ext_succeed_prob)
        {
            __PRINTF("强化时发生状态调整\n");
        }

        adjust[0] = adj1;       //成功概率
        adjust[2] = adj2;       //降一级概率

        if(ess.fail_reserve_level )
        {
            adjust[1] = 2.0;    //拥有特殊的保留石
        }

        for(size_t i =0; i < 12; i ++)
        {
            adjust2[i] = ess.fail_ext_succeed_prob[i];
        }
    }


    int level_result = 0;
    int rst = it.body->RefineAddon(refine_addon, level_result,adjust,adjust2);
    if(rst != item::REFINE_CAN_NOT_REFINE)
    {
        const char * tbuf[] = {"成功", "无法精炼" , "材料消失", "属性降低一级", "属性爆掉", "装备爆掉","未知1","未知2","未知3"};
        GLog::log(GLOG_INFO,"用户%d精炼物品%d[%s],精炼前级别%d 消耗幻仙石%d 概率物品%d",_parent->ID.id, item_type,tbuf[rst],level_result, material_need, rt_id);
        if(level_result >= 6)
        {
            GLog::refine(_parent->ID.id,item_type, level_result, rst, material_need);
        }
    }

    switch(rst)
    {
        case item::REFINE_CAN_NOT_REFINE:
        //无法进行精炼,发送精炼失败  这种情况不作任何变化
        _runner->error_message(S2C::ERR_REFINE_CAN_NOT_REFINE);
        return true;
        break;


        case item::REFINE_SUCCESS:
        //精炼成功
        _runner->refine_result(0);
        PlayerGetItemInfo(IL_INVENTORY,index);
        break;

        case item::REFINE_FAILED_LEVEL_0:
        //精炼一级失败,删除材料
        _runner->refine_result(1);
        break;

        default:
        GLog::log(GLOG_ERR,"精炼装备时返回了异常错误%d",rst);
        case item::REFINE_FAILED_LEVEL_1:
        //精炼二级失败,删除材料,降低一级 属性已经被自动更改
        _runner->refine_result(2);
        PlayerGetItemInfo(IL_INVENTORY,index);
        break;

        case item::REFINE_FAILED_LEVEL_2:
        //精炼三级失败,删除材料,属性已经被自动清除
        _runner->refine_result(3);
        PlayerGetItemInfo(IL_INVENTORY,index);
        break;
    }

    //在这里删除物品
    RemoveItems(material_id,material_need, S2C::DROP_TYPE_USE, true);

    //如果使用了调整石,那么删除之
    if(rt_index >= 0)
    {
        item& it = _inventory[rt_index];
        UpdateMallConsumptionDestroying(it.type, it.proc_type, 1);

        _inventory.DecAmount(rt_index, 1);
        _runner->player_drop_item(IL_INVENTORY,rt_index,rt_id, 1 ,S2C::DROP_TYPE_USE);
    }
    return true;
}
Исходный код выше оставил для тех, кто хочет прочесть его самостоятельно. Остальные пропускайте, ниже я прокомментирую все, что важно.

Для начала давайте разберемся со входными параметрами функции (передаются с клиента):
  • index – индекс улучшаемого предмета в инвентаре
  • item_type – тип предмета (оружие/бижутерия/броня и т.д.)
  • rt_index – индекс выбранного камня заточки в инвентаре (небесный камень, подземный камень, камень мироздания, жемчуг)
Далее следуют различные проверки и получение необходимых данных:
C++:
if(index >= _inventory.Size()) return false; // проверка, что индекс улучшаемого предмета не выходит за пределы размера инвентаря
item & it = _inventory[index]; // получение предмета, который подлежит улучшению
if(it.type ==-1 || it.body == NULL || item_type != it.type )  return false; // проверка, что предмет валидный

// проверка, что индекс камня заточки не выходит за пределы размера инвентаря (в случае использования)
if(rt_index >= 0 &&  (size_t)rt_index  >= _inventory.Size()) return false;

// refine_addon - id улучшаемой характеристики; material_need - сколько миражей необходимо для данного типа предмета для попытки улучшения
int material_need = 0xFFFFF;
int refine_addon = world_manager::GetDataMan().get_item_refine_addon(item_type,material_need);
if(refine_addon <=0 || material_need <= 0) return false; // проверка, что у предмета существует улучшаемая посредством точки характеристика

// получение id предмета для заточки (миража, get_refine_meterial_id() возвращает 11208, что соответствует id камня бессмертного в базе данных pw)
int material_id = world_manager::GetDataMan().get_refine_meterial_id();
if(!_inventory.IsItemExist(material_id, material_need)) return false; // проверка наличия достаточного количества миражей в инвентаре
Следующий шаг — изменении шансов результата заточки при использовании различный камней. Перед тем, как разбирать эту часть, стоит отметить, что при заточке снаряжения в PW выделяется 4 исхода:
  • REFINE_SUCCESS – успешная заточка
  • REFINE_FAILED_LEVEL_0 – неуспех, уровень заточки снаряжения не изменился
  • REFINE_FAILED_LEVEL_1 – неуспех, уровень заточки снизился на 1
  • REFINE_FAILED_LEVEL_2 – неуспех, уровень заточки сбросился до 0
C++:
/*
Ниже идет объявление двух массивов чисел, значение которых будут использованы для изменения шансов заточки.
adjust состоит из 4-х чисел, которые соответствует REFINE_SUCCESS REFINE_FAILED_LEVEL_0 REFINE_FAILED_LEVEL_1 REFINE_FAILED_LEVEL_2.
adjust2 состоит из 12-ти чисел, соответствующих изменению шанса успеха для каждого уровня заточки. Используется для камней мироздания и жемчуга.
*/

float adjust[4] = {0,0,0,0};
float adjust2[12] = {0,0,0,0,0,0,0,0,0,0,0,0};
int rt_id = -1;
if(rt_index >= 0) // используется камень улучшения
{
    rt_id = _inventory[rt_index].type; // получения id камня улучшения из инвентаря по индексу
    if(rt_id <= 0) return false; // проверка, что камень является валидным
    /*
    Далее идет запрос к "базе данных" для получения конкретных шансов при использовании камней для заточки.
    Именно поэтому я выше обращал внимание на разницу между данными и кодом.
    dt2 является выходным параметром функции get_data_ptr, в который записывается тип ответа.
    ess является возвращаемым значением функции get_data_ptr. Эта структура типа REFINE_TICKET_ESSENCE.
    Наиболее нас интересуют поля ext_reserved_prob, ext_succeed_prob, fail_reserve_level, fail_ext_succeed_prob,
    которые соответствуют изменению шансов при заточке с использованием разных камней.
    */
    DATA_TYPE dt2;
    const REFINE_TICKET_ESSENCE &ess= *(const REFINE_TICKET_ESSENCE*)world_manager::GetDataMan().get_data_ptr(rt_id, ID_SPACE_ESSENCE,dt2);
    if(dt2 != DT_REFINE_TICKET_ESSENCE || &ess == NULL) // проверка валидности возвращаемой структуры
    {
        return false;
    }

    // Проверка, что используемый камень заточки не предназначен только для заточки привязанного шмота в случае точки непривязанного предмета
    if(ess.binding_only && !(it.proc_type & item::ITEM_PROC_TYPE_BIND)) return false;
    // Проверка, что используемый камень заточки подходит для заточки предмета по уровню снаряжения
    if(ess.require_level_max && world_manager::GetDataMan().get_item_level(it.type) > ess.require_level_max) return false;

     /*
    Данная операция называется «clamp» — обрезание значений изменения шансов заточки к определенному интервалу.
    В простонародье — защита от дурака (мало ли что горе-сервер-мастера нарисуют в БД).
    В данной реализации "1" означает 100% шанс, а "0" гарантирует неудачу.
    */
    float adj1 = ess.ext_succeed_prob; // добавка к шансу успеха. Любое число от 0 до 1.
    float adj2 = ess.ext_reserved_prob;  // шанс того, что при неудаче будет сброшен только 1 уровень. Это значение может быть только 0 или 1.
    if(adj1 < 0) adj1 = 0;
    if(adj2 < 0) adj2 = 0;
    if(adj1 > 1.0) adj1 = 1.0;
    if(adj2 > 1.0) adj2 = 1.0;

    adjust[0] = adj1; // сохранение корректировки для шанса успеха (REFINE_SUCCESS)
    adjust[2] = adj2; // сохранение корректировки для шанса сброса на один уровень (REFINE_FAILED_LEVEL_1)

    /*
    fail_reserve_level используется для изменения шанса, что текущая точка будет сохранена при неуспехе (REFINE_FAILED_LEVEL_0),
    т.е. может принимать значение тоже либо 0, либо 1, только при использовании камней мироздания или жемчуга.
    */
    if(ess.fail_reserve_level )
    {
        adjust[1] = 2.0; // использование 2.0 здесь не имеет никакого смысла, 1.0 уже значит 100%-й шанс.
    }

    // сохранение корректировок шанса успеха на каждом уровне (используется только для камней мироздания или жемчуга)
    for(size_t i =0; i < 12; i ++)
    {
        adjust2[i] = ess.fail_ext_succeed_prob[i];
    }
}
Далее идет небольшой блок кода, который мы будем подробно обсуждать под следующим заголовком. Помимо логирования (кстати, забавно видеть, что точка выше +5 дополнительно логируется в отдельный файл 😄), здесь происходит вызов метода (=функции) RefineAddon класса equip_item. Сейчас нам важно отметь входные и выходные параметры:
На вход:
  • refine_addon – id улучшаемой характеристики. Не искал, где эти id-шники заданы в коде, но по сути это просто id для: м-деф/ф-деф/уклон для бижутерии, м-атака/ф-атака для колец и оружия, хп для остального
  • adjust – массив значений для корректировки шансов заточки, подробнее смотри комментарии к коду под спойлером "Изменение дефолтных шансов заточки"
  • adjust2[12] – массив значений для корректировки шансов заточки при использовании жемчуга или камней мироздания, подробнее смотри комментарии к коду под спойлером "Изменение дефолтных шансов заточки"
Выходные параметры:
  • level_result – уровень заточки снаряжения после попытки улучшения
  • rst – возвращаемое значение. Результат заточки, соответствует исходам, определенным выше (REFINE_SUCCESS ... REFINE_FAILED_LEVEL_2)
C++:
int level_result = 0; // определение выходного параметра уровня заточки после улучшения
// логика заточки тут, отдельно разберем в след главе. Метод вызывается у сущности предмета, который улучшаем, поэтому эту информацию передавать не надо.
int rst = it.body->RefineAddon(refine_addon, level_result,adjust,adjust2);
if(rst != item::REFINE_CAN_NOT_REFINE) // валидный результат
{
    /*
      Ниже просто две записи в журналы логов.
      Машинный перевод: "Успех", "Невозможно усовершенствовать", "Материал исчез", "Атрибут уменьшен на один уровень", "Атрибут взорван",
      "Оборудование взорвано", "Неизвестно 1", "Неизвестно 2", "Неизвестно 3"
    */
    const char * tbuf[] = {"成功", "无法精炼" , "材料消失", "属性降低一级", "属性爆掉", "装备爆掉","未知1","未知2","未知3"};
    GLog::log(GLOG_INFO,"用户%d精炼物品%d[%s],精炼前级别%d 消耗幻仙石%d 概率物品%d",_parent->ID.id, item_type,tbuf[rst],level_result, material_need, rt_id);
    if(level_result >= 6) // дополнительная запись в другой журнал (файл), если точка выше 5
    {
        GLog::refine(_parent->ID.id,item_type, level_result, rst, material_need);
    }
}
Далее идет обработка результата улучшение rst (возвращаемое значение метода RefineAddon), выполняется обмен данными между серверной частью приложения и игрой. Все вызовы функций, начинающихся с _runner — это отправка данных на сторону клиента.
C++:
switch(rst)
{
    case item::REFINE_CAN_NOT_REFINE: // обработка невалидного кейса: снаряжение не может быть улучшено
    _runner->error_message(S2C::ERR_REFINE_CAN_NOT_REFINE);
    return true;
    break;

    case item::REFINE_SUCCESS: // обработка успешного улучшение
    _runner->refine_result(0);
    /*
Насколько я понял, вызов PlayerGetItemInfo необходим для обновления характеристик персонажа на
    стороне клиента. Внутри содержатся отправки сообщения для клиентской части игры.
    */
    PlayerGetItemInfo(IL_INVENTORY,index);
    break;

    case item::REFINE_FAILED_LEVEL_0: // обработка случая, когда уровень заточки снаряжения не изменился
    _runner->refine_result(1);
    break;

    default: // еще более невалидный кейс, чем REFINE_CAN_NOT_REFINE
    GLog::log(GLOG_ERR,"精炼装备时返回了异常错误%d",rst);

    case item::REFINE_FAILED_LEVEL_1: // обработка неуспешного улучшение со сбросом уровня заточки на 1
    _runner->refine_result(2);
    PlayerGetItemInfo(IL_INVENTORY,index); // см. выше
    break;

    case item::REFINE_FAILED_LEVEL_2: // обработка неуспешного улучшение со сбросом заточки в ноль
    _runner->refine_result(3);
    PlayerGetItemInfo(IL_INVENTORY,index); // см. выше
    break;
}
Заключительный блок функции RefineItemAddon завершает попытку усовершенствования удалением используемых ресурсов, т.е. миража/-ей и камня улучшения, если таковой был применен.
C++:
// удаление камня бессмертных из инвентаря. Содержит в себе как серверную логику, так и отправку информации на сторону клиента
RemoveItems(material_id,material_need, S2C::DROP_TYPE_USE, true);

/*
Eсли использовался камень улучшения, то же самое делаем для него. По какой-то причине этот код не вынесен в отдельную функцию, в отличии от
RemoveItems, где первый входной параметр id предмета, а не его индекс в инвентаре
*/
if(rt_index >= 0)
{
    item& it = _inventory[rt_index]; // получения предмета по индексу из инвентаря
    /*
    Вызов данной функции не имеет непосредственного отношения к теме улучшения снаряжения. Они разбросаны по всему коду,
    как только изменяется количество айтемов в инвентаре. Я не стал разбираться, что скрывается под «Mall Consumption»
    */
    UpdateMallConsumptionDestroying(it.type, it.proc_type, 1);

    _inventory.DecAmount(rt_index, 1); // уменьшение количества камней улучшения на 1
    _runner->player_drop_item(IL_INVENTORY,rt_index,rt_id, 1 ,S2C::DROP_TYPE_USE); // отправка сообщения об этом клиенту
}
Фактичеки на этом цикл улучшения снаряжения завершается. Но мы только на половине пути, т.к. выше я лишь упомянул о вызове equip_item::RefineAddon. Пришло время заглянуть и ей «под капот».

equip_item::RefineAddon​

Если до этого мы разбирали метод класса «Player», то сейчас речь пойдет о методе класса «Equipment Item», то есть «предмет экипировки». Это означает, что теперь внутри функции доступны все поля (характеристики) класса улучшаемой экипировки. Как и ранее, сначала привожу полный код без комментариев.
C++:
struct refine_param_t
{
    int need_level;
    float prop[4];  //分别对应 成功
};

static refine_param_t refine_table[]=
{
    {0 ,{ 0.50, 0.7,  0 ,0 }},
    {1 ,{ 0.30, 0,    0 ,1 }},
    {2 ,{ 0.30, 0,    0 ,1 }},
    {3 ,{ 0.30, 0,    0 ,1 }},
    {4 ,{ 0.30, 0,    0 ,1 }},
    {5 ,{ 0.30, 0,    0 ,1 }},
    {6 ,{ 0.30, 0,    0 ,1 }},
    {7 ,{ 0.30, 0,    0 ,1 }},
    {8 ,{ 0.25, 0,    0 ,1 }},
    {9 ,{ 0.20, 0,    0 ,1 }},
    {10,{ 0.12, 0,    0 ,1 }},
    {11,{ 0.05, 0,    0 ,1 }},
};

static float refine_factor[] =
{
    0,    //not use
    1.0f,
    2.0f,
    3.05f,
    4.3f,
    5.75f,
    7.55f,
    9.95f,
    13.f,
    17.05f,
    22.3f,
    29.f,
    37.5f,
};

static int refine_failed_type[] =
{
    item::REFINE_SUCCESS,
    item::REFINE_FAILED_LEVEL_0,
    item::REFINE_FAILED_LEVEL_1,
    item::REFINE_FAILED_LEVEL_2,
};


int
equip_item::RefineAddon(int addon_id, int  & level_result, float adjust[4], float adjust2[12])
{
    //第一步寻找正确的addon内容
    size_t addon_level = 0;
    int addon_index = -1;
    size_t count = _total_addon.size();
    for(size_t i = 0; i < count; i ++)
    {
        addon_data & data = _total_addon[i];
        int id = addon_manager::GetAddonID(data.id);
        if(id == addon_id)
        {
            //得到已经升级后的数据
            addon_index = i;
            addon_level = data.arg[1];
            break;
        }
    }

    //超过或者达到最大的级别则不可升级
    if(addon_level >= sizeof(refine_table) / sizeof(refine_param_t)) return item::REFINE_CAN_NOT_REFINE;

    //保存原来的技能
    level_result = addon_level;

    //考虑概率
    float prop[4];
    memcpy(prop, refine_table[addon_level].prop,sizeof(prop));
    ASSERT(sizeof(prop) == sizeof(refine_table[addon_level].prop));
    //对prop进行修正
    prop[0] += adjust[0]; prop[1] += adjust[1];
    prop[2] += adjust[2]; prop[3] += adjust[3];

    if(adjust[1] > 0)
    {
        //若特殊保留概率大于0 则使用adjust2里面带有的成功概率 并忽视原有的成功概率
        prop[0] = adjust2[addon_level];
    }
    int rst = abase::RandSelect(prop, 4);
    int failed_type = refine_failed_type[rst];

    if(failed_type != item::REFINE_SUCCESS)
    {
        //未成功,考虑如何进行处理
        switch(failed_type)
        {
            case item::REFINE_FAILED_LEVEL_0: //无变化
            return failed_type;

            case item::REFINE_FAILED_LEVEL_1: //装备降级 1级
            //第一次就失败,装备无变化
            if(addon_level == 0 || addon_index == -1) return item::REFINE_FAILED_LEVEL_0;
            if(addon_level == 1)
            {
                //第二次失败,等同于归0
                _total_addon.erase(_total_addon.begin() + addon_index);
                OnRefreshItem();
                return failed_type;
            }

            //其他情况 回到后面进行降级处理
            break;

            case item::REFINE_FAILED_LEVEL_2: //装备归0
            if(addon_index != -1)
            {
                _total_addon.erase(_total_addon.begin() + addon_index);
                OnRefreshItem();
            }
            return failed_type;

            default:
            ASSERT(false);
            return failed_type;
        }
    }

    addon_data  newdata;
    if(!world_manager::GetDataMan().generate_addon(addon_id,newdata)) return item::REFINE_CAN_NOT_REFINE;
    if(addon_index == -1)
    {
        ASSERT(failed_type == item::REFINE_SUCCESS);
        addon_level = 1;
        //新生成一个addon
        newdata.arg[0] = (int)(newdata.arg[0] * refine_factor[addon_level] + 0.1f);
        newdata.arg[1] = 1;    //当前级别为level1
        _total_addon.push_back(newdata);
    }
    else
    {
        if(failed_type == item::REFINE_FAILED_LEVEL_1)
            addon_level -= 1;
        else
            addon_level += 1;

        _total_addon[addon_index].arg[0] = (int)(newdata.arg[0] * refine_factor[addon_level] + 0.1f);
        _total_addon[addon_index].arg[1] = addon_level;

    /*    if(addon_manager::RefineAddonData(_total_addon[addon_index], newdata, failed_type == item::REFINE_FAILED_LEVEL_1) != 0)
        {
            return item::REFINE_CAN_NOT_REFINE;
        }
        */
    }

    OnRefreshItem();
    return failed_type;
}

Пошаговый разбор стоит начать с комментирования структуры и глобальных массивов, объявленных перед реализацией метода.
C++:
/*
Объявления структуры – нового типа данных, содержащей в себе два поля:
float prop[4] – массив вещественных чисел из 4-х элементов, соответствует шансам
исходов REFINE_SUCCESS, REFINE_FAILED_LEVEL_0, REFINE_FAILED_LEVEL_1, REFINE_FAILED_LEVEL_2.
Теоретически сумма вероятностей всех исходов должна всегда быть равна 1, но китайцы на это
забили и позднее будет понятно, почему это ничего не ломает в логике заточки.
int need_level – уровень заточки, к которому применимы данные шансы.
*/
struct refine_param_t
{
    int need_level;
    float prop[4];
};

/*
Объявление глобального массива с шансами исходов для каждого уровня заточки снаряжения без учета камней улучшения.
Т.е. ниже приведены шансы заточки миражами, которые ничем не отличаются от таблички шансов, впервые опубликованной на
https://pwinfo.fandom.com/ru/wiki/Заточка под заголовком "Шансы успеха заточки".
*/
static refine_param_t refine_table[]=
{
    /*
    Порядок шансов: REFINE_SUCCESS (+1), REFINE_FAILED_LEVEL_0 (+0), REFINE_FAILED_LEVEL_1 (-1), REFINE_FAILED_LEVEL_2 (0)
    Еще раз отмечаю, что сумма вероятностей всех исходов должна быть равна 1, однако реализация RandomSelect ниже позволяет
    указывать шансы после REFINE_SUCCESS любые. На самом же деле, для точки шансы с 0 -> +1,
    шанс успех = 50%, шанс REFINE_FAILED_LEVEL_0 тоже 50% (а не 70%).
    */
    {0 ,{ 0.50, 0.7,  0 ,0 }}, // шансы  0  -> +1:  успех 50%, уровень не изменился 50%
    {1 ,{ 0.30, 0,    0 ,1 }}, // шансы +1  -> +2:  успех 30%, сброс в ноль 70%
    {2 ,{ 0.30, 0,    0 ,1 }}, // шансы +2  -> +3:  успех 30%, сброс в ноль 70%
    {3 ,{ 0.30, 0,    0 ,1 }}, // шансы +3  -> +4:  успех 30%, сброс в ноль 70%
    {4 ,{ 0.30, 0,    0 ,1 }}, // шансы +4  -> +5:  успех 30%, сброс в ноль 70%
    {5 ,{ 0.30, 0,    0 ,1 }}, // шансы +5  -> +6:  успех 30%, сброс в ноль 70%
    {6 ,{ 0.30, 0,    0 ,1 }}, // шансы +6  -> +7:  успех 30%, сброс в ноль 70%
    {7 ,{ 0.30, 0,    0 ,1 }}, // шансы +7  -> +8:  успех 30%, сброс в ноль 70%
    {8 ,{ 0.25, 0,    0 ,1 }}, // шансы +8  -> +9:  успех 25%, сброс в ноль 75%
    {9 ,{ 0.20, 0,    0 ,1 }}, // шансы +9  -> +10: успех 20%, сброс в ноль 80%
    {10,{ 0.12, 0,    0 ,1 }}, // шансы +10 -> +11: успех 12%, сброс в ноль 88%
    {11,{ 0.05, 0,    0 ,1 }}, // шансы +11 -> +12: успех 5%,  сброс в ноль 95%
};

/*
refine_factor используется для определения конечного улучшения на каждом уровне точки.
Известный факт, что буст к характеристики увеличиваете с повышением уровня заточки.
Например, добавка к здоровью для Безумного воина выглядит следующим образом:

1: +48
2: +96
3: +146
4: +206
5: +275
6: +362
7: +477
8: +624
9: +818
10: +1070
11: +1392
12: +1800

Базовое улучшение +48 хп.
Точка на +2 также добавляет 48хп (скейл-фактор 2), а +3 уже 50хп (скейл-фактор 3.05, 48 * 3.05 = 146),
Точка +12 уже добавляет аж 408 хп относительно точки +11 (48 * 37.5 = 1800)

*/
static float refine_factor[] =
{
    0,    //not use
    1.0f,
    2.0f,
    3.05f,
    4.3f,
    5.75f,
    7.55f,
    9.95f,
    13.f,
    17.05f,
    22.3f,
    29.f,
    37.5f,
};

/*
Объявление глобального массива, содержащего в себе все возможные исходы.
Этот массив будет использоваться для получения результата заточки после вызова функции рандома.
В данной реализации важен порядок элементов, ниже будет объяснено почему.
И да, название массива refine_failed_type с первым элементом REFINE_SUCCESS это класс :D
*/
static int refine_failed_type[] =
{
    item::REFINE_SUCCESS, // улучшение +1
    item::REFINE_FAILED_LEVEL_0, // нет улучшения, +0
    item::REFINE_FAILED_LEVEL_1, // сброс -1
    item::REFINE_FAILED_LEVEL_2, // сброс до 0
};
Далее повторим параметры функции. Хотя я их упоминал ранее при вызове метода, но как минимум имя одного из них изменилось:
  • addon_id – то, что раньше называлось refine_addon — id улучшаемой характеристики
  • level_result – выходной параметр (передается по ссылке), после завершения метода должен содержать в себе итоговый уровень заточки снаряжения (но не содержит, забыли китайцы присвоить значение 😂 т.к. level_result используется только для логирования, баг не нашли).
  • adjust – массив значений для корректировки шансов заточки, подробнее смотри комментарии к коду под спойлером «Изменение дефолтных шансов заточки»
  • adjust2[12] – массив значений для корректировки шансов заточки при использовании жемчуга или камней мироздания, подробнее смотри комментарии к коду под спойлером «Изменение дефолтных шансов заточки»
Первый блок кода содержит логику определения текущего уровня снаряжения и индекс улучшения (да, на мой взгляд довольно странный подход использовать везде как id, так и индекс, но как есть 🙃).
C++:
size_t addon_level = 0; // объявление искомых параметров — текущий уровень заточки
int addon_index = -1; // и индекс улучшения
size_t count = _total_addon.size(); // _total_addon — массив всех улучшений данной экипировки
for(size_t i = 0; i < count; i ++) // цикл по всем элементам массива
{
    addon_data & data = _total_addon[i]; // получение структуры улучшения
    int id = addon_manager::GetAddonID(data.id); // получение id рассматриваемого улучшения
    if(id == addon_id) // проверка, является ли рассматриваемое улучшение искомым (улучшаемым)
    {
        addon_index = i; // сохранение индекса улучшаемой характеристики
        addon_level = data.arg[1]; // сохранение текущего уровня точки
        break;
    }
}

/*
Очередная проверка невалидного кейса — текущий уровень снаряжения выше или равен 12.
Выражение sizeof(refine_table) / sizeof(refine_param_t) определяет размер массива refine_table,
который содержит 12 элементов с шансами заточки на каждом уровне.
В случае невалидного кейса происходит выход из функции и возврат REFINE_CAN_NOT_REFINE.
*/
if(addon_level >= sizeof(refine_table) / sizeof(refine_param_t)) return item::REFINE_CAN_NOT_REFINE;

/*
Если все ок, присваиваем выходному параметру текущий уровень точки.
Ниже после попытки улучшения level_result должен был бы быть обновлен, но это забыли сделать :D
*/
level_result = addon_level;
С нетерпением перехожу к следующему блоку, так как в нем наконец-то определяется результат заточки.
C++:
float prop[4]; // объявления массива итоговых вероятностей, будет передан в функцию RandSelect
memcpy(prop, refine_table[addon_level].prop,sizeof(prop)); // копирования вероятностей улучшения миражом для текущего уровня заточки из массива refine_table
ASSERT(sizeof(prop) == sizeof(refine_table[addon_level].prop)); // ASSERT — assertion (утверждение). Бесполезная проверка, всегда истина
// Корретировка шансов исходов при использовании камней улучшения
prop[0] += adjust[0]; // добавка к шансу REFINE_SUCCESS (+1), будет отличной от нуля при использовании Небесного или Подземного камня
prop[1] += adjust[1]; // добавка к шансу REFINE_FAILED_LEVEL_0 (+0), задана при использовании Камня мироздания и шаров, всегда 1 (100% шанс, если не успех)
prop[2] += adjust[2]; // добавка к шансу REFINE_FAILED_LEVEL_1 (-1), задана только при использовании Подземного камня, всегда 1 (100% шанс, если не успех)
prop[3] += adjust[3]; // добавка к шансу REFINE_FAILED_LEVEL_2 (0), всегда 0, не используется

/*
В случае, если вероятность REFINE_FAILED_LEVEL_0 отлична от нуля, т.е. используется шар или Камень мироздания, то стандартное значение вероятности успеха
улучшения переписывается значением из adjust2 (см. комментарии к коду под спойлером «Изменение дефолтных шансов заточки»).
Пусть X — текущий уровень заточки. Для шаров уровня > X, — шанс будет 1 (100%), иначе 0. Для мирозданок — соответствовать шансу из описания Камня мироздания.
*/
if(adjust[1] > 0)
{
    prop[0] = adjust2[addon_level];
}

/*
«Сердце» процесса заточки — вызов функция рандома. На вход принимает массив вероятностей событий и его размер. Возвращает индекс событие, которое произошло.
Ниже я разберу реализацию функции RandSelect и тогда наконец-то станет понятно, почему суммарное значение вероятностей здесь может быть больше 1 и почему важен порядок:
REFINE_SUCCESS, REFINE_FAILED_LEVEL_0, REFINE_FAILED_LEVEL_1, REFINE_FAILED_LEVEL_2
*/
int rst = abase::RandSelect(prop, 4);
int failed_type = refine_failed_type[rst]; // получения результата заточки. Все еще ору, что имя переменной failed_type, хотя здесь может лежать REFINE_SUCCESS
Перед тем, как разобрать функцию RandSelect, позвольте мне лирическое отступление.
Прежде всего, в рамках данной статьи мы НЕ будем разбирать реализацию рандома. Функция RandSelect является лишь оберткой для генератора случайных чисел (ГСЧ), используемого в PW. Вообще, в исходном коде игры и нет этой реализации ГСЧ. Как и любая серьезный проект, игра была создана на основе движка. В случае PW это Angelica, информации о котором в интернете, кстати, не так много. Так вот, именно в коде движка Angelica можно найти реализацию ГСЧ. Я смог найти исходники движка на просторах интернета, но код там нечитаемый без опирания на статью, по которой рандом был реализован. К слову, статью я тоже нашел — оставлю ссылку здесь. Ее можно было бы изучить и понять, но вот только вряд ли в этом будет смысл :) Это просто хорошо себя зарекомендовавшая реализация псевдослучайного генератора.
Вынес в спойлер, т.к. факт известный. Дело в том, что в цифровом мире не существует настоящего рандома. Цифровой мир определенный, всегда можно зафиксировать текущее состояние и всегда можно предсказать следующее (вопрос в сложности), т.к. все запрограммировано. Благо исследователи давно придумали такие алгоритмы псевдорандома, что предсказать следующее значение исходя из предыдущих невозможно без использования супер-компьютеров. Но псевдорандом обладает еще одной особенностью — он будет выдавать одну и ту же последовательность чисел при перезапуске программы. Это можно избежать, если «стартовать» последовательность с рандомного места. Так что большинство ГСЧ, инициализируется «сидом» — числом, которое должно быть случайным. Какой-то замкнутый круг получается, не правда ли? С этой проблемой помогают бороться производители процессоров, внедряя в свои чипы аналоговые механизмы, которые способны в произвольный момент времени вернуть 0 или 1 действительно рандомно (на основе теплового шума). Насколько мне известно, напрямую этот рандом не используется для реализации ГСЧ, но вот для генерации сида для псевдорандомного алгоритма — в самый раз!
Главное, что надо понимать: несмотря на эту позорную приставку «псевдо», эти алгоритмы давным-давно себя зарекомендовали как надежный инструмент и используются почти во всех областях разработки программного обеспечения.
В реализации RandSelect будет использоваться вызов функции RandomUniform. Пару слов о равномерном распределении случайной величины, и как обычно, без определений из Википедии ;) Последовательность чисел, генерируемая ГСЧ, должна подчиняться какому-то закону. В статистике/теории вероятностей выделяют несколько наиболее часто используемых законов: нормальное (гауссовское) распределение, равномерное распределение, распределение Пуассона. Все они математически обоснованы, имеют четко установленные свойства и удивительно часто ими можно описать процессы в реальном мире. Равномерное распределение, пожалуй что, наиболее интуитивно понятно. В нем значения случайной величины имеют одинаковую вероятность, а математическое ожидание (среднее значение) равно половине промежутка допустимых значений. Наиболее часто приводимый пример из жизни процесса, подчиняющегося равномерному распределению — это подбрасывание монетки. Важно помнить, что в равномерном распределение вероятность значения случайной величины не зависит от истории, т.е. предыдущих результатов. Другими словами, если у вас чудом выпало 10 решек подряд, вероятность 11 решки все так же остается 50%. То же самое и в заточке снаряжения. Я понимаю, что сложно принять, что шанс 8-го плюса после 7-ми + подряд абсолютно такой же, как после 7-ми -, но это факт и если вас это ломает, почитайте еще статей на этот счет — я уверен, их полно. Ну и последнее, но тоже очень важное: все законы определены на бесконечности. Это еще один «трюк», который может быть не так понятен людям без профильного образования. Если говорить простым языком: генерируемые по закону равномерного распределения числа будут соответствовать его свойствам только при большом числе проводимых испытаний. Чем меньше опытов проведено, тем выше вероятность того, что текущая последовательность чисел действительно равномерна на заданном промежутке.
Возвращаемся к RandSelect. В исходном коде представлено 2 реализации данной функции. Какая используется выбирается в зависимости от
наличия или отсутствия директивы #define __THREAD_SPEC_RAND__ (поточная спецификация рандома). Директива #define является параметры компиляции проекта. В зависимости от того, как будет собран проект, будет использоваться либо первая, либо вторая реализация. Разница между ними минимальная: в первом случае каждый поток имеет свой ГСЧ, в то время как во втором ГСЧ один на весь проект. Не смею предположить какая из реализаций используется на камбэке, да и узнать это невозможно. Но идейно они совершенно одинаковые.
C++:
#ifdef  __THREAD_SPEC_RAND__
inline int RandSelect(const float * option, int size)
{
    /*
    В случае использования поточной спецификации, обращаемся к ГСЧ, который
    является уникальным для данного потока. Запрашивается рандомное вещественное
    число в диапазоне [0,1] (диапазон по умолчанию)
    */
    float p = GetRandomGenInstance().RandomUniform();

    /*
    Этот цикл проверяет, к какому событию относится сгенерированное число.
    Напомню, что на вход у нас массив из 4 вероятностей различных исходов.
    Давайте рассмотрим корректные входные данные, где сумма вероятностей равна 1.
    Массив на вход: [0.5, 0.3, 0.1, 0.1]. Представим это графически:
    Имеется 4 промежутка, длина которых соответствует вероятностям:
         1        2    3  4
    [_ _ _ _ _][_ _ _][_][_]
    Представим, что RandomUniform() сгенерировало число 0.3.
    Т.к. 0.3 < 0.5, выбираем первый промежуток, т.е. произошло событие 1.
    Представим, что RandomUniform() вернуло число 0.9.
    Сначала проверяем, что это число не попало в первый промежуток:
    0.9 > 0.5. Для проверки следующего промежутка, нам нужно вычесть из числа
    размер первого промежутка, т.е. значение вероятности первого события:
    0.9 - 0.5 = 0.4; 0.4 > 0.3 => снова вычитаем второй промежуток и
    переходим к проверке третье промежутка: 0.4 - 0.3 = 0.1; 0.1 == 0.1.
    Т.к. знак в условие ниже нестрогий, число попадет в третий промежуток,
    а значит исходное число 0.9 соответствует тому, что произошло третье событие.
    Если сумма вероятностей событий на вход >= 1, данный алгоритм гарантирует, что
    будет выбран один из промежутков. Теперь, зная как реализован RandSelect, я
    предлагаю вам самостоятельно понять, почему передача входных массивов вида
    [0.5, 0.7, 0, 0] для +1 или [0.3, 0, 0, 1] для +2-+8 ничего не ломает в логике.
    */
    for(int i = 0; i < size; i ++,option ++)
    {
        if(p <= *option) return i;
        p -= *option;
    }

    /*
    Любопытный факт: если выполнение кода дошло до этой строчки, значит входные
    данные были невалидными, например, [0.3,0,0,0.5] при рандомном значении 0.9.
    В этом случае не следовало бы возвращать 0, который для данного вызова RandSelect
    является индексом события успешной заточки :D
    */
    return 0;
}
#else
inline int RandSelect(const float * option, int size)
{
    /*
    Данная функция отличается от предыдущей только способом обращения к ГСЧ.
    Т.к. в этой реализации используется глобальный ГСЧ __global_RandomGen, а игра
    так или иначе запущена многопоточно, нам необходимо заблокировать ГСЧ для
    остальных потоков, пока текущий взаимодействует с ним. Это классический
    подход использования глобальных переменных в многопоточном программировании для
    предотвращения класса проблем, известных под именем «data race».
    */
    spin_autolock keeper(__global_RandomLock); // блокировка ГСЧ для других потоков
    float p = __global_RandomGen.RandomUniform();

    /* Тот же самый код, см. описание выше */
    for(int i = 0; i < size; i ++,option ++)
    {
        if(p <= *option) return i;
        p -= *option;
    }

    return 0;
} // разблокировка использования ГСЧ для других потоков происходит автоматически тут
#endif
В заключительной части функции equip_item::RefineAddon идет блок обработки результата улучшения. При необходимости выполняется изменение параметров снаряжения.
C++:
/*
Cперва идут обработки всех случаев неудачного улучшения, кроме случая неудачи подземкой без сбития в ноль
(он будет обработан в следующей ветке).
*/
if(failed_type != item::REFINE_SUCCESS)
    switch(failed_type)
    {
        case item::REFINE_FAILED_LEVEL_0: // улучшение не произошло, уровень не изменился (+0 или мирозданка)
        return failed_type;

        case item::REFINE_FAILED_LEVEL_1: // улучшение не произошло, уровень снижен на 1 (подземный камень)
        if(addon_level == 0 || addon_index == -1) return item::REFINE_FAILED_LEVEL_0; // если точка была 0, то это кейс выше
        if(addon_level == 1) // если точка была +1, необходимо удалить улучшаемую характеристику у предмета
        {
            _total_addon.erase(_total_addon.begin() + addon_index); // удаление характеристики
            /*
            Функция OnRefreshItem вызывается при каждом изменении характеристик снаряжения.
            Тригерит обновление всех завязанных на снаряжение логик, таких как окно характеристик персонажа или хп-бар.
            */
            OnRefreshItem();
            return failed_type;
        }
        break; // если произошло уменьшение уровня заточки на 1, но при этом точка не сбита в ноль, нужно перейти во вторую часть функции

        case item::REFINE_FAILED_LEVEL_2: // улучшение не произошло, уровень снаряжения сброшен в ноль (небеска или без камня улучшения)
        if(addon_index != -1)
        {
            _total_addon.erase(_total_addon.begin() + addon_index); // удаление существующей улучшаемой характеристики у снаряжения
            OnRefreshItem(); // см. выше
        }
        return failed_type;

        default: // невалидные кейсы
        ASSERT(false);
        return failed_type;
    }
}
/*
Далее идет обработки случаев, в результате которых на предмете остается улучшение и имеет место изменения его уровня,
т.е. успех или неудача подземным камнем при изначальной точке выше +1
*/
addon_data  newdata;
if(!world_manager::GetDataMan().generate_addon(addon_id,newdata)) return item::REFINE_CAN_NOT_REFINE; // генерация нового улучшения
if(addon_index == -1) // эта проверка описывает случай, когда на предмете еще не существует улучшаемой характеристики (точка 0)
{
    ASSERT(failed_type == item::REFINE_SUCCESS); // проверка, что попали в этот блок при успешной заточке
    addon_level = 1; // сохранение нового уровня заточки снаряжения
    newdata.arg[0] = (int)(newdata.arg[0] * refine_factor[addon_level] + 0.1f); // присваивание значения улучшения в соответствии с refine_factor
    newdata.arg[1] = 1;    // сохранение того же уровня заточки в объекте, описывающем улучшение
    _total_addon.push_back(newdata); // добавление нового улучшения в список всех улучшений снаряжения
}
else // в этой ветке рассматриваются случаи, когда улучшаемый предмет уже обладает улучшаемой характеристикой
{
    if(failed_type == item::REFINE_FAILED_LEVEL_1) // если неуспех подземкой (попадаем сюда из break switch конструкции в первой части)
        addon_level -= 1; // снижаем текущий уровень на 1
    else // наконец-то всеми желанный случай — успешное улучшение
        addon_level += 1; // повышаем текущий уровень на 1

    /*
    Ниже идет корректировка существующих характеристик снаряжения в зависимости от результата заточки.
    Изменяется величина добавки к характеристике и текущий уровень улучшения.
    */
    _total_addon[addon_index].arg[0] = (int)(newdata.arg[0] * refine_factor[addon_level] + 0.1f);
    _total_addon[addon_index].arg[1] = addon_level;
}

OnRefreshItem(); // см. выше
return failed_type; // возврат результата улучшения снаряжения в gplayer_imp::RefineItemAddon
Ну вот, собственно, и все. Далее результат улучшения возвращается в player.cpp в gplayer_imp::RefineItemAddon, т.е. мы переходим к коду, прокомментированному под спойлером «Вызов логики улучшения предмета и логирование результата».

Выводы​

Основываясь на исходный код, можно сформулировать следующие утверждения:
  1. Т.к. ни в одну функция не передается история предыдущих кликов, о никаких «комбо» и речи быть не может. Так же как и зарекомендовавшие себя алгоритмы псевдорандома не имеют никаких тенденций, по которым вы могли бы предугадать, попадет ли следующее сканированное вещественное число в диапазон успеха ([0.0 - 0.45] для +2-+8 с с использованием Небесного камня).
  2. Здесь же сразу хочу напомнить, что ГСЧ неспецифичны для вашей сессии заточки. Он либо один на весь проект и генерирует тысячи чисел в единицу времени для разных логик игры, либо потоково-специфичный, что сократит нагрузку в N-потоков раз, но не отменит того факта, что вы однозначно не получаете следующее в последовательности число даже если кликаете кнопку улучшения снаряжения без какой-либо задержки.
    rand_usages.png
  3. Процесс закрытие/открытия окна, смены локации, смены шмотки, перезахода в игру и любые другие манипуляции никак не влияют на результат улучшения. Каждый следующий клик является запуском логики полного цикла улучшения. Именно поэтому можете свободно кликать кнопку улучшения подряд до точки, которую хотите сделать. Это никак не уменьшит шанс успеха.
  4. Интересно, что если пренебречь временем, которое вы тратите на смену улучшаемой шмотки, то можно сказать, что по результату улучшения текущей шмотки можно было бы судить об успехе улучшения другой. Другими словами, обидное ощущение, которые вы испытываете при ситуация, когда уже хотели тыкнуть основную шмотку, выбивая комбо на левой, но все же в последний момент передумали и в результате увидели +, действительно имеет место быть :) Однако с учетом затраты время на смену шмотки и установки небесного камня, рандом уже вернет не тот же самый результат, ну и шансы успеха на левой могут быть выше.

FAQ (будет дополняться)​

  1. Вопрос: Прочитал полностью. Так и не понял, как мне на этом заработать?
    Ответ: Быстро и надежно точно никак. Анализируйте таблицу шансов и рынок продажи переноса, оценивайте риски и вырабатывайте стратегию. Единственное, что могу посоветовать, используйте Небесный камень начиная с +3 (не +2) — это выгоднее приблизительно на 30%. Ну и не тратьте ресурсы на набивание комб 😄 хотя, с другой стороны, прокликивание левой шмотки можно рассмотреть как фарм следующей +3, что тоже ок, если вам так морально проще.
  2. Вопрос: Ты хоть сам веришь, что +12 можно заточить кликами подряд? 😂
    Ответ: Конечно верю! Ровно с тем же шансом, что и заточить +12 небесками любым другим способом. Шанс, к слову, 1.695 * 10⁻⁵, а математическое ожидание 60 000 кликов, т.е. около 49ккк по нынешней стоимости расходников. Это если кликать небом начиная с 0, стоимость существенно уменьшится, если тыкать небом с +3, но все еще не будет достаточно близкой к реальной цене +12 на серве (6.5ккк). Так что еще один совет к первому вопросу — точите перенос на продажу до +9. А в качестве пруфа реальности заточки кликами подряд, предлагаю ознакомиться с экспериментом Каменщика на поднятом сервере и с моим опытом точки на ComebackPW 😊 (бескрайняя благодарность брату за помощь в монтаже видео 🥰). Фактически, было 8 подходов, количество миражей разнилось от 750 до 4000.
  3. Вопрос: У самого получается на этом зарабатывать?
    Ответ: Да. Но детели раскрывать не хочу, дабы избежать лишней конкуренции.
 
Последнее редактирование:

Альк

Форум - мой дом
Игрок ComebackPW
Регистрация
28 Апр 2023
Сообщения
62
Реакции
22
Баллы
145
Сервер
  1. Жду 1.4.6 [X]
Статья любопытная, но не думаю, что те, кто верит в смену шмотки/локации, прочитают и изменят свое мнение. Для остальных просто подтверждение,что рандом - это рандом)

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

Часть про заработок и доходность сильно зависит от текущих цен и относится скорее к другой теме.
 
Последнее редактирование:

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
Да, большинство людей останутся при своем мнении, я давал себе об этом отчет, когда выполнял работу. Не остановило :D

Выглядит как полное противоречие смыслу статьи.
Если под смыслом статьи здесь подразумевается утверждение, что заточка снаряжения — независимый от предыдущего результата процесс, то я не вижу противоречия. Вероятно недостаточно точно изъяснился, но я лишь хотел подчеркнуть тот факт, что успешная заточка — это генерация вещественного числа, попадающего в диапазон [0;X], где X зависит от текущего уровня точки и камней улучшения. Это значит, что если не говорить о высоких точках, где Х значительно уменьшается, то успех на левой шмотке миражем (сгенерированное число попало в диапазон [0, 0.5]) с вероятностью 90% привел бы к успеху и на основной шмотке с использованием небесного камня (диапазон [0, 0.45]). Цикл запустится заново в любом из случаев, просто если пренебречь временем на замену шмотки (за которую кто-нибудь другой уже крутнет рандом), то можно сказать, что по результату клика миражем на левой можно судить, повезло бы тебе заточить основную в тот момент. А так это все тоже песочница, наблюдение, которое никак не применить с пользой для точки :)

Часть про заработок и доходность сильно зависит от текущих цен и относится скорее к другой теме.
Совершенно верно. Наверное, это людям бы было интереснее почитать, но я могу допустить, что раскрытие данной темы может повлиять на рынок продажи переноса не лучшем для меня образом, так что оставим это на будущее :whistle: а здесь лишь в общих словах
 
Последнее редактирование:

Альк

Форум - мой дом
Игрок ComebackPW
Регистрация
28 Апр 2023
Сообщения
62
Реакции
22
Баллы
145
Сервер
  1. Жду 1.4.6 [X]
с вероятностью 90% привел бы к успеху и на основной шмотке
Такая формулировка в глазах "верующего" человека может выглядеть как прямое указание на то, что если бы он поставил основную шмотку, то получил бы успешный результат (что не так, поскольку это несвязанные процессы). То, что здесь рассматривается как уточнение мелкой детали
которое никак не применить с пользой для точки
для другого будет основанием для очередного предрассудка. Поэтому мне и кажется данный пункт лишним.
 

Nаrkоtik

Вошел во вкус
Игрок ComebackPW
Регистрация
6 Сен 2024
Сообщения
42
Реакции
31
Баллы
25
Сервер
  1. Жду 1.4.6 [X]
Раньше и в открытие сундуков в N локах не верили =)
Точка офк фундаментальна рандом, но где то и при определенных условиях этот рандом не так рандомен.

p.s думал в теме речь пойдет за игру нипами xD
 

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
Точка офк фундаментальна рандом, но где то и при определенных условиях этот рандом не так рандомен.
Исходный код говорит об обратном :)
Раньше и в открытие сундуков в N локах не верили =)
А что это значит? Что такое N-локи? Не слышал об этом.
 

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
На правах автора темы, минутка флуда:
Сегодня я тыкнул с +6 до +9 подряд миражами, не заметив, что поставил не голую шмотку. К сожалению, хэпи энда не случилось — я так и вспомнил об этом и сбил ее небом на +10, осознав, что произошло, лишь спустя несколько минут. Ну чтож, хорошо, что не +11 😄

Кому интересно, видео есть, могу поделиться в тг, пишите.
 

pecador

Фанат Comeback`a
Игрок ComebackPW
Регистрация
18 Ноя 2022
Сообщения
2,178
Реакции
1,573
Баллы
420
Адрес
Города Истоков
Сервер
  1. 1.3.6
  2. Жду 1.3.6 [X]
Сексуальная тема, но жалко что останутся люди с таким мнением:
Раньше и в открытие сундуков в N локах не верили =)
Точка офк фундаментальна рандом, но где то и при определенных условиях этот рандом не так рандомен.

Кста, где-то на просторах инета встречал частичные нескоспиленые исходники 1.5.3 и 1.5.5 версии, вроде серверные файлы там тоже были.
 

Ty4ka🌧

ПОКРЫЛСЯ ПЫЛЬЮ
Игрок ComebackPW
Регистрация
23 Апр 2022
Сообщения
4,041
Реакции
3,345
Баллы
720
Сервер
  1. 1.3.6
Сексуальная тема
Ну тут скорее чисто ликбез-лекция по статичным десятилетиями вещам. Кто имел желание удостовериться в работе рандома давно уже сделали и свои практические эксперименты и выводы

Как по мне, реально сексуальная тема -- это неткод и всё что на нём повязано: микро/макроотбрасывания; серверный tickrate и клиентский cmdrate и их влияние на механики движения; интерполяция -- расчёт её величины в динамике и влияние на (не)возможность начать кастовать что-то или срыв каста на клиентской стороне. Если довести до ума один только неткод (до стандартов, хотя бы, игр середины 10-ых годов) можно было бы на порядок качество игры поднять
 

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
Как по мне, реально сексуальная тема -- это неткод и всё что на нём повязано: микро/макроотбрасывания; серверный tickrate и клиентский cmdrate и их влияние на механики движения; интерполяция -- расчёт её величины в динамике и влияние на (не)возможность начать кастовать что-то или срыв каста на клиентской стороне. Если довести до ума один только неткод (до стандартов, хотя бы, игр середины 10-ых годов) можно было бы на порядок качество игры поднять

Ну, такое только когда Кос с офером придет в лс 😂

Кто имел желание удостовериться в работе рандома давно уже сделали и свои практические эксперименты и выводы
Боже, как я это пропустил 😄 отличный материал, спасибо за линк
 
Последнее редактирование:

pecador

Фанат Comeback`a
Игрок ComebackPW
Регистрация
18 Ноя 2022
Сообщения
2,178
Реакции
1,573
Баллы
420
Адрес
Города Истоков
Сервер
  1. 1.3.6
  2. Жду 1.3.6 [X]
Как по мне, реально сексуальная тема -- это неткод и всё что на нём повязано: микро/макроотбрасывания; серверный tickrate и клиентский cmdrate и их влияние на механики движения; интерполяция -- расчёт её величины в динамике и влияние на (не)возможность начать кастовать что-то или срыв каста на клиентской стороне. Если довести до ума один только неткод (до стандартов, хотя бы, игр середины 10-ых годов) можно было бы на порядок качество игры поднять
Как-будто проще и быстрее переписать эту шнягу с нуля, чем рефакторить неткод китайской игры 20летней давности. В любом случае, как по мне, это уже за гранью фантастики, ибо такие мувы никогда себя не окупят, учитывая количество аудитории.
 

Представитель Continental

Форум - мой дом
Игрок ComebackPW
Регистрация
25 Мар 2024
Сообщения
262
Реакции
249
Баллы
145
Сервер
  1. Жду 1.3.6 [X]
Крутая статья, много текста. Спасибо.

Но я так и не понял, стоит ли верить Вей Сяо Бао, когда он говорит, что на такой то территории шанс улучшения выше?
 

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
сексуальная тема -- это неткод и всё что на нём повязано: микро/макроотбрасывания
не сдержался не поделиться: я на самом деле еще джунгли бегаю по фану регулярно, так что это моя личная боль
 

ЗХСТАНЮХА

Голос народа
Игрок ComebackPW
Регистрация
8 Июл 2024
Сообщения
10,838
Реакции
17,935
Баллы
1,105
Адрес
Город Оборотней
Сервер
  1. 1.3.6
  2. 1.4.6

Девиация

Лайкают даже админы
Игрок ComebackPW
Регистрация
21 Апр 2021
Сообщения
251
Реакции
203
Баллы
585
Сервер
  1. 1.3.6
  2. 1.4.6
Кто все прочитал - о чем текст?
 

Бочкарев

Здесь могла быть ваша реклама
Игрок ComebackPW
Регистрация
19 Мар 2023
Сообщения
453
Реакции
734
Баллы
305
Сервер
  1. 1.3.6
Кто все прочитал - о чем текст?
текст о том, что если попрыгать с бубном, сесть на бутылку, а потом сделать тык на +12, твои шансы на точку не вырастут от проделанных ранее мувов
текст о том, что с каждым неудачным тыком шанс на точку не увеличивается, а то есть люди, которые думают, что 50% успеха = 2 тыка = 100%
крч тут всякие кодеры программисты америку открывают после окончания онлайн школ
 
Последнее редактирование:

KoDim97

Форум - мой дом
Игрок ComebackPW
Регистрация
12 Авг 2024
Сообщения
54
Реакции
33
Баллы
105
Сервер
  1. 1.4.6
крч тут всякие кодеры программисты америку открывают после окончания онлайн школ
Ну во-первых это мимо, а во-вторых я прекрасно и до изучения кода понимал, как это работает. Мне было интересно посмотреть на реализацию и я поделился с теми, кому тоже.
 

Бочкарев

Здесь могла быть ваша реклама
Игрок ComebackPW
Регистрация
19 Мар 2023
Сообщения
453
Реакции
734
Баллы
305
Сервер
  1. 1.3.6
Ну во-первых это мимо, а во-вторых я прекрасно и до изучения кода понимал, как это работает. Мне было интересно посмотреть на реализацию и я поделился с теми, кому тоже.
это все меняет
даже исходный код
 
Сверху Снизу