Skip to content

PowerBuilder: calcFunc cache keyed by node.modKey conflates same-modKey nodes — the tree heatmap can show one twin's power for the other (and which one varies per launch) #10028

Description

@TheCityVault

Check version

  • I'm running the latest version of Path of Building and I've verified this by checking the changelog

Check for duplicates

  • I've checked for duplicate open and closed issues by using the search function of the issue tracker

Check for support

  • I've checked that the behaviour is supposed to be supported. If it isn't please open a feature request instead (Red text is a feature request).

What platform are you running Path of Building on?

Windows

What is the behaviour in-game?

N/A — the node-power heatmap is a PoB-only feature with no in-game counterpart. The correctness reference here is PoB's own cache-free computation of the same per-node value: computing calcFunc({addNodes={node}}) directly for each node (exactly what PowerBuilder caches) gives each node its true delta; the cached path shows one shared value for all same-modKey nodes. Driver output demonstrating the difference is below.

What is the behaviour in Path of Building?

Every member of a same-modKey node group displays the power delta of whichever member seeded the cache[node.modKey] entry first. On the attached test build, a 5-node group has two members whose true delta is +0.577%-class offence and three whose true delta is zero — the heatmap shows the non-zero value for all five (over-ranking three no-op nodes). Which member seeds the cache depends on pairs() order, so the displayed values also change between app launches (8 fresh processes produced 3 distinct node-power tables).

How to reproduce the issue

  1. Save the attached driver into a stock checkout's src/.
  2. luajit powerbuilder_conflation_repro.lua yourbuild.xml — scan mode finds a conflated group on any reasonably-sized build (no special build needed); the attached build code reproduces the exact output shown.
  3. Compare the DIRECT column (per-node truth) with the POWERBUILDER column (shared cached value); rerun to see the shared value flip across processes.

PoB for PoE1 build code

eNrtfVtz20ay8PPyV6BUle_Fso25Ysafvad0s6zEshVJtpO8uAbAQEQMAgwASla29r-fngFIAhRAgpJyObverXJEoLtnpu89N7z8n6-TxLnWeRFn6asd9MzdcXQaZGGcXr3a-XD5-qnY-Z9_jl6eqXL8PtqfxYl588_RP17av51EX-sE8FzAK1V-pcuPc1rkMzybqrQc6yw9Vb9m-XEWvtq5yBKVx8WO46s0jMtXO3tJnKsdJ0hUUbxTE_1q51ylVzrfcVQR6DQ8WL4wvYjiNDQvg7HKVVDq_K3pwt6szE6zEGAilRR6x5moOL3Igi-6PM6z2fTVDt1xrmN9UwFdnh8dNToXp3Xnzm-LBNpQOzDCf7w8S9Stzi9KVToF_ANdBUapK32oJvAv0FPJDIgR-oxwKRDeeb4WbX-WF-W2uBdTrcMl-DPJXdILfJbroyjSQRlf64M8Lg_GKg2WjbE-vA5Y_MxbB306S8p4msRGEjUGetZL_83drtA-2MusVMnh2cWSLuHPEGKUEcGk2ICXlQs8tw_yU1yOAfAebRjM_QQEck_ck6s0LvU9kA_UrCjjwKhy2mqcCk5c95lAElPisXWNn2VxkaUP4Ow92j2YJQn4iyZmr1jOdaHza1XG7U72wh9kEz9O9So7KAxKEsqlx1GvQeZavY8qUzlXYTwrTnWZ66Kh_b3cOFWpOsiKpZ6htaBnOgcnVrYw3A0IFzrIwO81URh7RuF_YkBT3QR623wbR7oFideCbjWeGqGjO558hl1OB7S05XCOLobCbU34fh06B3c_DPIimyUDIcuG2-VrLOq3FiTrtdND_XUpf7SOYBMSeb0x4iRtKhRaQ7AJiRjq7-F1ZlzDZuZYd3X05mwZNIVH-TPiYkww8WRv8BzfFnGgklP1NZ7MJhCzLtUXvWyQC7lGX6_GZQqerg8ZHFP_2F7Hue5DZMhj_ZE4S8J7tQjROCv6MCWjqN_PxOkbSNz2gmAG6dft0qD7UxhjzMtuMVesyXWCFwb6JA2WhPk6uh_S3IaNRo60rgWDcg7GaxIyP9GDcZbN1E5gqbKuu76xK53WLd42sj6BnqF1eG-1DsbHwOxzVS676T5zuSQMoi5xqeTrgsECRxBvLb8NcJPfa0NMB7sR5cMw7nDOg5BGMJWei8j6INrNRSCwtuluHtJn2PMQZb2oR6nOr24vxrFOlqPszyWa4PMuHqjpAEzL_iZ6UwyIDWuwizHuENQeDevNIFUeDgt22_bpWhVNv065129SFcsqjBa31iihhoQdUEK9UoL0J7xnefarKaKSLfH28kk2yxsBY_04KvDmMDDdFJyq8vFch7OgFQ1RvzrPC8L9BMrhlbG4G7Ggp0myFepeWargy2EWXumhKLaRrTDa_buYTafgZoxWrBCQ7rqgCyVH3Mi3hgC_B61umjddG5wHN7AEXm2AbU46BreygrHaFPfW5gsrzZAh0KtNbBboKfiNCcQJO_FxmjXnPqjbH6VfQ6U5qGy0gO1ithf2LLuB7o_NvFaxHTTkVsuO93Yl1-nvt6v0h4EPauAoDSFLA3tYbQMNxehq5jKegE8tikNVKies8_KPKo9VWmJD2ym0yoPxW5D_a5UkPriDVzvNp-bXCiKad-rlczuzaP46mUyzvHT0V_OfM5WXt_PpPQtonwCdooxTO21g5hKTHedinN3shddmFJdZlhSLOUE1neo0bNG4zLV21NyTBKYTdozmhxMks6LU-RtVjF9n-UQt5zaxmWA0724rvYU2_sVcyNR3OeX437v_IsxD8LfwGPzgjGGxiwX3OPxijEp4h1zpwS8spSS7yBMM_7s143kSGo47aQYDNGmmwBIIMkF2MZKC7mKPM75LqRRiFyHIonY5MzCYUg_vEuxCI1J43i7GHIldIgVHuwT-I3cRRpTsUiw5kMEehb7ZXhDMqbuLXeF6u0h4XO4yhFy0W_fYpfDWDowRaHEXUQQ9IMRjxLQNBDgQg-euMGQIIXSXEyTpLpPEgyeSMnhCEQJiXBAMz5lgu5i5TAAkNowQSEDvGXMtTQzUCGS8u8TjvGqFmhFKvisZPMZCMGkeI8-QpDBCSoFFgE6EGQ8HYhwLz5BnCMhTwncZ1EbQIOLQcSDlMoMLJCljlgseN8-l6-4iiWBA3APuQokADZo-cfMvjA06A2QYpOLQCDVIXCIX-OpCP0yfgFEuNf1DBhwoesLwFVhBhStNvzEMkHBhhs8ECBJakkYACNux2scSmO4iQxj-3SWIm_ERSGB3K70CKvAS2A994VgyQ1DAUEG-2AzYxbugk55RTczZLoJxe8B57nKQEfwwI0DAeSxA1MBrBkqFgP2MWR2Gihn4DA8AhCDoe63n3HaPS9eyw7X9AB5ywgmIGht1gGZB27jrAeOgjhUcOsUr2UMvhREOsJMYCVE7YMAEmcA4QEMx9E6YYZmBC2T1FfSSgKIK40vMDIzKb_faFoPAZEqw6cYqBBb14oJ5j615_-Plh_O39o9_jMtyWrx4_vzm5ubZVJXjLNJfIfF7FmST51NAAsfwtPgSJ8lTQ_b5Hvxv_-qE-LfTd5-Pwo_0-OYk924K_vHpz9HHYpJ8HKuJ9-no64W8nsrzn8mbD9OI7p-fu8n5V0Z-vnl68_NhdHamfw1Pr2SKjg-mP3-69TGeRb8FN1528-lNLH9Hb_az367fTZ5evkFPb76_lDczqn7_-sP3n5MMf5Zf353sv_vlg3j30_5X_-SX6PP1gXeUnxxn-Jo__eXn33__4c3t1_zp9DD9Ghen_OLjDx797J38-jlTv6LXp-Ev9Ow3cn3ovjv8_PSM85NZ7jO2d_5Onr-h-eXrpzdfPn__9NdPPx0dnr2-PNn__mRWnMsvl08v3138cPnu9uuXnz4fvU7K8t2b8kxMf9j3P_20P1aff_3l5Je4uDrde09jOpXllM2CvfHp_vc_vk72z_f2T_ZeT_bOv7w-_vHjj3sXJ_s_XB1d7n3a21dXr-P9jyevP10d_bh_DvBXH19fA9-839Kfbz6j_DB6d7N_9pkeX3rH5z--emUF-HwuwZfVKk9RibP-Zd2mkbc1vR0nLvXEir_OWToAjWttALI-QOt9l4DE7QO0ltqgSHoBjQdtAIo-QOsuG4Col6JxYQ1A3gdoLboBSHspGrfaAOwftYkwDUDcy0cTeJaAqHfU1mc3KMpeEYL3bRCcw7183tSQlyYjzWMIqjWR-W8nBG9xqcoyy5wscsqxdi5VoZI4cy4N-DiOSrPaFQc20cjL5_jQ_HthnMMJPCzm_qJ4XpSzNNRfn03Tq53lOMD37dS5RpU1nJharknrw4l9VDw_SY_VBLwNpDBFmavkEnKk5PlZRd_kLFVHi-d1F-G9r-vX-8e23Wp4_0Dud2apFAg5ZebsXWdx6PgaSgDnAjqZ6rDiwvM5G_5srtio-I0tq2yx0fEbW1bZYrORb2xZZYuwOdE3rqzYkCl5vrFllS029f_PYMvNOFPOG5370LSZmRrCkwhKgS93gzOm6I_wt6aH_Tyh3zlxGuRaFTp0XpuOOVXbzuEst1Maf7muEJMf_p_QleVPm9pB2jfVgZ1QMm2bP95lZfXOPJz_eGmZUThFlpfHelLs3x6eXbw2KyQrW0zqWSIDfaHLap6rifNqp8xn2kxsRWqWmOc_zmCYZtLKbT59W22ZS82M0nyxGUiNsxszjV1RvLydmlmfvbdvqzdv9ZUKbk1T89mrapaq7owTh_OZq_qhUySZ2Xilk4kuq91we8v-286D5iUzEHNaT51Cl-LkDuiBSoLCwuvULJmG9TBrTYQu1S-OkwzMEM-5UG8MxO5O6z2av7-upv2Mkp3pMo-jWIf7SWbme2253fXitzk_DdWrKs0_1aUKVamen0DeXzw3LKrUG_5axW-PwAmyWVrJMQW9rab_FihOhfN84Djp5mHW0j1KEzMLb9bbFwO9-2o5VLR-qAuV2QvD2HgMlfx0Nmioy9aGDhKjzaM8zpVZflkMrf79W8MUBkiuxhowigryMUdwqgA4S3TaGkfr6ZajqRbtjHcyU-d7M7PddcDQmk0uSllD8q6dnxvfiP5CO4dqeyNjIcBN9KEqxg22Np5tydQG5gBeWmjHgm9g5X6WlcXfnJO11Z9muZ4nCne9SfvtYN-5cCgn88xkSWSI0kKri-xluF0OCBNmi_dkdazNh9uGhybugJE1wNcMq15q6h9XDdAY2EelkjdmNakxrsazLe2igTlgUAbaqcAfU1YnkwkokTK2kDRG1X68lBcZNLI29mJwNUO7RjfHcCzKRsMPb535Fo475o-3NH98T_PfIpGYTM2Cc0caMX8xVHM6cgibpA5LI-rWHlN96h59hLz_VKXxdJb0-Li7ENv7ubs0htiNKUlaWM8f0b8bq6y3_O_leXaz4hjar7Z1e3cpDPUTNZZToT1mzlVLwxJ-l12ru5JuvNpexA3kIY7eQDsW_I9Q6jiIs1mx3GNWdKh1B8z2o57vGVuSqbdm1UeQhoi96ojT7MkfwJOLmzgq96IoiYNuO78DsD03ztU0Dg91oG4Hjdy26DSa3BA9Pmk1zdK_NAffwtCOogg0w2zfvhM8Fm-2Z_ESd0jcmAM_ph-5zL7GwTlwtTGwxrNtM44G6oARWWjHgn8Lht-ynW1t8r8nMAznyWlsDs7u5cG4S_Ttt9vzoY0_pLi1CE6NsSEiwOCudfE3n3tdeLi9pPy5y2lWz7esQe_rN80ihjmqqvM4vXpUfar2zjYnnuoH28461WhDppwq0D8gFNSnai_KPP6i75rFyuvt7WKFwICx1hhOjfIHjNkc6Imy_EqHNj3v8I13IbaOCHdJDJqmnSM5NdZ9J2utD-hxDYnyDcf6fISFWelskc3yQL_aMUN9QV-cqTTUkyyNZxPncpanu86HtNClY7qzxqVUrde1a4-4FqI41MVUxSvBuJuHlu6Si3PMXt7dh1vvU-cHg3uapWaztnP0dZpk1Wmj7dlokUN9b1bZtwtOHaV6cjsnWT02-9kt4tbMW2dwf3WnGqKs_7zQpV2WrRZezZ-W-xbiJJ3OSkvk1c4kLoLP_iyKTqvulLm9lOTo9eujg8uTj0f1cYQmiu3y53Q28U1Er_5rsswK8kLbUs4pZn5R_WkSGX1jO3IIniFOCjOaJFHTojG1aFZX654ngLeGmoV6Ey9u-eimtQTop3T0VecljPeTyoM81r39Wrzf0Kmqwaxeg-qjZu7P6CdUZYkHqiirM4M9nLI3lvRTMdeH9A7HvFyDezKZqqS35frtBk6UZm0dNDSG-tOcfFkvcrMSX0Gt4cviMHSvvOtjaf007LUifQSql_3I1dUefdj12zVctdeS9HK1etuPXk-q9GiUfdmPvDiGmqXApj4qC6g1lN5lqVVyMJq9ODFHyHolewQVzxykn-B7k4jW53v6KJ2Cj5qDrDWcPPZnZb8ZNyDW8MoepO_hkHnXj1odCe8Zg3m3xhO1zkP3MLQJ00-qOkTc68jWoVbHCXv5V59NXCOCemGnh_3V2zVMmJ9S7hl__XqNkVj_a7cyVQdUe8xlBWydw8iCLw8nY0_dPpzM6jHch1O02-F65V2_7Uf_UMYmXemgUuUsg4gYo3oYBWNbD6NwCSluOcv1vQmcr2YiS9zz9TnI4tRoJ_L87TrPUR8mvTeFebl9T_SzambwntjW_x_qyGzAWRsAFjBrBgIFpkk46knVB9EyeyCXezIeRMoOsdspLTm1Fa0qrHZybWuKlbOoL6dZ508qkA2EIC94sybzHEZpcUz9jVaJuVMtSx5G8M4lPA8hZm7dmE2hzp-Te9-V7y_lMJB7WVkATXss-9Bc7vFQHpoqs4NQf79ePp9XiPbAtqnZ6tPkF2VuJkV-z7LJz7Y0NX_VO2xJvasWsvfDGOSWW32bt2MAf5qf5n9pp4PqLb7m78UO334Cs0JX94BVS38Wo7E311Bp7ss9V1Da3L5wzvfOj0YHKs8NW37Qejp6a_4600k5qtOIFw6TbP5jXxXzq9DiRL9w3GdCemz0IY1_m2nn5PCFIwSKfKZQpMLIxUGIheuGCAscKh5y5KNIY8mJqwlWyKPCpR7lirHIwzLAAR_ZrlqmATE2qrctv3CwO6qPzkG3n-4_PX66P7Jg5_q3F46HR1BwJXEQm_d4hN3mbnYT-Bx7FYyuPJVjasYRxt85ubm9ZA5iLjwz075VbVmM_hWZWz0h3oX_foJocxv4PLtwbK7hVDoyQqzZas0z0xaY--gJItKgTiojc0w8Hz2h3nfmobnTw6nyRtMEPOf2ubmKo_n8X0GuotL2x7y2DDA9rlhjZv2yI8dMJC4hUYsV5rZBg1D5DjsncpqF9pJTJzf_moud2E6tLPWsSS8E3ghBNkLQjRBsIwTfCOFthBAbIea3tdn52rZd4bZdfXh38uOHo9GlOQNhzgAYjhvXN_oIJGdQXDj7xsQadhMxggPPZW6kCQop8zyOUYSoEEIhgpFLGZeIKe2qgHmR5FFIhZRgbB43GKOLsZrq3DE9atuQ1zQT0TQTNHpCELWnGmzJ4YBjXdVa5LbUxyitM79PyYnybFIdFSlWAWuT6wCs_mPuv0jM1nZo_TabOWN1rR3ctp9KRW2TNdKVWZfBTp0uOdoSJ_UB_GJkG22Cohr0DmSdkbeASS_d_zYr6dJw0qnhpyrJUn37_yDoFP-_cE61cZFxMRm9z1Nwsc6PM4hheVPRA9fV0tOBcgX1kIQowBTWXkBCj0URCXlAfU8yqTiKWOS6foRIpHytXE-wKJQt5fbQMkA04sM-xIdmdKCsrfZvVAGqUYHbH7j-UYwu8_jqCuxILV3qfnbjVNP-N2OdWm2tAoRzE5djgASA3fnfqFYa8NzgurObdCRb7rdCtNOSoydM3AkIrBll9pPYWCSkKLHxIakDqVTV0DxC_WdpZpfe0Y6MZXX1aLRcPGqqmlYo8MBRytDFPlI-kj6LKOaS-jgQkYyEDpHU0o2iEPse4aCUkIVoKRW8BgG1_Chp6FdDt7hYo1uNBOJglhd6IUsrxHqZqRYsOGNuhH6SlpBSxFfa5gEm7YCHVYLhGG9qA4mzvPMacgXWVKNq-sup5r-AQOXhW0q2yA3eZelTMxuc6nrJtDp-Zp3xU8_g2QtMl9nRN1doyd9VyfNZqp3jTIWjjzoxsdQe3Vgm0RSvSaKZh2lTcb2Iu1JicJQhlgIkGGFPEOqCt2Q-QowhRSQmOoKUAHEsKQoxJA1SgsbD_8PeJPpODn3e0GRBV3Jocvfg41Fi70MDlZjPWY9a50b3qgqr8m_bps9yXfYsvcHJM0GDOs6bzZl5DJv_2KnMRebSSKNFK72pL4abj3QB1WraphXv0yJRs6tx6URZ7tB5UmGs3qwHf0vB75GCr-OY22-4vDOH-XGmdTo_svw6A70sR4f2IrdbsCFlZpKWdow5ctcYMvJ405BDL0AUEh3scsY9HFHEuYxo6IUqirjQvsJYuiSgSka-K6kHZg_5PoIUyBdM8NUI1FMNH8__37BmJtvW3Fcw2mKRLwtha0x1uCnNpAeUyWKdYXruXcPsKWqJ6CxqnyBpny9uhGy-NDXBokhvWx3qt0jH1ELcdVd6u-vMpqYlj303ekqxM9982BpwVa2Y6TnHv3X20niikmJ0kOX5bFpWx72_GeyfY7BeR6Q9zNUVCPRdlhV6tHdlqoy9ySzRrXKaUMjouKegVNYMAqT0w0gQpVlAEfapUhIpqK5JyGhEAoa5AijCfeFrwFR3pqEa-R5r29XC9e8lEDmgN4XzTkHQA6U6B4UMIdsbPcE20b8oc51emTIB4kwrzWtGSsw3Znxm4qrfIE3cNlZmLj5tm1nLAygIvFVZY11Ax9uKQgNmZfbpNE7rjLNeWpkX0N9M5I6JdGm36AxHn2IoG8cqTkYH-SzUprpsarb0CYZaJvRU4AaUoNBXUaC0iJCLmO-FyPNUJHwX6mgkQe0ZR8SNAt_3WYiZ8Ibkhvs2mti40lR2d4RFy-f6WTordDV9Aj-t3zz6bRZPp_CrLv2385tdbJIdTuBoMr2FTqTOca6K6Whvoktw5XdLQCIkIySIAu16BGlP8ohjIkPwDdoTCAo_FqjQF24QEoW00pEbQKAmOCRIMEXW-AG6MpXWb3ZscwmHsE1vV02ZomZsNVXYqD2dWzHFGGy1aBCZb_KMnlRx1tjwMu9ddqn4VgA-rACEkNURl2bFF0h5gEWfbkHGaQF5BZSEapaWiZlZquZYIWQQWv99N380N542ck2A7E01LWhD06kQ2kM4MlfYkUi4WhEUMI_70nVBxwXnKvBAvSNKiMs9NwwB0MWh0NSnjEdD_MLdLHO1ZnzCNpd5T4wKLyxlnVk0wyJjzRA6apdj7Wk1hPmdlLS9NNNjNoR2m_CyvMNr1lKWUPwOVD30b3HxcZZbUNc65qGZErTnYUYHcalCSGRWYqeL_YAg8PBICcwCHHiMY0QVplqa8ssstXhYgcH4RAdIcZ_5kfBCA8HdoB07ubvORlarMdEOpE8QsTFBfy21GcJoLwwLxz7jzRKs8ZzhRoFUv4Xol0KZAsTsKokpeuyCtp1P_PsoW6cIcYcIX5ts_jwLR_t5psKxVvUxho51BCiYwwgCOjgyLITn-zoMBTg1CPSCIvtRFT9CPvN8DbE9iqTwIy8yiwiaIAp50BtwzD0LZg259iX-aN28Pt4Y8Zuz-HXQBTQ6zC92IiN8d563cSDOOYtN-IDSw1GLo4bOpf1K5oi0nGPdsm1ksfZR9DrB5si_JQf21u-WYp_uHZ8cjPaSYKwnEFDmS2Wg0cGXIk6MfKvb9uoZqIOx1qUat2K7R7FwQy-izOeQnzIauGHAIKyjUCpGCai20h5k5yJ0IcENMUGRj7WLsSJEsZZ-I9dtzSP1L2UsJP7BCNoufs1XSEH4wdiJZkkywt5ygmZxU2B7JVdXARLRNRM2UEIaxzZkX8IjKkGnAGmXAA-V7yc6n4tvPy4mMzCQlujMsUG_HXK0KzmWAoco4sxjWigoRYiWimjsQW4G_klFobnJH1HEUQQg0qNB5NMwZEiidfth-ouRgZITQyVnttM03EZnUfF3kiDrkuCFnZYxc55H4PjNOJZbC-yunyTR13HFhaYEwY5CV2Np1l58F0HxiMznIl3fbF_ypQexRzKoJaG6JmGEEESdQPpcYu4TEbm9c7ikP7q4I97ObyfmVNN8VQIcLSFL0S12WZjBzVOCk8lklpoc1y4jg4EZ0ZgFCLlYgIgjs23MqAcEiAVQF4FFDV9dRLiWjrLiN5MDt6t4f_eMhA9y3JfZVP3etnt71kc1lYYrn0A-GSAZMJdFmCLQFzcSDDPGzMcbo1D7rqaSRQFUbQz74AAUcwmEccyobisN7zN7715mT4eaPXf75xm3Mfc_eIsA8gYJ7qIj2P6gvsDb1torwpiGCAzZ1SiS3KOShCrwIZRijqMA8kcaKgUVNMOuDAJMQiigI18oyjkU1G3J4cd12GSo5FjDtde3-MKI7R4Eu_JoNrH8jfx190zoqbYfeF8szUEaC6WdOSxaOt_rG5005QapkBuFklEaeNw1nxnCxBNaC4YYlxKbpEgJTVykmcRCeiFTNHIpwp7HI33H4lre-KmHNsyoPaULgMVGjE7A7tipoOILlJ03Bb95azYQjKHY00XpnK4hB4IvSqiS_jTP2im9rgnaH3K7lGa2gZlegyxDs1vnoPqK1V3xEeFzgZiSlEvfLIuCrYF_VCELkZBBKAQiEptqjkgpJAPTM7skuQ60j30t2tvCSLP0bpkaaSzYmNKaOvWlz3fLHAcAQB6t904Msqp6v9jj06BnNhJMjA60iTpXJqaC1W--YnsNEZUU2ZzSE7JBH4fSoZ0rtb1j389h6FUCcJaVOg1u-2EvpvEXbTbCpUEWzO8P_68vD7Hb6es-pGm9aniSmq0H4JE_xnkcxlCw3zEWDJWf9nQIih9GBEKTCJEkvqTg_BSYSkADziMlvNAjmHBfUeyGkG5ECErCwGOrRUX12fkXlbq0Pd98VbNHl2KTDhjkSqGcNCvHjSTSYMwXicCLf4Ckexi5R1KVTgF0zR2-95P42u5cSupVq-4oI6kP_sYjkBBo7SkhCFTaHHgaEGImo0TgSawlVUGolfniGAuF6zJOvcgPINq0OY_avG6tAn_Xns3p2BnxxJu7gOWB3ZV57jsThog0Xzdnmv7myTnu32FvB2nU61QrM9bRWQ7FuTKXQ96Rny8JdSmk5WAxPAx9HnmeyyGySPOpX-T5kOtRTxjRcWG2iGqwIg0ZhMd9FHK3z3LemlytLc1eS1n633ZA2FoanXwiXaul6goqsw2qTSPQW5drTSNElYt91xUk8IkrhatCKZBgXoACKSUEZKhtuRtxKF9AswOfK5es1rnt-W7Rpaut4Z_G6WLWu_WivbVogGH8HbfqdMqqa2_zR_BD4VxWm7KmwHxdEeqRyIWSBFEWaAHaS5XHIqmw0KFGmgvKpAS_7yMkIwaOi6kwdCUhKxtFPXdw1sT-zKypveK9nNNuLJH8mSnT4hyZdtxnUBaA41ksvlRn8PpTonM91YCYr0maxnmsvxiF3s-S8tvRk7j6_mGX719828X5GJuJiTUJk1kiEp7AVEWCax6I0A-ZjjiLPAoVhGLg7LRQkCcRz8UK4jsSmoeSGC8YEbXq9tu-jbWX8mCI5qsdeVEv5qxbDK80y5yFum1D9-1adlTpMPe7uiaOc-ej-R7yH5ku8a4DGIm6AtZ-hOEAo23w63dSkjJfhcB5V_rMFyERykOBMgdAhUYKinZgOzdBl0lBFOGSSwJezXiswBVqNbA0ZlHcdU5K9Dop3GV9VRHedFLFVl4K98byezkWtp1n-ZAW4Fq-mKuwnOM4gr6vAb1R1_ZWQ1C6o-s46Qf9FAdf7PP_wn3qncbQtSEV6lz45Xyf3fbmVoFZEOChEsyc6xA0FFGouNmA5oc-iTCkW5BmEdcPGEJMQMTWoQ9hPBTgjMA7bZ9btcoAc0tWvcxWTdKPs2pmTc2PCf2XJFyiS3zZLHEOcrN8vSnd8j1wWCFyEfW0iiCzgtLOrAe5RDGOIygKaQgFIsacBfB_yRUkyiEKfGb2Xvmo35P9jSapHivdop17_R8rWfuWYj2uY1szh3sEQb6y8r7CkQsNDi1yBQ4x9TkOtSKe7wsmPagQI1cyxVyqGHYFWAILAACHAcaeihgJ1jo32RtV2YNcFpYbF0j-z7g10rUd9W2myrFzUWap3pSgBYKFUD8Gds5KE4G5yYwpJxgilsc4VJcylIqqMJBCMOxxhjSLAhFgn60K0JP_sQla7ZRWXdq3nOyvcl3ma5P9N9-0P0RpFeYk_HD-dvWD89VtrZVeNb5Bf-YD7PLm2IskK1vPa7zltzbmH3sXm1DsncuND9i7mxDMZSHOnn9bFGqu-g7eAn_ex1UabHsa-BFoIOfiRk1XCfFHGBB_hAF10ujQHkKkkG3tqR91aU-X-glK-Ir6VY-2UD97zHx1CNvoVvVV1lUK6IHaeS9BdGkFewStQI-gFegR-kG3ZOoSHD_Ish7DVQyjUd1ZNr-UrKJYrXM35IjIvZQaP1AlyQNH0JAe4ps6UH1v4yFK1G2X_IGDaPQAsXXOiSNBaNs51Y-2cE7dXNjGpKuTvUsE7776Sx7BB5CBkQEjKVZ4Vz96MO_oA-XfMCK0kZfHJrfeSmltZtNoQ27l-5d4ZBh3GoNx7-VT2FCJYo-KFYlWjwZJtF8eDZVC9H4uAT_YqTw8XdgqsnUJgg4UBHc9wlbcUvXoHil7Z7DcKi9ffqX18V39oyXWaGhCS6kgKwlt9WirfLSXJXiojClb7Uf96KF5Md9WTZfw7H7-epsWuykMDTpcopVatnqyBc_m3wKat0fvGyjpY6kuexxjfLgUHpp8DvVvBHt81QarR9vofkcH0ONwkjxWXbe9f2xEyge1jB8aq-5TTjY6j7dg_xKND0vW8OOImT4OmXuHsAa70Po5GI-tmkv1aOAcDEH8TsyrHj3Q3thjGcpjFC74sTrzaNM6jxYhhjpWJplYSRzrR3cEXU0yVx8GM3e8q1Bf2PvdP2mzklRUl8DbiyPs98KyNIqv6hy_-lFf2G7xF0-cMi4TbT5CFKmZndxpTE83vxvmqzQ0Xx-af2NsL4nz-de_W4D7OUAW9gD2WIeXmT36v_zSGOrCmRW6-o7F4nsafpYlWqV1ddKBYibX7clPS_9gHCeJ-TrURrzJ4sD6O2Cff3uucv0-r9ad6ntQN3S23fL8hOI2Xd7X2nzK6Vybm1OS2wGoU5WWY52lp-rXLD_OwqUcLrJE5XHRhXQYF2aV5-jNmfku1Pu0_h7O8H6aY1_2YKEtC7forvkk3eKkW3Gs03i2lUzNN862aG5VpsPkuOBpnK7w9Py2SFQ5VpulX335q0_vzhIVaLOrRec1tjZI1effFp3zXHcDgl0WrL_ftEAjm7AWK8zzT-3NMTGmaFOL-eKDbAs01o9zMQbNurCXP3cwva8NeHKPrplTQfdAqz9gPccQtB9-qU7Vl3Z1aId2YE47X-gkagyRDRjjquDYYMGdma-dLOQ9FGvr9hZf7loMy0WUDOGPtYE35vjMDTQMXmKiB8l_osFrHtZL1duwM19-OHKwHRgZbMVJa26rzQjJ-9Gmi_1IdwdF3QEKvY3Mluxvc76yv3M9gVIx3L_dS5LbQcK4CDL7mZjtrLfiz3mWJHZtvOnONmDOi4g5AkdmfbIXp4iv4uR9ZD9UBZ280sM6GVQZzhTEkqvkMr7SuTXgQf5sIZet9GZ-b2u_d3r5fJF4Vd_ssb_-OXr5_AzizfvIfkAI5PnP0f8CEQtACA==

Screenshots

Summary

CalcsTab:PowerBuilder() caches per-node power results by node.modKey.
Nodes with identical modKeys share one cache entry, so every member of a
same-modKey group displays the delta of whichever member pairs()
visited first — a per-launch coin, same pairs()-order family as
#10027 (separate issues per your preference in #9947).

For most twins this is harmless: nodes with identical mods usually have
identical toggle-deltas, so the shared value is right for both. It goes
wrong exactly when same-modKey nodes have different true deltas
position/structure-dependent cases, e.g. a cluster-jewel notable vs a
tree node with the same stats (different dependency cascades on
dealloc), or nodes under distance-scaling jewels (Split Personality
class). Then the heatmap silently shows a wrong delta for one twin, and
WHICH twin is wrong changes between launches.

Versions

Found on v2.65.0; still present in v2.66.2 (current release): the
modKey-keyed cache and its shared lookups are unchanged
(Classes/CalcsTab.lua:606-618cache[node.modKey] /
cache[node.modKey.."_remove"]).

Evidence (reproduced on an unmodified v2.66.2 source archive)

The driver below is self-contained: run it on ANY build with no extra
arguments and it scans same-modKey groups of unallocated nodes, computes
each member's TRUE delta directly (cache-free, mirroring PowerBuilder's
default-mode computation exactly — CalcsTab.lua:477/530/547), and
stops at the first group whose members disagree:

luajit powerbuilder_conflation_repro.lua yourbuild.xml

Verbatim from a cluster-heavy CI/ES test build (2,589 power-carrying
nodes) on the v2.66.2 archive:

node     same-key?  DIRECT offence           POWERBUILDER offence     modKey (first 40)
4219     true       0.0057731675377536949    0.0057731675377536949    [10 = Evasion|INC|-|-|-][5 = EnergyShiel
46136    true       0                        0.0057731675377536949    [10 = Evasion|INC|-|-|-][5 = EnergyShiel
58649    true       0                        0.0057731675377536949    [10 = Evasion|INC|-|-|-][5 = EnergyShiel
62795    true       0.0057731675377536949    0.0057731675377536949    [10 = Evasion|INC|-|-|-][5 = EnergyShiel
64501    true       0                        0.0057731675377536949    [10 = Evasion|INC|-|-|-][5 = EnergyShiel

Five nodes share one modKey; two genuinely carry +0.577%-class offence
power, three genuinely carry zero — the heatmap shows the non-zero
value for all five, i.e. it actively over-ranks three no-op nodes.

Cross-launch nondeterminism, same build: over 8 fresh processes the
full node-power table took three distinct values — at least three
conflated groups, each flipping between its members' true deltas
(0.00577… ↔ 0) depending on which member seeds the cache in that
process. First caught as a 2-node byte-flicker in k=3 fresh-process
comparison testing on v2.65.0.

Fix directions (mindful of your #9947 hash-weight constraint)

Per-node cache keys are the obvious fix and exactly the shape you
reported abandoning in #9947 for hash-weight reasons — the cache is why
PowerBuilder is tolerable at all, so a heavier key on the hot path is
unattractive. Two scoped alternatives with zero cost on the common path:

  1. Hazard-scoped keying: key by (modKey, nodeId) ONLY for nodes
    whose delta can be position-dependent (node inside any radius
    jewel's influence, or cluster-jewel nodes when the group spans
    cluster+tree). Everything else keeps the light modKey key.
  2. Hazard-scoped bypass: skip the cache (compute directly) for that
    same node set; the set is small in practice.

Either restores per-node truth exactly where conflation can be wrong,
and leaves the fast path untouched.

powerbuilder_conflation_repro.lua (self-contained driver — save into src/ and run)
-- powerbuilder_conflation_repro.lua
-- Repro for: PowerBuilder's calcFunc cache is keyed by node.modKey, so
-- same-modKey nodes share one cached power value even when their TRUE
-- per-node deltas differ; which member seeds the cache depends on pairs()
-- order (per-process). PoB v2.65.0 / v2.66.2.
-- Run from a stock PathOfBuilding checkout's src/ directory:
--     luajit powerbuilder_conflation_repro.lua build.xml nodeId [nodeId...]
-- For each node id: DIRECT = calcFunc({addNodes={node}}) computed for THAT
-- node alone (no cache); POWERBUILDER = node.power.offence after a normal
-- PowerBuilder pass. Same-modKey nodes print ONE shared POWERBUILDER value
-- while their DIRECT values differ -- and rerunning flips which member's
-- DIRECT value the shared one equals.

-- Usage A (scan -- works on any build with no extra info):
--     luajit powerbuilder_conflation_repro.lua build.xml
--   scans same-modKey groups of unallocated nodes and stops at the first
--   whose members have DIFFERENT direct deltas (a conflated group).
-- Usage B (explicit): pass the node ids of a known group.
local pathBuild = assert(arg[1], "arg1: build.xml")
local nodeIds = {}
for i = 2, #arg do
	nodeIds[#nodeIds + 1] = assert(tonumber(arg[i]), "nodeId: " .. tostring(arg[i]))
end
for i = #arg, 1, -1 do arg[i] = nil end

package.path = package.path .. ";../runtime/lua/?.lua;../runtime/lua/?/init.lua"
package.cpath = package.cpath .. ";../runtime/?.dll"
dofile("HeadlessWrapper.lua")

local f = assert(io.open(pathBuild, "r"), "cannot open " .. tostring(pathBuild))
local xml = f:read("*a")
f:close()

loadBuildFromXML(xml, "pb-conflation-repro")
wipeGlobalCache()
build.calcsTab:BuildOutput()

-- DIRECT per-node deltas (the cache-free ground truth), mirroring
-- PowerBuilder's default-mode computation EXACTLY (CalcsTab.lua:477,530,547):
-- useFullDPS = false when powerStat is nil; offence =
-- CalculateCombinedOffDefStat(node-added output, calcBase)
local calcFunc, calcBase = build.calcsTab:GetMiscCalculator(build)
local useFullDPS = false
local function directOffence(node)
	local out = calcFunc({ addNodes = { [node] = true } }, useFullDPS)
	return build.calcsTab:CalculateCombinedOffDefStat(out, calcBase)
end

if #nodeIds == 0 then
	-- scan mode: group unallocated power-eligible nodes by modKey, find the
	-- first group whose members' DIRECT deltas differ
	local groups = {}
	local order = {}
	for id, node in pairs(build.spec.nodes) do
		if not node.alloc and node.modKey and node.modKey ~= "" and type(id) == "number"
		   and not node.isProxy and not node.ascendancyName then
			if not groups[node.modKey] then
				groups[node.modKey] = {}
				order[#order + 1] = node.modKey
			end
			table.insert(groups[node.modKey], id)
		end
	end
	table.sort(order)
	for _, key in ipairs(order) do
		local ids = groups[key]
		if #ids >= 2 and #ids <= 6 then
			table.sort(ids)
			local vals = {}
			local differ = false
			for _, id in ipairs(ids) do
				vals[id] = string.format("%.17g", directOffence(build.spec.nodes[id]))
				if vals[id] ~= vals[ids[1]] then differ = true end
			end
			if differ then
				nodeIds = ids
				print("scan: conflated group found, modKey = " .. string.sub(key, 1, 60))
				break
			end
		end
	end
	assert(#nodeIds > 0, "scan found no conflated group on this build; try another build")
end

local direct = {}
for _, id in ipairs(nodeIds) do
	local node = assert(build.spec.nodes[id], "unknown node id " .. id)
	assert(not node.alloc, "node " .. id .. " is allocated; this driver uses the addNodes path")
	direct[id] = directOffence(node)
end

-- normal PowerBuilder pass (what the heatmap shows)
build.calcsTab.powerStat = nil
build.calcsTab:PowerBuilder()

print(string.format("%-8s %-10s %-24s %-24s %s", "node", "same-key?", "DIRECT offence", "POWERBUILDER offence", "modKey (first 40)"))
for _, id in ipairs(nodeIds) do
	local node = build.spec.nodes[id]
	print(string.format("%-8d %-10s %-24.17g %-24.17g %s",
		id, tostring(node.modKey == build.spec.nodes[nodeIds[1]].modKey),
		direct[id], (node.power and node.power.offence) or -1,
		string.sub(node.modKey or "", 1, 40)))
end
print("rerun: the shared POWERBUILDER value flips to a different member's DIRECT value across processes")

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions