forked from Courseplay/courseplay
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfields.lua
More file actions
361 lines (314 loc) · 12.7 KB
/
fields.lua
File metadata and controls
361 lines (314 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
-- Field scanner
-- original algorithm by upsidedown, 24 Nov 2013 / incorporation into Courseplay by Jakob Tischler, 27 Nov 2013
function courseplay.fields:setUpFieldsIngameData()
--self = courseplay.fields
self:dbg("call setUpIngameData()", 'scan');
self.fieldChannels = { g_currentMission.cultivatorChannel, g_currentMission.ploughChannel, g_currentMission.sowingChannel, g_currentMission.sowingWidthChannel };
self.lastChannel = g_currentMission.cultivatorChannel;
self.ingameDataSetUp = true;
end;
function courseplay.fields:setAllFieldEdges()
--self = courseplay.fields
self.curFieldScanIndex = self.curFieldScanIndex + 1;
if self.curFieldScanIndex > g_currentMission.fieldDefinitionBase.numberOfFields then
self.allFieldsScanned = true;
self.numAvailableFields = table.maxn(self.fieldData);
self:dbg(string.format('%d fields scanned - done', self.curFieldScanIndex - 1), 'scan');
return;
end;
self:dbg(string.rep('-', 50) .. '\ncall setAllFieldEdges() START (curFieldScandIndex=' .. tostring(self.curFieldScanIndex) .. ')', 'scan');
local scanStep = 5;
local maxN = 2000;
local numDirectionTries = 10;
local fieldDef = g_currentMission.fieldDefinitionBase.fieldDefs[self.curFieldScanIndex];
if fieldDef ~= nil then
local fieldNum = fieldDef.fieldNumber;
local initObject = fieldDef.fieldMapIndicator;
local x,_,z = getWorldTranslation(initObject);
if fieldNum and initObject and x and z then
local isField = courseplay:is_field(x, z, 0.1, 0.1);
self:dbg(string.format("fieldDef %d (fieldNum=%d): x,z=%.1f,%.1f, isField=%s", self.curFieldScanIndex, fieldNum, x, z, tostring(isField)), 'scan');
if isField then
self:setSingleFieldEdgePath(initObject, x, z, scanStep, maxN, numDirectionTries, fieldNum, false, 'scan');
end;
self.numAvailableFields = table.maxn(self.fieldData);
else
self:dbg(string.format('fieldDef %s: fieldNum=%s, initObject=%s, x,z=%s,%s -> cancel', tostring(self.curFieldScanIndex), tostring(fieldNum), tostring(initObject), tostring(x), tostring(z)), 'scan');
end;
else
self:dbg(string.format('fieldDef %s is nil', tostring(self.curFieldScanIndex)), 'scan');
end;
--Debug
if self.debugScannedFields then
--self:dbg(tableShow(courseplay.fields.fieldData, "fieldData"), 'scan');
end;
self:dbg('setAllFieldEdges() END\n' .. string.rep('-', 50), 'scan');
end;
function courseplay.fields:getSingleFieldEdge(initObject, scanStep, maxN, randomDir)
--self = courseplay.fields
scanStep = scanStep or 5;
maxN = maxN or math.floor(10000/scanStep); --10 km circumference should be enough. otherwise state maxN as parameter
randomDir = randomDir or false;
local x0,_,z0 = getWorldTranslation(initObject);
local x,y,z = localDirectionToWorld(initObject, 0, 0, 1);
if randomDir then
math.randomseed(g_currentMission.time)
x = 2*math.random()-1;
z = 2*math.random()-1;
end;
local length = Utils.vector2Length(x,z);
local dX = x/length;
local dZ = z/length;
local isField = courseplay:is_field(x0, z0, 0.1, 0.1);
local coordinates, xValues, zValues = {}, {}, {};
if isField then
local dis = 0;
local isSearchPointOnField = true;
local stepA = 1;
local stepB = -.05;
local xx, zz;
while isSearchPointOnField do --search fast forward (1m steps)
dis = dis + stepA;
xx = x0 + dis*dX;
zz = z0 + dis*dZ;
isSearchPointOnField = courseplay:is_field(xx, zz, 0.1, 0.1);
if math.abs(dis) > 2000 then
break;
end;
end;
self:dbg(string.format('\tfound first point past field border: xx=%s, zz=%s, dis=%s', tostring(xx), tostring(zz), tostring(dis)), 'scan');
while not isSearchPointOnField do --then backtrace in small 5cm steps
dis = dis + stepB;
xx = x0 + dis*dX;
zz = z0 + dis*dZ;
isSearchPointOnField = courseplay:is_field(xx, zz, 0.1, 0.1);
end;
self:dbg(string.format('\ttrace back, border point found: xx=%s, zz=%s, dis=%s', tostring(xx), tostring(zz), tostring(dis)), 'scan');
--now we have a point very close to the field boundary but definitely inside :)
local tg = createTransformGroup("scanner");
link(getRootNode(), tg);
local probe1 = createTransformGroup("probe1");
link(tg, probe1);
setTranslation(probe1,-scanStep,0,0);
--rotate 90° against initObject --unnecessary, as it's already been rotated in the above line (x coordinate)
--local _,ry,_ = getWorldRotation(initObject);
--setRotation(tg,0,ry,0)
if randomDir then
rotate(tg,0,2*math.pi*math.random(),0)
else
rotate(tg,0,math.pi/2,0) --turn side
end;
self:dbg(string.format('\trotate tg'), 'scan');
-- local dirX = dZ;
-- local dirZ = -dirX; --90° of search direction;
local px, pz = xx, zz;
while #coordinates < maxN do
setTranslation(tg,px,y,pz)
setTranslation(probe1,-scanStep,0,0); --reset scanstep (already rotated)
--local rx,ry,ry = getRotation(probe1);
px,_,pz = getWorldTranslation(probe1);
local rotAngle = 0.1;
local turnSign = 1.0;
local return2field = not courseplay:is_field(px, pz, 0.1, 0.1); --there is NO guarantee that probe1 (px,pz) is in field just because tg is!!!
while courseplay:is_field(px, pz, 0.1, 0.1) or return2field do
rotate(tg,0,rotAngle*turnSign,0)
rotAngle = rotAngle*1.05;
--rotAngle = rotAngle + 0.1; --alternative for performance tuning, don't know which one is better
turnSign = -turnSign;
px,_,pz = getWorldTranslation(probe1);
if return2field then
if courseplay:is_field(px, pz, 0.1, 0.1) then
return2field = false;
end;
end;
end;
local cnt, maxcnt = 0, 0;
while not courseplay:is_field(px, pz, 0.1, 0.1) do
rotate(tg,0,0.01*turnSign,0)
px,_,pz = getWorldTranslation(probe1);
--self:dbg('\t\trotate back', 'scan');
cnt = cnt+1;
if cnt > 2*math.pi/.01 then
translate(probe1,.5*scanStep,0,0);
cnt = 0;
maxcnt = maxcnt + 1;
if maxcnt > 2 then
break;
end;
end;
end;
if not courseplay:is_field(px, pz, 0.1, 0.1) then
self:dbg('\tlost point', 'scan');
break;
end;
--print("found")
table.insert(coordinates, { cx = px, cy = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, px, 1, pz), cz = pz });
table.insert(xValues, px);
table.insert(zValues, pz);
self:dbg(string.format('\tpoint %d set: cx=%s, cz=%s', #coordinates, tostring(px), tostring(pz)), 'scan');
if #coordinates > 5 then
local dis0 = Utils.vector2Length(px-coordinates[1].cx, pz-coordinates[1].cz)
--print(dis0)
if dis0 < scanStep*1.25 then --otherwise start and end points can be very close together
self:dbg(string.format('\tdistance to first point [%.2f] < scanStep*1.25 [%.2f] -> break', dis0, scanStep * 1.25), 'scan');
break;
end;
end;
end;
unlink(probe1);
unlink(tg);
delete(probe1);
delete(tg);
if coordinates and xValues and zValues then
self:dbg(string.format('\tget: #coordinates=%d, #xValues=%d, #zValues=%d', #coordinates, #xValues, #zValues), 'scan');
else
self:dbg(string.format('\tget: coordinates=%s, xValues=%s, zValues=%s', tostring(coordinates), tostring(xValues), tostring(zValues)), 'scan');
end;
return coordinates, xValues, zValues;
end;
end;
function courseplay.fields:setSingleFieldEdgePath(initObject, initX, initZ, scanStep, maxN, numDirectionTries, fieldNum, returnPoints, dbgType)
for try=1,numDirectionTries do
local edgePoints, xValues, zValues = self:getSingleFieldEdge(initObject, scanStep, maxN, try > 1);
if edgePoints then
local numEdgePoints = #edgePoints;
--self:dbg(string.format("\ttry %d: %d edge points found, #xValues=%s, #zValues=%s", try, numEdgePoints, tostring(#xValues), tostring(#zValues)), dbgType);
if numEdgePoints >= 30 then
self:dbg(string.format("\ttry %d: %d edge points found", try, numEdgePoints), dbgType);
if courseplay:pointInPolygon_v2(edgePoints, xValues, zValues, initX, initZ) then
self:dbg('\t\tinitObject is in poly --> valid, no retry', dbgType);
if returnPoints then
return edgePoints;
end;
if fieldNum then
if self.fieldData[fieldNum] == nil then
self.fieldData[fieldNum] = {
fieldNum = fieldNum;
points = edgePoints;
numPoints = #edgePoints;
dimensions = {
minX = math.min(unpack(xValues));
maxX = math.max(unpack(xValues));
minZ = math.min(unpack(zValues));
maxZ = math.max(unpack(zValues));
};
name = string.format("%s %d", courseplay:loc('COURSEPLAY_FIELD'), fieldNum);
};
self:dbg(string.format('\t\tcourseplay.fields.fieldData[%d] == nil => set as .fieldData[%d], break', fieldNum, fieldNum), dbgType);
else
self:dbg(string.format('\t\tcourseplay.fields.fieldData[%d] ~= nil => ignore scan, break', fieldNum), dbgType);
end;
break;
end;
else
self:dbg(string.format('\t\tinitObject is NOT in poly --> invalid, retry=%s', tostring(try<numDirectionTries)), dbgType);
end;
else
self:dbg(string.format("\ttry %d: %d edge points found --> invalid, retry=%s", try, numEdgePoints, tostring(try<numDirectionTries)), dbgType);
end;
else
self:dbg(string.format('\ttry %d: edgePoints is nil -> invalid, retry=%s', try, tostring(try<numDirectionTries)), dbgType);
end;
end;
if returnPoints then
return nil;
end;
end;
--XML SAVING
function courseplay.fields:openOrCreateXML(forceCreation)
--self = courseplay.fields
-- returns the file if success, nil else
forceCreation = forceCreation or false;
local xmlFile;
local savegame = g_careerScreen.savegames[g_careerScreen.selectedIndex];
if savegame ~= nil then
local filePath = savegame.savegameDirectory .. "/courseplayFields.xml"
if fileExists(filePath) and (not forceCreation) then
xmlFile = loadXMLFile("fieldsFile", filePath);
else
xmlFile = createXMLFile("fieldsFile", filePath, 'XML');
end;
else
--this is a problem... xmlFile stays nil
end;
return xmlFile;
end;
function courseplay.fields:saveAllCustomFields()
--self = courseplay.fields
-- saves fields to xml-file
-- opening the file with io.open will delete its content...
if g_server ~= nil then
local savegame = g_careerScreen.savegames[g_careerScreen.selectedIndex];
if savegame ~= nil and self.numAvailableFields > 0 then
local file = io.open(savegame.savegameDirectory .. '/courseplayFields.xml', 'w');
if file ~= nil then
file:write('<?xml version="1.0" encoding="utf-8" standalone="no" ?>\n<XML>\n');
file:write('\t<fields>\n')
for i,fieldData in pairs(self.fieldData) do
if fieldData.isCustom then
file:write(string.format('\t\t<field fieldNum="%d" numPoints="%d">\n', fieldData.fieldNum, fieldData.numPoints));
for j,point in ipairs(fieldData.points) do
file:write(string.format('\t\t\t<point%d pos="%.2f %.2f %.2f" />\n', j, point.cx, point.cy, point.cz));
end;
file:write('\t\t</field>\n');
end;
end;
file:write('\t</fields>\n</XML>');
file:close();
else
print("Error: Courseplay's custom fields could not be saved to " .. tostring(savegame.savegameDirectory) .. "/courseplayFields.xml");
end;
end;
end;
end;
--XML LOADING
function courseplay.fields:loadAllCustomFields()
--self = courseplay.fields
if g_server ~= nil then
local savegame = g_careerScreen.savegames[g_careerScreen.selectedIndex];
if savegame ~= nil then
local filePath = savegame.savegameDirectory .. "/courseplayFields.xml"
if fileExists(filePath) then
local xmlFile = loadXMLFile("fieldsFile", filePath);
local i = 0;
while true do
local key = string.format('XML.fields.field(%d)', i);
if not hasXMLProperty(xmlFile, key) then
break;
end;
local fieldNum = getXMLInt(xmlFile, key .. '#fieldNum');
local numPoints = getXMLInt(xmlFile, key .. '#numPoints');
if fieldNum and numPoints and numPoints > 0 then
local fieldData = {
fieldNum = fieldNum;
points = {};
numPoints = numPoints;
name = string.format("%s %d (%s)", courseplay:loc('COURSEPLAY_FIELD'), fieldNum, courseplay:loc('COURSEPLAY_USER'));
isCustom = true;
};
for j=1,numPoints do
local pointKey = key .. '.point' .. j;
if hasXMLProperty(xmlFile, pointKey) then
local x,y,z = Utils.getVectorFromString(getXMLString(xmlFile, pointKey .. '#pos'));
if x and y and z then
table.insert(fieldData.points, { cx = x, cy = y, cz = z });
end;
end;
end;
self.fieldData[fieldNum] = fieldData;
if self.debugCustomLoadedFields then
self:dbg(tableShow(fieldData, 'fieldData[' .. fieldNum .. ']'), 'customLoad');
end;
self.numAvailableFields = table.maxn(self.fieldData);
end;
i = i + 1;
end;
end;
end;
end;
end;
function courseplay.fields:dbg(str, debugType)
if (debugType == 'scan' and self.debugScannedFields) or (debugType == 'customLoad' and self.debugCustomLoadedFields) then
print(tostring(str));
end;
end;