• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

real Talk - Floats in Warcraft 3

Status
Not open for further replies.
Level 14
Joined
Dec 12, 2012
Messages
1,007
realTalk - Floats in Warcraft 3

1. Introduction

In Warcraft 3 there exist almost 100 different native datatypes. Most of those datatypes extend of handle, which can be seen as "base-class" of the Jass type system. Apart from those, there exist seven basic types:


  • nothing The incomplete type for denoting "no type"
  • boolean A simple true/false datatype
  • integer The standard 32 bit integer datatype
  • real A 32 bit floating point datatype
  • string A charakter array for representing text
  • code A function pointer for a function that takes no arguments
  • handle The base of all further derived types


For most of these types the usage is pretty clear and they basically work "as expected". There are some limitations on string that it can't grow infinitly or that you can't make arrays of type code, but those are quite understandable and straightforward. The only exception to this is the datatype real. There exist probably more rumors and wrong/misleading information about this datatype than for all of the other six basic types together. I have read things like 0.001 is the smallest real in Wc3, that long reals are interpreted as strings and many more.

This is especially remarkable since the most fundamental things in Wc3 are modelled with reals: Unit health, mana, damage and armor, just to mention a few of them. Since the real datatype is so important and widely used in Wc3, I think its time to collect the most important things about it in one article which can serve as future reference. During some investigation about reals in Wc3 I found some interesting points I want to share, discuss and hopefully extend by further ideas and findings by other users. Once this is done, the goal is to publish this as a tutorial that covers everything relevant about the real datatype.

The findings and/or conclusions might be relevant for building the first robust UNIT_STATE_LIFE/UNIT_STATE_MANA library in Wc3.

2. Reals in General

Standard floating point datatypes (in many languages called float when refering to single precision) already can cause quite some trouble when programming if you don't know about the exact behavior of floats. There exist great tutorials on this topic, but there are quite some things one has to consider when working with reals and accuracy matters.

To make things worse, reals in Wc3 behave in quite some points different than the IEEE 754 standard. This article therefore won't focus on explaining floating point numbers in detail, since there already exist quite massive literature and references on this topic, but on the differences and pitfalls with floats in Jass compared to most other programming/scripting languages.


3. Reals in Warcraft 3

3.1 Operator accuracy

First lets consider the standard algorithm to calculate the smallest positive floating point number that can be represented by the corresponding datatype. For all reference examples, standard C++ is used.


C++:
#include <iostream>
#include <limits>

int main()
{
	std::cout.precision(std::numeric_limits<float>::max_digits10);

	int i = 0;
	float f = 1.0f;

	while (f != 0.0f)
	{
		std::cout << i++ << '\t' << f << '\n';
		f /= 2.0f;
	}
}

Code:
0	1
1	0.5
2	0.25
3	0.125
4	0.0625
5	0.03125
6	0.015625
7	0.0078125
8	0.00390625
9	0.001953125
10	0.0009765625
11	0.00048828125
12	0.000244140625
13	0.000122070313
14	6.10351563e-005
15	3.05175781e-005
16	1.52587891e-005
17	7.62939453e-006
18	3.81469727e-006
19	1.90734863e-006
20	9.53674316e-007
21	4.76837158e-007
22	2.38418579e-007
23	1.1920929e-007
24	5.96046448e-008
25	2.98023224e-008
26	1.49011612e-008
27	7.4505806e-009
28	3.7252903e-009
29	1.86264515e-009
30	9.31322575e-010
31	4.65661287e-010
32	2.32830644e-010
33	1.16415322e-010
34	5.82076609e-011
35	2.91038305e-011
36	1.45519152e-011
37	7.27595761e-012
38	3.63797881e-012
39	1.8189894e-012
40	9.09494702e-013
41	4.54747351e-013
42	2.27373675e-013
43	1.13686838e-013
44	5.68434189e-014
45	2.84217094e-014
46	1.42108547e-014
47	7.10542736e-015
48	3.55271368e-015
49	1.77635684e-015
50	8.8817842e-016
51	4.4408921e-016
52	2.22044605e-016
53	1.11022302e-016
54	5.55111512e-017
55	2.77555756e-017
56	1.38777878e-017
57	6.9388939e-018
58	3.46944695e-018
59	1.73472348e-018
60	8.67361738e-019
61	4.33680869e-019
62	2.16840434e-019
63	1.08420217e-019
64	5.42101086e-020
65	2.71050543e-020
66	1.35525272e-020
67	6.77626358e-021
68	3.38813179e-021
69	1.69406589e-021
70	8.47032947e-022
71	4.23516474e-022
72	2.11758237e-022
73	1.05879118e-022
74	5.29395592e-023
75	2.64697796e-023
76	1.32348898e-023
77	6.6174449e-024
78	3.30872245e-024
79	1.65436123e-024
80	8.27180613e-025
81	4.13590306e-025
82	2.06795153e-025
83	1.03397577e-025
84	5.16987883e-026
85	2.58493941e-026
86	1.29246971e-026
87	6.46234854e-027
88	3.23117427e-027
89	1.61558713e-027
90	8.07793567e-028
91	4.03896783e-028
92	2.01948392e-028
93	1.00974196e-028
94	5.04870979e-029
95	2.5243549e-029
96	1.26217745e-029
97	6.31088724e-030
98	3.15544362e-030
99	1.57772181e-030
100	7.88860905e-031
101	3.94430453e-031
102	1.97215226e-031
103	9.86076132e-032
104	4.93038066e-032
105	2.46519033e-032
106	1.23259516e-032
107	6.16297582e-033
108	3.08148791e-033
109	1.54074396e-033
110	7.70371978e-034
111	3.85185989e-034
112	1.92592994e-034
113	9.62964972e-035
114	4.81482486e-035
115	2.40741243e-035
116	1.20370622e-035
117	6.01853108e-036
118	3.00926554e-036
119	1.50463277e-036
120	7.52316385e-037
121	3.76158192e-037
122	1.88079096e-037
123	9.40395481e-038
124	4.7019774e-038
125	2.3509887e-038
126	1.17549435e-038
127	5.87747175e-039
128	2.93873588e-039
129	1.46936794e-039
130	7.34683969e-040
131	3.67341985e-040
132	1.83670992e-040
133	9.18354962e-041
134	4.59177481e-041
135	2.2958874e-041
136	1.1479437e-041
137	5.73971851e-042
138	2.86985925e-042
139	1.43492963e-042
140	7.17464814e-043
141	3.58732407e-043
142	1.79366203e-043
143	8.96831017e-044
144	4.48415509e-044
145	2.24207754e-044
146	1.12103877e-044
147	5.60519386e-045
148	2.80259693e-045
149	1.40129846e-045



As one can see, the algorithm stops after 150 iterations and yields the smallest positive floating point number representable with 32 bit, namely 1.40129846e-045. This value is not "normal", but I will come to this later.

First lets try out the same thing in vJass, if we get identical results. If Wc3 reals are 32 bit floats, we should get identical results. So here is a possible implementation and output of the above algorithm in vJass:


JASS:
scope RealTest initializer onInit
	private function onInit takes nothing returns nothing
		local real r = 1.0
		local integer i = 0
		
		loop
			call BJDebugMsg(I2S(i) + " " + R2SW(r, 50, 50))
			
			set r = r/2.0
			set i = i + 1
			
			exitwhen r == 0.0
		endloop
		call BJDebugMsg("Finished") // Just to make sure we didn't hit the OP limit
	endfunction
endscope


Code:
0	1
1	0.5
2	0.25
3	0.125
4	0.0625
5	0.03125
6	0.015625
7	0.0078125
8	0.00390625
9	0.001953125
Finished



Now that was unexpected!

The same algorithm produces a very different output and terminates after 10 iterations leading to a minimum float value of 0.001953125. The question is now, why is that? Is the real datatype a 32 float at all if it behaves that different? If not, what is it? And why does it terminate at such a strange value like 0.001953125?

To answer those questions, we rearrange the vJass algorithm a bit:


JASS:
scope RealTest initializer onInit
	private function onInit takes nothing returns nothing
		local real r = 1.0
		local integer i = 0
		
		loop
			call BJDebugMsg(I2S(i) + " " + R2SW(r, 50, 50))
			
			set r = r/2.0
			set i = i + 1
			
			exitwhen not (r != 0.0) // <- Only difference here
		endloop
		call BJDebugMsg("Finished") // Just to make sure we didn't hit the OP limit
	endfunction
endscope


Code:
0	1.000000000
1	0.500000000
2	0.250000000
3	0.125000000
4	0.062500000
5	0.031250000
6	0.015625000
7	0.007812500
8	0.003906250
9	0.001953125
10	0.000976563
11	0.000488281
12	0.000244141
13	0.000122070
14	0.000061035
15	0.000030518
16	0.000015259
17	0.000007629
18	0.000003815
19	0.000001907
20	0.000000954
21	0.000000477
22	0.000000238
23	0.000000119
24	0.000000060
25	0.000000030
26	0.000000015
27	0.000000007
28	0.000000004
29	0.000000002
30	0.000000001
31	0.000000000
32	0.000000000
33	0.000000000
34	0.000000000
35	0.000000000
36	0.000000000
37	0.000000000
38	0.000000000
39	0.000000000
40	0.000000000
41	0.000000000
42	0.000000000
43	0.000000000
44	0.000000000
45	0.000000000
46	0.000000000
47	0.000000000
48	0.000000000
49	0.000000000
50	0.000000000
51	0.000000000
52	0.000000000
53	0.000000000
54	0.000000000
55	0.000000000
56	0.000000000
57	0.000000000
58	0.000000000
59	0.000000000
60	0.000000000
61	0.000000000
62	0.000000000
63	0.000000000
64	0.000000000
65	0.000000000
66	0.000000000
67	0.000000000
68	0.000000000
69	0.000000000
70	0.000000000
71	0.000000000
72	0.000000000
73	0.000000000
74	0.000000000
75	0.000000000
76	0.000000000
77	0.000000000
78	0.000000000
79	0.000000000
80	0.000000000
81	0.000000000
82	0.000000000
83	0.000000000
84	0.000000000
85	0.000000000
86	0.000000000
87	0.000000000
88	0.000000000
89	0.000000000
90	0.000000000
91	0.000000000
92	0.000000000
93	0.000000000
94	0.000000000
95	0.000000000
96	0.000000000
97	0.000000000
98	0.000000000
99	0.000000000
100	0.000000000
101	0.000000000
102	0.000000000
103	0.000000000
104	0.000000000
105	0.000000000
106	0.000000000
107	0.000000000
108	0.000000000
109	0.000000000
110	0.000000000
111	0.000000000
112	0.000000000
113	0.000000000
114	0.000000000
115	0.000000000
116	0.000000000
117	0.000000000
118	0.000000000
119	0.000000000
120	0.000000000
121	0.000000000
122	0.000000000
123	0.000000000
124	0.000000000
125	0.000000000
126	0.000000000
Finished



This looks much better already!

There are two things immediately attracting attention. First, we get visible results until 0.000000001, which is the smallest positive number that can be displayed with R2SW. Since Wc3 doesn't support scientific notation, all numbers smaller than this number will be displayed as 0.000000000, even though they are not zero.

Second, and much more important, the number of iterations and accuracy of the real datatye suddenly increased significantly. The only thing we changed is operator== to not operator!= which should be logically equivalent, but in fact are not in Jass. It seems Blizzard wanted to make things "easier" for people when working with reals and therefore implemented the operator== for reals with some epsilon. However, they only did so for this operator, not for the other comparison operators like operator!=. This property was first reported by masda70 and is quantified here. I.e. the epsilon used by operator== must be somewhere around 0.001. So the first conclusion we can draw here is:

1 Conclusion: If you need maximum accuracy when comparing two reals for equality, use not operator!= instead of operator==. All other comparison operators also have the usual accuracy and might safely be used.

But lets proceed.

3.2 Normals and Denormals

You might remember me talking about "normal" numbers before. Also there is still something strange: Even with the usage of the not !=operator , we still get different results with our vJass algorithm compared to the C++ algorithm. The vJass version terminates after 127 steps, while the C++ version terminates after 150 steps. So is a C++ float still more accurate than a Jass real? The answer is yes and no.

One important thing to notice is, that in C++ typically denormal numbers are used. Denormal numbers are smaller than the smallest "normal" floating point value and add aditional accuracy to the floating point type by using a trick in their representation. While denormal numbers have advantage that the invariant x == y <-> x - y == 0 always holds, they also are notorious for affecting performance in a negative way.

Since Blizzard didn't seem to care about so small values (rounding with operator==, no scientific notation etc.), it makes sense that they disabled denormals in Wc3. We can do the same in C++ by modifying our test program slightly:


C++:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include <iostream>
#include <limits>

int main()
{
	_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); // Disable denormals
	std::cout.precision(std::numeric_limits<float>::max_digits10);

	int i = 0;
	float f = 1.0f;

	while (f != 0.0f)
	{
		std::cout << i++ << '\t' << f << '\n';
		f /= 2.0f;
	}
}

Code:
0	1
1	0.5
2	0.25
3	0.125
4	0.0625
5	0.03125
6	0.015625
7	0.0078125
8	0.00390625
9	0.001953125
10	0.0009765625
11	0.00048828125
12	0.000244140625
13	0.000122070313
14	6.10351563e-005
15	3.05175781e-005
16	1.52587891e-005
17	7.62939453e-006
18	3.81469727e-006
19	1.90734863e-006
20	9.53674316e-007
21	4.76837158e-007
22	2.38418579e-007
23	1.1920929e-007
24	5.96046448e-008
25	2.98023224e-008
26	1.49011612e-008
27	7.4505806e-009
28	3.7252903e-009
29	1.86264515e-009
30	9.31322575e-010
31	4.65661287e-010
32	2.32830644e-010
33	1.16415322e-010
34	5.82076609e-011
35	2.91038305e-011
36	1.45519152e-011
37	7.27595761e-012
38	3.63797881e-012
39	1.8189894e-012
40	9.09494702e-013
41	4.54747351e-013
42	2.27373675e-013
43	1.13686838e-013
44	5.68434189e-014
45	2.84217094e-014
46	1.42108547e-014
47	7.10542736e-015
48	3.55271368e-015
49	1.77635684e-015
50	8.8817842e-016
51	4.4408921e-016
52	2.22044605e-016
53	1.11022302e-016
54	5.55111512e-017
55	2.77555756e-017
56	1.38777878e-017
57	6.9388939e-018
58	3.46944695e-018
59	1.73472348e-018
60	8.67361738e-019
61	4.33680869e-019
62	2.16840434e-019
63	1.08420217e-019
64	5.42101086e-020
65	2.71050543e-020
66	1.35525272e-020
67	6.77626358e-021
68	3.38813179e-021
69	1.69406589e-021
70	8.47032947e-022
71	4.23516474e-022
72	2.11758237e-022
73	1.05879118e-022
74	5.29395592e-023
75	2.64697796e-023
76	1.32348898e-023
77	6.6174449e-024
78	3.30872245e-024
79	1.65436123e-024
80	8.27180613e-025
81	4.13590306e-025
82	2.06795153e-025
83	1.03397577e-025
84	5.16987883e-026
85	2.58493941e-026
86	1.29246971e-026
87	6.46234854e-027
88	3.23117427e-027
89	1.61558713e-027
90	8.07793567e-028
91	4.03896783e-028
92	2.01948392e-028
93	1.00974196e-028
94	5.04870979e-029
95	2.5243549e-029
96	1.26217745e-029
97	6.31088724e-030
98	3.15544362e-030
99	1.57772181e-030
100	7.88860905e-031
101	3.94430453e-031
102	1.97215226e-031
103	9.86076132e-032
104	4.93038066e-032
105	2.46519033e-032
106	1.23259516e-032
107	6.16297582e-033
108	3.08148791e-033
109	1.54074396e-033
110	7.70371978e-034
111	3.85185989e-034
112	1.92592994e-034
113	9.62964972e-035
114	4.81482486e-035
115	2.40741243e-035
116	1.20370622e-035
117	6.01853108e-036
118	3.00926554e-036
119	1.50463277e-036
120	7.52316385e-037
121	3.76158192e-037
122	1.88079096e-037
123	9.40395481e-038
124	4.7019774e-038
125	2.3509887e-038
126	1.17549435e-038



And there you go, we get exactly the same output with out C++ program like with our vJass program. The algorithms terminate after 127 iterations and yield the smallest positive float number, 1.17549435e-038 which also equals std::numeric_limits<float>::min(). This can be seen as a prove for our second conclusion about reals in Wc3:

2 Conclusion: The Warcraft 3 real datatype is a 32 bit float with disabled denormal numbers.


3.3 Inf, NaN and 1.0e9

The IEEE Standard also defines Inf (Infinity) and NaN (Not a Number). For example the result of the division 1/0 is defined as positive infinty, while the division 0/0 for example yields a NaN.

Warcraft 3 behaves quite different here. A NaN doesn't seem to exist at all, since division by zero causes a thread crash and therefore termination of the function trying to compute a NaN. More interesting, when looking at the object editor it seems that reals are capped at 1.0e9, because even with holding shift it is not possible to enter bigger values in object fields. But the real datatype is capable of storing bigger numbers (exactly like the float datatype in C++), so this seems to be related exclusivly to the object editor. Even the Inf value does exist (computalbe for example with the Pow function), it is even displayed correctly:

JASS:
call BJDebugMsg(R2S(Pow(2.0, 128.0))) // Displays: inf

However, it does not behave like an Inf. In especially, every attempt to decrease an inf value should either result in inf, -inf or NaN. However, multiplying a Wc3 inf with 2 will result in 0. So this is definitly not the expected behavior and therefore leads to our next conclusion:

3 Conclusion: Warcraft 3 reals cannot be NaN. They can be much larger than 1.0e9, i.e. up to 1.70141183e+038 (the biggest standard float) and even inf. However, the value inf doesn't behave like an IEEE 754 inf but like a "normal" value.


3.4 Minimum Unit Life and the problem with literals

Another interesting question is, what is the minimum health value a unit can have without dying. While this is a quite important question for a game like Wc3 and it would be quite intuitive that this boarder is marked by zero, in Wc3 things are different. It is well known that units can't survive with, for example 0.2 health and it seems the health boarder is around 0.406 which is also widely used in scripts. However, it turned out this constant is not accurate. There are quite some scripts that rely on the fact that GetWidgetLife(u) < 0.406 and even though chances are very low that this won't be fullfilled during normal gameplay, there is the chance of having a bug due to this. Using the absolute correct value would erase this chance.

So one could simply come to the idea to decrease the value 0.406 (maybe combined with a binary search) to get the smallest possible health value. However, there is a problem with this approach. I.e. you always have to consider the current spacing of the floating point value. Floats are not evenly spaced like integers (where the absolute difference between two successive integers is always 1). When dealing with floats, this difference depends on the magnitude of the float itself.

You might be familiar with this phenomenon from the object editor. If you set a units health to the maximum object editor value (1000000000 == 1.0e9), you are within a region where two floats might differ by at minimum a value of 64. So when you try to enter for example a value of 999999950, you will in fact get a unit with 999999936 health. This is because the prior float of 1000000000 is 999999936, which differ by 64. So any attempt to use values in between of 999999936 and 1000000000 will be either rounded up or down towards the closest representative floating point value. Since 999999950 is closer to 999999936, it will be rounded down.

The same problem applies to floats in Jass. For example a value like 0.405014016 is not directly representable as a float and is rounded to 0.405014008. Now operating on the last digit is very dangerous, because it is in the order of magnitude of 1.0e-9. However, floats within the size of 0.405014008 have a minimum difference of 2.98023224e-8, which is almost 30 times larger. So operating on the last digit will lead to rounding towards next representable floats and might skip several values without the user even noticing.

The only save way to do this is to use some function that can compute the prior representable float with exact precision. In Wc3 such a function does not exist, so we have to use C++ once again:


C++:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include <iostream>
#include <limits>
#include <boost\math\special_functions\next.hpp>

int main()
{
	_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); // Disable denormals
	std::cout.precision(std::numeric_limits<float>::max_digits10);

	float f = 0.406f;

	// Now decrease f using float_prior from boost and test the value until the unit dies
	std::cout << boost::math::float_prior(f) << '\n';
}


This code can be combined with a binary search. I have done this and can now present the absolute correct values for units minimum life:

JASS:
call SetWidgetLife(u, 0.404998779) // Unit alive
call SetWidgetLife(u, 0.404998749) // Unit dead
// Difference: 2.98023224e-8

You can also use an online IEEE converter to compute the next representable floating value. Just make sure to use 9 significant digits (which equals std::numeric_limits<float>::max_digits10 and is neccessary for perfect serialisation accuracy). This leads us to our forth conclusion:

4 Conclusion: A units minimum life is exactly 0.404998779. The prior representable float is 0.404998749 (which has a difference of 2.98023224e-8 to the former one) and is the largest value which makes a unit die.


4. Outlook and Todo

There is still a lot of research to be done and experiments to be carried out. Every further insight in the internals of the Warcraft 3 real datatype might improve existing resources, enable new possiblities and fix existing problems. Future points of interest could include (not exhaustive):

  • Find an efficient way to represent very small/big real values with the lack of scientific notation
  • Implement a precision library for more convenient real handling
  • Evaluate how the LESS_THAN etc. constants work, i.e. if they have the same accuracy as the real datatype or not.
  • Find out why the NOT_EQUAL constant doesn't work with unit state events.
  • Identify the accuracy of damage events
  • And many more ...

Enclosed some concrete examples that are strange and need further investigation:


1. Dealing damage

The code

JASS:
call UnitDamageTarget(u, u, 0.0000000298023224, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, null)

should deal the specified damage, but GetEventDamage shows 0.158952544. It seems the smaller the literal gets, the bigger the actual damage is. Interesting would be by which regularity the damage increases and why.

2. Literal values

It seems that

JASS:
local real r = 0.0000000000000000000000000000001

is the smallest postive literal that is not equal zero. However, this is not yet verified and it would be interesting why this is so since reals can represent smaller values than that.


So before this will be submitted as a tutorial I place it here in the lab to discuss this topic and include further findings and important facts about the Warcraft 3 real datatype.
 
Last edited:

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
WC3 uses float32 but SC2 uses a fixed point type. Why has Blizzard made that change?
Faster to compute (integer units used as opposed to floating point units on the processor). Also more accurate results for comparison.

I guess there are problems with floats? WC3 seems fine though.
They are not very portable. The rounding used and behaviour can vary from compiler to compiler. Although compiler options do usually exist to use IEEE compliant floats (which should be portable) depending on platform these might not be natively support by the floating point units and so require very slow software emulation. Additionally there are problems with rounding and error. I have best seen this in WC3 maps where your hero stats vary largely (eg from 50,000,000 strength to 500) the result can be negative life regeneration or minus maximum life (instant death).

Chances are unit death occurs at less than or equal to 0.405 as a C++ constant. Any other digits are likely the result of floating point error.

One also cannot rule out a nice looking floating point binary representation for the constant as it could have been hexed in.

I would be careful hard-coding such a constant into scripts. It might be subject to change in patches as different compilers could use a slightly different constant.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
BPower said:
Wow, this covers already a lot of important[/COLOR] information.

PurgeandFire said:
Awesome writeup! I've always been curious about this myself.

Bribe said:
looking_for_help is always pioneering cool stuff

Flux said:
Interesting and very informative. Looking forward on the minimum health a unit can have to stay alive.

Thanks a lot.
I updated the article including a chapter about the minimum health a unit can have.

Dr Super Good said:
Faster to compute (integer units used as opposed to floating point units on the processor). Also more accurate results for comparison.

Yeah, I guess its mostly for precision problems. Wc3 wouldn't really need reals at all, so I guess they wanted to avoid these problems in SC2.

Dr Super Good said:
Chances are unit death occurs at less than or equal to 0.405 as a C++ constant. Any other digits are likely the result of floating point error.

Floats are also exact, just different than ints, so this isn't really related to floating point errors.

Alive: 0.405014016
Dead: 0.405014015

They could be wrong though.

Carefull, you are operating in a range which exceeds the float accuracy. I did some further test and added a new chapter 3.4 Minimum Unit Life and the problem with literals.

There the absolute minimun units health is identified and documented.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
Floats are also exact, just different than ints, so this isn't really related to floating point errors.
Yes but the constant used for comparison would have been choosen sensibly. It looks like pretty close to 0.405 to me.

You must remember the programmers are more likely to have something like 0.405 in the WC3 source code than "0.404998779" or "0.404998749". It could even be a formula of sorts (405.0/1000.0). It could also be a convenient hex value.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Yes but the constant used for comparison would have been choosen sensibly. It looks like pretty close to 0.405 to me.

You must remember the programmers are more likely to have something like 0.405 in the WC3 source code than "0.404998779" or "0.404998749". It could even be a formula of sorts (405.0/1000.0). It could also be a convenient hex value.

If it would be hardcoded 0.405 in the source code it would be the closest representable float to 0.405. Floats don't magically change their accuracy. Could be a formula, yes.

At the end we don't know why it is such a strange value and all those options are just wild guesses. But that also doesn't really matter, the important thing is that we know that value.
 
Level 13
Joined
Nov 7, 2014
Messages
571
What's so special about this ~0.405 constant anyway? Why not 0.5 or 0.125?
Maybe it has some kind of property which leads to some kind of an optimization?
Who knows...

Quick! =) replace your IsUnitAlive|Dead functions with:
JASS:
function IsUnitAlive takes unit u returns boolean
    return GetWidgetLife(u) >= 0.404998779
endfunction

function IsUnitDead takes unit u returns boolean
    return GetWidgetLife(u) <= 0.404998749
endfunction

Anyway searching for "floating point RTS" leads to intresting reads (like this one).
It's kind of funny to know now that one of the reasons replays from a different
patch can't be viewed is because of floating point numbers.

In this article there's some information about Synchronous RTS in general, floating point and desyncs.
I guess floats are responsible for desyncs?
GetLocationZ anyone? =)
JASS:
// This function is asynchronous. The values it returns are not guaranteed synchronous between each player.
//  If you attempt to use it in a synchronous manner, it may cause a desync.
native GetLocationZ             takes location whichLocation returns real
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
At the end we don't know why it is such a strange value and all those options are just wild guesses.
The fact remains it is hard coded as some value. Fact is it would come from source other than some guy one day deciding to enter the lines "0.404998779" or "0.404998749". Hence the reasons I stated are not "just wild guesses".

But that also doesn't really matter, the important thing is that we know that value.
For this build... Nothing says it will always be that value. You have no proof it was not some other value in previous builds and so have no proof that it will not change with future builds. If it changes any hard-coded constants will become inaccurate so one might as well use 0.405.

What's so special about this ~0.405 constant anyway? Why not 0.5 or 0.125?
Exactly. Why did they choose that value? That is the big question. They could have choosen 1.0, or even 0 itself but they did not choose 0.405 but instead "0.404998779" or "0.404998749" depending on what comparison is used.

Maybe it has some kind of property which leads to some kind of an optimization?
Unlikely giving the scope.

Quick! =) replace your IsUnitAlive|Dead functions with:
Does JASS even evaluate floats to such a precision?

It's kind of funny to know now that one of the reasons replays from a different
patch can't be viewed is because of floating point numbers.
Actually the main reason is engine changes. Clearly a replay showing off a bug will not work after the bug is fixed since at that point of the game it will out of sync as the bug will not happen. SC2 solved this to some extent by using older client versions to run old replays using old data, the reason for all the patch MPQ (soon CASC) files.

GetLocationZ anyone? =)
That is asynchronous for another reason. Walkable destructibles are factored into it but their geometry is only rendered when near the player camera and their animation variance and state is asynchronous. Additionally terrain mesh deformations caused by skills like War Stomp, Thunder Clap, Shockwave, etc produce different results at a given time between Windows and Mac clients as well as depending on client graphic settings.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Quick! =) replace your IsUnitAlive|Dead functions with:
Jass:
function IsUnitAlive takes unit u returns boolean
return GetWidgetLife(u) >= 0.404998779
endfunction

function IsUnitDead takes unit u returns boolean
return GetWidgetLife(u) <= 0.404998749
endfunction
if not explicit required you should still use native UnitAlive takes unit id returns boolean
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
The fact remains it is hard coded as some value. Fact is it would come from source other than some guy one day deciding to enter the lines "0.404998779" or "0.404998749". Hence the reasons I stated are not "just wild guesses".

The only fact here is that we don't know. Nothing more to say about that, what else is assuming a "convenient hex value" as a reason for that value, than a wild guess?

For this build... Nothing says it will always be that value. You have no proof it was not some other value in previous builds and so have no proof that it will not change with future builds. If it changes any hard-coded constants will become inaccurate so one might as well use 0.405.

Quite the contrary, you are the one not having a proof of anything you are saying here.

I just tested the value with the versions 1.20e, 1.21b, 1.22, 1.23, 1.24, 1.25b and the latest one. And it works for all of them. So because "some day" maybe "something might change" (when was the last patch again?) is not really a good argument. If Blizzard decides to release a patch which has a breaking change there is nothing we can do about it anyway. But thats also not the point of this thread.

Exactly. Why did they choose that value? That is the big question. They could have choosen 1.0, or even 0 itself but they did not choose 0.405 but instead "0.404998779" or "0.404998749" depending on what comparison is used.

No, it doesn't depend on the comparison. 0.404998779 means alive, 0.404998749 (which is the next smaller float) means dead.

Does JASS even evaluate floats to such a precision?

Read the initial post please.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
Nothing more to say about that, what else is assuming a "convenient hex value" as a reason for that value, than a wild guess?
Because understanding where the value comes from is important. Yes it is a magic number but is it really a magic number? Especially such a non-round magic number is very unusual for a game.

Quite the contrary, you are the one not having a proof of anything you are saying here.

I just tested the value with the versions 1.20e, 1.21b, 1.22, 1.23, 1.24, 1.25b and the latest one. And it works for all of them. So because "some day" maybe "something might change" (when was the last patch again?) is not really a good argument. If Blizzard decides to release a patch which has a breaking change there is nothing we can do about it anyway. But thats also not the point of this thread.
My proof for what the constant could be is from practical programming experience. All such constants have reasoning behind them and also could be subject to change. It is important to avoid such magic number constants where possible for future compatibility.

For example when I coded in some constants into an open source game I made sure to comment the reasoning why they were chosen.

Read the initial post please.
Nowhere does it clearly answer such a simple question as to what the maximum enterable JASS precision of a real is. Instead you have 4 unrelated sections talking about other interesting but not related things. Sure knowing the maximum object editor range is useful but that does not answer what the maximum enterable JASS precision is.

Speaking of object editor real range, is the range limit object editor related or object file format related? If the file format is binary it should be using 32 bits to save the float so it should allow any possible floating point value to be used (probably limited to "denormal" range).
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
My theory is less technical but less boring.

0.405 is what they tried to go for, never mind the semantics that reals are inaccurate. You might wonder why they didn't just do 0.49999 for dead and 0.5 for alive. This is Blizzard, they were not that into accuracy and don't always think things through. It probably started as "0.5 and up is alive and 0.4 and below is dead". So they said "what if it's in-between? So what they did, is consider almost anything above 4 alive instead of almost anything below 5 dead (they arr more of a glass half full company). So if it's 0.41 it's alive and if it's dead it's 0.40. If it stradles the middle it rounds to the nearest 0.01 when checked if it's living or dead. So you get 0.405 as alive and <0.405 as dead. Why the floating point inaccuracy? Because HP in-game doesn't use floating point, just like unitstate events don't.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Because understanding where the value comes from is important. Yes it is a magic number but is it really a magic number? Especially such a non-round magic number is very unusual for a game.

Of course it is important and would be very interesting. But as long as noone finds a compelling theory behind that value (and search for such a theory is only possible with knowing the exact value in the first place, which was one of the reason I posted it here), we can only guess. The value in hex is 0x3ecf5c00 (alive)/0x3ecf5bff (dead), those just don't look convenient for me.

Clearly such a number is very unusual, but I don't think Blizzard chose this value for some specific reason, but more what Bribe said.

My proof for what the constant could be is from practical programming experience. All such constants have reasoning behind them and also could be subject to change. It is important to avoid such magic number constants where possible for future compatibility.

For example when I coded in some constants into an open source game I made sure to comment the reasoning why they were chosen.

Thats no proof. I already doubt the first assumption, that there is a deep reasoning behind that value.

Its nice to know how you would have done it, but we have to deal with how Blizzard did it. And we have no such reasoning available from Blizzard. They might even documented that value internally, but that doesn't really help us ;)

Nowhere does it clearly answer such a simple question as to what the maximum enterable JASS precision of a real is. Instead you have 4 unrelated sections talking about other interesting but not related things. Sure knowing the maximum object editor range is useful but that does not answer what the maximum enterable JASS precision is.

  1. Read the post, it answers the question. The open question is why it is that constant, but JASS can evaluate much higher precisions than the given ones for unit minimum health.
  2. We don't even need to know the maximum enterable JASS precision when it comes to the question whether JASS is accurate enough to evaluate literals like 0.404998779. So your demand for the maximum enterable precision is not related to your initial question if JASS is accurate enough for literals like 0.404998779. It is accurate enough and allows literals of much smaller magnitude.
  3. The sections aren't unrelated. Section 3.2 explaines the findings from section 3.1, section 3.3 explaines some corner cases and section 3.4 speaks about the representation as literals.

Speaking of object editor real range, is the range limit object editor related or object file format related? If the file format is binary it should be using 32 bits to save the float so it should allow any possible floating point value to be used (probably limited to "denormal" range).

If the file format is not binary it can still store any possible floating point value. So whats your point here?



Thats a very good idea, I will definitly include this (of course with giving credits to muzzel and Crigges).

EDIT:

Found a way for vJass (the loop-return hack):

JASS:
scope MyScope initializer onInit
	function realToIndex takes real r returns integer
		loop
			return r
		endloop
		return 0
	endfunction

	function cleanInt takes integer i returns integer
		return i
	endfunction

	function indexToReal takes integer i returns real
		loop
			return i
		endloop
		return 0.0
	endfunction

	function cleanReal takes real r returns real
		return r
	endfunction

	private function onInit takes nothing returns nothing
		local integer i = cleanInt(realToIndex(123.456))
		local real r = cleanReal(indexToReal(i))
		call BJDebugMsg(R2S(r)) //prints 123.456
	endfunction
endscope

compiles just fine and works. Nice, now we have reinterpret_cast in vJass :)
 
Last edited:

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
The value in hex is 0x3ecf5c00 (alive)/0x3ecf5bff (dead), those just don't look convenient for me.
Look again? It is at some byte overflow boarder.
fraction -> 0x4F5C00 or 0x4F5BFF
exponent -> 0x9F
sign ->0x00

Being at such a boarder makes it a much less random number. This is much more round than 0.405 (0x3ECF5C29). Maybe it is 0.405 with the epsilon error applied to it in some way? Or maybe they zeroed out the last byte?
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
[...]

compiles just fine and works. Nice, now we have reinterpret_cast in vJass :)

My improved version of pjass which i recommend catches this error unless called with +rb ...
best case would be a pseudo-native which is implemented by a compiler. would also help with the potential of inlining the clean functions.

e:
pjass 10m also catches this error, so im not sure what you're using/talking about.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Look again? It is at some byte overflow boarder.
fraction -> 0x4F5C00 or 0x4F5BFF
exponent -> 0x9F
sign ->0x00

Being at such a boarder makes it a much less random number. This is much more round than 0.405 (0x3ECF5C29). Maybe it is 0.405 with the epsilon error applied to it in some way? Or maybe they zeroed out the last byte?

Sounds plausible, but the value is still strange... the epsilon between 0.405 would then be 1.22189522e-6/1.25169754e-6, which is still very unusual...

LeP said:
My improved version of pjass which i recommend catches this error unless called with +rb ...
best case would be a pseudo-native which is implemented by a compiler. would also help with the potential of inlining the clean functions.

A built-in compiler pseudo-native would be nice, but until then we can use something like this library, works as well:

JASS:
/************************************************************************
*
*	ReinterpretCast v1.0.0.0
*	------------------------
*	Allows the user to reinterpret integers as reals and vice versa.
*	Credits go to Crigges and muzzel who discovered this technique.
*	
*
*	Public functions and API
*	------------------------
*	function ReinterpretI2R takes integer i returns real
*		Reinterprets an integer value as a real
*
*	function ReinterpretR2I takes real r returns integer
*		Reinterprets a real value as an integer
*
************************************************************************/
library ReinterpretCast 
	private function realToIndex takes real r returns integer
        loop
            return r
        endloop
        return 0
    endfunction

    private function cleanInt takes integer i returns integer
        return i
    endfunction

    private function indexToReal takes integer i returns real
        loop
            return i
        endloop
        return 0.0
    endfunction

    private function cleanReal takes real r returns real
        return r
    endfunction
	
	function ReinterpretI2R takes integer i returns real
		return cleanReal(indexToReal(i))
	endfunction

	function ReinterpretR2I takes real r returns integer
		return cleanInt(realToIndex(r))
	endfunction
endlibrary
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
pjass 10m also catches this error, so im not sure what you're using/talking about.

I use the standard 1.0k, the creator of 1.0m said himself that he has no idea what he is doing so yeah...

I don't use 1.0n until now because I never had the need to do so... but it also shouldn't catch that error since its valid Jass and it limits the possiblities of vJass.

Bribe said:
The cleanReal/cleanInt will still need to have a dummy return in order for them to not get inlined.

True, although I guess this will be mostly used for testing stuff and not in a final (optimized) map.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
I use the standard 1.0k, the creator of 1.0m said himself that he has no idea what he is doing so yeah...

you don't need much knowlegde for the change he made.

I don't use 1.0n until now because I never had the need to do so... but it also shouldn't catch that error since its valid Jass and it limits the possiblities of vJass.

I would argue that it is not valid jass. It's only valid by accident. I mean blizz released their patch to fix all the typecasting and just missed this one.
Also it's like realy hard to adjust pjass to this. And pjass already has the functionality to let this through: +rb. But then all returnbugs are ignored.

But, as a hacky solution to a hacky problem i've coded something into pjass for this.
You now can add //+rb in the line right above a function declaration and i'll enable +rb for just that function.

But it's hacky because a //+rb anywhere else in the file will throw an error. I hope i'll fix this eventually.

You can download it here
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
I would argue that it is not valid jass. It's only valid by accident. I mean blizz released their patch to fix all the typecasting and just missed this one.

Yes, but its a good accident because its usefull.

Also it's like realy hard to adjust pjass to this. And pjass already has the functionality to let this through: +rb. But then all returnbugs are ignored.

But, as a hacky solution to a hacky problem i've coded something into pjass for this.
You now can add //+rb in the line right above a function declaration and i'll enable +rb for just that function.

But it's hacky because a //+rb anywhere else in the file will throw an error. I hope i'll fix this eventually.

You can download it here

No offense, but I think these kind of hacks only make the situation worse. There are already enough compatability issues between the different pjass versions and IMO Jass should be a subset of vJass. So every valid Jass program should also be a valid vJass program.

But I think its also Ok to keep this function in an experimental state which only compiles in vanilla Jass and pjass up to 1.0k since it will most likely only be used to investigate some properties about reals.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
No offense, but I think these kind of hacks only make the situation worse. There are already enough compatability issues between the different pjass versions and IMO Jass should be a subset of vJass. So every valid Jass program should also be a valid vJass program.

But I think its also Ok to keep this function in an experimental state which only compiles in vanilla Jass and pjass up to 1.0k since it will most likely only be used to investigate some properties about reals.

There are valid use-cases apart from investigating some properties about reals.
And I think one should be able to use these functions without erros popping up.
Also pjass should be as close as possible to what warcraft 3 accepts as valid jass and should not orientate itself around jasshelper which is not known for it's good syntax handling.
And it's only a hack cause you currently can't add that comment anywhere else. Once that's fixed i think it's perfectly valid. It's just a comment; it's even backward compatible and shit. Maybe add a commandline option to enable it, but that's it.

It's sad that i have to work around such stupid bugs but it's frankly my best option.

I would also advise against using pjass 10k. It lets through some pretty bad code like this
JASS:
function a takes handle h returns unit
	return h
endfunction
Happy debugging!

"Compabilty issues" between different pjass verions are bugfixes made neccessary because blizzard patched warcraft 3. Like, i don't even get that argument: you don't want updates for your software? Those "compabilty issues" are only there because people don't upgrade their software. People are always told to download the newset jasshelper but for pjass it's incompatible verions? wtf

If people manage to download the latest jasshelper they'll also manage to download the newest pjass.

And again, pjass always had the ability to ignore returnbug errors: +rb.
So even you can use a not horrible broken pjass and still enjoy no errors via a simple +rb.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
There are valid use-cases apart from investigating some properties about reals.
And I think one should be able to use these functions without erros popping up.
Also pjass should be as close as possible to what warcraft 3 accepts as valid jass and should not orientate itself around jasshelper which is not known for it's good syntax handling.

Agreed.

And it's only a hack cause you currently can't add that comment anywhere else. Once that's fixed i think it's perfectly valid. It's just a comment; it's even backward compatible and shit. Maybe add a commandline option to enable it, but that's it.

No, because a comment is a comment and should never influence a compiler no matter where you put it?

It's sad that i have to work around such stupid bugs but it's frankly my best option.

Can't you just make an exception for this type combination instead of using a comment as compile attribute?

I would also advise against using pjass 10k. It lets through some pretty bad code like this

Its just that in "production" code I don't write such things anyway. And if I want to do experiments I always use vanilla Jass because I know I can't trust any of the existing pjass versions, so for me it doesn't really change anything.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
Can't you just make an exception for this type combination instead of using a comment as compile attribute?

I woulnd't think so. If i allow typecasting from real to int and int to real it would allow this function but also others.
And you don't want other functions with possible errors. Rather a false positive than a false negative.
And yes, these types of errors do happen. I have examples.
And the thing is, pjass is dumb. It does not build an AST for example. It's a good thing most of the time because that's why pjass is so fast.
But without an AST i can't really check against a specific function to match exactly our conversation function.
(Not even an AST would be enugh, i'd still have to check for alpha-congruence.)

So...
No, because a comment is a comment and should never influence a compiler no matter where you put it?

It shouldn't, but what other option do i have?
I think a comment is pretty convinient for this.
Older pjass verions will simply ignore it.
jasshelper will ignore it.
people wont write it by accident.
and without out, newer pjass verions will just work as they used to.

As i said, bestcase would be a "native" done by the compiler but that's rather unlikely if you're using jasshelper isn't it?

Its just that in "production" code I don't write such things anyway. And if I want to do experiments I always use vanilla Jass because I know I can't trust any of the existing pjass versions, so for me it doesn't really change anything.

If you found a thing warcraft allows but pjass does not, that's a bug and should be filed.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Actually, //! is also a comment but it gets used all the time.

Well, having something like

JASS:
//! +rb
function bar takes handle h returns integer
    return h
    return 0
endfunction

would be better because it signals to the user something "special" is happening there, while a plain comment looks pretty strange.

Of course the syntax could be improved a bit towards allowing such attributes more generically:

JASS:
//! attribute(unchecked_return)
function bar takes handle h returns integer
    return h
    return 0
endfunction

@LeP, is something like this doable without too much effort? If so I could include the keywords into the new TESH as well.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
@LeP, is something like this doable without too much effort? If so I could include the keywords into the new TESH as well.

Yes. I ofcourse prefer the //! +rb options but the other one would be possible aswell.
But are there any other uses for annotations (second option) in jass? Could you name some? That would greatly increase my motivation for implementing the second option.

Also, are you sure jasshelper doesn't eat //!-comments?

And would multiline annotations be allowed?
JASS:
//! annotation1
//! annotation2
function foo ...
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Yes. I ofcourse prefer the //! +rb options but the other one would be possible aswell.

Thats good to hear.

But are there any other uses for annotations (second option) in jass? Could you name some? That would greatly increase my motivation for implementing the second option.

The ones from misc.c


Code:
pjass -h           Display this help
pjass -v           Display version information and exit
pjass -e1          Ignores error level 1
pjass +e2          Undo Ignore of error level 2
pjass +s           Enable strict downcast evaluation
pjass -s           Disable strict downcast evaluation
pjass +rb          Enable returnbug
pjass -rb          Disable returnbug
pjass -            Read from standard input (may appear in a list)


Not all of those are relevant (like showing help) but +-e1 and +-s could be usefull. Also it is always good when implementing a new feature to make it as extendable and generic as possible. Maybe we want to include further attributes in future and then we have to rework the syntax again (which then breaks backwards compatability etc.). So I think it would be worth the effort.

Also using something like //! annotation1 is much more readable than //! +rb because users can directly understand it from reading the code. Using comand line options forces users to look them up or memorize them.

Finally they could be integrated much better into the TESH because they would just be identifiers (+rb contains an operator+), so it would also be easier to type.

And would multiline annotations be allowed?
JASS:
//! annotation1
//! annotation2
function foo ...

Probably yes... But for the beginning I think one annotation would be sufficient, because it can always be extended to more annotations without breaking backwards compatability.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Did some further tests and there are some new interesting facts.

I used the reinterpret cast to verify the unit minimum life 0.404998779. For the specific test it turned out the minimum health value represented as integer is 1053776896, which is exactly the integer representation of 0.404998779. That answers at least the following question:

Does JASS even evaluate floats to such a precision?

Yes, it does.
However, this is also the maximum precision of floating point literals, that Jass evaluates. Using just one digit more seems to break the parser:


JASS:
scope RealTest initializer onInit
    private function onInit takes nothing returns nothing
		local real r1 = 0.404998779
		local real r2 = 0.4049987792
		local real r3 = 0.40499877929

       
		call BJDebugMsg("r1 = " + R2SW(r1, 20, 20)) // 0.404998752
		call BJDebugMsg("r2 = " + R2SW(r2, 20, 20)) // -0.173736288
		call BJDebugMsg("r3 = " + R2SW(r3, 20, 20)) // 1.517721056
    endfunction
endscope


So while the first real is displayed correct for the first 7 digits (which is possibly the accuracy of the R2SW conversion function), the other two literals with "higher" accuracy produce garbage values. So using literals of such length can lead to completly wrong numbers.


Second finding: The value for a units minimum life of 0.404998779 is wrong - at least in the general case.

When doing the tests to find that value, I assumed that the value is a constant and therefore independent of any other factors. After some more tests I can now tell that this is not the case and a units minimum life depends of its current life. The value 0.404998779 is only true for a unit having 535 health (a standard rifleman). So this answers another questions:

You must remember the programmers are more likely to have something like 0.405 in the WC3 source code than "0.404998779" or "0.404998749". It could even be a formula of sorts (405.0/1000.0). It could also be a convenient hex value.

It definitly seems to be some kind of formula.

Using the following code, the minimum life for a given unit can be evaluated easily:


JASS:
scope MyScope initializer onInit

	globals
		private unit u
		private integer health = 1053890000 // Initial health
		private real healthToBeChecked = 400000
		private integer unitId = 'ocat'
		private timer time
		private boolean check = false
	endglobals

	native UnitAlive takes unit id returns boolean
	
	private function callback takes nothing returns nothing
		call SetWidgetLife(u, healthToBeChecked)
		call SetWidgetLife(u, ReinterpretI2R(health))
		
		if not UnitAlive(u) then
			if not check then
				call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "Increase initial health!")
			endif
		
			call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, I2S(health + 1))
			call PauseTimer(time)
			call DestroyTimer(time)
		endif
		set check = true
		set health = health - 1
	endfunction
	
	private function onInit takes nothing returns nothing
		set u = CreateUnit(Player(0), unitId, 0.0, 0.0, 0.0)
		call SetHeroLevel(u, 10, true)
		call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "Health: " + R2S(healthToBeChecked))
		set time = CreateTimer()
		call TimerStart(time, 0.0, true, function callback)
	endfunction
endscope


By just setting the health of the unit used in the code (here a rifleman) in the object editor to the value to be tested. I have done this for several health values:

Code:
Unit health        Minimum health         Minimum health            Minimum health
(current)          (float)                (integer)                 (hex)
-----------------------------------------------------------------------------------
0.5                0.405000031            1053776938                0x3ecf5c2a
1                  0.405000031            1053776938                0x3ecf5c2a
5                  0.404999971            1053776936                0x3ecf5c28
10                 0.40500021             1053776944                0x3ecf5c30
50                 0.405000687            1053776960                0x3ecf5c40
100                0.405002594            1053777024                0x3ecf5c80
500                0.405014038            1053777408                0x3ecf5e00
1000               0.404998779            1053776896                0x3ecf5c00
5000               0.405029297            1053777920                0x3ecf6000
10000              0.404785156            1053769728                0x3ecf4000
50000              0.404296875            1053753344                0x3ecf0000
100000             0.40234375             1053687808                0x3ece0000
250000             0.3984375              1053556736                0x3ecc0000
500000             0.390625               1053294592                0x3ec80000
750000             0.40625                1053818880                0x3ed00000
1000000            0.40625                1053818880                0x3ed00000

Here we can see multiple interesting things:

  • First, the value of a units minimum health clearly somehow depends from its current health. However, there seems to be no linear relation since the values are not monotonically increasing or decreasing.
  • Second, the value varies much more than we thought before: a unit with 500000 health can survive a life of only 0.390625! Thats siginificantly less than the commonly used value of 0.406.
  • And third, the value is pending around 0.405 (especially for "common" health values), even though not very accurate. So Blizzard seems to use some calculation that takes into account the current units health and has its boarder at around 0.405.

Next it would be interesting to identify the exact formula that is used which gives equal results to the above table.
 
Last edited:
Level 12
Joined
Mar 13, 2012
Messages
1,121
Quite sure a relation of minimum health to be alive and the units maximum health is either very loosely or doesn't exist at all.

It's simply mind-boggling how one game can contain that many oddities. Almost as if some weird god made it as a giant puzzle.

In other games you have normal formulas and stuff. But in wc3 there is so much strange sh** and e-v-e-r-y little mechanic has corner cases. Best game ever.
 
  • Like
Reactions: pyf

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Doesn't <= use the same approximation that == uses? If so, you could at least throw off some oddball innaccuracy. But seeing it vary from 0.39 to 0.406 is quite hopeless. I can see why Blizzard just uses < 1 in Blizzard.j - they probably didn't understand what was going on, themselves.

For damage systems, perhaps if the health dips below 0.41 you can just give the killer the benefit of the doubt at that point. Damage Engine mostly compensates for this already by setting the soon-to-be-dead unit's life to RMaxBJ(lifeAfterDamage, 0.41), but in runoff cases where the original damage is less than 0.01 it won't work. I think the minimum non-zero from physical damage is more than that, so that error could only happen from UnitDamageTarget. Spell damage will potentially have errors as it relies on 0.405 and a triggered kill function, so I'll need to raise my life comparison to consider anything below 0.40625 as needing to be killed.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
The value clearly is 0.405 (look at 1 hp example). The variations can easily be caused by error.

How this error is being introduced is a big question. If current life was stored as a float it should keep the same precision independent of maximum value.

Lets say something stupid like current life was stored as a fraction of lost life (0 = full health, 1.0 = dead). This would mean that small current life amounts require less significant fractional parts (where floating point error occurs) as maximum life becomes bigger. Death check could then be working back from the absolute constant (0.405) into a current life fraction (using maximum life, 1-0.405/maxlife) and then comparing that with the current life lost fraction to determine death. Maybe this is not how it is done, but something similar must be happening to introduce the error.

looking_for_help could you post the hex values of all your determined "Minimum Health" amounts in the table? I have a feeling that the fractional part uses fewer and fewer least significant bits as the maximum life value increases. This would explain the 8 bit truncation I raised previously.
 
Level 13
Joined
Nov 7, 2014
Messages
571
There's a nice trick that nightelf players use before healing with their moonwells.
They drop their items first and then heal which results into more current life
because items (like periapt of vitality +150 life) give +xxx to maximum life but
they also give minimal life and that's based on the current life / maximum life.
So if the unit is at max life both it's current and max life would be increased with 150 but if it's at 50% life it's current life would only go +75 (in the periapt of vitality's case).

I am not sure if that's relevant at all... =)
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Ezekiel12 said:
Quite sure a relation of minimum health to be alive and the units maximum health is either very loosely or doesn't exist at all.

Maximum health is not relevant, only current health is (see below).

Doesn't <= use the same approximation that == uses?

From my tests no, only operator== uses some epsilon.

Doesn't <= use the same approximation that == uses? If so, you could at least throw off some oddball innaccuracy. But seeing it vary from 0.39 to 0.406 is quite hopeless. I can see why Blizzard just uses < 1 in Blizzard.j - they probably didn't understand what was going on, themselves.

Yes, I can imagine that too :D
Although I don't understand how something that simple was made so complicated...

Dr Super Good said:
looking_for_help could you post the hex values of all your determined "Minimum Health" amounts in the table? I have a feeling that the fractional part uses fewer and fewer least significant bits as the maximum life value increases. This would explain the 8 bit truncation I raised previously.

I updated the table with more values (like 0.5 health) and included the hex representation of the minimum values as well (I also updated the vJass code slightly).

I did some further tests and there is at least some good news:

  • The values seem to be independent of the units type. I did tests with heros and non-organic units and the values were always the same as listed in the table.
  • The values only depend from the units current health, not maximum health. Maximum health doesn't seem to have any influence at all.

Dr Super Good said:
How this error is being introduced is a big question.

Yes, that would be really interesting... But I just can't believe they just didn't used some hardcoded constant, thats really annoying :D
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
• The values only depend from the units current health, not maximum health. Maximum health doesn't seem to have any influence at all.
How on earth does this make sense? A unit with "1000000" current life cannot also have "0.40625" current life as that is two different values.

What you are trying to say is that the current life amount being reduced to the minimum life amount results in different amounts of minimum life depending on the order of magnitude of current life.

If you look at the table there it is kind of obvious that there is a loss of precision of current life when going from a very large amount of life to a very small amount of life. This likely means that SetWidgetLife internally works by applying a differential to the units current health (as opposed to a outright set of current health) since as the differential increases in order of magnitude, the resulting minimum life decreases in precision (which the differential explains since those bits are now being used to hold the larger previous current life orders of magnitude).

EDIT Solved it I think, it is working by differential, let me just throw together an example script and write it up.

This script is a pile of garbage but proves it. Yes I have tried it for many entries on your table and it agrees with them. These tests included the minmum case (0.5) and maximum case (1000000) which both agreed with the table.
JASS:
scope MyScope initializer onInit     
    private function onInit takes nothing returns nothing
        //3ED00000
        local integer i = 1053753344
        local real a = 50000.0
        local real b = ReinterpretI2R(i)
        local real c = b - a
        local real e = a + c
        local integer out = ReinterpretR2I(e)
        local integer temp
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, I2S(i))
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, R2S(e))
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, I2S(out))
        if e < 0.405 then
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "DEAD")
        else
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "ALIVE")
        endif
        loop
            set i = i - 1
            set b = ReinterpretI2R(i)
            set c = b - a
            set e = a + c
            set temp = ReinterpretR2I(e)
            exitwhen temp != out
        endloop
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, I2S(i))
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, R2S(e))
        call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, I2S(temp))
        if e < 0.405 then
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "DEAD")
        else
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 60000.0, "ALIVE")
        endif
    endfunction
endscope

These lines are of interest and where the error comes from.
JASS:
    // compute the amount of life gained
    local real c = b - a

JASS:
    // compute the new current life
    local real e = a + c

SetWidgetLife converts the amount specified into an amount of life gained. It is important that it is life gained and not life lost since otherwise the rounding is off (rounds wrong way). This life gained amount is then added to the current life to produce the new current life. If this new current life is less than 0.405 (yes, exactly 0.405 in float, not more, not less) then the unit is dead.

As such the minimum life a unit can have before dying is exactly 0.405 as float. All the strange values above are the result of how SetWidgetLife works by applying a life gain to the unit current life and not that the minimum life constant is changing. If it did not 0 life on death (it does that right?) you could probably confirm these results from GetWidgetLife (SetWidgetLife followed by GetWidgetLife should be returning different values, I have not tried this and I will leave it up to you).
 
Last edited:
Level 14
Joined
Dec 12, 2012
Messages
1,007
What you are trying to say is that the current life amount being reduced to the minimum life amount results in different amounts of minimum life depending on the order of magnitude of current life.

Yes, thats what I meant ;)

If you look at the table there it is kind of obvious that there is a loss of precision of current life when going from a very large amount of life to a very small amount of life. This likely means that SetWidgetLife internally works by applying a differential to the units current health (as opposed to a outright set of current health) since as the differential increases in order of magnitude, the resulting minimum life decreases in precision (which the differential explains since those bits are now being used to hold the larger previous current life orders of magnitude).

You are right, I guess thats it, nice! This also explains the strange rounding errors which depend of the units current health.

SetWidgetLife isn't a real setter in the means that it performs an assignment to the value of the units health but something like this:

JASS:
function SetWidgetLife takes unit u, real newLife returns nothing
    local real health = GetWidgetLife(u)
    local real lifeGained = newLife - health
    call SetWidgetLifeInternal(u, health + lifeGained)
endfunction

where we have no access to the internal, real setter function, which I call SetWidgetLifeInternal here in this example.

If this new current life is less than 0.405 (yes, exactly 0.405 in float, not more, not less) then the unit is dead.

Almost, yes. The correct formula in the Wc3 source must be something like

C++:
if (get_unit_health(u) <= 0.405f)
    kill_unit(u);

so exactly 0.405 in float means already dead. This can be verified like

JASS:
// Shift the units health to a range where accuracy is high enough,
// such that diferential errors are minimized but the unit is still alive
call SetWidgetLife(u, 0.5)
call SetWidgetLife(u, ReinterpretI2R(1053776938)) // alive
call SetWidgetLife(u, ReinterpretI2R(1053776937)) // dead

So the value 1053776938, which is 0.405000031, marks the boarder. The prior float (1053776937) is 0.405000001, which is the closest representable float to 0.405 and means the unit is dead.


So the main problem is how to determine if a unit will die given its current life and a certain amount of damage. Also using SetWidgetLife can not be considered safe when used to set values very close to 0.405. This thread is more productive than I hoped :)


LeP said:
I woulnd't think so. If i allow typecasting from real to int and int to real it would allow this function but also others.
And you don't want other functions with possible errors. Rather a false positive than a false negative.
And yes, these types of errors do happen. I have examples.

I thought this through again and I think I have to disagree since

JASS:
function f1 takes nothing returns integer
    return 1.0
endfunction

function f2 takes nothing returns real
    return 1
endfunction

is perfectly valid Jass? At least it compiles without complaints for me.
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,191
So the value 1053776938, which is 0.405000031, marks the boarder. The prior float (1053776937) is 0.405000001, which is the closest representable float to 0.405 and means the unit is dead.
How does that explain my script working? The values used are from your table of minimum life.

So the main problem is how to determine if a unit will die given its current life and a certain amount of damage. Also using SetWidgetLife can not be considered safe when used to set values very close to 0.405. This thread is more productive than I hoped :)
Should be easy seeing how damage is some form of life delta. SetWidgetLife can be made safe by repeatedly using it with increasing precision.

Have you tried Setting the life property using the appropriate unit natives (the ones used by GUI)? They might function differently, or might be just the same.
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
I thought this through again and I think I have to disagree since

JASS:
function f1 takes nothing returns integer
    return 1.0
endfunction

function f2 takes nothing returns real
    return 1
endfunction

is perfectly valid Jass? At least it compiles without complaints for me.

i dont get what youre trying to tell me
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
i dont get what youre trying to tell me

Your argument was:

I would argue that it is not valid jass. It's only valid by accident. I mean blizz released their patch to fix all the typecasting and just missed this one.

I disagree. Its just like in many other programming languages (like for example C or C++). There its also valid to write something like

C++:
int i = 0.5;
double d = 5;

Jass should be a subset of vJass, so I don't think this behavior should be changed by pjass.

Edit: You can get the closest bigger value from 0 in C++ with using http://en.cppreference.com/w/cpp/numeric/math/nextafter, as it seems, it outputs the same value, but you dont have to iterativelly divide by 2

Yes, but I prefer boost::math::float_next, easier to use ;).
I just used the iterativelly divide by 2 method because I wanted to have an equivalent vJass program to demonstrate differences.

Dr Super Good said:
How does that explain my script working? The values used are from your table of minimum life.

What do you mean? The minimum health value of 0.405000031 is also listed in the table for an initial health of 0.5 and 1.

Dr Super Good said:
SetWidgetLife can be made safe by repeatedly using it with increasing precision.

Actually it should be enough to shift the units life once into a region where its safe to operate like in the code I posted. As a function that could look like this:

JASS:
function SetWidgetLifeSafe takes unit u, real newLife returns nothing
    if newLife < 0.5 then
        call SetWidgetLife(u, 0.5) // Shift the health into max. precision range
    endif
    call SetWidgetLife(u, newLife)
endfunction

That also makes sense since floats have their highest accuracy within the range [-1, 1]. With that function one can directly apply the minimum health to a unit regardless of its current life.

Dr Super Good said:
Have you tried Setting the life property using the appropriate unit natives (the ones used by GUI)? They might function differently, or might be just the same.

Just tested them a bit, seems like they work exactly the same unfortunatly...
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
I disagree. Its just like in many other programming languages (like for example C or C++). There its also valid to write something like

C++:
int i = 0.5;
double d = 5;

Well first of all jass is not c++. Also c++ is not a language i would go to for sane language design.
And you are mixing different things here. real r = 5 is perfectly valid jass and both pjass and wc3 accept this.
integer i = 0.5 is not valid jass and neither pjass nor wc3 accept that.
wc3 had always had special rules for return-values. hence the returnbug.

so code like this, while "valid" jass is in more than 99% of the time not what the user wants
JASS:
function test takes nothing returns real
    return 1123477881
endfunction

function init takes nothing returns nothing
    call BJDebugMsg(R2S(test()))
endfunction

i'm a fan of strong type systems and early errors and warnings. You know, things that make programming actually easier.

Jass should be a subset of vJass, so I don't think this behavior should be changed by pjass.

No, vjass should be a superset of jass since we can actually modify vjass but not jass.

e:
a maybe more illustrative example:
JASS:
constant function health_when_some_abillity_should_kick_in takes nothing returns real
    return 100
endfunction

// this will never fire (assuming a units health can't be negative)
if GetWidgetLife(some_unit) < health_when_some_abillity_should_kick_in() then
endif
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Well first of all jass is not c++. Also c++ is not a language i would go to for sane language design.

True, but C++ also shows a warning here, which would be the best solution here too IMO.

so code like this, while "valid" jass is in more than 99% of the time not what the user wants

This is exactly where the problems begin IMO since the user can just want that. Its ok check for "common made errors", but that should not disable functionality. At the end, that is what warnings were invented for.

No, vjass should be a superset of jass since we can actually modify vjass but not jass.

Whats the difference? At the end it means the same, that every valid Jass program should also be a valid vJass program. And thats not the case but maybe we have different concepts of the terms subset/superset?

What I mean is that

JASS:
function a takes handle h returns unit
	return h
endfunction
Happy debugging!

doesn't compile in vanilla Jass anyway, so we are only talking about the integer/real case, right?
 

LeP

LeP

Level 13
Joined
Feb 13, 2008
Messages
539
True, but C++ also shows a warning here, which would be the best solution here too IMO.
I disagree (i think this is virtually always an error) and i don't even know if jasshelper can handle warnings and wouldn't just treat them as errors.
Also my g++ (4.9.2) only throws an error with some -W flag which i don't like.

This is exactly where the problems begin IMO since the user can just want that. Its ok check for "common made errors", but that should not disable functionality. At the end, that is what warnings were invented for.

That's why i added //! +rb.

Whats the difference? At the end it means the same, that every valid Jass program should also be a valid vJass program. And thats not the case but maybe we have different concepts of the terms subset/superset?

Yes every jass program should be a valid vjass one. But jasshelper does not accept every valid jass program as valid.
I want pjass to accept every valid jass program so i wont budge for jasshelper. I don't realy care if jasshelper accepts invalid jass programs or rejects valid jass. I'd wish someone would fix jasshelper in that way but these things should not bring pjass down. (And if someone fixes jasshelper they could just add pseudo-natives.)



Yes that's in the return case. local integer i = 1.4 does not work.
As i said, wc3 always treated returns different.
 
Status
Not open for further replies.
Top