1 /// NWScript functions and types implemented in D
2 module nwn.nwscript.functions;
3 
4 import std.stdint;
5 import std.array;
6 import std.string;
7 import std.conv: to;
8 import std.meta;
9 
10 import nwn.tlk;
11 static import nwn.gff;
12 static import nwn.fastgff;
13 import nwn.twoda;
14 import nwn.nwscript.constants;
15 import nwn.nwscript.resources;
16 import nwn.nwscript.extensions;
17 
18 public import nwn.types;
19 
20 
21 ///
22 enum OBJECT_INVALID = NWObject.max;
23 
24 
25 ///
26 NWString GetName(ST)(ref ST objectGff) if(isGffStruct!ST) {
27 	mixin(ImportGffLib!ST);
28 	scope resolv = getStrRefResolver();
29 	if(auto fname = "FirstName" in objectGff){
30 		auto firstName = fname.get!GffLocString.resolve(resolv);
31 		auto lastName = objectGff["LastName"].get!GffLocString.resolve(resolv);
32 		if(lastName != "")
33 			return [firstName, lastName].join(" ");
34 		return firstName;
35 	}
36 	if(auto localizedName = "LocalizedName" in objectGff)
37 		return localizedName.get!GffLocString.resolve(resolv);
38 	else if(auto locName = "LocName" in objectGff)
39 		return locName.get!GffLocString.resolve(resolv);
40 	return "";
41 }
42 ///
43 NWString GetFirstName(ST)(ref ST objectGff) if(isGffStruct!ST) {
44 	mixin(ImportGffLib!ST);
45 	if("FirstName" in objectGff)
46 		return objectGff["FirstName"].get!GffLocString.resolve(getStrRefResolver());
47 	else if("LocalizedName" in objectGff)
48 		return objectGff["LocalizedName"].get!GffLocString.resolve(getStrRefResolver());
49 	return "";
50 }
51 ///
52 NWString GetLastName(ST)(ref ST objectGff) if(isGffStruct!ST) {
53 	mixin(ImportGffLib!ST);
54 	if("LastName" in objectGff)
55 		return objectGff["LastName"].get!GffLocString.resolve(getStrRefResolver());
56 	return "";
57 }
58 
59 ///
60 void SetFirstName(ref nwn.gff.GffStruct objectGff, in NWString name){
61 	import nwn.gff;
62 	int32_t language = getStrRefResolver().standartTable.language;
63 	if("FirstName" in objectGff)
64 		objectGff["FirstName"].get!GffLocString.strings = [language * 2: name];
65 	else if("LocalizedName" in objectGff)
66 		objectGff["LocalizedName"].get!GffLocString.strings = [language * 2: name];
67 	else assert(0, "No FirstName GFF field");
68 }
69 ///
70 void SetLastName(ref nwn.gff.GffStruct objectGff, in NWString name){
71 	import nwn.gff;
72 	assert("LastName" in objectGff, "No LastName GFF field");
73 	int32_t language = 0;
74 	if(getStrRefResolver() !is null)
75 		language = getStrRefResolver().standartTable.language;
76 	objectGff["LastName"].get!GffLocString.strings = [language * 2: name];
77 }
78 
79 unittest{
80 	immutable dogeUtc = cast(immutable ubyte[])import("doge.utc");
81 
82 	immutable dialogTlk = cast(immutable ubyte[])import("dialog.tlk");
83 	immutable userTlk = cast(immutable ubyte[])import("user.tlk");
84 	initStrRefResolver(new StrRefResolver(
85 		new Tlk(dialogTlk),
86 		new Tlk(userTlk)
87 	));
88 
89 	{
90 		auto obj = new nwn.gff.Gff(dogeUtc).root;
91 
92 		assert(GetName(obj) == "Doge");
93 
94 		SetFirstName(obj, "Sir doge");
95 		assert(GetName(obj) == "Sir doge");
96 
97 		SetLastName(obj, "the mighty");
98 		assert(GetName(obj) == "Sir doge the mighty");
99 		assert(GetFirstName(obj) == "Sir doge");
100 		assert(GetLastName(obj) == "the mighty");
101 	}
102 	{
103 		auto obj = new nwn.fastgff.FastGff(dogeUtc).root;
104 		assert(GetName(obj) == "Doge");
105 	}
106 
107 }
108 
109 
110 ///
111 bool GetIsItemPropertyValid(NWItemproperty ip){
112 	return ip.type != uint16_t.max;
113 }
114 
115 
116 
117 private template TypeToNWTypeConst(T){
118 	static if     (is(T == NWInt))       enum TypeToNWTypeConst = 1;
119 	else static if(is(T == NWFloat))     enum TypeToNWTypeConst = 2;
120 	else static if(is(T == NWString))    enum TypeToNWTypeConst = 3;
121 	else static if(is(T == NWObject))    enum TypeToNWTypeConst = 4;
122 	else static if(is(T == NWLocation))  enum TypeToNWTypeConst = 5;
123 	else static assert(0, "Invalid type");
124 }
125 
126 
127 private void SetLocal(T)(ref nwn.gff.GffStruct oObject, NWString sVarName, T value){
128 	import nwn.gff;
129 	if("VarTable" in oObject){
130 		foreach(ref var ; oObject["VarTable"].get!GffList){
131 			// TODO: check behaviour when setting two variables with the same name and different types
132 			if(var["Name"].get!GffString == sVarName){
133 				var["Type"].get!GffDWord = TypeToNWTypeConst!T;
134 				static if     (is(T == NWInt))       var["Value"].get!GffInt = value;
135 				else static if(is(T == NWFloat))     var["Value"].get!GffFloat = value;
136 				else static if(is(T == NWString))    var["Value"].get!GffString = value;
137 				else static if(is(T == NWObject))    var["Value"].get!GffDWord = value;
138 				else static if(is(T == NWLocation))  static assert(0, "Not implemented");
139 				else static assert(0, "Invalid type");
140 				return;
141 			}
142 		}
143 	}
144 	else{
145 		oObject["VarTable"] = GffList();
146 	}
147 
148 	auto var = nwn.gff.GffStruct();
149 	var["Name"] = sVarName;
150 	var["Type"] = GffDWord(TypeToNWTypeConst!T);
151 	static if     (is(T == NWInt))       var["Value"] = GffInt(value);
152 	else static if(is(T == NWFloat))     var["Value"] = GffFloat(value);
153 	else static if(is(T == NWString))    var["Value"] = value;
154 	else static if(is(T == NWObject))    var["Value"] = GffDWord(value);
155 	else static if(is(T == NWLocation))  static assert(0, "Not implemented");
156 	else static assert(0, "Invalid type");
157 
158 	oObject["VarTable"].get!GffList ~= var;
159 }
160 
161 ///
162 alias SetLocalInt = SetLocal!NWInt;
163 ///
164 alias SetLocalFloat = SetLocal!NWFloat;
165 ///
166 alias SetLocalString = SetLocal!NWString;
167 ///
168 alias SetLocalObject = SetLocal!NWObject;
169 ///
170 //alias SetLocalLocation = SetLocal!NWLocation;
171 
172 
173 private T GetLocal(T, ST)(ref ST oObject, NWString sVarName) if(isGffStruct!ST) {
174 	mixin(ImportGffLib!ST);
175 	if("VarTable" in oObject){
176 		foreach(var ; oObject["VarTable"].get!GffList){
177 			if(var["Name"].get!GffString == sVarName && var["Type"].get!GffDWord == TypeToNWTypeConst!T){
178 				static if     (is(T == NWInt))       return var["Value"].get!GffInt;
179 				else static if(is(T == NWFloat))     return var["Value"].get!GffFloat;
180 				else static if(is(T == NWString))    return var["Value"].get!GffString;
181 				else static if(is(T == NWObject))    return var["Value"].get!GffDWord;
182 				else static if(is(T == NWLocation))  static assert(0, "Not implemented");
183 				else static assert(0, "Invalid type");
184 			}
185 		}
186 	}
187 	return NWInitValue!T;
188 }
189 ///
190 NWInt GetLocalInt(ST)(ref ST oObject, NWString sVarName){ return GetLocal!NWInt(oObject, sVarName); };
191 ///
192 NWFloat GetLocalFloat(ST)(ref ST oObject, NWString sVarName){ return GetLocal!NWFloat(oObject, sVarName); };
193 ///
194 NWString GetLocalString(ST)(ref ST oObject, NWString sVarName){ return GetLocal!NWString(oObject, sVarName); };
195 ///
196 NWObject GetLocalObject(ST)(ref ST oObject, NWString sVarName){ return GetLocal!NWObject(oObject, sVarName); };
197 ///
198 //alias GetLocalLocation = GetLocal!NWLocation;
199 
200 
201 unittest{
202 	import std.math;
203 	immutable dogeUtc = cast(immutable ubyte[])import("doge.utc");
204 	{
205 		auto obj = new nwn.gff.Gff(dogeUtc).root;
206 
207 		SetLocalInt(obj, "TestInt", 5);
208 		SetLocalFloat(obj, "TestFloat", 5.3);
209 		SetLocalString(obj, "TestString", "Hello");
210 
211 		assert(GetLocalInt(obj, "TestInt") == 5);
212 
213 		assert(approxEqual(GetLocalFloat(obj, "TestFloat"), 5.3f));
214 		assert(GetLocalString(obj, "TestString") == "Hello");
215 		assert(GetLocalString(obj, "yolooo") == "");
216 		assert(GetLocalObject(obj, "TestInt") == OBJECT_INVALID);
217 
218 		DeleteLocalInt(obj, "TestString");
219 		assert(GetLocalString(obj, "TestString") == "Hello");
220 		DeleteLocalString(obj, "TestString");
221 		assert(GetLocalString(obj, "TestString") == "");
222 	}
223 	{
224 		auto obj = new nwn.fastgff.FastGff(dogeUtc).root;
225 		assert(GetLocalInt(obj, "aaabbb") == 0);
226 	}
227 
228 }
229 
230 private void DeleteLocal(T)(ref nwn.gff.GffStruct oObject, NWString sVarName){
231 	import nwn.gff;
232 	import std.algorithm: remove;
233 	if("VarTable" in oObject){
234 		foreach(i, ref var ; oObject["VarTable"].get!GffList){
235 			if(var["Name"].get!GffString == sVarName && var["Type"].get!GffDWord == TypeToNWTypeConst!T){
236 				oObject["VarTable"].get!GffList.children = oObject["VarTable"].get!GffList.children.remove(i);
237 				return;
238 			}
239 		}
240 	}
241 }
242 ///
243 alias DeleteLocalInt = DeleteLocal!NWInt;
244 ///
245 alias DeleteLocalFloat = DeleteLocal!NWFloat;
246 ///
247 alias DeleteLocalString = DeleteLocal!NWString;
248 ///
249 alias DeleteLocalObject = DeleteLocal!NWObject;
250 ///
251 alias DeleteLocalLocation = DeleteLocal!NWLocation;
252 
253 
254 
255 ///
256 NWInt GetItemPropertyType(NWItemproperty ip){
257 	return ip.type;
258 }
259 ///
260 NWInt GetItemPropertySubType(NWItemproperty ip){
261 	return ip.subType;
262 }
263 ///
264 NWInt GetItemPropertyCostTableValue(NWItemproperty ip){
265 	return ip.costValue;
266 }
267 ///
268 NWInt GetItemPropertyParam1Value(NWItemproperty ip){
269 	return ip.p1;
270 }
271 
272 
273 private static nwn.gff.GffStruct[] currentGetItemPropertyRangeGff;
274 private static const(nwn.fastgff.GffStruct)[] currentGetItemPropertyRangeFastGff;
275 
276 ///
277 NWItemproperty GetFirstItemProperty(ST)(ref ST oItem) if(isGffStruct!ST) {
278 	mixin(ImportGffLib!ST);
279 	static if(is(ST: nwn.gff.GffStruct))
280 		alias currentGetItemPropertyRange = currentGetItemPropertyRangeGff;
281 	else
282 		alias currentGetItemPropertyRange = currentGetItemPropertyRangeFastGff;
283 
284 	if(auto val = "PropertiesList" in oItem)
285 		currentGetItemPropertyRange = val.get!GffList;
286 	else
287 		return NWInitValue!NWItemproperty;
288 
289 	if(currentGetItemPropertyRange.empty)
290 		return NWInitValue!NWItemproperty;
291 
292 	return currentGetItemPropertyRange.front.toNWItemproperty;
293 }
294 
295 ///
296 NWItemproperty GetNextItemProperty(ST)(ref ST oItem) if(isGffStruct!ST) {
297 	mixin(ImportGffLib!ST);
298 	static if(is(ST: nwn.gff.GffStruct))
299 		alias currentGetItemPropertyRange = currentGetItemPropertyRangeGff;
300 	else
301 		alias currentGetItemPropertyRange = currentGetItemPropertyRangeFastGff;
302 
303 	if(currentGetItemPropertyRange.empty)
304 		return NWInitValue!NWItemproperty;
305 
306 	currentGetItemPropertyRange.popFront();
307 
308 	if(currentGetItemPropertyRange.empty)
309 		return NWInitValue!NWItemproperty;
310 
311 	return currentGetItemPropertyRange.front.toNWItemproperty;
312 }
313 
314 unittest{
315 	immutable itemData = cast(immutable ubyte[])import("ceinturedeschevaliersquidisentni.uti");
316 
317 	foreach(GFF ; AliasSeq!(nwn.gff.Gff, nwn.fastgff.FastGff)){
318 		auto item = new GFF(itemData).root;
319 
320 		size_t index = 0;
321 		auto ip = GetFirstItemProperty(item);
322 		while(GetIsItemPropertyValid(ip)){
323 			assert(ip == item["PropertiesList"][index].toNWItemproperty, "Mismatch on ip " ~ index.to!string);
324 			index++;
325 			ip = GetNextItemProperty(item);
326 		}
327 		assert(index == 2);
328 	}
329 }
330 
331 ///
332 NWInt GetBaseItemType(ST)(in ST oItem) if(isGffStruct!ST) {
333 	mixin(ImportGffLib!ST);
334 	return oItem["BaseItem"].to!NWInt;
335 }
336 
337 ///
338 void AddItemProperty(NWInt nDurationType, NWItemproperty ipProperty, ref nwn.gff.GffStruct oItem, NWFloat fDuration=0.0f){
339 	import nwn.gff;
340 	assert(nDurationType == DURATION_TYPE_PERMANENT, "Only permanent is supported");
341 	if("PropertiesList" !in oItem){
342 		oItem["PropertiesList"] = GffList();
343 	}
344 
345 	static nwn.gff.GffStruct buildPropStruct(in NWItemproperty iprp){
346 		nwn.gff.GffStruct ret;
347 		with(ret){
348 			assert(iprp.type < getTwoDA("itempropdef").rows);
349 
350 			ret["PropertyName"] = GffWord(iprp.type);
351 
352 			immutable subTypeTable = getTwoDA("itempropdef").get("SubTypeResRef", iprp.type);
353 			if(subTypeTable is null)
354 				assert(iprp.subType==uint16_t.max || iprp.subType == 0, format!"subType=%d is pointing to non-existent SubTypeTable (for iprp=%s)"(iprp.subType, iprp));
355 			else
356 				assert(iprp.subType!=uint16_t.max, "iprp.subType must be defined");
357 
358 			ret["Subtype"] = GffWord(iprp.subType);
359 
360 			string costTableResRef = getTwoDA("itempropdef").get("CostTableResRef", iprp.type);
361 			if(costTableResRef is null)
362 				assert(iprp.costValue==uint16_t.max || iprp.costValue == 0, format!"costValue=%d is pointing to non-existent CostTableResRef (for iprp=%s)"(iprp.costValue, iprp));
363 			else
364 				assert(iprp.costValue!=uint16_t.max, "iprp.costValue must be defined");
365 
366 			ret["CostTable"] = GffByte(costTableResRef !is null? costTableResRef.to!ubyte : 0);
367 			ret["CostValue"] = GffWord(iprp.costValue);
368 
369 			immutable paramTableResRef = getTwoDA("itempropdef").get("Param1ResRef", iprp.type);
370 			if(paramTableResRef !is null){
371 				assert(iprp.p1!=uint8_t.max, "iprp.p1 must be defined for IPRP " ~ iprp.to!string);
372 				ret["Param1"] = GffByte(paramTableResRef.to!ubyte);
373 				ret["Param1Value"] = GffByte(iprp.p1);
374 			}
375 			else{
376 				assert(iprp.p1==uint8_t.max || iprp.p1==0, format!"iprp.p1 pointing to non-existent Param1ResRef (for iprp=%s)"(iprp.to!string));
377 				ret["Param1"] = GffByte(0);
378 				ret["Param1Value"] = GffByte(0);
379 			}
380 
381 			//ret["Param2"] = GffByte(0);
382 			//ret["Param2Value"] = GffByte(0);
383 			ret["ChanceAppear"] = GffByte(100);
384 			ret["UsesPerDay"] = GffByte(255);
385 			ret["Useable"] = GffByte(1);
386 		}
387 		return ret;
388 	}
389 
390 	oItem["PropertiesList"].get!GffList ~= buildPropStruct(ipProperty);
391 }
392 
393 ///
394 void RemoveItemProperty(ref nwn.gff.GffStruct oItem, NWItemproperty ipProperty){
395 	import nwn.gff;
396 	import std.algorithm;
397 
398 	auto subTypeTable = getTwoDA("itempropdef").get("SubTypeResRef", ipProperty.type);
399 	bool hasSubType = subTypeTable !is null && subTypeTable != "";
400 	auto costValueTable = getTwoDA("itempropdef").get("CostTableResRef", ipProperty.type);
401 	bool hasCostValue = costValueTable !is null && costValueTable != "" && costValueTable.to!int > 0;
402 	auto param1Table = getTwoDA("itempropdef").get("Param1ResRef", ipProperty.type);
403 	bool hasParam1 = param1Table !is null && param1Table != "" && param1Table.to!int >= 0;
404 
405 	oItem["PropertiesList"].get!GffList.children = oItem["PropertiesList"].get!GffList.children.remove!((node){
406 		auto ip = node.toNWItemproperty;
407 
408 		if(ip.type != ipProperty.type)
409 			return false;
410 		if(hasSubType && ip.subType != ipProperty.subType)
411 			return false;
412 		if(hasCostValue && ip.costValue != ipProperty.costValue)
413 			return false;
414 		if(hasParam1 && ip.p1 != ipProperty.p1)
415 			return false;
416 		return true;
417 	});
418 }
419 
420 unittest{
421 	import nwn.gff;
422 	immutable itemData = cast(immutable ubyte[])import("ceinturedeschevaliersquidisentni.uti");
423 	auto item = new Gff(itemData).root;
424 
425 	initTwoDAPaths(["unittest/2da"]);
426 
427 	auto ipRegen10 = NWItemproperty(51, 0, 10);
428 
429 	AddItemProperty(DURATION_TYPE_PERMANENT, ipRegen10, item);
430 	assert(item["PropertiesList"].get!GffList.length == 3);
431 
432 	auto addedProp = item["PropertiesList"].get!GffList[$-1];
433 	assert(addedProp["PropertyName"].get!GffWord == 51);
434 	assert(addedProp["Subtype"].get!GffWord == 0);
435 	assert(addedProp["CostValue"].get!GffWord == 10);
436 	assert(addedProp["CostTable"].get!GffByte == 2);
437 	assert(addedProp["Param1"].get!GffByte == 0);
438 	assert(addedProp["Param1Value"].get!GffByte == 0);
439 	//assert(addedProp["Param2"].get!GffByte == 0);
440 	//assert(addedProp["Param2Value"].get!GffByte == 0);
441 	assert(addedProp["ChanceAppear"].get!GffByte == 100);
442 	assert(addedProp["UsesPerDay"].get!GffByte == 255);
443 	assert(addedProp["Useable"].get!GffByte == 1);
444 
445 	auto ipRegen5 = NWItemproperty(51, 0, 5);
446 	RemoveItemProperty(item, ipRegen5);
447 	assert(item["PropertiesList"].get!GffList.length == 3);
448 
449 	RemoveItemProperty(item, ipRegen10);
450 	assert(item["PropertiesList"].get!GffList.length == 2);
451 }
452 
453 ///
454 NWInt GetItemPropertyDurationType(NWItemproperty iprp){
455 	// duration is not stored in the struct
456 	return DURATION_TYPE_PERMANENT;
457 }
458 
459 ///
460 NWInt GetItemPropertyCostTable(NWItemproperty iProp){
461 	return getTwoDA("itempropdef").get!NWInt("CostTableResRef", iProp.type, -1);
462 }
463 
464 
465 
466 ///
467 NWString Get2DAString(NWString s2DA, NWString sColumn, NWInt nRow){
468 	return getTwoDA(s2DA).get!NWString(sColumn, nRow, "");
469 }
470 
471 
472 ///
473 NWString IntToString(NWInt nInteger){
474 	return nInteger.to!NWString;
475 }
476 ///
477 NWFloat IntToFloat(NWInt nInteger){
478 	return nInteger.to!NWFloat;
479 }
480 ///
481 NWInt FloatToInt(NWFloat fFloat){
482 	return fFloat.to!NWInt;
483 }
484 ///
485 NWString FloatToString(NWFloat fFloat, NWInt nWidth=18, NWInt nDecimals=9){
486 	return format("%" ~ nWidth.to!string ~ "." ~ nDecimals.to!string ~ "f", fFloat);
487 }
488 ///
489 NWInt StringToInt(NWString sNumber){
490 	return sNumber.to!NWInt;
491 }
492 ///
493 NWFloat StringToFloat(NWString sNumber){
494 	return sNumber.to!NWFloat;
495 }