Genshin Impact Server Reverse-Engineering Part 1: Artifacts

0x1 Genshin Impact Leaks Story

So someone in mihoyo leaks significantly amount of data from official production server, rumors said this is his revenge for being fired, but I heard he got caught already. RIP.

Leaked data consists of all server side configuration data( some shell scripts ), compiled game server binary(3.2 dev ~ 3.4 dev) and game related data( all game characters skill/talent till 4.0 dev etc…).

This leak is pretty huge, we can see all the characters skills till 4.0 dev, even some of them aren’t really useful because it is unfinished. Many people already publish what they found like Alhatham skillset. This part is easy because they are all just plain text json files.

As a junior reverse engineer, I’d like to challenge myself with the real big guy, like the real game server binary(2.4 Gb).

I’m reversing the 3.4 dev game server binary, which is newest in leaked data.

Loading such a large binary in IDA is pain in the ass, I spent almost 2 days just waiting IDA finish it, and the final idb was 11.3 Gb, largest idb I have seen in my life.

IDA recognizes 1598472 functions, thank god this file has dwarf debug information with full symbols and structure definitions, or I’ve given up already lol.

0x2 Artifact Generation Logic

The first thing I want to inspect was how the game server generate artifacts, or does it has bias for some properties.

BTW, artifacts and weapons has almost same logic, seems they all called reliquary in code.

Let’s see how reliquary initializes, this function seems like handle the reliquary initializations:

int32_t __cdecl Reliquary::init(Reliquary *const this, Player *player, bool is_first_create)
{
....
  if ( is_first_create )
  {
    ItemComp = Player::getItemComp(player);
    ItemId = Item::getItemId(this);
    main_prop_id = PlayerItemComp::generateReliquaryMainPropId(ItemComp, ItemId);
    if ( !main_prop_id )
    {
      common::milog::MiLogStream::create(
        &v25,
        &common::milog::MiLogDefault::default_log_obj_,
        4u,
        "./src/player/item/reliquary.cpp",
        "init",
        165);
      v12 = common::milog::MiLogStream::operator<<<char [45],(char *[45])0>(
              &v25,
              (const char (*)[45])"generateReliquaryMainPropId failed, item_id:");
      val = Item::getItemId(this);
      common::milog::MiLogStream::operator<<<unsigned int,(unsigned int *)0>(v12, &val);
      common::milog::MiLogStream::~MiLogStream(&v25);
      result = -1;
      goto LABEL_29;
    }
    Reliquary::setMainPropId(this, main_prop_id);
    std::vector<unsigned int>::clear(&this->append_prop_id_vec_);
    for ( idx = 0; ; ++idx )
    {
      if ( *(_BYTE *)(((unsigned __int64)&reliquary_config_ptr->append_prop_num >> 3) + 0x7FFF8000) != 0
        && (char)((((_BYTE)reliquary_config_ptr + 92) & 7) + 3) >= *(_BYTE *)(((unsigned __int64)&reliquary_config_ptr->append_prop_num >> 3)
                                                                            + 0x7FFF8000) )
      {
        __asan_report_load4(&reliquary_config_ptr->append_prop_num);
      }
      if ( idx >= reliquary_config_ptr->append_prop_num )
        break;
      ServiceBox::findService<GameserverService>();
      GameserverService::getConfig((GameserverService *const)&v24);
      p_reliquary_config_mgr = &std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false>::operator->((const std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false> *const)&v24)->design_config.txt_config_mgr.reliquary_config_mgr;
      MainPropId = Reliquary::getMainPropId(this);
      v14 = Item::getItemId(this);
      *(_DWORD *)(v3 + 32) = ReliquaryExcelConfigMgr::generateAppendPropId(
                               p_reliquary_config_mgr,
                               v14,
                               MainPropId,
                               &this->append_prop_id_vec_);
....
}

As you can see, it generates main property first, and then generates all sub properties(append props).

Seems legit, let’s see how it generate main property.

0x3 Artifact Main Property Generation

Looks like main property generation is handled by PlayerItemComp::generateReliquaryMainPropId, let’s inspect it.

 ....
    v99 = std::__detail::_Node_const_iterator<std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>,false,false>::operator*(&__for_begin);
    main_prop_id = std::get<0ul,unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>(v99);
    guarantee_config = (std::tuple_element<1,const std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig> >::type *)std::get<1ul,unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>(v99);
    guarantee_data = std::map<unsigned int,ReliquaryMainPropGuaranteeData>::operator[](
                       (std::map<unsigned int,ReliquaryMainPropGuaranteeData> *const)(v2 + 176),
                       main_prop_id);
    old_guarantee_data_ptr = common::tools::MiscUtils::findMapValuePointer<std::map<unsigned int,ReliquaryMainPropGuaranteeData>>(
                               &guarantee_depot->main_prop_guarantee_data_map,
                               main_prop_id);
    if ( old_guarantee_data_ptr )

I smelled something unusual by just scrolling the code, what the heck is guarantee_config ?

Looks like our artifact isn’t really fully random, they implemented some sort of pity function.

At very first of the function, it calls another function to generate a main property:

  ServiceBox::findService<GameserverService>();
  GameserverService::getConfig((GameserverService *const)(v2 + 144));
  reliquary_config_mgr = &std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false>::operator->((const std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false> *const)(v2 + 144))->design_config.txt_config_mgr.reliquary_config_mgr;
  std::shared_ptr<Config>::~shared_ptr((std::shared_ptr<Config> *const)(v2 + 144));
  *(_DWORD *)(v2 + 32) = ReliquaryExcelConfigMgr::generateMainPropId(reliquary_config_mgr, *(_DWORD *)(v2 + 96));
  reliquary_config_ptr = data::ReliquaryExcelConfigMgrBase::findReliquaryExcelConfig(
                           reliquary_config_mgr,
                           *(unsigned int *)(v2 + 96));

Let’s jump into ReliquaryExcelConfigMgr::generateMainPropId .

....
  *(_DWORD *)(v2 + 80) = item_id;
  reliquary_config_ptr = data::ReliquaryExcelConfigMgrBase::findReliquaryExcelConfig(this, *(unsigned int *)(v2 + 80));
....
      if ( common::tools::RandomUtils::weightSelectOne<data::ReliquaryMainPropExcelConfig,unsigned int data::ReliquaryMainPropExcelConfig::*>(
             main_prop_vec,
             (uint32_t *)(v2 + 64),
             (unsigned int *)0x14,
             0) )
      {
....
        v15 = *(unsigned int *)(v2 + 64);
        if ( v15 < std::vector<data::ReliquaryMainPropExcelConfig>::size(main_prop_vec) )
        {
          if ( *(_BYTE *)(((v2 + 64) >> 3) + 0x7FFF8000) != 0 && *(_BYTE *)(((v2 + 64) >> 3) + 0x7FFF8000) <= 3 )
            __asan_report_load4(v2 + 64);
          v20 = std::vector<data::ReliquaryMainPropExcelConfig>::operator[](main_prop_vec, *(unsigned int *)(v2 + 64));
          if ( *(_BYTE *)(((unsigned __int64)&v20->id >> 3) + 0x7FFF8000) != 0
            && *(_BYTE *)(((unsigned __int64)&v20->id >> 3) + 0x7FFF8000) <= 3 )
          {
            __asan_report_load4(&v20->id);
          }
          result = v20->id;
....

I remove some asan stuff to simplify the procedure, as you can see, the server called common::tools::RandomUtils::weightSelectOne to select the main property, this function name implies the main property is selected via some weights, not really pure random.

In weightSelectOne:

  for ( idx = 0LL; idx < std::vector<data::ReliquaryMainPropExcelConfig>::size(input_vec); ++idx )
  {
    v7 = (unsigned __int64)member + (_QWORD)std::vector<data::ReliquaryMainPropExcelConfig>::operator[](input_vec, idx);
    new_sum_weight = *(_DWORD *)v7 + sum_weight;
    if ( new_sum_weight < sum_weight )
      goto LABEL_12;
    v8 = (unsigned int *)((char *)member
                        + (_QWORD)std::vector<data::ReliquaryMainPropExcelConfig>::operator[](input_vec, idx));
    if ( new_sum_weight >= *v8 )
      v9 = 0;
    else
LABEL_12:
      v9 = 1;
    if ( v9 )
    {
      result = -1;
      goto LABEL_40;
    }
    sum_weight = new_sum_weight;
  }
.....
    *select_idx = 0;
    if ( random_seed )
      rand_weight = common::tools::RandomUtils::rand<unsigned int>(random_seed) % sum_weight;
    else
      rand_weight = common::tools::RandomUtils::rand<unsigned int>() % sum_weight;
    for ( idx_0 = 0LL; idx_0 < std::vector<data::ReliquaryMainPropExcelConfig>::size(input_vec); ++idx_0 )
    {
      v12 = (unsigned int *)((char *)member
                           + (_QWORD)std::vector<data::ReliquaryMainPropExcelConfig>::operator[](input_vec, idx_0));
      if ( rand_weight < *v12 )
      {
        v13 = idx_0;
        *select_idx = v13;
        break;
      }
      v14 = (unsigned __int64)member
          + (_QWORD)std::vector<data::ReliquaryMainPropExcelConfig>::operator[](input_vec, idx_0);
      rand_weight -= *(_DWORD *)v14;
    }
    result = 0;

Looks like a standard random bias selection algorithm, the member parameter(0x14 here) indicates which column contains weight data, the seed is 0 means no seed.

Since this data in configured in server side, we cannot know server config without access to the prod server, but the leaker actually dumped the server config so here’s the table!

Main property ID(Parsed by me) Main depot ID(Parsed) Type Weights
FIGHT_PROP_HP Sand-1000 2 0
FIGHT_PROP_HP_PERCENT Sand-1000 3 1334
FIGHT_PROP_ATTACK Sand-1000 5 0
FIGHT_PROP_ATTACK_PERCENT Sand-1000 6 1333
FIGHT_PROP_DEFENSE Sand-1000 8 0
FIGHT_PROP_DEFENSE_PERCENT Sand-1000 9 1333
FIGHT_PROP_CHARGE_EFFICIENCY Sand-1000 23 500
FIGHT_PROP_ELEMENT_MASTERY Sand-1000 28 500
FIGHT_PROP_FIRE_SUB_HURT Sand-1000 50 0
FIGHT_PROP_ELEC_SUB_HURT Sand-1000 51 0
FIGHT_PROP_ICE_SUB_HURT Sand-1000 56 0
FIGHT_PROP_WATER_SUB_HURT Sand-1000 52 0
FIGHT_PROP_WIND_SUB_HURT Sand-1000 54 0
FIGHT_PROP_ROCK_SUB_HURT Sand-1000 55 0
FIGHT_PROP_GRASS_SUB_HURT Sand-1000 53 0
FIGHT_PROP_ATTACK Feather-2000 5 1000
FIGHT_PROP_HP Circlet-3000 2 0
FIGHT_PROP_HP_PERCENT Circlet-3000 3 1100
FIGHT_PROP_ATTACK Circlet-3000 5 0
FIGHT_PROP_ATTACK_PERCENT Circlet-3000 6 1100
FIGHT_PROP_DEFENSE Circlet-3000 8 0
FIGHT_PROP_DEFENSE_PERCENT Circlet-3000 9 1100
FIGHT_PROP_CRITICAL Circlet-3000 20 500
FIGHT_PROP_CRITICAL_HURT Circlet-3000 22 500
FIGHT_PROP_HEAL_ADD Circlet-3000 26 500
FIGHT_PROP_ELEMENT_MASTERY Circlet-3000 28 200
FIGHT_PROP_HP Flower-4000 2 1000
FIGHT_PROP_HP Goblet-5000 2 0
FIGHT_PROP_HP_PERCENT Goblet-5000 3 770
FIGHT_PROP_ATTACK Goblet-5000 5 0
FIGHT_PROP_ATTACK_PERCENT Goblet-5000 6 770
FIGHT_PROP_DEFENSE Goblet-5000 8 0
FIGHT_PROP_DEFENSE_PERCENT Goblet-5000 9 760
FIGHT_PROP_ELEMENT_MASTERY Goblet-5000 28 100
FIGHT_PROP_FIRE_ADD_HURT Goblet-5000 40 200
FIGHT_PROP_ELEC_ADD_HURT Goblet-5000 41 200
FIGHT_PROP_ICE_ADD_HURT Goblet-5000 46 200
FIGHT_PROP_WATER_ADD_HURT Goblet-5000 42 200
FIGHT_PROP_WIND_ADD_HURT Goblet-5000 44 200
FIGHT_PROP_ROCK_ADD_HURT Goblet-5000 45 200
FIGHT_PROP_GRASS_ADD_HURT Goblet-5000 43 200
FIGHT_PROP_PHYSICAL_ADD_HURT Goblet-5000 30 200

Wait, what about the guarantee thing?

Let’s go back to PlayerItemComp::generateReliquaryMainPropId. I removed all the asan stuff and log stuff.

// Generate main property 
 *(_DWORD *)(v2 + 96) = reliquary_id;
  ServiceBox::findService<GameserverService>();
  GameserverService::getConfig((GameserverService *const)(v2 + 144));
  reliquary_config_mgr = &std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false>::operator->((const std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false> *const)(v2 + 144))->design_config.txt_config_mgr.reliquary_config_mgr;
  std::shared_ptr<Config>::~shared_ptr((std::shared_ptr<Config> *const)(v2 + 144));
  *(_DWORD *)(v2 + 32) = ReliquaryExcelConfigMgr::generateMainPropId(reliquary_config_mgr, *(_DWORD *)(v2 + 96));
  reliquary_config_ptr = data::ReliquaryExcelConfigMgrBase::findReliquaryExcelConfig(
                           reliquary_config_mgr,
                           *(unsigned int *)(v2 + 96));
......
  if ( !common::tools::MiscUtils::isContains<std::unordered_set<unsigned int> const,unsigned int>(
          &reliquary_config_mgr->guaranteed_main_prop_depot_id_set,
          &reliquary_config_ptr->main_prop_depot_id) )
  {
    v10 = *(_DWORD *)(v2 + 32);
    goto LABEL_112;
  }
  if ( reliquary_config_ptr->rank_level <= 4 )
  {
    v10 = *(_DWORD *)(v2 + 32);
    goto LABEL_112;
  }
  *(_DWORD *)(v2 + 48) = reliquary_config_ptr->set_id;
  if ( !*(_DWORD *)(v2 + 48) )
  {
    v10 = *(_DWORD *)(v2 + 32);
    goto LABEL_112;
  }
  reliquary_set_config_ptr = data::ReliquaryExcelConfigMgrBase::findReliquarySetExcelConfig(
                               reliquary_config_mgr,
                               *(_DWORD *)(v2 + 48));
....
  *(_DWORD *)(v2 + 64) = reliquary_set_config_ptr->guarantee_depot_id;
  if ( !*(_DWORD *)(v2 + 64) )
  {
    v10 = *(_DWORD *)(v2 + 32);
    goto LABEL_112;
  }
...
  ServiceBox::findService<GameserverService>();
  GameserverService::getConfig((GameserverService *const)(v2 + 144));
  v23 = std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false>::operator->((const std::__shared_ptr_access<Config,(__gnu_cxx::_Lock_policy)2,false,false> *const)(v2 + 144));
  isReliquaryGuaranteeClosed = FeatureSwitchMgr::isReliquaryGuaranteeClosed(&v23->feature_switch_mgr);
  std::shared_ptr<Config>::~shared_ptr((std::shared_ptr<Config> *const)(v2 + 144));
  if ( isReliquaryGuaranteeClosed )
  {
...
    v10 = *(_DWORD *)(v2 + 32);
    goto LABEL_112;
  }
...
  v28 = (char *)(v2 + 64);
  guarantee_depot = std::map<unsigned int,ReliquaryGuaranteeDepot>::operator[](
                      &this->reliquary_guarantee_depot_map_,
                      (const std::map<unsigned int,ReliquaryGuaranteeDepot>::key_type *)(v2 + 64));
  v29 = *(_DWORD *)(v2 + 64);
  guarantee_depot->guarantee_depot_id = v29;
  std::map<unsigned int,ReliquaryMainPropGuaranteeData>::map((std::map<unsigned int,ReliquaryMainPropGuaranteeData> *const)(v2 + 176));
  *(_QWORD *)(v2 + 144) = 0LL;
  *(_QWORD *)(v2 + 152) = 0LL;
  __for_range = &reliquary_config_mgr->reliquary_main_prop_guarantee_excel_config_map;
  __for_begin._M_cur = std::unordered_map<unsigned int,data::ReliquaryMainPropGuaranteeExcelConfig>::begin(&reliquary_config_mgr->reliquary_main_prop_guarantee_excel_config_map)._M_cur;
  __for_end._M_cur = std::unordered_map<unsigned int,data::ReliquaryMainPropGuaranteeExcelConfig>::end(&reliquary_config_mgr->reliquary_main_prop_guarantee_excel_config_map)._M_cur;
  while ( std::__detail::operator!=<std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>,false>(
            &__for_begin,
            &__for_end) )
  {
    v99 = std::__detail::_Node_const_iterator<std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>,false,false>::operator*(&__for_begin);
    main_prop_id = std::get<0ul,unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>(v99);
    guarantee_config = (std::tuple_element<1,const std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig> >::type *)std::get<1ul,unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>(v99);
    guarantee_data = std::map<unsigned int,ReliquaryMainPropGuaranteeData>::operator[](
                       (std::map<unsigned int,ReliquaryMainPropGuaranteeData> *const)(v2 + 176),
                       main_prop_id);
    old_guarantee_data_ptr = common::tools::MiscUtils::findMapValuePointer<std::map<unsigned int,ReliquaryMainPropGuaranteeData>>(
                               &guarantee_depot->main_prop_guarantee_data_map,
                               main_prop_id);
    if ( old_guarantee_data_ptr )
    {
      guarantee_count = old_guarantee_data_ptr->guarantee_count;
      if ( guarantee_count < guarantee_config->min_guarantee_count )
        goto LABEL_54;
      if ( v36 <= guarantee_config->max_guarantee_count )
      {
        v56 = old_guarantee_data_ptr->miss_count + 1;
        guarantee_data->miss_count = v56;
        main_prop_depot_id = guarantee_config->main_prop_depot_id;
        if ( main_prop_depot_id == reliquary_config_ptr->main_prop_depot_id
          && old_guarantee_data_ptr->miss_count >= old_guarantee_data_ptr->guarantee_count )
        {
          *(_DWORD *)(v2 + 112) = *main_prop_id;
          *(_DWORD *)(v2 + 116) = old_guarantee_data_ptr->guarantee_count;
          *(_DWORD *)(v2 + 120) = old_guarantee_data_ptr->miss_count - old_guarantee_data_ptr->guarantee_count;
if ( !std::optional<PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData>::has_value((const std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144))
            || (v62 = std::optional<PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData>::value((std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144)),
                PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData::isBetterThan(
                  (const PlayerItemComp::generateReliquaryMainPropId::TriggeredMainPropData *const)(v2 + 112),
                  v62)) )
          {
            std::optional_PlayerItemComp::generateReliquaryMainPropId_uint32_t_::TriggeredMainPropData_::operator__PlayerItemComp::generateReliquaryMainPropId_uint32_t_::TriggeredMainPropData__(
              (std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144),
              (PlayerItemComp::generateReliquaryMainPropId::TriggeredMainPropData *)(v2 + 112));
          }
        }
      }
      else
      {
        guarantee_data->miss_count = 1;
        max_guarantee_count = guarantee_config->max_guarantee_count;
        v39 = common::tools::RandomUtils::rand<unsigned int>(guarantee_config->min_guarantee_count, max_guarantee_count);
        guarantee_data->guarantee_count = v39;
      }
    }
    else
    {
      guarantee_data->miss_count = 1;
      v31 = guarantee_config->max_guarantee_count;
      v32 = v31;
      v33 = common::tools::RandomUtils::rand<unsigned int>(guarantee_config->min_guarantee_count, v31);
      guarantee_data->guarantee_count = v33;
    }
    std::__detail::_Node_const_iterator<std::pair<unsigned int const,data::ReliquaryMainPropGuaranteeExcelConfig>,false,false>::operator++(&__for_begin);
  }
 *(_DWORD *)(v2 + 80) = *(_DWORD *)(v2 + 32);
  if ( std::optional<PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData>::has_value((const std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144)) )
  {
    v64 = std::optional<PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData>::value((std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144));
    v65 = v64;
    *(_DWORD *)(v2 + 80) = v65->main_prop_id;
  }
  guarantee_data_ptr = common::tools::MiscUtils::findMapValuePointer<std::map<unsigned int,ReliquaryMainPropGuaranteeData>>(
                         (std::map<unsigned int,ReliquaryMainPropGuaranteeData> *)(v2 + 176),
                         (const std::map<unsigned int,ReliquaryMainPropGuaranteeData>::key_type *)(v2 + 80));
  if ( guarantee_data_ptr )
  {
    guarantee_data_ptr->miss_count = 0;
    if ( std::optional<PlayerItemComp::generateReliquaryMainPropId(unsigned int)::TriggeredMainPropData>::has_value((const std::optional<PlayerItemComp::generateReliquaryMainPropId(uint32_t)::TriggeredMainPropData> *const)(v2 + 144)) )
    {
      guarantee_config_ptr = data::ReliquaryExcelConfigMgrBase::findReliquaryMainPropGuaranteeExcelConfig(
                               reliquary_config_mgr,
                               *(_DWORD *)(v2 + 80));
      v79 = guarantee_config_ptr->max_guarantee_count;
      v80 = v79;
      v81 = common::tools::RandomUtils::rand<unsigned int>(guarantee_config_ptr->min_guarantee_count, v79);
      guarantee_data_ptr->guarantee_count = v81;
    }
    v10 = *(_DWORD *)(v2 + 80);
    goto LABEL_111;
  }
  v10 = *(_DWORD *)(v2 + 32);
LABEL_111:
  std::map<unsigned int,ReliquaryMainPropGuaranteeData>::~map((std::map<unsigned int,ReliquaryMainPropGuaranteeData> *const)(v2 + 176));
LABEL_112:
  result = v10;
  return result;

In line 19, we can see that pity isn’t activated if artifact rank <=4.

In line 46, server can switch pity on/off at anytime. But it seems this is on for public official server.

In line 100, server checks all property pity count, add all property that matches pity count into a list.

In line 129, server selects first pity property that matches the selected depot.

So in short, the main property generation is weight biased and have a pity system for unlucky people like me.

Another question, what’s the probability for pity?

Fortunately, the leaker dumped that too, here’s the table.

Main property ID(Parsed) Depot(Parsed) Type Pity count Min/Max
FIGHT_PROP_HP_PERCENT Sand-1000 3 19 1;2
FIGHT_PROP_ATTACK_PERCENT Sand-1000 6 19 1;2
FIGHT_PROP_DEFENSE_PERCENT Sand-1000 9 19 1;2
FIGHT_PROP_CHARGE_EFFICIENCY Sand-1000 23 50 1;2
FIGHT_PROP_ELEMENT_MASTERY Sand-1000 28 50 1;2
FIGHT_PROP_ATTACK Feather-2000 5 5 1;2
FIGHT_PROP_HP_PERCENT Circlet-3000 3 23 1;2
FIGHT_PROP_ATTACK_PERCENT Circlet-3000 6 23 1;2
FIGHT_PROP_DEFENSE_PERCENT Circlet-3000 9 23 1;2
FIGHT_PROP_CRITICAL Circlet-3000 20 50 1;2
FIGHT_PROP_CRITICAL_HURT Circlet-3000 22 50 1;2
FIGHT_PROP_HEAL_ADD Circlet-3000 26 50 1;2
FIGHT_PROP_ELEMENT_MASTERY Circlet-3000 28 125 1;2
FIGHT_PROP_HP Flower-4000 2 5 1;2
FIGHT_PROP_HP_PERCENT Goblet-5000 3 26 1;2
FIGHT_PROP_ATTACK_PERCENT Goblet-5000 6 26 1;2
FIGHT_PROP_DEFENSE_PERCENT Goblet-5000 9 27 1;2
FIGHT_PROP_ELEMENT_MASTERY Goblet-5000 28 200 1;2
FIGHT_PROP_FIRE_ADD_HURT Goblet-5000 40 100 1;2
FIGHT_PROP_ELEC_ADD_HURT Goblet-5000 41 100 1;2
FIGHT_PROP_ICE_ADD_HURT Goblet-5000 46 100 1;2
FIGHT_PROP_WATER_ADD_HURT Goblet-5000 42 100 1;2
FIGHT_PROP_WIND_ADD_HURT Goblet-5000 44 100 1;2
FIGHT_PROP_ROCK_ADD_HURT Goblet-5000 45 100 1;2
FIGHT_PROP_GRASS_ADD_HURT Goblet-5000 43 100 1;2
FIGHT_PROP_PHYSICAL_ADD_HURT Goblet-5000 30 100 1;2

So you are guaranteed to get a element mastery goblet in 200 artifacts generation? Bruh…

0x4 Artifact Sub Properties Generation

OK, how about sub properties?

After main property was generated, server will generate some sub properties for this new artifact, via ReliquaryExcelConfigMgr::generateAppendPropId:

....
          is_upgrade = 0;
          if ( std::vector<unsigned int>::size(append_prop_id_vec) > 3 )
          {
            *(std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::const_iterator *)(v5 + 368) = std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::begin(group_prop_config_map);
            *(std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::const_iterator *)(v5 + 400) = std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::end(group_prop_config_map);
            while ( 1 )
            {
              v28 = (char *)(v5 + 400);
              if ( !std::operator!=(
                      (const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::_Self *)(v5 + 368),
                      (const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::_Self *)(v5 + 400)) )
                break;
              v51 = std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>>::operator*((const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > > *const)(v5 + 368));
              group_id_0 = std::get<0ul,unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>(v51);
              append_prop_config_vec_0 = (std::tuple_element<1,const std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::type *)std::get<1ul,unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>(v51);
              if ( std::set<unsigned int>::count((const std::set<unsigned int> *const)(v5 + 1104), group_id_0) )
              {
                __lasta = std::vector<data::ReliquaryAffixExcelConfig>::end(append_prop_config_vec_0)._M_current;
                M_current = std::vector<data::ReliquaryAffixExcelConfig>::begin(append_prop_config_vec_0)._M_current;
                *(std::vector<data::ReliquaryAffixExcelConfig>::iterator *)(v5 + 432) = std::vector<data::ReliquaryAffixExcelConfig>::end((std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 656));
                __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig const*,std::vector<data::ReliquaryAffixExcelConfig>>::__normal_iterator<data::ReliquaryAffixExcelConfig*>(
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *const)(v5 + 464),
                  (const __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *)(v5 + 432));
                std::vector<data::ReliquaryAffixExcelConfig>::insert<__gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig const*,std::vector<data::ReliquaryAffixExcelConfig>>,void>(
                  (std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 656),
                  *(std::vector<data::ReliquaryAffixExcelConfig>::const_iterator *)(v5 + 464),
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> >)M_current,
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> >)__lasta);
              }
              std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>>::operator++((std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > > *const)(v5 + 368));
            }
            is_upgrade = 1;
          }
          else
          {
            *(std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::const_iterator *)(v5 + 240) = std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::begin(group_prop_config_map);
            *(std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::const_iterator *)(v5 + 272) = std::map<unsigned int,std::vector<data::ReliquaryAffixExcelConfig>>::end(group_prop_config_map);
            while ( 1 )
            {
              v28 = (char *)(v5 + 272);
              if ( !std::operator!=(
                      (const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::_Self *)(v5 + 240),
                      (const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::_Self *)(v5 + 272)) )
                break;
              v54 = std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>>::operator*((const std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > > *const)(v5 + 240));
              group_id = std::get<0ul,unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>(v54);
              append_prop_config_vec = (std::tuple_element<1,const std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > >::type *)std::get<1ul,unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>(v54);
              if ( !std::set<unsigned int>::count((const std::set<unsigned int> *const)(v5 + 1104), group_id) )
              {
                __last = std::vector<data::ReliquaryAffixExcelConfig>::end(append_prop_config_vec)._M_current;
                v29 = std::vector<data::ReliquaryAffixExcelConfig>::begin(append_prop_config_vec)._M_current;
                *(std::vector<data::ReliquaryAffixExcelConfig>::iterator *)(v5 + 304) = std::vector<data::ReliquaryAffixExcelConfig>::end((std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 656));
                __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig const*,std::vector<data::ReliquaryAffixExcelConfig>>::__normal_iterator<data::ReliquaryAffixExcelConfig*>(
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *const)(v5 + 336),
                  (const __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *)(v5 + 304));
                std::vector<data::ReliquaryAffixExcelConfig>::insert<__gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig const*,std::vector<data::ReliquaryAffixExcelConfig>>,void>(
                  (std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 656),
                  *(std::vector<data::ReliquaryAffixExcelConfig>::const_iterator *)(v5 + 336),
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> >)v29,
                  (__gnu_cxx::__normal_iterator<const data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> >)__last);
              }
              std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig>>>::operator++((std::_Rb_tree_const_iterator<std::pair<unsigned int const,std::vector<data::ReliquaryAffixExcelConfig> > > *const)(v5 + 240));
            }
          }
          __for_range_2 = (std::vector<data::ReliquaryAffixExcelConfig> *)(v5 + 656);

          *(std::vector<data::ReliquaryAffixExcelConfig>::iterator *)(v5 + 496) = std::vector<data::ReliquaryAffixExcelConfig>::begin(__for_range_2);
          *(std::vector<data::ReliquaryAffixExcelConfig>::iterator *)(v5 + 528) = std::vector<data::ReliquaryAffixExcelConfig>::end(__for_range_2);
          while ( __gnu_cxx::operator!=<data::ReliquaryAffixExcelConfig *,std::vector<data::ReliquaryAffixExcelConfig>>(
                    (const __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *)(v5 + 496),
                    (const __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *)(v5 + 528)) )
          {
            append_prop_config = __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig *,std::vector<data::ReliquaryAffixExcelConfig>>::operator*((const __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *const)(v5 + 496));
            p_prop_type = &append_prop_config->prop_type;

            prop_type = append_prop_config->prop_type;
 
            if ( prop_type != main_prop_config_ptr->prop_type )
              std::vector<data::ReliquaryAffixExcelConfig>::emplace_back<data::ReliquaryAffixExcelConfig const&>(
                (std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 592),
                append_prop_config,
                append_prop_config);
            __gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig *,std::vector<data::ReliquaryAffixExcelConfig>>::operator++((__gnu_cxx::__normal_iterator<data::ReliquaryAffixExcelConfig*,std::vector<data::ReliquaryAffixExcelConfig> > *const)(v5 + 496));
          }
          *(_DWORD *)(v5 + 64) = 0;
          if ( is_upgrade )
            v33 = 32LL;
          else
            v33 = 28LL;
          if ( common::tools::RandomUtils::weightSelectOne<data::ReliquaryAffixExcelConfig,unsigned int data::ReliquaryAffixExcelConfig::*>(
                 (const std::vector<data::ReliquaryAffixExcelConfig> *)(v5 + 592),
                 (uint32_t *)(v5 + 64),
                 (unsigned int *)v33,
                 0) )
....

            v35 = *(unsigned int *)(v5 + 64);
            if ( v35 < std::vector<data::ReliquaryAffixExcelConfig>::size((const std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 592)) )
            {
              v40 = std::vector<data::ReliquaryAffixExcelConfig>::operator[](
                      (std::vector<data::ReliquaryAffixExcelConfig> *const)(v5 + 592),
                      *(unsigned int *)(v5 + 64));
              id = v40->id;
            }
....

Interesting parameter, is_upgrade, hmm.

Does this function also implements artifact upgrade? Yes. xref shows this function also being called from Reliquary::upgradeLevel !

Also, do you guys see the familiar weightSelectOne, LOL.

Apparently, the sub properties generation and upgrade also has some sort of weights.

And, guess what, the leaker also leaks this configuration, so here’s the table:

For the sake of simplicity, I’ll only show the 5 star artifact related data.

Append Property Affix Group Type Property Value Random weights Upgrade weights
FIGHT_PROP_HP 501 2 2 209.13 150 1000
FIGHT_PROP_HP 501 2 2 239 150 1000
FIGHT_PROP_HP 501 2 2 268.88 150 1000
FIGHT_PROP_HP 501 2 2 298.75 150 1000
FIGHT_PROP_HP_PERCENT 501 3 3 0.0408 100 1000
FIGHT_PROP_HP_PERCENT 501 3 3 0.0466 100 1000
FIGHT_PROP_HP_PERCENT 501 3 3 0.0525 100 1000
FIGHT_PROP_HP_PERCENT 501 3 3 0.0583 100 1000
FIGHT_PROP_ATTACK 501 5 5 13.62 150 1000
FIGHT_PROP_ATTACK 501 5 5 15.56 150 1000
FIGHT_PROP_ATTACK 501 5 5 17.51 150 1000
FIGHT_PROP_ATTACK 501 5 5 19.45 150 1000
FIGHT_PROP_ATTACK_PERCENT 501 6 6 0.0408 100 1000
FIGHT_PROP_ATTACK_PERCENT 501 6 6 0.0466 100 1000
FIGHT_PROP_ATTACK_PERCENT 501 6 6 0.0525 100 1000
FIGHT_PROP_ATTACK_PERCENT 501 6 6 0.0583 100 1000
FIGHT_PROP_DEFENSE 501 8 8 16.2 150 1000
FIGHT_PROP_DEFENSE 501 8 8 18.52 150 1000
FIGHT_PROP_DEFENSE 501 8 8 20.83 150 1000
FIGHT_PROP_DEFENSE 501 8 8 23.15 150 1000
FIGHT_PROP_DEFENSE_PERCENT 501 9 9 0.051 100 1000
FIGHT_PROP_DEFENSE_PERCENT 501 9 9 0.0583 100 1000
FIGHT_PROP_DEFENSE_PERCENT 501 9 9 0.0656 100 1000
FIGHT_PROP_DEFENSE_PERCENT 501 9 9 0.0729 100 1000
FIGHT_PROP_CHARGE_EFFICIENCY 501 23 23 0.0453 100 1000
FIGHT_PROP_CHARGE_EFFICIENCY 501 23 23 0.0518 100 1000
FIGHT_PROP_CHARGE_EFFICIENCY 501 23 23 0.0583 100 1000
FIGHT_PROP_CHARGE_EFFICIENCY 501 23 23 0.0648 100 1000
FIGHT_PROP_ELEMENT_MASTERY 501 24 28 16.32 100 1000
FIGHT_PROP_ELEMENT_MASTERY 501 24 28 18.65 100 1000
FIGHT_PROP_ELEMENT_MASTERY 501 24 28 20.98 100 1000
FIGHT_PROP_ELEMENT_MASTERY 501 24 28 23.31 100 1000
FIGHT_PROP_CRITICAL 501 20 20 0.0272 75 1000
FIGHT_PROP_CRITICAL 501 20 20 0.0311 75 1000
FIGHT_PROP_CRITICAL 501 20 20 0.035 75 1000
FIGHT_PROP_CRITICAL 501 20 20 0.0389 75 1000
FIGHT_PROP_CRITICAL_HURT 501 22 22 0.0544 75 1000
FIGHT_PROP_CRITICAL_HURT 501 22 22 0.0622 75 1000
FIGHT_PROP_CRITICAL_HURT 501 22 22 0.0699 75 1000
FIGHT_PROP_CRITICAL_HURT 501 22 22 0.0777 75 1000

Yes, as you can see, the critical rate/damage properties has lowest weights as expected, and trash properties(Flat ATK/HP/DEF) has highest weights, twice more than critical rate/damage, bruh….

To my surprise, the upgrade weights are all the same, means once the artifact has 4 sub properties, you have even chance to upgrade any of them!

0x5 Final words

No wonder you cannot get a perfect artifact, the weights are just unfair.

I’ll publish more server side reverse engineering interesting findings, stay tuned!

Also, feel free to comment if you have any questions!