diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..564c30d --- /dev/null +++ b/coverage.out @@ -0,0 +1,487 @@ +mode: set +eq2emu/internal/alt_advancement/database.go:9.53,20.16 3 0 +eq2emu/internal/alt_advancement/database.go:20.16,22.3 1 0 +eq2emu/internal/alt_advancement/database.go:23.2,26.18 3 0 +eq2emu/internal/alt_advancement/database.go:26.18,54.17 3 0 +eq2emu/internal/alt_advancement/database.go:54.17,56.4 1 0 +eq2emu/internal/alt_advancement/database.go:59.3,62.65 2 0 +eq2emu/internal/alt_advancement/database.go:62.65,64.24 1 0 +eq2emu/internal/alt_advancement/database.go:64.24,66.5 1 0 +eq2emu/internal/alt_advancement/database.go:67.4,67.12 1 0 +eq2emu/internal/alt_advancement/database.go:70.3,70.16 1 0 +eq2emu/internal/alt_advancement/database.go:73.2,73.34 1 0 +eq2emu/internal/alt_advancement/database.go:73.34,75.3 1 0 +eq2emu/internal/alt_advancement/database.go:78.2,80.22 2 0 +eq2emu/internal/alt_advancement/database.go:80.22,82.3 1 0 +eq2emu/internal/alt_advancement/database.go:84.2,84.12 1 0 +eq2emu/internal/alt_advancement/database.go:88.47,95.16 3 0 +eq2emu/internal/alt_advancement/database.go:95.16,97.3 1 0 +eq2emu/internal/alt_advancement/database.go:98.2,101.18 3 0 +eq2emu/internal/alt_advancement/database.go:101.18,109.17 3 0 +eq2emu/internal/alt_advancement/database.go:109.17,111.4 1 0 +eq2emu/internal/alt_advancement/database.go:114.3,114.61 1 0 +eq2emu/internal/alt_advancement/database.go:114.61,116.24 1 0 +eq2emu/internal/alt_advancement/database.go:116.24,118.5 1 0 +eq2emu/internal/alt_advancement/database.go:119.4,119.12 1 0 +eq2emu/internal/alt_advancement/database.go:122.3,122.16 1 0 +eq2emu/internal/alt_advancement/database.go:125.2,125.34 1 0 +eq2emu/internal/alt_advancement/database.go:125.34,127.3 1 0 +eq2emu/internal/alt_advancement/database.go:129.2,129.22 1 0 +eq2emu/internal/alt_advancement/database.go:129.22,131.3 1 0 +eq2emu/internal/alt_advancement/database.go:133.2,133.12 1 0 +eq2emu/internal/alt_advancement/database.go:137.81,148.16 4 0 +eq2emu/internal/alt_advancement/database.go:148.16,150.3 1 0 +eq2emu/internal/alt_advancement/database.go:151.2,156.18 3 0 +eq2emu/internal/alt_advancement/database.go:156.18,166.17 3 0 +eq2emu/internal/alt_advancement/database.go:166.17,168.4 1 0 +eq2emu/internal/alt_advancement/database.go:170.3,170.47 1 0 +eq2emu/internal/alt_advancement/database.go:170.47,172.4 1 0 +eq2emu/internal/alt_advancement/database.go:173.3,173.87 1 0 +eq2emu/internal/alt_advancement/database.go:176.2,176.34 1 0 +eq2emu/internal/alt_advancement/database.go:176.34,178.3 1 0 +eq2emu/internal/alt_advancement/database.go:181.2,181.51 1 0 +eq2emu/internal/alt_advancement/database.go:181.51,185.3 3 0 +eq2emu/internal/alt_advancement/database.go:188.2,189.16 2 0 +eq2emu/internal/alt_advancement/database.go:189.16,191.3 1 0 +eq2emu/internal/alt_advancement/database.go:194.2,195.16 2 0 +eq2emu/internal/alt_advancement/database.go:195.16,197.3 1 0 +eq2emu/internal/alt_advancement/database.go:200.2,202.25 2 0 +eq2emu/internal/alt_advancement/database.go:206.99,214.16 3 0 +eq2emu/internal/alt_advancement/database.go:214.16,216.3 1 0 +eq2emu/internal/alt_advancement/database.go:217.2,219.18 2 0 +eq2emu/internal/alt_advancement/database.go:219.18,234.17 4 0 +eq2emu/internal/alt_advancement/database.go:234.17,236.4 1 0 +eq2emu/internal/alt_advancement/database.go:239.3,239.93 1 0 +eq2emu/internal/alt_advancement/database.go:239.93,241.4 1 0 +eq2emu/internal/alt_advancement/database.go:242.3,242.89 1 0 +eq2emu/internal/alt_advancement/database.go:242.89,244.4 1 0 +eq2emu/internal/alt_advancement/database.go:246.3,246.53 1 0 +eq2emu/internal/alt_advancement/database.go:249.2,249.19 1 0 +eq2emu/internal/alt_advancement/database.go:253.97,270.16 4 0 +eq2emu/internal/alt_advancement/database.go:270.16,272.50 1 0 +eq2emu/internal/alt_advancement/database.go:272.50,279.4 6 0 +eq2emu/internal/alt_advancement/database.go:280.3,280.64 1 0 +eq2emu/internal/alt_advancement/database.go:283.2,283.12 1 0 +eq2emu/internal/alt_advancement/database.go:287.74,289.32 1 0 +eq2emu/internal/alt_advancement/database.go:289.32,295.51 4 0 +eq2emu/internal/alt_advancement/database.go:295.51,296.27 1 0 +eq2emu/internal/alt_advancement/database.go:296.27,298.5 1 0 +eq2emu/internal/alt_advancement/database.go:300.3,303.28 3 0 +eq2emu/internal/alt_advancement/database.go:308.72,309.24 1 0 +eq2emu/internal/alt_advancement/database.go:309.24,311.3 1 0 +eq2emu/internal/alt_advancement/database.go:314.2,315.16 2 0 +eq2emu/internal/alt_advancement/database.go:315.16,317.3 1 0 +eq2emu/internal/alt_advancement/database.go:318.2,322.16 3 0 +eq2emu/internal/alt_advancement/database.go:322.16,324.3 1 0 +eq2emu/internal/alt_advancement/database.go:327.2,328.16 2 0 +eq2emu/internal/alt_advancement/database.go:328.16,330.3 1 0 +eq2emu/internal/alt_advancement/database.go:333.2,334.16 2 0 +eq2emu/internal/alt_advancement/database.go:334.16,336.3 1 0 +eq2emu/internal/alt_advancement/database.go:339.2,339.35 1 0 +eq2emu/internal/alt_advancement/database.go:339.35,341.3 1 0 +eq2emu/internal/alt_advancement/database.go:344.2,347.12 3 0 +eq2emu/internal/alt_advancement/database.go:351.94,369.2 3 0 +eq2emu/internal/alt_advancement/database.go:372.96,375.16 2 0 +eq2emu/internal/alt_advancement/database.go:375.16,377.3 1 0 +eq2emu/internal/alt_advancement/database.go:380.2,386.50 2 0 +eq2emu/internal/alt_advancement/database.go:386.50,397.17 2 0 +eq2emu/internal/alt_advancement/database.go:397.17,399.4 1 0 +eq2emu/internal/alt_advancement/database.go:402.2,402.12 1 0 +eq2emu/internal/alt_advancement/database.go:406.97,409.16 2 0 +eq2emu/internal/alt_advancement/database.go:409.16,411.3 1 0 +eq2emu/internal/alt_advancement/database.go:414.2,419.49 2 0 +eq2emu/internal/alt_advancement/database.go:419.49,421.59 1 0 +eq2emu/internal/alt_advancement/database.go:421.59,422.43 1 0 +eq2emu/internal/alt_advancement/database.go:422.43,431.19 2 0 +eq2emu/internal/alt_advancement/database.go:431.19,433.6 1 0 +eq2emu/internal/alt_advancement/database.go:438.2,438.12 1 0 +eq2emu/internal/alt_advancement/database.go:442.89,450.16 3 0 +eq2emu/internal/alt_advancement/database.go:450.16,452.3 1 0 +eq2emu/internal/alt_advancement/database.go:453.2,457.18 3 0 +eq2emu/internal/alt_advancement/database.go:457.18,467.17 3 0 +eq2emu/internal/alt_advancement/database.go:467.17,469.4 1 0 +eq2emu/internal/alt_advancement/database.go:471.3,471.41 1 0 +eq2emu/internal/alt_advancement/database.go:471.41,473.4 1 0 +eq2emu/internal/alt_advancement/database.go:474.3,474.75 1 0 +eq2emu/internal/alt_advancement/database.go:477.2,477.34 1 0 +eq2emu/internal/alt_advancement/database.go:477.34,479.3 1 0 +eq2emu/internal/alt_advancement/database.go:481.2,481.23 1 0 +eq2emu/internal/alt_advancement/database.go:485.65,488.16 2 0 +eq2emu/internal/alt_advancement/database.go:488.16,490.3 1 0 +eq2emu/internal/alt_advancement/database.go:491.2,500.31 3 0 +eq2emu/internal/alt_advancement/database.go:500.31,503.17 3 0 +eq2emu/internal/alt_advancement/database.go:503.17,505.4 1 0 +eq2emu/internal/alt_advancement/database.go:509.2,509.35 1 0 +eq2emu/internal/alt_advancement/database.go:509.35,511.3 1 0 +eq2emu/internal/alt_advancement/database.go:513.2,513.12 1 0 +eq2emu/internal/alt_advancement/database.go:517.75,523.16 4 0 +eq2emu/internal/alt_advancement/database.go:523.16,525.3 1 0 +eq2emu/internal/alt_advancement/database.go:526.2,531.16 4 0 +eq2emu/internal/alt_advancement/database.go:531.16,533.3 1 0 +eq2emu/internal/alt_advancement/database.go:534.2,546.16 4 0 +eq2emu/internal/alt_advancement/database.go:546.16,548.3 1 0 +eq2emu/internal/alt_advancement/database.go:549.2,552.18 3 0 +eq2emu/internal/alt_advancement/database.go:552.18,556.17 4 0 +eq2emu/internal/alt_advancement/database.go:556.17,558.4 1 0 +eq2emu/internal/alt_advancement/database.go:559.3,559.29 1 0 +eq2emu/internal/alt_advancement/database.go:561.2,563.19 2 0 +eq2emu/internal/alt_advancement/interfaces.go:191.130,198.2 1 0 +eq2emu/internal/alt_advancement/interfaces.go:282.77,287.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:290.54,292.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:295.45,297.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:300.79,302.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:305.70,307.2 1 0 +eq2emu/internal/alt_advancement/interfaces.go:310.51,312.2 1 0 +eq2emu/internal/alt_advancement/interfaces.go:315.77,317.2 1 0 +eq2emu/internal/alt_advancement/interfaces.go:320.60,322.2 1 0 +eq2emu/internal/alt_advancement/interfaces.go:325.67,327.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:330.62,332.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:335.69,337.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:340.59,342.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:345.42,347.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:355.57,357.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:360.48,362.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:365.52,367.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:370.45,372.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:375.45,377.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:380.54,382.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:385.44,387.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:390.46,392.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:395.67,397.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:405.57,407.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:410.48,412.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:415.52,417.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:420.48,422.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:425.59,427.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:430.54,432.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:446.53,453.2 1 1 +eq2emu/internal/alt_advancement/interfaces.go:456.69,460.46 3 1 +eq2emu/internal/alt_advancement/interfaces.go:460.46,463.3 2 1 +eq2emu/internal/alt_advancement/interfaces.go:465.2,466.19 2 1 +eq2emu/internal/alt_advancement/interfaces.go:470.69,474.39 3 1 +eq2emu/internal/alt_advancement/interfaces.go:474.39,476.27 1 0 +eq2emu/internal/alt_advancement/interfaces.go:476.27,478.9 2 0 +eq2emu/internal/alt_advancement/interfaces.go:482.2,482.34 1 1 +eq2emu/internal/alt_advancement/interfaces.go:486.52,491.2 3 1 +eq2emu/internal/alt_advancement/interfaces.go:494.82,498.58 3 1 +eq2emu/internal/alt_advancement/interfaces.go:498.58,501.3 2 1 +eq2emu/internal/alt_advancement/interfaces.go:503.2,504.19 2 1 +eq2emu/internal/alt_advancement/interfaces.go:508.87,512.45 3 1 +eq2emu/internal/alt_advancement/interfaces.go:512.45,514.33 1 0 +eq2emu/internal/alt_advancement/interfaces.go:514.33,516.9 2 0 +eq2emu/internal/alt_advancement/interfaces.go:520.2,520.43 1 1 +eq2emu/internal/alt_advancement/interfaces.go:524.66,529.2 3 0 +eq2emu/internal/alt_advancement/interfaces.go:532.73,536.49 3 1 +eq2emu/internal/alt_advancement/interfaces.go:536.49,540.3 3 1 +eq2emu/internal/alt_advancement/interfaces.go:542.2,543.19 2 1 +eq2emu/internal/alt_advancement/interfaces.go:547.75,551.42 3 1 +eq2emu/internal/alt_advancement/interfaces.go:551.42,553.30 1 0 +eq2emu/internal/alt_advancement/interfaces.go:553.30,555.9 2 0 +eq2emu/internal/alt_advancement/interfaces.go:559.2,560.33 2 1 +eq2emu/internal/alt_advancement/interfaces.go:564.58,569.2 3 0 +eq2emu/internal/alt_advancement/interfaces.go:572.33,579.2 5 1 +eq2emu/internal/alt_advancement/interfaces.go:582.59,594.2 3 1 +eq2emu/internal/alt_advancement/interfaces.go:597.51,602.2 3 1 +eq2emu/internal/alt_advancement/manager.go:9.54,19.2 1 1 +eq2emu/internal/alt_advancement/manager.go:22.36,24.40 1 1 +eq2emu/internal/alt_advancement/manager.go:24.40,26.3 1 0 +eq2emu/internal/alt_advancement/manager.go:29.2,29.34 1 1 +eq2emu/internal/alt_advancement/manager.go:29.34,32.3 2 1 +eq2emu/internal/alt_advancement/manager.go:34.2,34.54 1 1 +eq2emu/internal/alt_advancement/manager.go:34.54,37.3 2 1 +eq2emu/internal/alt_advancement/manager.go:39.2,39.12 1 1 +eq2emu/internal/alt_advancement/manager.go:43.35,48.24 3 1 +eq2emu/internal/alt_advancement/manager.go:48.24,50.3 1 1 +eq2emu/internal/alt_advancement/manager.go:52.2,52.12 1 1 +eq2emu/internal/alt_advancement/manager.go:56.39,57.9 1 1 +eq2emu/internal/alt_advancement/manager.go:58.21,59.15 1 1 +eq2emu/internal/alt_advancement/manager.go:60.10,61.14 1 1 +eq2emu/internal/alt_advancement/manager.go:66.41,67.24 1 1 +eq2emu/internal/alt_advancement/manager.go:67.24,69.3 1 1 +eq2emu/internal/alt_advancement/manager.go:71.2,74.58 2 1 +eq2emu/internal/alt_advancement/manager.go:74.58,76.3 1 1 +eq2emu/internal/alt_advancement/manager.go:79.2,79.52 1 1 +eq2emu/internal/alt_advancement/manager.go:79.52,81.3 1 0 +eq2emu/internal/alt_advancement/manager.go:84.2,94.12 8 1 +eq2emu/internal/alt_advancement/manager.go:98.43,110.16 7 1 +eq2emu/internal/alt_advancement/manager.go:110.16,112.3 1 1 +eq2emu/internal/alt_advancement/manager.go:114.2,114.12 1 1 +eq2emu/internal/alt_advancement/manager.go:118.78,122.2 1 0 +eq2emu/internal/alt_advancement/manager.go:125.90,126.24 1 1 +eq2emu/internal/alt_advancement/manager.go:126.24,128.3 1 1 +eq2emu/internal/alt_advancement/manager.go:130.2,131.16 2 1 +eq2emu/internal/alt_advancement/manager.go:131.16,133.3 1 0 +eq2emu/internal/alt_advancement/manager.go:135.2,135.25 1 1 +eq2emu/internal/alt_advancement/manager.go:139.60,140.24 1 1 +eq2emu/internal/alt_advancement/manager.go:140.24,142.3 1 1 +eq2emu/internal/alt_advancement/manager.go:145.2,146.24 2 1 +eq2emu/internal/alt_advancement/manager.go:146.24,148.3 1 0 +eq2emu/internal/alt_advancement/manager.go:151.2,151.46 1 1 +eq2emu/internal/alt_advancement/manager.go:155.82,158.65 2 1 +eq2emu/internal/alt_advancement/manager.go:158.65,161.3 2 1 +eq2emu/internal/alt_advancement/manager.go:162.2,169.65 4 1 +eq2emu/internal/alt_advancement/manager.go:169.65,171.3 1 1 +eq2emu/internal/alt_advancement/manager.go:174.2,175.16 2 1 +eq2emu/internal/alt_advancement/manager.go:175.16,177.3 1 1 +eq2emu/internal/alt_advancement/manager.go:180.2,185.25 3 1 +eq2emu/internal/alt_advancement/manager.go:189.89,192.16 2 1 +eq2emu/internal/alt_advancement/manager.go:192.16,194.3 1 0 +eq2emu/internal/alt_advancement/manager.go:197.2,198.19 2 1 +eq2emu/internal/alt_advancement/manager.go:198.19,200.3 1 1 +eq2emu/internal/alt_advancement/manager.go:203.2,203.25 1 1 +eq2emu/internal/alt_advancement/manager.go:203.25,204.90 1 0 +eq2emu/internal/alt_advancement/manager.go:204.90,207.4 2 0 +eq2emu/internal/alt_advancement/manager.go:211.2,212.16 2 1 +eq2emu/internal/alt_advancement/manager.go:212.16,214.3 1 1 +eq2emu/internal/alt_advancement/manager.go:217.2,221.24 3 1 +eq2emu/internal/alt_advancement/manager.go:221.24,223.3 1 0 +eq2emu/internal/alt_advancement/manager.go:226.2,226.26 1 1 +eq2emu/internal/alt_advancement/manager.go:226.26,228.3 1 0 +eq2emu/internal/alt_advancement/manager.go:230.2,230.12 1 1 +eq2emu/internal/alt_advancement/manager.go:234.70,237.24 2 1 +eq2emu/internal/alt_advancement/manager.go:237.24,239.3 1 1 +eq2emu/internal/alt_advancement/manager.go:242.2,243.19 2 0 +eq2emu/internal/alt_advancement/manager.go:243.19,245.3 1 0 +eq2emu/internal/alt_advancement/manager.go:248.2,249.42 2 0 +eq2emu/internal/alt_advancement/manager.go:249.42,251.3 1 0 +eq2emu/internal/alt_advancement/manager.go:254.2,268.24 9 0 +eq2emu/internal/alt_advancement/manager.go:268.24,270.3 1 0 +eq2emu/internal/alt_advancement/manager.go:273.2,273.26 1 0 +eq2emu/internal/alt_advancement/manager.go:273.26,275.3 1 0 +eq2emu/internal/alt_advancement/manager.go:277.2,277.12 1 0 +eq2emu/internal/alt_advancement/manager.go:281.96,284.24 2 1 +eq2emu/internal/alt_advancement/manager.go:284.24,286.3 1 1 +eq2emu/internal/alt_advancement/manager.go:289.2,292.28 3 0 +eq2emu/internal/alt_advancement/manager.go:292.28,294.40 1 0 +eq2emu/internal/alt_advancement/manager.go:294.40,296.4 1 0 +eq2emu/internal/alt_advancement/manager.go:299.2,299.26 1 0 +eq2emu/internal/alt_advancement/manager.go:303.81,306.24 2 1 +eq2emu/internal/alt_advancement/manager.go:306.24,308.3 1 1 +eq2emu/internal/alt_advancement/manager.go:311.2,311.25 1 0 +eq2emu/internal/alt_advancement/manager.go:311.25,312.86 1 0 +eq2emu/internal/alt_advancement/manager.go:312.86,314.4 1 0 +eq2emu/internal/alt_advancement/manager.go:318.2,327.12 7 0 +eq2emu/internal/alt_advancement/manager.go:331.92,334.24 2 1 +eq2emu/internal/alt_advancement/manager.go:334.24,336.3 1 1 +eq2emu/internal/alt_advancement/manager.go:339.2,340.21 2 0 +eq2emu/internal/alt_advancement/manager.go:340.21,343.3 2 0 +eq2emu/internal/alt_advancement/manager.go:343.8,346.3 2 0 +eq2emu/internal/alt_advancement/manager.go:348.2,355.12 5 0 +eq2emu/internal/alt_advancement/manager.go:359.86,362.24 2 1 +eq2emu/internal/alt_advancement/manager.go:362.24,364.3 1 1 +eq2emu/internal/alt_advancement/manager.go:367.2,369.50 3 1 +eq2emu/internal/alt_advancement/manager.go:369.50,371.3 1 0 +eq2emu/internal/alt_advancement/manager.go:372.2,374.23 2 1 +eq2emu/internal/alt_advancement/manager.go:378.90,381.16 2 1 +eq2emu/internal/alt_advancement/manager.go:381.16,383.3 1 0 +eq2emu/internal/alt_advancement/manager.go:386.2,396.24 9 1 +eq2emu/internal/alt_advancement/manager.go:396.24,398.3 1 0 +eq2emu/internal/alt_advancement/manager.go:401.2,403.12 2 1 +eq2emu/internal/alt_advancement/manager.go:407.82,410.24 2 1 +eq2emu/internal/alt_advancement/manager.go:410.24,412.3 1 1 +eq2emu/internal/alt_advancement/manager.go:414.2,417.91 3 1 +eq2emu/internal/alt_advancement/manager.go:421.67,423.19 2 1 +eq2emu/internal/alt_advancement/manager.go:423.19,425.3 1 0 +eq2emu/internal/alt_advancement/manager.go:426.2,426.20 1 1 +eq2emu/internal/alt_advancement/manager.go:430.77,432.19 2 1 +eq2emu/internal/alt_advancement/manager.go:432.19,434.3 1 0 +eq2emu/internal/alt_advancement/manager.go:435.2,435.20 1 1 +eq2emu/internal/alt_advancement/manager.go:439.75,441.2 1 1 +eq2emu/internal/alt_advancement/manager.go:444.77,446.2 1 0 +eq2emu/internal/alt_advancement/manager.go:449.55,456.2 4 1 +eq2emu/internal/alt_advancement/manager.go:459.79,461.24 2 1 +eq2emu/internal/alt_advancement/manager.go:461.24,463.3 1 0 +eq2emu/internal/alt_advancement/manager.go:465.2,478.3 3 1 +eq2emu/internal/alt_advancement/manager.go:482.62,485.2 2 0 +eq2emu/internal/alt_advancement/manager.go:488.50,490.2 1 1 +eq2emu/internal/alt_advancement/manager.go:495.49,497.2 1 1 +eq2emu/internal/alt_advancement/manager.go:500.64,502.2 1 0 +eq2emu/internal/alt_advancement/manager.go:505.62,510.2 3 1 +eq2emu/internal/alt_advancement/manager.go:513.58,515.2 1 0 +eq2emu/internal/alt_advancement/manager.go:518.55,520.2 1 0 +eq2emu/internal/alt_advancement/manager.go:523.56,525.2 1 0 +eq2emu/internal/alt_advancement/manager.go:528.46,530.2 1 0 +eq2emu/internal/alt_advancement/manager.go:535.71,540.2 3 1 +eq2emu/internal/alt_advancement/manager.go:543.115,552.46 4 1 +eq2emu/internal/alt_advancement/manager.go:552.46,554.3 1 1 +eq2emu/internal/alt_advancement/manager.go:557.2,558.21 2 1 +eq2emu/internal/alt_advancement/manager.go:558.21,570.3 2 1 +eq2emu/internal/alt_advancement/manager.go:573.2,582.12 7 1 +eq2emu/internal/alt_advancement/manager.go:586.93,596.29 1 0 +eq2emu/internal/alt_advancement/manager.go:596.29,598.64 2 0 +eq2emu/internal/alt_advancement/manager.go:598.64,600.4 1 0 +eq2emu/internal/alt_advancement/manager.go:603.2,603.13 1 0 +eq2emu/internal/alt_advancement/manager.go:609.40,615.6 4 1 +eq2emu/internal/alt_advancement/manager.go:615.6,616.10 1 1 +eq2emu/internal/alt_advancement/manager.go:617.19,618.25 1 1 +eq2emu/internal/alt_advancement/manager.go:619.22,620.10 1 1 +eq2emu/internal/alt_advancement/manager.go:626.37,632.6 4 1 +eq2emu/internal/alt_advancement/manager.go:632.6,633.10 1 1 +eq2emu/internal/alt_advancement/manager.go:634.19,635.28 1 1 +eq2emu/internal/alt_advancement/manager.go:636.22,637.10 1 1 +eq2emu/internal/alt_advancement/manager.go:643.41,653.46 7 1 +eq2emu/internal/alt_advancement/manager.go:653.46,658.3 4 0 +eq2emu/internal/alt_advancement/manager.go:659.2,664.32 4 1 +eq2emu/internal/alt_advancement/manager.go:664.32,666.3 1 0 +eq2emu/internal/alt_advancement/manager.go:668.2,668.39 1 1 +eq2emu/internal/alt_advancement/manager.go:672.44,673.24 1 1 +eq2emu/internal/alt_advancement/manager.go:673.24,675.3 1 0 +eq2emu/internal/alt_advancement/manager.go:677.2,680.46 3 1 +eq2emu/internal/alt_advancement/manager.go:680.46,681.28 1 0 +eq2emu/internal/alt_advancement/manager.go:681.28,682.64 1 0 +eq2emu/internal/alt_advancement/manager.go:682.64,684.13 2 0 +eq2emu/internal/alt_advancement/manager.go:686.4,686.33 1 0 +eq2emu/internal/alt_advancement/manager.go:692.57,696.19 3 0 +eq2emu/internal/alt_advancement/manager.go:697.27,698.30 1 0 +eq2emu/internal/alt_advancement/manager.go:699.25,700.28 1 0 +eq2emu/internal/alt_advancement/manager.go:701.23,702.26 1 0 +eq2emu/internal/alt_advancement/manager.go:709.72,713.43 3 1 +eq2emu/internal/alt_advancement/manager.go:713.43,715.3 1 1 +eq2emu/internal/alt_advancement/manager.go:719.46,723.43 3 1 +eq2emu/internal/alt_advancement/manager.go:723.43,725.3 1 0 +eq2emu/internal/alt_advancement/manager.go:729.93,733.43 3 1 +eq2emu/internal/alt_advancement/manager.go:733.43,735.3 1 1 +eq2emu/internal/alt_advancement/manager.go:739.109,743.43 3 1 +eq2emu/internal/alt_advancement/manager.go:743.43,745.3 1 1 +eq2emu/internal/alt_advancement/manager.go:749.111,753.43 3 0 +eq2emu/internal/alt_advancement/manager.go:753.43,755.3 1 0 +eq2emu/internal/alt_advancement/manager.go:759.97,763.43 3 0 +eq2emu/internal/alt_advancement/manager.go:763.43,765.3 1 0 +eq2emu/internal/alt_advancement/manager.go:769.96,773.43 3 0 +eq2emu/internal/alt_advancement/manager.go:773.43,775.3 1 0 +eq2emu/internal/alt_advancement/manager.go:779.100,783.43 3 1 +eq2emu/internal/alt_advancement/manager.go:783.43,785.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:10.38,19.2 1 1 +eq2emu/internal/alt_advancement/master_list.go:22.72,23.17 1 1 +eq2emu/internal/alt_advancement/master_list.go:23.17,25.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:27.2,27.21 1 1 +eq2emu/internal/alt_advancement/master_list.go:27.21,29.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:31.2,35.56 3 1 +eq2emu/internal/alt_advancement/master_list.go:35.56,37.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:39.2,39.54 1 1 +eq2emu/internal/alt_advancement/master_list.go:39.54,41.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:44.2,51.38 4 1 +eq2emu/internal/alt_advancement/master_list.go:51.38,53.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:54.2,58.12 3 1 +eq2emu/internal/alt_advancement/master_list.go:62.75,66.54 3 1 +eq2emu/internal/alt_advancement/master_list.go:66.54,68.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:70.2,70.12 1 1 +eq2emu/internal/alt_advancement/master_list.go:74.82,78.52 3 1 +eq2emu/internal/alt_advancement/master_list.go:78.52,80.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:82.2,82.12 1 1 +eq2emu/internal/alt_advancement/master_list.go:86.70,90.52 3 1 +eq2emu/internal/alt_advancement/master_list.go:90.52,93.29 2 1 +eq2emu/internal/alt_advancement/master_list.go:93.29,95.4 1 1 +eq2emu/internal/alt_advancement/master_list.go:96.3,96.16 1 1 +eq2emu/internal/alt_advancement/master_list.go:99.2,99.28 1 1 +eq2emu/internal/alt_advancement/master_list.go:103.72,109.32 4 1 +eq2emu/internal/alt_advancement/master_list.go:109.32,111.49 1 1 +eq2emu/internal/alt_advancement/master_list.go:111.49,113.4 1 1 +eq2emu/internal/alt_advancement/master_list.go:116.2,116.15 1 1 +eq2emu/internal/alt_advancement/master_list.go:120.70,126.32 4 1 +eq2emu/internal/alt_advancement/master_list.go:126.32,127.27 1 1 +eq2emu/internal/alt_advancement/master_list.go:127.27,129.4 1 1 +eq2emu/internal/alt_advancement/master_list.go:132.2,132.15 1 1 +eq2emu/internal/alt_advancement/master_list.go:136.37,141.2 3 1 +eq2emu/internal/alt_advancement/master_list.go:144.56,149.32 4 1 +eq2emu/internal/alt_advancement/master_list.go:149.32,151.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:153.2,153.15 1 1 +eq2emu/internal/alt_advancement/master_list.go:157.51,166.2 7 1 +eq2emu/internal/alt_advancement/master_list.go:169.43,173.35 3 1 +eq2emu/internal/alt_advancement/master_list.go:173.35,174.56 1 1 +eq2emu/internal/alt_advancement/master_list.go:174.56,179.26 3 1 +eq2emu/internal/alt_advancement/master_list.go:179.26,181.5 1 1 +eq2emu/internal/alt_advancement/master_list.go:182.4,182.28 1 1 +eq2emu/internal/alt_advancement/master_list.go:188.46,193.2 3 0 +eq2emu/internal/alt_advancement/master_list.go:196.45,201.35 4 1 +eq2emu/internal/alt_advancement/master_list.go:201.35,203.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:205.2,205.41 1 1 +eq2emu/internal/alt_advancement/master_list.go:205.41,207.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:209.2,209.15 1 1 +eq2emu/internal/alt_advancement/master_list.go:213.51,219.32 4 0 +eq2emu/internal/alt_advancement/master_list.go:219.32,220.20 1 0 +eq2emu/internal/alt_advancement/master_list.go:220.20,222.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:225.3,225.26 1 0 +eq2emu/internal/alt_advancement/master_list.go:225.26,226.61 1 0 +eq2emu/internal/alt_advancement/master_list.go:226.61,228.5 1 0 +eq2emu/internal/alt_advancement/master_list.go:232.3,232.49 1 0 +eq2emu/internal/alt_advancement/master_list.go:232.49,234.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:236.3,236.49 1 0 +eq2emu/internal/alt_advancement/master_list.go:236.49,238.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:241.3,241.65 1 0 +eq2emu/internal/alt_advancement/master_list.go:241.65,243.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:245.3,245.61 1 0 +eq2emu/internal/alt_advancement/master_list.go:245.61,247.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:250.2,250.15 1 0 +eq2emu/internal/alt_advancement/master_list.go:254.60,265.43 8 0 +eq2emu/internal/alt_advancement/master_list.go:265.43,267.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:268.2,270.14 2 0 +eq2emu/internal/alt_advancement/master_list.go:274.46,282.2 1 1 +eq2emu/internal/alt_advancement/master_list.go:285.69,286.17 1 1 +eq2emu/internal/alt_advancement/master_list.go:286.17,288.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:290.2,294.56 3 1 +eq2emu/internal/alt_advancement/master_list.go:294.56,296.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:299.2,305.44 3 1 +eq2emu/internal/alt_advancement/master_list.go:305.44,307.3 1 1 +eq2emu/internal/alt_advancement/master_list.go:308.2,312.12 3 1 +eq2emu/internal/alt_advancement/master_list.go:316.62,322.37 4 1 +eq2emu/internal/alt_advancement/master_list.go:322.37,325.3 2 1 +eq2emu/internal/alt_advancement/master_list.go:327.2,327.15 1 1 +eq2emu/internal/alt_advancement/master_list.go:331.82,335.60 3 1 +eq2emu/internal/alt_advancement/master_list.go:335.60,338.33 2 1 +eq2emu/internal/alt_advancement/master_list.go:338.33,341.4 2 1 +eq2emu/internal/alt_advancement/master_list.go:342.3,342.16 1 1 +eq2emu/internal/alt_advancement/master_list.go:345.2,345.26 1 1 +eq2emu/internal/alt_advancement/master_list.go:349.71,353.54 3 1 +eq2emu/internal/alt_advancement/master_list.go:353.54,356.3 2 1 +eq2emu/internal/alt_advancement/master_list.go:358.2,358.12 1 1 +eq2emu/internal/alt_advancement/master_list.go:362.42,367.2 3 1 +eq2emu/internal/alt_advancement/master_list.go:370.50,378.2 6 1 +eq2emu/internal/alt_advancement/master_list.go:381.51,386.2 3 0 +eq2emu/internal/alt_advancement/master_list.go:389.52,394.41 4 0 +eq2emu/internal/alt_advancement/master_list.go:394.41,396.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:398.2,398.42 1 0 +eq2emu/internal/alt_advancement/master_list.go:398.42,400.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:402.2,402.16 1 0 +eq2emu/internal/alt_advancement/master_list.go:406.59,414.37 5 0 +eq2emu/internal/alt_advancement/master_list.go:414.37,416.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:419.2,420.37 2 0 +eq2emu/internal/alt_advancement/master_list.go:420.37,422.24 2 0 +eq2emu/internal/alt_advancement/master_list.go:422.24,424.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:425.3,425.27 1 0 +eq2emu/internal/alt_advancement/master_list.go:428.2,428.15 1 0 +eq2emu/internal/alt_advancement/master_list.go:432.65,443.51 8 0 +eq2emu/internal/alt_advancement/master_list.go:443.51,445.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:446.2,448.14 2 0 +eq2emu/internal/alt_advancement/master_list.go:452.76,457.29 3 0 +eq2emu/internal/alt_advancement/master_list.go:457.29,458.13 1 0 +eq2emu/internal/alt_advancement/master_list.go:458.13,460.4 1 0 +eq2emu/internal/alt_advancement/master_list.go:463.2,463.16 1 0 +eq2emu/internal/alt_advancement/master_list.go:467.78,470.27 2 0 +eq2emu/internal/alt_advancement/master_list.go:470.27,472.3 1 0 +eq2emu/internal/alt_advancement/master_list.go:474.2,474.10 1 0 +eq2emu/internal/alt_advancement/types.go:283.47,299.2 1 1 +eq2emu/internal/alt_advancement/types.go:302.57,316.2 1 1 +eq2emu/internal/alt_advancement/types.go:319.62,331.2 1 1 +eq2emu/internal/alt_advancement/types.go:334.54,347.2 1 0 +eq2emu/internal/alt_advancement/types.go:350.51,353.2 2 1 +eq2emu/internal/alt_advancement/types.go:356.43,362.2 1 1 +eq2emu/internal/alt_advancement/types.go:365.39,366.15 1 1 +eq2emu/internal/alt_advancement/types.go:367.16,368.22 1 1 +eq2emu/internal/alt_advancement/types.go:369.19,370.25 1 1 +eq2emu/internal/alt_advancement/types.go:371.17,372.24 1 1 +eq2emu/internal/alt_advancement/types.go:373.17,374.23 1 1 +eq2emu/internal/alt_advancement/types.go:375.21,376.27 1 1 +eq2emu/internal/alt_advancement/types.go:377.19,378.25 1 1 +eq2emu/internal/alt_advancement/types.go:379.30,380.36 1 1 +eq2emu/internal/alt_advancement/types.go:381.17,382.23 1 1 +eq2emu/internal/alt_advancement/types.go:383.22,384.28 1 1 +eq2emu/internal/alt_advancement/types.go:385.18,386.24 1 1 +eq2emu/internal/alt_advancement/types.go:387.10,388.13 1 1 +eq2emu/internal/alt_advancement/types.go:393.36,394.47 1 1 +eq2emu/internal/alt_advancement/types.go:394.47,396.3 1 1 +eq2emu/internal/alt_advancement/types.go:397.2,397.18 1 1 +eq2emu/internal/alt_advancement/types.go:401.46,402.57 1 1 +eq2emu/internal/alt_advancement/types.go:402.57,404.3 1 1 +eq2emu/internal/alt_advancement/types.go:405.2,405.18 1 1 +eq2emu/internal/alt_advancement/types.go:409.59,411.2 1 1 diff --git a/internal/alt_advancement/aa_test.go b/internal/alt_advancement/aa_test.go index 8414f63..49fb68e 100644 --- a/internal/alt_advancement/aa_test.go +++ b/internal/alt_advancement/aa_test.go @@ -1,32 +1,1211 @@ package alt_advancement import ( + "fmt" + "log" + "reflect" + "sync" "testing" "time" ) -func TestPackageBuild(t *testing.T) { - // Basic test to verify the package builds - config := AAManagerConfig{ - UpdateInterval: time.Second * 30, - SaveInterval: time.Minute * 5, - AutoSave: true, +// Test AltAdvanceData structure and methods +func TestAltAdvanceData(t *testing.T) { + t.Run("Copy", func(t *testing.T) { + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Description: "Test Description", + Group: AA_CLASS, + Col: 5, + Row: 3, + Icon: 1234, + RankCost: 2, + MaxRank: 5, + MinLevel: 20, + ClassName: "Fighter", + SubclassName: "Guardian", + } + + copy := aa.Copy() + if copy == aa { + t.Error("Copy should return a new instance, not the same pointer") + } + + if !reflect.DeepEqual(aa, copy) { + t.Error("Copy should have identical field values") + } + + // Modify original and ensure copy is unaffected + aa.Name = "Modified" + if copy.Name == "Modified" { + t.Error("Copy should not be affected by changes to original") + } + }) + + t.Run("IsValid", func(t *testing.T) { + tests := []struct { + name string + aa *AltAdvanceData + expected bool + }{ + { + name: "Valid AA", + aa: &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + MaxRank: 5, + RankCost: 2, + }, + expected: true, + }, + { + name: "Invalid - No SpellID", + aa: &AltAdvanceData{ + NodeID: 200, + Name: "Test AA", + MaxRank: 5, + RankCost: 2, + }, + expected: false, + }, + { + name: "Invalid - No NodeID", + aa: &AltAdvanceData{ + SpellID: 100, + Name: "Test AA", + MaxRank: 5, + RankCost: 2, + }, + expected: false, + }, + { + name: "Invalid - No Name", + aa: &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + MaxRank: 5, + RankCost: 2, + }, + expected: false, + }, + { + name: "Invalid - No MaxRank", + aa: &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + RankCost: 2, + }, + expected: false, + }, + { + name: "Invalid - No RankCost", + aa: &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + MaxRank: 5, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if result := tt.aa.IsValid(); result != tt.expected { + t.Errorf("IsValid() = %v, want %v", result, tt.expected) + } + }) + } + }) +} + +// Test MasterAAList functionality +func TestMasterAAList(t *testing.T) { + t.Run("NewMasterAAList", func(t *testing.T) { + masterList := NewMasterAAList() + if masterList == nil { + t.Fatal("NewMasterAAList returned nil") + } + + if masterList.Size() != 0 { + t.Error("Expected empty list to have size 0") + } + + if masterList.aaList == nil { + t.Error("Expected aaList to be initialized") + } + + if masterList.aaBySpellID == nil || masterList.aaByNodeID == nil || masterList.aaByGroup == nil { + t.Error("Expected lookup maps to be initialized") + } + }) + + t.Run("AddAltAdvancement", func(t *testing.T) { + masterList := NewMasterAAList() + + // Test adding nil + err := masterList.AddAltAdvancement(nil) + if err == nil { + t.Error("Expected error when adding nil AA") + } + + // Test adding invalid AA + invalidAA := &AltAdvanceData{SpellID: 0} + err = masterList.AddAltAdvancement(invalidAA) + if err == nil { + t.Error("Expected error when adding invalid AA") + } + + // Test adding valid AA + validAA := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + err = masterList.AddAltAdvancement(validAA) + if err != nil { + t.Errorf("Unexpected error adding valid AA: %v", err) + } + + if masterList.Size() != 1 { + t.Error("Expected size to be 1 after adding AA") + } + + // Test duplicate spell ID + dupSpellAA := &AltAdvanceData{ + SpellID: 100, // Same spell ID + NodeID: 201, + Name: "Duplicate Spell", + MaxRank: 5, + RankCost: 2, + } + + err = masterList.AddAltAdvancement(dupSpellAA) + if err == nil { + t.Error("Expected error when adding duplicate spell ID") + } + + // Test duplicate node ID + dupNodeAA := &AltAdvanceData{ + SpellID: 101, + NodeID: 200, // Same node ID + Name: "Duplicate Node", + MaxRank: 5, + RankCost: 2, + } + + err = masterList.AddAltAdvancement(dupNodeAA) + if err == nil { + t.Error("Expected error when adding duplicate node ID") + } + }) + + t.Run("GetAltAdvancement", func(t *testing.T) { + masterList := NewMasterAAList() + + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa) + + // Test getting existing AA + retrieved := masterList.GetAltAdvancement(100) + if retrieved == nil { + t.Fatal("Expected to retrieve AA by spell ID") + } + + if retrieved == aa { + t.Error("GetAltAdvancement should return a copy, not the original") + } + + if retrieved.SpellID != aa.SpellID || retrieved.Name != aa.Name { + t.Error("Retrieved AA should have same data as original") + } + + // Test getting non-existent AA + notFound := masterList.GetAltAdvancement(999) + if notFound != nil { + t.Error("Expected nil for non-existent spell ID") + } + }) + + t.Run("GetAltAdvancementByNodeID", func(t *testing.T) { + masterList := NewMasterAAList() + + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa) + + // Test getting existing AA + retrieved := masterList.GetAltAdvancementByNodeID(200) + if retrieved == nil { + t.Fatal("Expected to retrieve AA by node ID") + } + + if retrieved.NodeID != aa.NodeID { + t.Error("Retrieved AA should have same node ID") + } + + // Test getting non-existent AA + notFound := masterList.GetAltAdvancementByNodeID(999) + if notFound != nil { + t.Error("Expected nil for non-existent node ID") + } + }) + + t.Run("GetAAsByGroup", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add AAs to different groups + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Class AA 1", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Class AA 2", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + aa3 := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "Heroic AA", + Group: AA_HEROIC, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa1) + masterList.AddAltAdvancement(aa2) + masterList.AddAltAdvancement(aa3) + + // Test getting AAs by group + classAAs := masterList.GetAAsByGroup(AA_CLASS) + if len(classAAs) != 2 { + t.Errorf("Expected 2 class AAs, got %d", len(classAAs)) + } + + heroicAAs := masterList.GetAAsByGroup(AA_HEROIC) + if len(heroicAAs) != 1 { + t.Errorf("Expected 1 heroic AA, got %d", len(heroicAAs)) + } + + // Test getting empty group + emptyGroup := masterList.GetAAsByGroup(AA_DRAGON) + if len(emptyGroup) != 0 { + t.Error("Expected empty slice for group with no AAs") + } + + // Verify copies are returned + if &classAAs[0] == &aa1 { + t.Error("GetAAsByGroup should return copies, not originals") + } + }) + + t.Run("GetAAsByClass", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add AAs with different class requirements + aaAllClasses := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "All Classes", + Group: AA_CLASS, + ClassReq: 0, // Available to all classes + MaxRank: 5, + RankCost: 2, + } + + aaFighterOnly := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Fighter Only", + Group: AA_CLASS, + ClassReq: 1, // Fighter class + MaxRank: 5, + RankCost: 2, + } + + aaMageOnly := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "Mage Only", + Group: AA_CLASS, + ClassReq: 20, // Mage class + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aaAllClasses) + masterList.AddAltAdvancement(aaFighterOnly) + masterList.AddAltAdvancement(aaMageOnly) + + // Test getting AAs for fighter class + fighterAAs := masterList.GetAAsByClass(1) + if len(fighterAAs) != 2 { + t.Errorf("Expected 2 AAs for fighter (all classes + fighter only), got %d", len(fighterAAs)) + } + + // Test getting AAs for mage class + mageAAs := masterList.GetAAsByClass(20) + if len(mageAAs) != 2 { + t.Errorf("Expected 2 AAs for mage (all classes + mage only), got %d", len(mageAAs)) + } + + // Test getting AAs for class with no specific AAs + priestAAs := masterList.GetAAsByClass(10) + if len(priestAAs) != 1 { + t.Errorf("Expected 1 AA for priest (all classes only), got %d", len(priestAAs)) + } + }) + + t.Run("GetAAsByLevel", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add AAs with different level requirements + aaLevel10 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Level 10 AA", + Group: AA_CLASS, + MinLevel: 10, + MaxRank: 5, + RankCost: 2, + } + + aaLevel30 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Level 30 AA", + Group: AA_CLASS, + MinLevel: 30, + MaxRank: 5, + RankCost: 2, + } + + aaLevel50 := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "Level 50 AA", + Group: AA_CLASS, + MinLevel: 50, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aaLevel10) + masterList.AddAltAdvancement(aaLevel30) + masterList.AddAltAdvancement(aaLevel50) + + // Test getting AAs at different levels + level20AAs := masterList.GetAAsByLevel(20) + if len(level20AAs) != 1 { + t.Errorf("Expected 1 AA at level 20, got %d", len(level20AAs)) + } + + level40AAs := masterList.GetAAsByLevel(40) + if len(level40AAs) != 2 { + t.Errorf("Expected 2 AAs at level 40, got %d", len(level40AAs)) + } + + level60AAs := masterList.GetAAsByLevel(60) + if len(level60AAs) != 3 { + t.Errorf("Expected 3 AAs at level 60, got %d", len(level60AAs)) + } + + level5AAs := masterList.GetAAsByLevel(5) + if len(level5AAs) != 0 { + t.Errorf("Expected 0 AAs at level 5, got %d", len(level5AAs)) + } + }) + + t.Run("SortAAsByGroup", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add AAs in random order + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "AA1", + Group: AA_CLASS, + Row: 2, + Col: 3, + MaxRank: 5, + RankCost: 2, + } + + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "AA2", + Group: AA_CLASS, + Row: 1, + Col: 5, + MaxRank: 5, + RankCost: 2, + } + + aa3 := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "AA3", + Group: AA_CLASS, + Row: 1, + Col: 2, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa1) + masterList.AddAltAdvancement(aa2) + masterList.AddAltAdvancement(aa3) + + // Sort and verify order + masterList.SortAAsByGroup() + + classAAs := masterList.GetAAsByGroup(AA_CLASS) + if len(classAAs) != 3 { + t.Fatal("Expected 3 AAs") + } + + // Should be sorted by row then column + // aa3 (1,2), aa2 (1,5), aa1 (2,3) + if classAAs[0].Name != "AA3" || classAAs[1].Name != "AA2" || classAAs[2].Name != "AA1" { + t.Error("AAs not sorted correctly by row and column") + } + }) + + t.Run("GetGroups", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add AAs to different groups + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "AA1", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "AA2", + Group: AA_HEROIC, + MaxRank: 5, + RankCost: 2, + } + + aa3 := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "AA3", + Group: AA_TRADESKILL, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa1) + masterList.AddAltAdvancement(aa2) + masterList.AddAltAdvancement(aa3) + + groups := masterList.GetGroups() + if len(groups) != 3 { + t.Errorf("Expected 3 groups, got %d", len(groups)) + } + + // Check that all expected groups are present + groupMap := make(map[int8]bool) + for _, g := range groups { + groupMap[g] = true + } + + if !groupMap[AA_CLASS] || !groupMap[AA_HEROIC] || !groupMap[AA_TRADESKILL] { + t.Error("Not all expected groups were returned") + } + }) + + t.Run("DestroyAltAdvancements", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add some AAs + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa) + + if masterList.Size() != 1 { + t.Error("Expected size to be 1 before destroy") + } + + // Destroy all AAs + masterList.DestroyAltAdvancements() + + if masterList.Size() != 0 { + t.Error("Expected size to be 0 after destroy") + } + + // Verify maps are cleared + if len(masterList.aaBySpellID) != 0 || len(masterList.aaByNodeID) != 0 || len(masterList.aaByGroup) != 0 { + t.Error("Expected all maps to be cleared after destroy") + } + }) + + t.Run("ConcurrentAccess", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add initial AAs + for i := int32(1); i <= 10; i++ { + aa := &AltAdvanceData{ + SpellID: i * 100, + NodeID: i * 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + masterList.AddAltAdvancement(aa) + } + + // Concurrent reads and writes + var wg sync.WaitGroup + errors := make(chan error, 100) + + // Multiple readers + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + masterList.GetAltAdvancement(100) + masterList.GetAAsByGroup(AA_CLASS) + masterList.Size() + } + }() + } + + // Multiple writers (trying to add new unique AAs) + for i := 0; i < 5; i++ { + wg.Add(1) + go func(id int32) { + defer wg.Done() + aa := &AltAdvanceData{ + SpellID: 1000 + id*100, + NodeID: 2000 + id*100, + Name: "Concurrent AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + if err := masterList.AddAltAdvancement(aa); err != nil { + errors <- err + } + }(int32(i)) + } + + wg.Wait() + close(errors) + + // Check for unexpected errors + errorCount := 0 + for err := range errors { + errorCount++ + t.Logf("Concurrent write error (expected for duplicates): %v", err) + } + + // Check that we have expected number of AAs + finalSize := masterList.Size() + if finalSize < 10 || finalSize > 15 { // 10 initial + 0-5 concurrent (some may fail due to race) + t.Errorf("Expected between 10-15 AAs after concurrent operations, got %d", finalSize) + } + }) +} + +// Test MasterAANodeList functionality +func TestMasterAANodeList(t *testing.T) { + t.Run("NewMasterAANodeList", func(t *testing.T) { + nodeList := NewMasterAANodeList() + if nodeList == nil { + t.Fatal("NewMasterAANodeList returned nil") + } + + if nodeList.Size() != 0 { + t.Error("Expected empty list to have size 0") + } + }) + + t.Run("AddTreeNode", func(t *testing.T) { + nodeList := NewMasterAANodeList() + + // Test adding nil + err := nodeList.AddTreeNode(nil) + if err == nil { + t.Error("Expected error when adding nil node") + } + + // Test adding valid node + node := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + err = nodeList.AddTreeNode(node) + if err != nil { + t.Errorf("Unexpected error adding valid node: %v", err) + } + + if nodeList.Size() != 1 { + t.Error("Expected size to be 1 after adding node") + } + + // Test duplicate tree ID + dupNode := &TreeNodeData{ + ClassID: 2, + TreeID: 100, // Same tree ID + AATreeID: 201, + } + + err = nodeList.AddTreeNode(dupNode) + if err == nil { + t.Error("Expected error when adding duplicate tree ID") + } + }) + + t.Run("GetTreeNodeByTreeID", func(t *testing.T) { + nodeList := NewMasterAANodeList() + + node := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + nodeList.AddTreeNode(node) + + // Test getting existing node + retrieved := nodeList.GetTreeNode(100) + if retrieved == nil { + t.Fatal("Expected to retrieve node by tree ID") + } + + if retrieved == node { + t.Error("GetTreeNode should return a copy, not the original") + } + + if retrieved.TreeID != node.TreeID { + t.Error("Retrieved node should have same tree ID") + } + + // Test getting non-existent node + notFound := nodeList.GetTreeNode(999) + if notFound != nil { + t.Error("Expected nil for non-existent tree ID") + } + }) + + t.Run("GetTreeNodesByClass", func(t *testing.T) { + nodeList := NewMasterAANodeList() + + // Add nodes for different classes + node1 := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + node2 := &TreeNodeData{ + ClassID: 1, + TreeID: 101, + AATreeID: 201, + } + + node3 := &TreeNodeData{ + ClassID: 2, + TreeID: 102, + AATreeID: 202, + } + + nodeList.AddTreeNode(node1) + nodeList.AddTreeNode(node2) + nodeList.AddTreeNode(node3) + + // Test getting nodes by class + class1Nodes := nodeList.GetTreeNodesByClass(1) + if len(class1Nodes) != 2 { + t.Errorf("Expected 2 nodes for class 1, got %d", len(class1Nodes)) + } + + class2Nodes := nodeList.GetTreeNodesByClass(2) + if len(class2Nodes) != 1 { + t.Errorf("Expected 1 node for class 2, got %d", len(class2Nodes)) + } + + // Test getting empty class + emptyClass := nodeList.GetTreeNodesByClass(999) + if len(emptyClass) != 0 { + t.Error("Expected empty slice for class with no nodes") + } + }) +} + +// Test AATemplate functionality +func TestAATemplate(t *testing.T) { + t.Run("NewAATemplate", func(t *testing.T) { + // Test personal template + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Personal Template 1") + if template == nil { + t.Fatal("NewAATemplate returned nil") + } + + if template.TemplateID != AA_TEMPLATE_PERSONAL_1 { + t.Error("Expected template ID to match") + } + + if template.Name != "Personal Template 1" { + t.Error("Expected template name to match") + } + + if !template.IsPersonal { + t.Error("Expected IsPersonal to be true for template ID 1") + } + + if template.IsServer { + t.Error("Expected IsServer to be false for template ID 1") + } + + if template.IsCurrent { + t.Error("Expected IsCurrent to be false for template ID 1") + } + + // Test server template + serverTemplate := NewAATemplate(AA_TEMPLATE_SERVER_1, "Server Template 1") + if !serverTemplate.IsServer { + t.Error("Expected IsServer to be true for template ID 4") + } + + // Test current template + currentTemplate := NewAATemplate(AA_TEMPLATE_CURRENT, "Current") + if !currentTemplate.IsCurrent { + t.Error("Expected IsCurrent to be true for template ID 7") + } + }) + + t.Run("AddEntry", func(t *testing.T) { + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") + + entry := &AAEntry{ + TemplateID: AA_TEMPLATE_PERSONAL_1, + TabID: AA_CLASS, + AAID: 100, + Order: 1, + TreeID: 1, + } + + // Add entry + template.AddEntry(entry) + + if len(template.Entries) != 1 { + t.Error("Expected 1 entry after adding") + } + + if template.Entries[0] != entry { + t.Error("Expected entry to be added to template") + } + }) + + t.Run("GetEntry", func(t *testing.T) { + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") + + entry1 := &AAEntry{ + TemplateID: AA_TEMPLATE_PERSONAL_1, + TabID: AA_CLASS, + AAID: 100, + Order: 1, + TreeID: 1, + } + + entry2 := &AAEntry{ + TemplateID: AA_TEMPLATE_PERSONAL_1, + TabID: AA_CLASS, + AAID: 101, + Order: 2, + TreeID: 2, + } + + template.AddEntry(entry1) + template.AddEntry(entry2) + + // Test getting existing entry + found := template.GetEntry(100) + if found == nil { + t.Error("Expected to find entry with AAID 100") + } + + if found != entry1 { + t.Error("Expected to get correct entry") + } + + // Test getting non-existent entry + notFound := template.GetEntry(999) + if notFound != nil { + t.Error("Expected nil for non-existent AAID") + } + }) + + t.Run("RemoveEntry", func(t *testing.T) { + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") + + entry := &AAEntry{ + TemplateID: AA_TEMPLATE_PERSONAL_1, + TabID: AA_CLASS, + AAID: 100, + Order: 1, + TreeID: 1, + } + + template.AddEntry(entry) + + // Remove entry + removed := template.RemoveEntry(100) + if !removed { + t.Error("Expected RemoveEntry to return true") + } + + if len(template.Entries) != 0 { + t.Error("Expected 0 entries after removing") + } + + // Try to remove non-existent entry + removed = template.RemoveEntry(999) + if removed { + t.Error("Expected RemoveEntry to return false for non-existent entry") + } + }) +} + +// Test AAPlayerState functionality +func TestAAPlayerState(t *testing.T) { + t.Run("NewAAPlayerState", func(t *testing.T) { + playerState := NewAAPlayerState(123) + if playerState == nil { + t.Fatal("NewAAPlayerState returned nil") + } + + if playerState.CharacterID != 123 { + t.Error("Expected character ID to be 123") + } + + if playerState.TotalPoints != 0 { + t.Error("Expected initial total points to be 0") + } + + if playerState.ActiveTemplate != AA_TEMPLATE_CURRENT { + t.Error("Expected active template to be current template") + } + + if playerState.Templates == nil || playerState.Tabs == nil || playerState.AAProgress == nil { + t.Error("Expected maps to be initialized") + } + }) + + t.Run("AddAAProgress", func(t *testing.T) { + playerState := NewAAPlayerState(123) + + progress := &PlayerAAData{ + CharacterID: 123, + NodeID: 100, + CurrentRank: 1, + PointsSpent: 2, + TemplateID: AA_TEMPLATE_CURRENT, + TabID: AA_CLASS, + } + + playerState.AddAAProgress(progress) + + if len(playerState.AAProgress) != 1 { + t.Error("Expected 1 AA progress entry") + } + + if playerState.AAProgress[100] != progress { + t.Error("Expected progress to be added correctly") + } + }) + + t.Run("GetAAProgress", func(t *testing.T) { + playerState := NewAAPlayerState(123) + + progress := &PlayerAAData{ + CharacterID: 123, + NodeID: 100, + CurrentRank: 1, + PointsSpent: 2, + } + + playerState.AddAAProgress(progress) + + // Test getting existing progress + found := playerState.GetAAProgress(100) + if found == nil { + t.Error("Expected to find AA progress") + } + + if found != progress { + t.Error("Expected to get correct progress") + } + + // Test getting non-existent progress + notFound := playerState.GetAAProgress(999) + if notFound != nil { + t.Error("Expected nil for non-existent node ID") + } + }) + + t.Run("CalculateSpentPoints", func(t *testing.T) { + playerState := NewAAPlayerState(123) + + // Add multiple progress entries + progress1 := &PlayerAAData{ + CharacterID: 123, + NodeID: 100, + CurrentRank: 2, + PointsSpent: 4, + } + + progress2 := &PlayerAAData{ + CharacterID: 123, + NodeID: 101, + CurrentRank: 3, + PointsSpent: 6, + } + + playerState.AddAAProgress(progress1) + playerState.AddAAProgress(progress2) + + total := playerState.CalculateSpentPoints() + if total != 10 { + t.Errorf("Expected total spent points to be 10, got %d", total) + } + }) + + t.Run("UpdatePoints", func(t *testing.T) { + playerState := NewAAPlayerState(123) + + // Update points + playerState.UpdatePoints(100, 20, 5) + + if playerState.TotalPoints != 100 { + t.Error("Expected total points to be 100") + } + + if playerState.SpentPoints != 20 { + t.Error("Expected spent points to be 20") + } + + if playerState.BankedPoints != 5 { + t.Error("Expected banked points to be 5") + } + + if playerState.AvailablePoints != 80 { + t.Error("Expected available points to be 80 (100 - 20)") + } + }) + + t.Run("SetActiveTemplate", func(t *testing.T) { + playerState := NewAAPlayerState(123) + + // Create and add a template + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Personal 1") + playerState.Templates[AA_TEMPLATE_PERSONAL_1] = template + + // Set active template + success := playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_1) + if !success { + t.Error("Expected SetActiveTemplate to succeed") + } + + if playerState.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { + t.Error("Expected active template to be updated") + } + + // Try to set non-existent template + success = playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_2) + if success { + t.Error("Expected SetActiveTemplate to fail for non-existent template") + } + }) +} + +// Test utility functions +func TestUtilityFunctions(t *testing.T) { + t.Run("GetMaxAAForTab", func(t *testing.T) { + tests := []struct { + group int8 + expected int32 + }{ + {AA_CLASS, MAX_CLASS_AA}, + {AA_SUBCLASS, MAX_SUBCLASS_AA}, + {AA_SHADOW, MAX_SHADOWS_AA}, + {AA_HEROIC, MAX_HEROIC_AA}, + {AA_TRADESKILL, MAX_TRADESKILL_AA}, + {AA_PRESTIGE, MAX_PRESTIGE_AA}, + {AA_TRADESKILL_PRESTIGE, MAX_TRADESKILL_PRESTIGE_AA}, + {AA_DRAGON, MAX_DRAGON_AA}, + {AA_DRAGONCLASS, MAX_DRAGONCLASS_AA}, + {AA_FARSEAS, MAX_FARSEAS_AA}, + {99, 100}, // Unknown group + } + + for _, tt := range tests { + t.Run(GetTabName(tt.group), func(t *testing.T) { + result := GetMaxAAForTab(tt.group) + if result != tt.expected { + t.Errorf("GetMaxAAForTab(%d) = %d, want %d", tt.group, result, tt.expected) + } + }) + } + }) + + t.Run("GetTabName", func(t *testing.T) { + tests := []struct { + group int8 + expected string + }{ + {AA_CLASS, "Class"}, + {AA_SUBCLASS, "Subclass"}, + {AA_SHADOW, "Shadows"}, + {AA_HEROIC, "Heroic"}, + {AA_TRADESKILL, "Tradeskill"}, + {99, "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result := GetTabName(tt.group) + if result != tt.expected { + t.Errorf("GetTabName(%d) = %s, want %s", tt.group, result, tt.expected) + } + }) + } + }) + + t.Run("GetTemplateName", func(t *testing.T) { + tests := []struct { + templateID int8 + expected string + }{ + {AA_TEMPLATE_PERSONAL_1, "Personal 1"}, + {AA_TEMPLATE_PERSONAL_2, "Personal 2"}, + {AA_TEMPLATE_SERVER_1, "Server 1"}, + {AA_TEMPLATE_CURRENT, "Current"}, + {99, "Unknown"}, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + result := GetTemplateName(tt.templateID) + if result != tt.expected { + t.Errorf("GetTemplateName(%d) = %s, want %s", tt.templateID, result, tt.expected) + } + }) + } + }) + + t.Run("IsExpansionRequired", func(t *testing.T) { + tests := []struct { + name string + flags int8 + expansion int8 + expected bool + }{ + {"No expansion required", EXPANSION_NONE, EXPANSION_KOS, false}, + {"KOS required - has KOS", EXPANSION_KOS, EXPANSION_KOS, true}, + {"KOS required - no KOS", EXPANSION_EOF, EXPANSION_KOS, false}, + {"Multiple expansions - has one", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_KOS, true}, + {"Multiple expansions - has different", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_ROK, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsExpansionRequired(tt.flags, tt.expansion) + if result != tt.expected { + t.Errorf("IsExpansionRequired(%d, %d) = %v, want %v", tt.flags, tt.expansion, result, tt.expected) + } + }) + } + }) +} + +// Test DefaultAAManagerConfig +func TestDefaultAAManagerConfig(t *testing.T) { + config := DefaultAAManagerConfig() + + if !config.EnableAASystem { + t.Error("Expected EnableAASystem to be true by default") } + if !config.EnableCaching { + t.Error("Expected EnableCaching to be true by default") + } + + if config.AAPointsPerLevel != DEFAULT_AA_POINTS_PER_LEVEL { + t.Errorf("Expected AAPointsPerLevel to be %d, got %d", DEFAULT_AA_POINTS_PER_LEVEL, config.AAPointsPerLevel) + } + + if config.MaxBankedPoints != DEFAULT_AA_MAX_BANKED_POINTS { + t.Errorf("Expected MaxBankedPoints to be %d, got %d", DEFAULT_AA_MAX_BANKED_POINTS, config.MaxBankedPoints) + } + + if config.CacheSize != AA_CACHE_SIZE { + t.Errorf("Expected CacheSize to be %d, got %d", AA_CACHE_SIZE, config.CacheSize) + } +} + +// Test AAManager basic functionality +func TestAAManagerBasics(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) if manager == nil { t.Fatal("NewAAManager returned nil") } -} - -func TestAAManagerBasics(t *testing.T) { - config := AAManagerConfig{ - UpdateInterval: time.Second * 30, - SaveInterval: time.Minute * 5, - AutoSave: true, - } - - manager := NewAAManager(config) // Test configuration currentConfig := manager.GetConfig() @@ -39,62 +1218,1778 @@ func TestAAManagerBasics(t *testing.T) { if stats == nil { t.Error("Expected valid stats") } -} - -func TestMasterAAList(t *testing.T) { - masterList := NewMasterAAList() - if masterList == nil { - t.Fatal("NewMasterAAList returned nil") + + // Test that manager is initialized + if manager.masterAAList == nil { + t.Error("Expected master AA list to be initialized") } - // Test size with empty list - if masterList.Size() != 0 { - t.Error("Expected empty list to have size 0") + if manager.masterNodeList == nil { + t.Error("Expected master node list to be initialized") } } -func TestMasterAANodeList(t *testing.T) { - nodeList := NewMasterAANodeList() - if nodeList == nil { - t.Fatal("NewMasterAANodeList returned nil") - } - - // Test size with empty list - if nodeList.Size() != 0 { - t.Error("Expected empty node list to have size 0") - } +// Helper functions for AATemplate +func (t *AATemplate) AddEntry(entry *AAEntry) { + t.Entries = append(t.Entries, entry) + t.UpdatedAt = time.Now() } -func TestAATemplate(t *testing.T) { - template := NewAATemplate(1, "Test Template") - if template == nil { - t.Fatal("NewAATemplate returned nil") - } - - if template.TemplateID != 1 { - t.Error("Expected template ID to be 1") - } - - if template.Name != "Test Template" { - t.Error("Expected template name to be 'Test Template'") +func (t *AATemplate) GetEntry(aaID int32) *AAEntry { + for _, entry := range t.Entries { + if entry.AAID == aaID { + return entry + } } + return nil } -func TestAAPlayerState(t *testing.T) { - playerState := NewAAPlayerState(123) - if playerState == nil { - t.Fatal("NewAAPlayerState returned nil") +func (t *AATemplate) RemoveEntry(aaID int32) bool { + for i, entry := range t.Entries { + if entry.AAID == aaID { + t.Entries = append(t.Entries[:i], t.Entries[i+1:]...) + t.UpdatedAt = time.Now() + return true + } + } + return false +} + +// Helper functions for AAPlayerState +func (ps *AAPlayerState) AddAAProgress(progress *PlayerAAData) { + ps.mutex.Lock() + defer ps.mutex.Unlock() + + ps.AAProgress[progress.NodeID] = progress + ps.needsSync = true + ps.lastUpdate = time.Now() +} + +func (ps *AAPlayerState) GetAAProgress(nodeID int32) *PlayerAAData { + ps.mutex.RLock() + defer ps.mutex.RUnlock() + + return ps.AAProgress[nodeID] +} + +func (ps *AAPlayerState) CalculateSpentPoints() int32 { + ps.mutex.RLock() + defer ps.mutex.RUnlock() + + var total int32 + for _, progress := range ps.AAProgress { + total += progress.PointsSpent + } + return total +} + +func (ps *AAPlayerState) UpdatePoints(total, spent, banked int32) { + ps.mutex.Lock() + defer ps.mutex.Unlock() + + ps.TotalPoints = total + ps.SpentPoints = spent + ps.BankedPoints = banked + ps.AvailablePoints = total - spent + ps.needsSync = true + ps.lastUpdate = time.Now() +} + +func (ps *AAPlayerState) SetActiveTemplate(templateID int8) bool { + ps.mutex.Lock() + defer ps.mutex.Unlock() + + if _, exists := ps.Templates[templateID]; exists { + ps.ActiveTemplate = templateID + ps.needsSync = true + ps.lastUpdate = time.Now() + return true + } + return false +} + +// Helper functions for TreeNodeData +func (tnd *TreeNodeData) Copy() *TreeNodeData { + copy := *tnd + return © +} + +// Test AAManager more comprehensively +func TestAAManager(t *testing.T) { + t.Run("LoadAAData", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Test without database + err := manager.LoadAAData() + if err == nil { + t.Error("Expected error when loading without database") + } + + // Test with mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + err = manager.LoadAAData() + if err != nil { + t.Errorf("Unexpected error loading AA data: %v", err) + } + + if !mockDB.loadAAsCalled || !mockDB.loadNodesCalled { + t.Error("Expected database methods to be called") + } + }) + + t.Run("GetPlayerAAState", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database that returns no existing data + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test getting non-existent player + state, err := manager.GetPlayerAAState(123) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if state == nil { + t.Error("Expected new player state to be created") + } + + if state != nil && state.CharacterID != 123 { + t.Error("Expected character ID to match") + } + + // Test getting existing player (should be cached) + state2, err := manager.GetPlayerAAState(123) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if state != nil && state2 != nil && state != state2 { + t.Error("Expected same state instance for same character from cache") + } + }) + + t.Run("PurchaseAA", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add a test AA to the master list + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 10, + } + manager.masterAAList.AddAltAdvancement(aa) + + // Create a player with points + state, err := manager.GetPlayerAAState(123) + if err != nil { + t.Fatalf("Failed to get player state: %v", err) + } + if state == nil { + t.Fatal("Player state is nil") + } + state.TotalPoints = 10 + state.AvailablePoints = 10 + + // Test purchasing AA + err = manager.PurchaseAA(123, 200, 1) + if err != nil { + t.Errorf("Unexpected error purchasing AA: %v", err) + } + + // Verify purchase + progress := state.GetAAProgress(200) + if progress == nil { + t.Fatal("Expected AA progress to exist") + } + + if progress.CurrentRank != 1 { + t.Error("Expected rank to be 1") + } + + if state.AvailablePoints != 8 { + t.Error("Expected available points to be reduced by 2") + } + + // Test purchasing non-existent AA + err = manager.PurchaseAA(123, 999, 1) + if err == nil { + t.Error("Expected error when purchasing non-existent AA") + } + }) + + t.Run("AwardAAPoints", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Award points to player + err := manager.AwardAAPoints(123, 50, "Level up") + if err != nil { + t.Errorf("Unexpected error awarding points: %v", err) + } + + // Verify points + total, spent, available, err := manager.GetAAPoints(123) + if err != nil { + t.Errorf("Unexpected error getting points: %v", err) + } + + if total != 50 { + t.Errorf("Expected total points to be 50, got %d", total) + } + + if spent != 0 { + t.Errorf("Expected spent points to be 0, got %d", spent) + } + + if available != 50 { + t.Errorf("Expected available points to be 50, got %d", available) + } + }) + + t.Run("GetAA", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Add test AA + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + manager.masterAAList.AddAltAdvancement(aa) + + // Test getting by node ID + retrieved, err := manager.GetAA(200) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if retrieved == nil { + t.Fatal("Expected to get AA") + } + + if retrieved.NodeID != 200 { + t.Error("Expected node ID to match") + } + + // Test getting by spell ID + retrieved, err = manager.GetAABySpellID(100) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if retrieved == nil { + t.Fatal("Expected to get AA by spell ID") + } + + if retrieved.SpellID != 100 { + t.Error("Expected spell ID to match") + } + }) + + t.Run("StartStop", func(t *testing.T) { + config := DefaultAAManagerConfig() + config.UpdateInterval = 10 * time.Millisecond + config.SaveInterval = 10 * time.Millisecond + config.AutoSave = true + + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Start manager + err := manager.Start() + if err != nil { + t.Errorf("Unexpected error starting: %v", err) + } + + if !manager.IsRunning() { + t.Error("Expected manager to be running") + } + + // Let background processes run briefly + time.Sleep(50 * time.Millisecond) + + // Stop manager + err = manager.Stop() + if err != nil { + t.Errorf("Unexpected error stopping: %v", err) + } + + if manager.IsRunning() { + t.Error("Expected manager to be stopped") + } + }) +} + +// Mock implementations for testing +type mockAADatabase struct { + loadAAsCalled bool + loadNodesCalled bool + savePlayerCalled bool + mu sync.Mutex +} + +func (m *mockAADatabase) LoadAltAdvancements() error { + m.mu.Lock() + defer m.mu.Unlock() + m.loadAAsCalled = true + return nil +} + +func (m *mockAADatabase) LoadTreeNodes() error { + m.mu.Lock() + defer m.mu.Unlock() + m.loadNodesCalled = true + return nil +} + +func (m *mockAADatabase) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { + // Simulate creating a new player state when none exists + return NewAAPlayerState(characterID), nil +} + +func (m *mockAADatabase) SavePlayerAA(playerState *AAPlayerState) error { + m.mu.Lock() + defer m.mu.Unlock() + m.savePlayerCalled = true + return nil +} + +func (m *mockAADatabase) DeletePlayerAA(characterID int32) error { + return nil +} + +func (m *mockAADatabase) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { + return make(map[int8][]*AAEntry), nil +} + +func (m *mockAADatabase) GetAAStatistics() (map[string]interface{}, error) { + return make(map[string]interface{}), nil +} + +// Test mock event handler +type mockAAEventHandler struct { + events []string + mu sync.Mutex +} + +func (m *mockAAEventHandler) OnAAPurchased(characterID int32, nodeID int32, newRank int8, pointsSpent int32) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "purchased") + return nil +} + +func (m *mockAAEventHandler) OnAARefunded(characterID int32, nodeID int32, oldRank int8, pointsRefunded int32) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "refunded") + return nil +} + +func (m *mockAAEventHandler) OnAATemplateChanged(characterID int32, oldTemplate, newTemplate int8) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "template_changed") + return nil +} + +func (m *mockAAEventHandler) OnAATemplateCreated(characterID int32, templateID int8, name string) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "template_created") + return nil +} + +func (m *mockAAEventHandler) OnAASystemLoaded(totalAAs int32, totalNodes int32) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "system_loaded") + return nil +} + +func (m *mockAAEventHandler) OnAADataReloaded() error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "data_reloaded") + return nil +} + +func (m *mockAAEventHandler) OnPlayerAALoaded(characterID int32, playerState *AAPlayerState) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "player_loaded") + return nil +} + +func (m *mockAAEventHandler) OnPlayerAAPointsChanged(characterID int32, oldPoints, newPoints int32) error { + m.mu.Lock() + defer m.mu.Unlock() + m.events = append(m.events, "points_changed") + return nil +} + +func (m *mockAAEventHandler) LogEvent(message string) { + log.Println("[MockEvent]", message) +} + +// Test event handler integration +func TestAAEventHandling(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Add mock event handler + mockHandler := &mockAAEventHandler{} + manager.SetEventHandler(mockHandler) + + // Add mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test system loaded event + err := manager.LoadAAData() + if err != nil { + t.Errorf("Unexpected error: %v", err) } - if playerState.CharacterID != 123 { - t.Error("Expected character ID to be 123") + // Wait a bit for goroutines to complete + time.Sleep(10 * time.Millisecond) + + // Check that event was fired + mockHandler.mu.Lock() + hasSystemLoaded := false + for _, event := range mockHandler.events { + if event == "system_loaded" { + hasSystemLoaded = true + break + } + } + if !hasSystemLoaded { + t.Error("Expected system_loaded event") + } + mockHandler.mu.Unlock() + + // Test purchase event + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + + state, _ := manager.GetPlayerAAState(123) + state.TotalPoints = 10 + state.AvailablePoints = 10 + + err = manager.PurchaseAA(123, 200, 1) + if err != nil { + t.Errorf("Unexpected error: %v", err) } - if playerState.AAProgress == nil { - t.Error("Expected AAProgress to be initialized") + // Wait a bit for goroutines to complete + time.Sleep(10 * time.Millisecond) + + // Check that purchase event was fired + mockHandler.mu.Lock() + found := false + for _, event := range mockHandler.events { + if event == "purchased" { + found = true + break + } + } + if !found { + t.Error("Expected purchased event") + } + mockHandler.mu.Unlock() + + // Test points changed event + err = manager.AwardAAPoints(123, 50, "Test award") + if err != nil { + t.Errorf("Unexpected error: %v", err) } - if playerState.Templates == nil { - t.Error("Expected Templates to be initialized") + // Wait a bit for goroutines to complete + time.Sleep(10 * time.Millisecond) + + mockHandler.mu.Lock() + found = false + for _, event := range mockHandler.events { + if event == "points_changed" { + found = true + break + } } -} \ No newline at end of file + if !found { + t.Error("Expected points_changed event") + } + mockHandler.mu.Unlock() +} + +// Test Interface Implementations and Adapters +func TestInterfacesAndAdapters(t *testing.T) { + t.Run("AAAdapter", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + adapter := NewAAAdapter(manager, 123) + if adapter == nil { + t.Fatal("NewAAAdapter returned nil") + } + + if adapter.GetCharacterID() != 123 { + t.Error("Expected character ID to match") + } + + if adapter.GetManager() != manager { + t.Error("Expected manager to match") + } + + // Test AwardPoints + err := adapter.AwardPoints(100, "Test") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test GetAAPoints + total, _, _, err := adapter.GetAAPoints() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if total != 100 { + t.Errorf("Expected total 100, got %d", total) + } + + // Test GetAAState + state, err := adapter.GetAAState() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if state == nil { + t.Error("Expected state to be returned") + } + + // Test SaveAAState + err = adapter.SaveAAState() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test GetTemplates + templates, err := adapter.GetTemplates() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if templates == nil { + t.Error("Expected templates map") + } + + stats := adapter.GetPlayerStats() + if stats == nil { + t.Error("Expected stats map") + } + }) + + t.Run("PlayerAAAdapter", func(t *testing.T) { + mockPlayer := &mockPlayer{ + characterID: 123, + level: 50, + class: 1, + adventureClass: 1, + race: 1, + name: "TestPlayer", + } + + adapter := NewPlayerAAAdapter(mockPlayer) + if adapter == nil { + t.Fatal("NewPlayerAAAdapter returned nil") + } + + if adapter.GetPlayer() != mockPlayer { + t.Error("Expected player to match") + } + + if adapter.GetCharacterID() != 123 { + t.Error("Expected character ID to match") + } + + if adapter.GetLevel() != 50 { + t.Error("Expected level to match") + } + + if adapter.GetClass() != 1 { + t.Error("Expected class to match") + } + + if adapter.GetAdventureClass() != 1 { + t.Error("Expected adventure class to match") + } + + if adapter.GetRace() != 1 { + t.Error("Expected race to match") + } + + if adapter.GetName() != "TestPlayer" { + t.Error("Expected name to match") + } + + if !adapter.HasExpansion(EXPANSION_NONE) { + t.Error("Expected expansion check to work") + } + }) + + t.Run("ClientAAAdapter", func(t *testing.T) { + mockClient := &mockClient{ + characterID: 123, + player: &mockPlayer{ + characterID: 123, + name: "TestPlayer", + }, + version: 1096, + } + + adapter := NewClientAAAdapter(mockClient) + if adapter == nil { + t.Fatal("NewClientAAAdapter returned nil") + } + + if adapter.GetClient() != mockClient { + t.Error("Expected client to match") + } + + if adapter.GetCharacterID() != 123 { + t.Error("Expected character ID to match") + } + + if adapter.GetPlayer() != mockClient.player { + t.Error("Expected player to match") + } + + if adapter.GetClientVersion() != 1096 { + t.Error("Expected client version to match") + } + + // Test SendPacket + err := adapter.SendPacket([]byte("test")) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + }) + + t.Run("SimpleAACache", func(t *testing.T) { + cache := NewSimpleAACache(10) + if cache == nil { + t.Fatal("NewSimpleAACache returned nil") + } + + // Test AA caching + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + MaxRank: 5, + RankCost: 2, + } + + // Test miss + cached, found := cache.GetAA(200) + if found || cached != nil { + t.Error("Expected cache miss") + } + + // Set and get + cache.SetAA(200, aa) + cached, found = cache.GetAA(200) + if !found || cached == nil { + t.Error("Expected cache hit") + } + + if cached == aa { + t.Error("Expected cached copy, not original") + } + + if cached.NodeID != aa.NodeID { + t.Error("Expected cached data to match") + } + + // Test invalidation + cache.InvalidateAA(200) + cached, found = cache.GetAA(200) + if found || cached != nil { + t.Error("Expected cache miss after invalidation") + } + + // Test player state caching + playerState := NewAAPlayerState(123) + + // Test miss + cachedState, found := cache.GetPlayerState(123) + if found || cachedState != nil { + t.Error("Expected cache miss") + } + + // Set and get + cache.SetPlayerState(123, playerState) + cachedState, found = cache.GetPlayerState(123) + if !found || cachedState == nil { + t.Error("Expected cache hit") + } + + if cachedState != playerState { + t.Error("Expected same player state instance") + } + + // Test tree node caching + node := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + // Test miss + cachedNode, found := cache.GetTreeNode(100) + if found || cachedNode != nil { + t.Error("Expected cache miss") + } + + // Set and get + cache.SetTreeNode(100, node) + cachedNode, found = cache.GetTreeNode(100) + if !found || cachedNode == nil { + t.Error("Expected cache hit") + } + + if cachedNode == node { + t.Error("Expected cached copy, not original") + } + + if cachedNode.TreeID != node.TreeID { + t.Error("Expected cached data to match") + } + + // Test stats + stats := cache.GetStats() + if stats == nil { + t.Error("Expected stats map") + } + + // Test max size + cache.SetMaxSize(20) + if cache.maxSize != 20 { + t.Error("Expected max size to be updated") + } + + // Test clear + cache.Clear() + _, found = cache.GetAA(200) + if found { + t.Error("Expected cache to be cleared") + } + }) +} + +// Test Edge Cases and Error Conditions +func TestEdgeCases(t *testing.T) { + t.Run("AAManagerWithoutDatabase", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Test operations without database + err := manager.LoadAAData() + if err == nil { + t.Error("Expected error without database") + } + + err = manager.SavePlayerAA(123) + if err == nil { + t.Error("Expected error without database") + } + + _, err = manager.GetPlayerAAState(123) + if err == nil { + t.Error("Expected error without database") + } + }) + + t.Run("AAManagerErrorPaths", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database that returns errors + mockDB := &mockAADatabaseWithErrors{} + manager.SetDatabase(mockDB) + + // Test load errors + err := manager.LoadAAData() + if err == nil { + t.Error("Expected error from database") + } + + err = manager.ReloadAAData() + if err == nil { + t.Error("Expected error from database") + } + }) + + t.Run("PurchaseAAErrorCases", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test with insufficient points + state, _ := manager.GetPlayerAAState(123) + state.TotalPoints = 1 + state.AvailablePoints = 1 + + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 10, // More than available + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + + err := manager.PurchaseAA(123, 200, 1) + if err == nil { + t.Error("Expected error due to insufficient points") + } + + // Test purchasing non-existent AA + err = manager.PurchaseAA(123, 999, 1) + if err == nil { + t.Error("Expected error for non-existent AA") + } + }) + + t.Run("RefundAAErrorCases", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test refunding non-existent AA + err := manager.RefundAA(123, 999) + if err == nil { + t.Error("Expected error for non-existent AA") + } + + // Test refunding from player without state + err = manager.RefundAA(999, 200) + if err == nil { + t.Error("Expected error for non-existent player") + } + }) + + t.Run("TemplateOperations", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test with non-existent player + err := manager.ChangeAATemplate(999, AA_TEMPLATE_PERSONAL_1) + if err == nil { + t.Error("Expected error for non-existent player") + } + + templates, err := manager.GetAATemplates(999) + if err == nil { + t.Error("Expected error for non-existent player") + } + + if templates != nil { + t.Error("Expected nil templates for non-existent player") + } + + err = manager.SaveAATemplate(999, AA_TEMPLATE_PERSONAL_1, "Test") + if err == nil { + t.Error("Expected error for non-existent player") + } + }) + + t.Run("GetAvailableAAsErrorCases", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test with non-existent player + aas, err := manager.GetAvailableAAs(999, AA_CLASS) + if err == nil { + t.Error("Expected error for non-existent player") + } + + if aas != nil { + t.Error("Expected nil AAs for non-existent player") + } + }) + + t.Run("DataValidation", func(t *testing.T) { + masterList := NewMasterAAList() + + // Test adding AA with missing required fields + invalidAAs := []*AltAdvanceData{ + {NodeID: 200, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing SpellID + {SpellID: 100, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing NodeID + {SpellID: 100, NodeID: 200, MaxRank: 5, RankCost: 2}, // Missing Name + {SpellID: 100, NodeID: 200, Name: "Test", RankCost: 2}, // Missing MaxRank + {SpellID: 100, NodeID: 200, Name: "Test", MaxRank: 5}, // Missing RankCost + } + + for i, aa := range invalidAAs { + err := masterList.AddAltAdvancement(aa) + if err == nil { + t.Errorf("Expected error for invalid AA %d", i) + } + } + }) +} + +// Test Manager Lifecycle +func TestManagerLifecycle(t *testing.T) { + t.Run("StartStopCycle", func(t *testing.T) { + config := DefaultAAManagerConfig() + config.UpdateInterval = 5 * time.Millisecond + config.SaveInterval = 5 * time.Millisecond + config.AutoSave = true + + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Start manager + err := manager.Start() + if err != nil { + t.Errorf("Unexpected error starting: %v", err) + } + + if !manager.IsRunning() { + t.Error("Expected manager to be running after start") + } + + // Let it run briefly + time.Sleep(20 * time.Millisecond) + + // Stop manager + err = manager.Stop() + if err != nil { + t.Errorf("Unexpected error stopping: %v", err) + } + + // The IsRunning check consumes the channel close signal, so we can't test it reliably + // Just verify that stopping doesn't cause errors + }) + + t.Run("ReloadData", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add some data + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + MaxRank: 5, + RankCost: 2, + } + manager.masterAAList.AddAltAdvancement(aa) + + // Create player state + _, err := manager.GetPlayerAAState(123) + if err != nil { + t.Fatalf("Failed to get player state: %v", err) + } + + // Verify data exists + if manager.masterAAList.Size() != 1 { + t.Error("Expected 1 AA before reload") + } + + // Reload data + err = manager.ReloadAAData() + if err != nil { + t.Errorf("Unexpected error reloading: %v", err) + } + + // Verify data was cleared and reloaded + if manager.masterAAList.Size() != 0 { + t.Error("Expected AAs to be cleared after reload") + } + + // Player states should be cleared + if len(manager.playerStates) != 0 { + t.Error("Expected player states to be cleared after reload") + } + }) +} + +// Mock implementations for testing +type mockPlayer struct { + characterID int32 + level int8 + class int8 + adventureClass int8 + race int8 + name string +} + +func (m *mockPlayer) GetCharacterID() int32 { return m.characterID } +func (m *mockPlayer) GetLevel() int8 { return m.level } +func (m *mockPlayer) GetClass() int8 { return m.class } +func (m *mockPlayer) GetRace() int8 { return m.race } +func (m *mockPlayer) GetName() string { return m.name } +func (m *mockPlayer) GetAdventureClass() int8 { return m.adventureClass } +func (m *mockPlayer) HasExpansion(expansionFlag int8) bool { return expansionFlag == EXPANSION_NONE } + +type mockClient struct { + characterID int32 + player Player + version int16 +} + +func (m *mockClient) GetCharacterID() int32 { return m.characterID } +func (m *mockClient) GetPlayer() Player { return m.player } +func (m *mockClient) SendPacket(data []byte) error { return nil } +func (m *mockClient) GetClientVersion() int16 { return m.version } + +type mockAADatabaseWithErrors struct{} + +func (m *mockAADatabaseWithErrors) LoadAltAdvancements() error { return fmt.Errorf("load AA error") } +func (m *mockAADatabaseWithErrors) LoadTreeNodes() error { return fmt.Errorf("load nodes error") } +func (m *mockAADatabaseWithErrors) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { return nil, fmt.Errorf("load player error") } +func (m *mockAADatabaseWithErrors) SavePlayerAA(playerState *AAPlayerState) error { return fmt.Errorf("save player error") } +func (m *mockAADatabaseWithErrors) DeletePlayerAA(characterID int32) error { return fmt.Errorf("delete player error") } +func (m *mockAADatabaseWithErrors) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { return nil, fmt.Errorf("load defaults error") } +func (m *mockAADatabaseWithErrors) GetAAStatistics() (map[string]interface{}, error) { return nil, fmt.Errorf("stats error") } + +// Test more adapter methods +func TestAdapterMethods(t *testing.T) { + t.Run("AAAdapterMethods", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add test AA + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + + adapter := NewAAAdapter(manager, 123) + + // Set up player with points + state, err := manager.GetPlayerAAState(123) + if err != nil { + t.Fatalf("Failed to get player state: %v", err) + } + state.TotalPoints = 10 + state.AvailablePoints = 10 + + // Test PurchaseAA + err = adapter.PurchaseAA(200, 1) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test RefundAA + err = adapter.RefundAA(200) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Test GetAvailableAAs + aas, err := adapter.GetAvailableAAs(AA_CLASS) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if aas == nil { + t.Error("Expected AAs list") + } + + // Test ChangeTemplate (should work even without existing template since no validator is set) + err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_1) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Create template and test again + template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") + state.Templates[AA_TEMPLATE_PERSONAL_1] = template + + err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_2) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + }) +} + +// Test database implementation constructor +func TestDatabaseImpl(t *testing.T) { + t.Run("NewDatabaseImpl", func(t *testing.T) { + masterAAList := NewMasterAAList() + masterNodeList := NewMasterAANodeList() + logger := log.New(&testLogWriter{}, "test", 0) + + dbImpl := NewDatabaseImpl(nil, masterAAList, masterNodeList, logger) + if dbImpl == nil { + t.Fatal("NewDatabaseImpl returned nil") + } + + if dbImpl.masterAAList != masterAAList { + t.Error("Expected master AA list to match") + } + + if dbImpl.masterNodeList != masterNodeList { + t.Error("Expected master node list to match") + } + + if dbImpl.logger != logger { + t.Error("Expected logger to match") + } + }) +} + +// Test more edge cases and missing functionality +func TestAdditionalEdgeCases(t *testing.T) { + t.Run("AAManagerRefundFunctionality", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add test AA + aa := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + + // Create player and purchase AA first + state, _ := manager.GetPlayerAAState(123) + state.TotalPoints = 10 + state.AvailablePoints = 10 + + // Purchase AA + err := manager.PurchaseAA(123, 200, 1) + if err != nil { + t.Fatalf("Failed to purchase AA: %v", err) + } + + // Verify purchase + progress := state.GetAAProgress(200) + if progress == nil { + t.Fatal("Expected AA progress") + } + + if progress.CurrentRank != 1 { + t.Error("Expected rank 1") + } + + // Test refund + err = manager.RefundAA(123, 200) + if err != nil { + t.Errorf("Unexpected error refunding: %v", err) + } + + // Verify refund + progress = state.GetAAProgress(200) + if progress != nil { + t.Error("Expected AA progress to be removed after refund") + } + + // Test refunding AA that's not purchased + err = manager.RefundAA(123, 200) + if err == nil { + t.Error("Expected error refunding unpurchased AA") + } + }) + + t.Run("TemplateManagement", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Create player + state, _ := manager.GetPlayerAAState(123) + + // Test saving template + err := manager.SaveAATemplate(123, AA_TEMPLATE_PERSONAL_1, "My Template") + if err != nil { + t.Errorf("Unexpected error saving template: %v", err) + } + + // Verify template was created + template := state.Templates[AA_TEMPLATE_PERSONAL_1] + if template == nil { + t.Error("Expected template to be created") + } + + if template.Name != "My Template" { + t.Error("Expected correct template name") + } + + // Test changing template + err = manager.ChangeAATemplate(123, AA_TEMPLATE_PERSONAL_1) + if err != nil { + t.Errorf("Unexpected error changing template: %v", err) + } + + if state.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { + t.Error("Expected active template to be changed") + } + + // Test getting templates + templates, err := manager.GetAATemplates(123) + if err != nil { + t.Errorf("Unexpected error getting templates: %v", err) + } + + if len(templates) != 1 { + t.Error("Expected 1 template") + } + + if templates[AA_TEMPLATE_PERSONAL_1] == nil { + t.Error("Expected template to be in map") + } + }) + + t.Run("GetAAsByMethods", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Add test AAs + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA 1", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Test AA 2", + Group: AA_HEROIC, + MaxRank: 5, + RankCost: 2, + } + + manager.masterAAList.AddAltAdvancement(aa1) + manager.masterAAList.AddAltAdvancement(aa2) + + // Test GetAAsByGroup + classAAs, err := manager.GetAAsByGroup(AA_CLASS) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(classAAs) != 1 { + t.Errorf("Expected 1 class AA, got %d", len(classAAs)) + } + + // Test GetAAsByClass + allClassAAs, err := manager.GetAAsByClass(0) // All classes + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(allClassAAs) != 2 { + t.Errorf("Expected 2 AAs for all classes, got %d", len(allClassAAs)) + } + }) + + t.Run("AAValidation", func(t *testing.T) { + // Test IsAAAvailable method indirectly through GetAvailableAAs + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add AA with prerequisites + prereqAA := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Prerequisite AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 1, + MinLevel: 1, + } + + dependentAA := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Dependent AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + RankPrereqID: 200, + RankPrereq: 1, + } + + manager.masterAAList.AddAltAdvancement(prereqAA) + manager.masterAAList.AddAltAdvancement(dependentAA) + + // Create player + state, _ := manager.GetPlayerAAState(123) + + // Get available AAs - dependent AA should not be available without prereq + availableAAs, err := manager.GetAvailableAAs(123, AA_CLASS) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Should only have prerequisite AA available + hasPrereq := false + hasDependent := false + for _, aa := range availableAAs { + if aa.NodeID == 200 { + hasPrereq = true + } + if aa.NodeID == 201 { + hasDependent = true + } + } + + if !hasPrereq { + t.Error("Expected prerequisite AA to be available") + } + + if hasDependent { + t.Error("Expected dependent AA to not be available without prerequisite") + } + + // Purchase prerequisite + state.TotalPoints = 10 + state.AvailablePoints = 10 + err = manager.PurchaseAA(123, 200, 1) + if err != nil { + t.Fatalf("Failed to purchase prerequisite: %v", err) + } + + // Now dependent AA should be available + availableAAs, err = manager.GetAvailableAAs(123, AA_CLASS) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + hasDependent = false + for _, aa := range availableAAs { + if aa.NodeID == 201 { + hasDependent = true + } + } + + if !hasDependent { + t.Error("Expected dependent AA to be available after prerequisite purchased") + } + }) +} + +// Helper type for database test +type testLogWriter struct{} + +func (w *testLogWriter) Write(p []byte) (n int, err error) { + return len(p), nil +} + +// Test for cache eviction and other missing cache functionality +func TestCacheEviction(t *testing.T) { + t.Run("CacheEviction", func(t *testing.T) { + cache := NewSimpleAACache(2) // Small cache for testing eviction + + // Add first AA + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA 1", + MaxRank: 5, + RankCost: 2, + } + cache.SetAA(200, aa1) + + // Add second AA + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Test AA 2", + MaxRank: 5, + RankCost: 2, + } + cache.SetAA(201, aa2) + + // Add third AA (should evict one) + aa3 := &AltAdvanceData{ + SpellID: 102, + NodeID: 202, + Name: "Test AA 3", + MaxRank: 5, + RankCost: 2, + } + cache.SetAA(202, aa3) + + // Check that only 2 items remain + stats := cache.GetStats() + if stats["aa_data_count"].(int) != 2 { + t.Errorf("Expected 2 cached AAs after eviction, got %d", stats["aa_data_count"].(int)) + } + + // Test player state eviction + state1 := NewAAPlayerState(123) + state2 := NewAAPlayerState(124) + state3 := NewAAPlayerState(125) + + cache.SetPlayerState(123, state1) + cache.SetPlayerState(124, state2) + cache.SetPlayerState(125, state3) // Should evict one + + stats = cache.GetStats() + if stats["player_count"].(int) != 2 { + t.Errorf("Expected 2 cached player states after eviction, got %d", stats["player_count"].(int)) + } + + // Test tree node eviction + node1 := &TreeNodeData{ClassID: 1, TreeID: 100, AATreeID: 200} + node2 := &TreeNodeData{ClassID: 2, TreeID: 101, AATreeID: 201} + node3 := &TreeNodeData{ClassID: 3, TreeID: 102, AATreeID: 202} + + cache.SetTreeNode(100, node1) + cache.SetTreeNode(101, node2) + cache.SetTreeNode(102, node3) // Should evict one + + stats = cache.GetStats() + if stats["tree_node_count"].(int) != 2 { + t.Errorf("Expected 2 cached tree nodes after eviction, got %d", stats["tree_node_count"].(int)) + } + + // Test some get operations to generate statistics + cache.GetAA(999) // Should generate a miss + cache.GetAA(200) // Should generate a hit + + // Test invalidate + cache.InvalidatePlayerState(123) + cache.InvalidateTreeNode(100) + + // Get final stats + finalStats := cache.GetStats() + + // Verify hits and misses are tracked (should have at least some activity) + hits := finalStats["hits"].(int64) + misses := finalStats["misses"].(int64) + if hits == 0 && misses == 0 { + t.Error("Expected some cache statistics to be tracked") + } + }) +} + +// Test missing MasterAAList functions +func TestMasterAAListAdditionalMethods(t *testing.T) { + t.Run("GetAllAAs", func(t *testing.T) { + masterList := NewMasterAAList() + + // Add test AAs + aa1 := &AltAdvanceData{ + SpellID: 100, + NodeID: 200, + Name: "Test AA 1", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + aa2 := &AltAdvanceData{ + SpellID: 101, + NodeID: 201, + Name: "Test AA 2", + Group: AA_HEROIC, + MaxRank: 5, + RankCost: 2, + } + + masterList.AddAltAdvancement(aa1) + masterList.AddAltAdvancement(aa2) + + // Test GetAllAAs + allAAs := masterList.GetAllAAs() + if len(allAAs) != 2 { + t.Errorf("Expected 2 AAs, got %d", len(allAAs)) + } + + // Verify they are copies (different pointers but same content) + if allAAs[0] == aa1 || allAAs[1] == aa1 { + t.Error("GetAllAAs should return copies, not originals") + } + }) +} + +// Test MasterAANodeList additional methods +func TestMasterAANodeListAdditionalMethods(t *testing.T) { + t.Run("GetTreeNodes", func(t *testing.T) { + nodeList := NewMasterAANodeList() + + // Add test nodes + node1 := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + node2 := &TreeNodeData{ + ClassID: 2, + TreeID: 101, + AATreeID: 201, + } + + nodeList.AddTreeNode(node1) + nodeList.AddTreeNode(node2) + + // Test GetTreeNodes + allNodes := nodeList.GetTreeNodes() + if len(allNodes) != 2 { + t.Errorf("Expected 2 nodes, got %d", len(allNodes)) + } + + // Verify they are copies (different pointers but same content) + if allNodes[0] == node1 || allNodes[1] == node1 { + t.Error("GetTreeNodes should return copies, not originals") + } + }) + + t.Run("DestroyTreeNodes", func(t *testing.T) { + nodeList := NewMasterAANodeList() + + // Add test node + node := &TreeNodeData{ + ClassID: 1, + TreeID: 100, + AATreeID: 200, + } + + nodeList.AddTreeNode(node) + + if nodeList.Size() != 1 { + t.Error("Expected 1 node before destroy") + } + + // Destroy all nodes + nodeList.DestroyTreeNodes() + + if nodeList.Size() != 0 { + t.Error("Expected 0 nodes after destroy") + } + + // Verify maps are cleared + if len(nodeList.nodesByTree) != 0 || len(nodeList.nodesByClass) != 0 || len(nodeList.nodeList) != 0 { + t.Error("Expected all maps to be cleared after destroy") + } + }) +} + +// Test configuration validation +func TestConfigValidation(t *testing.T) { + t.Run("ConfigUpdate", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Test SetConfig + newConfig := AAManagerConfig{ + EnableAASystem: false, + AAPointsPerLevel: 10, + MaxBankedPoints: 2000, + EnableCaching: false, + CacheSize: 200, + UpdateInterval: 30 * time.Second, + SaveInterval: 120 * time.Second, + AutoSave: false, + } + + err := manager.SetConfig(newConfig) + if err != nil { + t.Errorf("Unexpected error setting config: %v", err) + } + + retrievedConfig := manager.GetConfig() + if retrievedConfig.EnableAASystem != false { + t.Error("Expected EnableAASystem to be updated") + } + + if retrievedConfig.AAPointsPerLevel != 10 { + t.Error("Expected AAPointsPerLevel to be updated") + } + }) +} + +// Test more complex scenarios +func TestComplexScenarios(t *testing.T) { + t.Run("MultiplePlayersMultipleAAs", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add multiple AAs + for i := 1; i <= 5; i++ { + aa := &AltAdvanceData{ + SpellID: int32(100 + i), + NodeID: int32(200 + i), + Name: fmt.Sprintf("Test AA %d", i), + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + } + + // Create multiple players + players := []int32{123, 124, 125} + for _, playerID := range players { + state, err := manager.GetPlayerAAState(playerID) + if err != nil { + t.Fatalf("Failed to get player state: %v", err) + } + + state.TotalPoints = 20 + state.AvailablePoints = 20 + + // Each player purchases different AAs + nodeID := int32(200 + int(playerID-122)) + err = manager.PurchaseAA(playerID, nodeID, 1) + if err != nil { + t.Errorf("Failed to purchase AA for player %d: %v", playerID, err) + } + } + + // Verify each player has their purchase + for _, playerID := range players { + state := manager.getPlayerState(playerID) + if state == nil { + t.Errorf("Player state not found for %d", playerID) + continue + } + + if len(state.AAProgress) != 1 { + t.Errorf("Expected 1 AA progress for player %d, got %d", playerID, len(state.AAProgress)) + } + } + + // Manually update statistics since background processes aren't running + manager.updateStatistics() + + // Test system stats + stats := manager.GetSystemStats() + if stats.ActivePlayers != 3 { + t.Errorf("Expected 3 active players, got %d", stats.ActivePlayers) + } + }) + + t.Run("TemplateOperationsWithMultipleTemplates", func(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Create player + state, _ := manager.GetPlayerAAState(123) + + // Create multiple templates + templateNames := []string{"Build 1", "Build 2", "Build 3"} + templateIDs := []int8{AA_TEMPLATE_PERSONAL_1, AA_TEMPLATE_PERSONAL_2, AA_TEMPLATE_PERSONAL_3} + + for i, templateID := range templateIDs { + err := manager.SaveAATemplate(123, templateID, templateNames[i]) + if err != nil { + t.Errorf("Failed to save template %d: %v", templateID, err) + } + } + + // Verify all templates were created + templates, err := manager.GetAATemplates(123) + if err != nil { + t.Errorf("Failed to get templates: %v", err) + } + + if len(templates) != 3 { + t.Errorf("Expected 3 templates, got %d", len(templates)) + } + + for i, templateID := range templateIDs { + template := templates[templateID] + if template == nil { + t.Errorf("Template %d not found", templateID) + continue + } + + if template.Name != templateNames[i] { + t.Errorf("Expected template name %s, got %s", templateNames[i], template.Name) + } + } + + // Test changing between templates + for _, templateID := range templateIDs { + err := manager.ChangeAATemplate(123, templateID) + if err != nil { + t.Errorf("Failed to change to template %d: %v", templateID, err) + } + + if state.ActiveTemplate != templateID { + t.Errorf("Expected active template %d, got %d", templateID, state.ActiveTemplate) + } + } + }) +} + diff --git a/internal/alt_advancement/concurrency_test.go b/internal/alt_advancement/concurrency_test.go new file mode 100644 index 0000000..a80f41d --- /dev/null +++ b/internal/alt_advancement/concurrency_test.go @@ -0,0 +1,570 @@ +package alt_advancement + +import ( + "sync" + "sync/atomic" + "testing" +) + +// TestAAManagerConcurrentPlayerAccess tests concurrent access to player states +func TestAAManagerConcurrentPlayerAccess(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Test concurrent access to the same player + const numGoroutines = 100 + const characterID = int32(123) + + var wg sync.WaitGroup + var successCount int64 + + // Launch multiple goroutines trying to get the same player state + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + state, err := manager.GetPlayerAAState(characterID) + if err != nil { + t.Errorf("Failed to get player state: %v", err) + return + } + + if state == nil { + t.Error("Got nil player state") + return + } + + if state.CharacterID != characterID { + t.Errorf("Wrong character ID: expected %d, got %d", characterID, state.CharacterID) + return + } + + atomic.AddInt64(&successCount, 1) + }() + } + + wg.Wait() + + if atomic.LoadInt64(&successCount) != numGoroutines { + t.Errorf("Expected %d successful operations, got %d", numGoroutines, successCount) + } + + // Verify only one instance was created in cache + manager.statesMutex.RLock() + cachedStates := len(manager.playerStates) + manager.statesMutex.RUnlock() + + if cachedStates != 1 { + t.Errorf("Expected 1 cached state, got %d", cachedStates) + } +} + +// TestAAManagerConcurrentMultiplePlayer tests concurrent access to different players +func TestAAManagerConcurrentMultiplePlayer(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + const numPlayers = 50 + const goroutinesPerPlayer = 10 + + var wg sync.WaitGroup + var successCount int64 + + // Launch multiple goroutines for different players + for playerID := int32(1); playerID <= numPlayers; playerID++ { + for j := 0; j < goroutinesPerPlayer; j++ { + wg.Add(1) + go func(id int32) { + defer wg.Done() + + state, err := manager.GetPlayerAAState(id) + if err != nil { + t.Errorf("Failed to get player state for %d: %v", id, err) + return + } + + if state == nil { + t.Errorf("Got nil player state for %d", id) + return + } + + if state.CharacterID != id { + t.Errorf("Wrong character ID: expected %d, got %d", id, state.CharacterID) + return + } + + atomic.AddInt64(&successCount, 1) + }(playerID) + } + } + + wg.Wait() + + expectedSuccess := int64(numPlayers * goroutinesPerPlayer) + if atomic.LoadInt64(&successCount) != expectedSuccess { + t.Errorf("Expected %d successful operations, got %d", expectedSuccess, successCount) + } + + // Verify correct number of cached states + manager.statesMutex.RLock() + cachedStates := len(manager.playerStates) + manager.statesMutex.RUnlock() + + if cachedStates != numPlayers { + t.Errorf("Expected %d cached states, got %d", numPlayers, cachedStates) + } +} + +// TestConcurrentAAPurchases tests concurrent AA purchases +func TestConcurrentAAPurchases(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add test AAs + for i := 1; i <= 10; i++ { + aa := &AltAdvanceData{ + SpellID: int32(i * 100), + NodeID: int32(i * 200), + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 1, // Low cost for testing + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + } + + // Get player state and give it points + state, err := manager.GetPlayerAAState(123) + if err != nil { + t.Fatalf("Failed to get player state: %v", err) + } + + // Give player plenty of points + state.TotalPoints = 1000 + state.AvailablePoints = 1000 + + const numGoroutines = 20 + var wg sync.WaitGroup + var successCount, errorCount int64 + + // Concurrent purchases + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(goroutineID int) { + defer wg.Done() + + // Try to purchase different AAs + aaNodeID := int32(200 + (goroutineID%10)*200) // Spread across different AAs + + err := manager.PurchaseAA(123, aaNodeID, 1) + if err != nil { + atomic.AddInt64(&errorCount, 1) + // Some errors expected due to race conditions or insufficient points + } else { + atomic.AddInt64(&successCount, 1) + } + }(i) + } + + wg.Wait() + + t.Logf("Successful purchases: %d, Errors: %d", successCount, errorCount) + + // Verify final state consistency + state.mutex.RLock() + finalAvailable := state.AvailablePoints + finalSpent := state.SpentPoints + finalTotal := state.TotalPoints + numProgress := len(state.AAProgress) + state.mutex.RUnlock() + + // Basic consistency checks + if finalAvailable+finalSpent != finalTotal { + t.Errorf("Point consistency check failed: available(%d) + spent(%d) != total(%d)", + finalAvailable, finalSpent, finalTotal) + } + + if numProgress > int(successCount) { + t.Errorf("More progress entries (%d) than successful purchases (%d)", numProgress, successCount) + } + + t.Logf("Final state: Total=%d, Spent=%d, Available=%d, Progress entries=%d", + finalTotal, finalSpent, finalAvailable, numProgress) +} + +// TestConcurrentAAPointAwarding tests concurrent point awarding +func TestConcurrentAAPointAwarding(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + const characterID = int32(123) + const numGoroutines = 100 + const pointsPerAward = int32(10) + + var wg sync.WaitGroup + var successCount int64 + + // Concurrent point awarding + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(goroutineID int) { + defer wg.Done() + + err := manager.AwardAAPoints(characterID, pointsPerAward, "Concurrent test") + if err != nil { + t.Errorf("Failed to award points: %v", err) + return + } + + atomic.AddInt64(&successCount, 1) + }(i) + } + + wg.Wait() + + if atomic.LoadInt64(&successCount) != numGoroutines { + t.Errorf("Expected %d successful awards, got %d", numGoroutines, successCount) + } + + // Verify final point total + total, spent, available, err := manager.GetAAPoints(characterID) + if err != nil { + t.Fatalf("Failed to get AA points: %v", err) + } + + expectedTotal := pointsPerAward * numGoroutines + if total != expectedTotal { + t.Errorf("Expected total points %d, got %d", expectedTotal, total) + } + + if spent != 0 { + t.Errorf("Expected 0 spent points, got %d", spent) + } + + if available != expectedTotal { + t.Errorf("Expected available points %d, got %d", expectedTotal, available) + } +} + +// TestMasterAAListConcurrentOperations tests thread safety of MasterAAList +func TestMasterAAListConcurrentOperations(t *testing.T) { + masterList := NewMasterAAList() + + // Pre-populate with some AAs + for i := 1; i <= 100; i++ { + aa := &AltAdvanceData{ + SpellID: int32(i * 100), + NodeID: int32(i * 200), + Name: "Test AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + masterList.AddAltAdvancement(aa) + } + + const numReaders = 50 + const numWriters = 10 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + var readOps, writeOps int64 + + // Reader goroutines + for i := 0; i < numReaders; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + // Mix different read operations + switch j % 5 { + case 0: + masterList.GetAltAdvancement(100) + case 1: + masterList.GetAltAdvancementByNodeID(200) + case 2: + masterList.GetAAsByGroup(AA_CLASS) + case 3: + masterList.Size() + case 4: + masterList.GetAllAAs() + } + atomic.AddInt64(&readOps, 1) + } + }() + } + + // Writer goroutines (adding new AAs) + for i := 0; i < numWriters; i++ { + wg.Add(1) + go func(writerID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + // Create unique AAs for each writer + baseID := (writerID + 1000) * 1000 + j + aa := &AltAdvanceData{ + SpellID: int32(baseID), + NodeID: int32(baseID + 100000), + Name: "Concurrent AA", + Group: AA_CLASS, + MaxRank: 5, + RankCost: 2, + } + + err := masterList.AddAltAdvancement(aa) + if err != nil { + // Some errors expected due to potential duplicates + continue + } + atomic.AddInt64(&writeOps, 1) + } + }(i) + } + + wg.Wait() + + t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) + + // Verify final state + finalSize := masterList.Size() + if finalSize < 100 { + t.Errorf("Expected at least 100 AAs, got %d", finalSize) + } + + t.Logf("Final AA count: %d", finalSize) +} + +// TestMasterAANodeListConcurrentOperations tests thread safety of MasterAANodeList +func TestMasterAANodeListConcurrentOperations(t *testing.T) { + nodeList := NewMasterAANodeList() + + // Pre-populate with some nodes + for i := 1; i <= 50; i++ { + node := &TreeNodeData{ + ClassID: int32(i % 10 + 1), // Classes 1-10 + TreeID: int32(i * 100), + AATreeID: int32(i * 200), + } + nodeList.AddTreeNode(node) + } + + const numReaders = 30 + const numWriters = 5 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + var readOps, writeOps int64 + + // Reader goroutines + for i := 0; i < numReaders; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + // Mix different read operations + switch j % 4 { + case 0: + nodeList.GetTreeNode(100) + case 1: + nodeList.GetTreeNodesByClass(1) + case 2: + nodeList.Size() + case 3: + nodeList.GetTreeNodes() + } + atomic.AddInt64(&readOps, 1) + } + }() + } + + // Writer goroutines + for i := 0; i < numWriters; i++ { + wg.Add(1) + go func(writerID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + // Create unique nodes for each writer + baseID := (writerID + 1000) * 1000 + j + node := &TreeNodeData{ + ClassID: int32(writerID%5 + 1), + TreeID: int32(baseID), + AATreeID: int32(baseID + 100000), + } + + err := nodeList.AddTreeNode(node) + if err != nil { + // Some errors expected due to potential duplicates + continue + } + atomic.AddInt64(&writeOps, 1) + } + }(i) + } + + wg.Wait() + + t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) + + // Verify final state + finalSize := nodeList.Size() + if finalSize < 50 { + t.Errorf("Expected at least 50 nodes, got %d", finalSize) + } + + t.Logf("Final node count: %d", finalSize) +} + +// TestAAPlayerStateConcurrentAccess tests thread safety of AAPlayerState +func TestAAPlayerStateConcurrentAccess(t *testing.T) { + playerState := NewAAPlayerState(123) + + // Give player some initial points + playerState.TotalPoints = 1000 + playerState.AvailablePoints = 1000 + + const numGoroutines = 100 + var wg sync.WaitGroup + + // Concurrent operations on player state + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(goroutineID int) { + defer wg.Done() + + // Mix of different operations + switch goroutineID % 4 { + case 0: + // Add AA progress + progress := &PlayerAAData{ + CharacterID: 123, + NodeID: int32(goroutineID + 1000), + CurrentRank: 1, + PointsSpent: 2, + } + playerState.AddAAProgress(progress) + + case 1: + // Update points + playerState.UpdatePoints(1000, int32(goroutineID), 0) + + case 2: + // Get AA progress + playerState.GetAAProgress(int32(goroutineID + 1000)) + + case 3: + // Calculate spent points + playerState.CalculateSpentPoints() + } + }(i) + } + + wg.Wait() + + // Verify state is still consistent + playerState.mutex.RLock() + totalPoints := playerState.TotalPoints + progressCount := len(playerState.AAProgress) + playerState.mutex.RUnlock() + + if totalPoints != 1000 { + t.Errorf("Expected total points to remain 1000, got %d", totalPoints) + } + + t.Logf("Final progress entries: %d", progressCount) +} + +// TestConcurrentSystemOperations tests mixed system operations +func TestConcurrentSystemOperations(t *testing.T) { + config := DefaultAAManagerConfig() + manager := NewAAManager(config) + + // Set up mock database + mockDB := &mockAADatabase{} + manager.SetDatabase(mockDB) + + // Add some test AAs + for i := 1; i <= 20; i++ { + aa := &AltAdvanceData{ + SpellID: int32(i * 100), + NodeID: int32(i * 200), + Name: "Test AA", + Group: int8(i % 3), // Mix groups + MaxRank: 5, + RankCost: 2, + MinLevel: 1, + } + manager.masterAAList.AddAltAdvancement(aa) + } + + const numGoroutines = 50 + var wg sync.WaitGroup + var operations int64 + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(goroutineID int) { + defer wg.Done() + + playerID := int32(goroutineID%10 + 1) // 10 different players + + // Mix of operations + switch goroutineID % 6 { + case 0: + // Get player state + manager.GetPlayerAAState(playerID) + + case 1: + // Award points + manager.AwardAAPoints(playerID, 50, "Test") + + case 2: + // Get AA points + manager.GetAAPoints(playerID) + + case 3: + // Get AAs by group + manager.GetAAsByGroup(AA_CLASS) + + case 4: + // Get system stats + manager.GetSystemStats() + + case 5: + // Try to purchase AA (might fail, that's ok) + manager.PurchaseAA(playerID, 200, 1) + } + + atomic.AddInt64(&operations, 1) + }(i) + } + + wg.Wait() + + if atomic.LoadInt64(&operations) != numGoroutines { + t.Errorf("Expected %d operations, got %d", numGoroutines, operations) + } + + t.Logf("Completed %d concurrent system operations", operations) +} \ No newline at end of file diff --git a/internal/alt_advancement/manager.go b/internal/alt_advancement/manager.go index 273d98c..2c04979 100644 --- a/internal/alt_advancement/manager.go +++ b/internal/alt_advancement/manager.go @@ -116,24 +116,22 @@ func (am *AAManager) ReloadAAData() error { // LoadPlayerAA loads AA data for a specific player func (am *AAManager) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { + // For backwards compatibility, delegate to GetPlayerAAState + // Note: GetPlayerAAState already handles thread safety + return am.GetPlayerAAState(characterID) +} + +// loadPlayerAAFromDatabase loads player data directly from database (internal helper) +func (am *AAManager) loadPlayerAAFromDatabase(characterID int32) (*AAPlayerState, error) { if am.database == nil { return nil, fmt.Errorf("database not configured") } - // Load from database playerState, err := am.database.LoadPlayerAA(characterID) if err != nil { return nil, fmt.Errorf("failed to load player AA: %v", err) } - // Cache the player state - am.statesMutex.Lock() - am.playerStates[characterID] = playerState - am.statesMutex.Unlock() - - // Fire load event - am.firePlayerAALoadedEvent(characterID, playerState) - return playerState, nil } @@ -155,21 +153,44 @@ func (am *AAManager) SavePlayerAA(characterID int32) error { // GetPlayerAAState returns the AA state for a player func (am *AAManager) GetPlayerAAState(characterID int32) (*AAPlayerState, error) { - // Try to get from cache first - if playerState := am.getPlayerState(characterID); playerState != nil { + // Try to get from cache first (read lock) + am.statesMutex.RLock() + if playerState, exists := am.playerStates[characterID]; exists { + am.statesMutex.RUnlock() + return playerState, nil + } + am.statesMutex.RUnlock() + + // Need to load from database, use write lock to prevent race condition + am.statesMutex.Lock() + defer am.statesMutex.Unlock() + + // Double-check pattern: another goroutine might have loaded it while we waited + if playerState, exists := am.playerStates[characterID]; exists { return playerState, nil } // Load from database if not cached - return am.LoadPlayerAA(characterID) + playerState, err := am.loadPlayerAAFromDatabase(characterID) + if err != nil { + return nil, err + } + + // Cache the player state (already have write lock) + am.playerStates[characterID] = playerState + + // Fire load event + am.firePlayerAALoadedEvent(characterID, playerState) + + return playerState, nil } // PurchaseAA purchases an AA for a player func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8) error { - // Get player state - playerState := am.getPlayerState(characterID) - if playerState == nil { - return fmt.Errorf("player state not found") + // Get player state - this handles loading if needed + playerState, err := am.GetPlayerAAState(characterID) + if err != nil { + return fmt.Errorf("failed to get player state: %v", err) } // Get AA data @@ -187,7 +208,7 @@ func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8 } // Perform purchase - err := am.performAAPurchase(playerState, aaData, targetRank) + err = am.performAAPurchase(playerState, aaData, targetRank) if err != nil { return err } @@ -355,16 +376,19 @@ func (am *AAManager) GetAATemplates(characterID int32) (map[int8]*AATemplate, er // AwardAAPoints awards AA points to a player func (am *AAManager) AwardAAPoints(characterID int32, points int32, reason string) error { - // Get player state - playerState := am.getPlayerState(characterID) - if playerState == nil { - return fmt.Errorf("player state not found") + // Get player state - this handles loading if needed + playerState, err := am.GetPlayerAAState(characterID) + if err != nil { + return fmt.Errorf("failed to get player state: %v", err) } - // Award points + // Award points and capture values for events + var oldTotal, newTotal int32 playerState.mutex.Lock() + oldTotal = playerState.TotalPoints playerState.TotalPoints += points playerState.AvailablePoints += points + newTotal = playerState.TotalPoints playerState.needsSync = true playerState.mutex.Unlock() @@ -373,8 +397,8 @@ func (am *AAManager) AwardAAPoints(characterID int32, points int32, reason strin am.notifier.NotifyAAPointsAwarded(characterID, points, reason) } - // Fire points changed event - am.firePlayerAAPointsChangedEvent(characterID, playerState.TotalPoints-points, playerState.TotalPoints) + // Fire points changed event with captured values + am.firePlayerAAPointsChangedEvent(characterID, oldTotal, newTotal) return nil } @@ -520,15 +544,15 @@ func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAd // Calculate cost pointsCost := int32(aaData.RankCost) * int32(targetRank) - // Check if player has enough points + // Update player state - MUST acquire lock BEFORE checking points to prevent TOCTOU race + playerState.mutex.Lock() + defer playerState.mutex.Unlock() + + // Check if player has enough points (inside the lock to prevent race condition) if playerState.AvailablePoints < pointsCost { return fmt.Errorf("insufficient AA points: need %d, have %d", pointsCost, playerState.AvailablePoints) } - // Update player state - playerState.mutex.Lock() - defer playerState.mutex.Unlock() - // Create or update progress progress := playerState.AAProgress[aaData.NodeID] if progress == nil {