diff options
Diffstat (limited to 'src/main/smp')
| -rw-r--r-- | src/main/smp/sys.smp | 4 | ||||
| -rw-r--r-- | src/main/smp/sys/audio.smp | 29 | ||||
| -rw-r--r-- | src/main/smp/sys/draw.smp | 18 | ||||
| -rw-r--r-- | src/main/smp/sys/draw/Button.smp | 41 | ||||
| -rw-r--r-- | src/main/smp/sys/draw/Canvas.smp | 176 | ||||
| -rw-r--r-- | src/main/smp/sys/gfx.smp | 42 | ||||
| -rw-r--r-- | src/main/smp/sys/text.smp | 5 | ||||
| -rw-r--r-- | src/main/smp/sys/time.smp | 191 | ||||
| -rw-r--r-- | src/main/smp/sys/time/DateTime.smp | 11 | ||||
| -rw-r--r-- | src/main/smp/sys/time/Timers.smp | 55 |
10 files changed, 572 insertions, 0 deletions
diff --git a/src/main/smp/sys.smp b/src/main/smp/sys.smp index 1201315b..296635ae 100644 --- a/src/main/smp/sys.smp +++ b/src/main/smp/sys.smp @@ -2,12 +2,16 @@ // top-level default standard library import val adt = adt; // Abstract Data Types +val audio = audio; // Optional background sound (miniaudio) val ary = ary; // Array utilities val aryi64 = aryi64; // Variable sized array of i64 val aryu8 = aryu8; // Variable sized array of u8 val char = char; // Character utilities val io = io; // IO library +val gfx = gfx; // Optional pixel window (MiniFB) +val draw = draw; // Software raster drawing in Simple val text = text; // Typed text conversions +val time = time; // Clocks, calendar text, and cooperative timers val libc = libc; // libc FFI bindings val rt = rt; // Run Time support val scan = scan; // A simple string parser diff --git a/src/main/smp/sys/audio.smp b/src/main/smp/sys/audio.smp new file mode 100644 index 00000000..89e6dfa9 --- /dev/null +++ b/src/main/smp/sys/audio.smp @@ -0,0 +1,29 @@ +// Optional background audio. All API calls belong on the game/main thread. +int OK = 0; +int ERROR = -1; +int BOOP = 1; +int WIN = 2; +int LOSE = 3; +int RATE = 48000; +{ i32 } simple_audio_open = "C"; +{ i64 u32 -> i32 } simple_audio_load = "C"; +{ i32 -> i32 } simple_audio_play = "C"; +{ i32 } simple_audio_stop = "C"; +{ i32 } simple_audio_playing = "C"; +{ i32 } simple_audio_close = "C"; +// Missing/disabled backend returns ERROR quietly; the game may remain silent. +val open = { -> simple_audio_open(); }; +// Mono signed 16-bit PCM at RATE, up to ten seconds. Copies into native memory. +// Load at startup: up to 28 custom handles, valid until close, then reload. +val load = { i16[~] samples -> + i64 ptr = samples; + simple_audio_load(ptr,samples#); +}; +// Starts/restarts immediately, replacing the previous sound; never waits for it. +val play = { int sound -> simple_audio_play(sound); }; +val boop = { -> simple_audio_play(BOOP); }; +val win = { -> simple_audio_play(WIN); }; +val lose = { -> simple_audio_play(LOSE); }; +val stop = { -> simple_audio_stop(); }; +val playing = { -> simple_audio_playing(); }; +val close = { -> simple_audio_close(); }; diff --git a/src/main/smp/sys/draw.smp b/src/main/smp/sys/draw.smp new file mode 100644 index 00000000..0e4a27d4 --- /dev/null +++ b/src/main/smp/sys/draw.smp @@ -0,0 +1,18 @@ +// Software raster drawing. No window or native calls; pixels are packed RGB (0x00RRGGBB). +u32 BLACK = 0x000000; +u32 WHITE = 0xffffff; +u32 RED = 0xe05252; +u32 GREEN = 0x48b88c; +u32 BLUE = 0x558ee6; + +// Components are clamped to bytes rather than wrapping. +val rgb = { int r, int g, int b -> + r = r<0 ? 0 : r>255 ? 255 : r; + g = g<0 ? 0 : g>255 ? 255 : g; + b = b<0 ? 0 : b>255 ? 255 : b; + u32 color = (r<<16) | (g<<8) | b; + return color; +}; + +val Canvas = Canvas; // Pixel canvas and drawing methods +val Button = Button; // Pointer state and rendering for a rectangular button diff --git a/src/main/smp/sys/draw/Button.smp b/src/main/smp/sys/draw/Button.smp new file mode 100644 index 00000000..edf01119 --- /dev/null +++ b/src/main/smp/sys/draw/Button.smp @@ -0,0 +1,41 @@ +// Feed pointer state once per event-loop iteration. Activate on release inside, +// only after a press that began inside. No native/window dependency. +struct Button { + int x, y, width, height; + u8[~] !label; + bool !enabled=true, !hovered=false, !pressed=false; + bool !_held=false, !_armed=false; + new Button = { int px, int py, int w, int h, u8[~] text -> + x=px; y=py; width=w; height=h; label=text; + }; + val contains = { i64 px, i64 py -> + if( width<=0 || height<=0 ) return false; + i64 right=x, bottom=y; right+=width; bottom+=height; + if( px<x || py<y || px>=right || py>=bottom ) return false; + return true; + }; + val update = { i64 px, i64 py, bool down -> + hovered=false; + if( enabled ) hovered=contains(px,py); + if( !enabled ) _armed=false; + if( down && !_held ) _armed=hovered; + var clicked=false; + if( !down ) if( _held ) if( _armed ) if( hovered ) clicked=true; + if( !down ) _armed=false; + _held=down; + pressed=false; + if( down ) if( _armed ) if( hovered ) pressed=true; + return clicked; + }; + val draw = { !sys.draw.Canvas screen, int scale, u32 foreground, u32 background, u32 active -> + screen.fillRect(x,y,width,height,enabled && (hovered || pressed) ? active : background); + screen.rect(x,y,width,height,2,enabled ? foreground : active); + if( scale>0 ) { + i64 textWidth=label#; textWidth=(textWidth*4-1)*scale; + if( textWidth<=width-8 && 5*scale<=height-8 ) + screen.text(x+(width-textWidth)/2,y+(height-5*scale)/2+(pressed ? 1 : 0), + label,scale,enabled ? foreground : active); + } + return self; + }; +}; diff --git a/src/main/smp/sys/draw/Canvas.smp b/src/main/smp/sys/draw/Canvas.smp new file mode 100644 index 00000000..8c01b4b3 --- /dev/null +++ b/src/main/smp/sys/draw/Canvas.smp @@ -0,0 +1,176 @@ +struct Canvas { + u32 width; + u32 height; + u32[] pixels; + + new Canvas = { int w, int h -> + i64 count = w; + count *= h; + if( w<=0 || h<=0 || count>2147483647 ) libc._exit(1); + width = w; + height = h; + pixels = new u32[count]; + }; + + val clear = { u32 color -> + for( int i=0; i<pixels#; i++ ) pixels[i] = color; + return self; + }; + + val pixel = { i64 x, i64 y, u32 color -> + if( 0<=x<width && 0<=y<height ) pixels[y*width+x] = color; + return self; + }; + + // Integer coordinates, top-left origin. Rectangle extents are half-open. + val fillRect = { i64 x, i64 y, i64 w, i64 h, u32 color -> + if( w<=0 || h<=0 ) return self; + i64 right = x; right += w; + i64 bottom = y; bottom += h; + if( right<=0 || bottom<=0 || x>=width || y>=height ) return self; + int x0 = x<0 ? 0 : x, y0 = y<0 ? 0 : y; + int x1 = right>width ? width : right, y1 = bottom>height ? height : bottom; + for( int py=y0; py<y1; py++ ) + for( int px=x0; px<x1; px++ ) pixels[py*width+px] = color; + return self; + }; + + // Outline stays inside the rectangle. Wide borders become a filled rect. + val rect = { i64 x, i64 y, i64 w, i64 h, i64 thickness, u32 color -> + if( w<=0 || h<=0 || thickness<=0 ) return self; + if( thickness>=w || thickness>=h || thickness>=w-thickness || thickness>=h-thickness ) + return fillRect(x,y,w,h,color); + fillRect(x,y,w,thickness,color); + fillRect(x,y+h-thickness,w,thickness,color); + fillRect(x,y+thickness,thickness,h-2*thickness,color); + return fillRect(x+w-thickness,y+thickness,thickness,h-2*thickness,color); + }; + + // Integer centers are pixel centers. Circle outlines extend inward. + val fillCircle = { i64 cx, i64 cy, int radius, u32 color -> + return _circle(cx,cy,radius,0,true,color); + }; + val circle = { i64 cx, i64 cy, int radius, int thickness, u32 color -> + return _circle(cx,cy,radius,thickness,false,color); + }; + val _circle = { i64 cx, i64 cy, int radius, int thickness, bool filled, u32 color -> + if( radius<0 || (!filled && thickness<=0) ) return self; + i64 left = cx; left -= radius; + i64 top = cy; top -= radius; + i64 right = cx; right += radius; + i64 bottom = cy; bottom += radius; + if( right<0 || bottom<0 || left>=width || top>=height ) return self; + int x0 = left<0 ? 0 : left, y0 = top<0 ? 0 : top; + int x1 = right>=width ? width-1 : right, y1 = bottom>=height ? height-1 : bottom; + i64 outer2 = radius; + outer2 *= radius; + i64 inner = filled ? -1 : radius-thickness; + for( int py=y0; py<=y1; py++ ) + for( int px=x0; px<=x1; px++ ) { + i64 dx = px; dx -= cx; + i64 dy = py; dy -= cy; + i64 d2 = dx*dx+dy*dy; + if( d2<=outer2 && (inner<=0 || d2>inner*inner) ) pixels[py*width+px] = color; + } + return self; + }; + + // Round caps; a zero-length segment is a dot. Only the clipped bounding + // box is scanned. Floating distances avoid overflowing coordinate squares. + val line = { i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness, u32 color -> + if( thickness<=0 ) return self; + i64 pad = thickness/2+1; + i64 left = x0<x1 ? x0 : x1; left -= pad; + i64 top = y0<y1 ? y0 : y1; top -= pad; + i64 right = x0>x1 ? x0 : x1; right += pad; + i64 bottom = y0>y1 ? y0 : y1; bottom += pad; + if( right<0 || bottom<0 || left>=width || top>=height ) return self; + int bx0 = left<0 ? 0 : left, by0 = top<0 ? 0 : top; + int bx1 = right>=width ? width-1 : right, by1 = bottom>=height ? height-1 : bottom; + for( int py=by0; py<=by1; py++ ) + for( int px=bx0; px<=bx1; px++ ) + if( _onLine(px,py,x0,y0,x1,y1,thickness) ) pixels[py*width+px] = color; + return self; + }; + + // Small 3x5 bitmap font for game labels. Lowercase maps to uppercase. + // Newline returns to x; unsupported characters draw a question mark. + val text = { i64 x, i64 y, u8[~] str, int scale, u32 color -> + if( scale<=0 ) return self; + i64 px = x, py = y; + for( int i=0; i<str#; i++ ) { + u8 c = str[i]; + if( c=='\n' ) { px=x; py+=6*scale; continue; } + int bits = _glyph(c); + for( int row=0; row<5; row++ ) + for( int col=0; col<3; col++ ) + if( (bits >> (14-row*3-col)) & 1 ) + fillRect(px+col*scale,py+row*scale,scale,scale,color); + px += 4*scale; + } + return self; + }; +}; + +val _glyph = { u8 c -> + // Five octal digits: one 3-bit row per digit, top to bottom. + if( 'a'<=c<='z' ) c -= 'a'-'A'; + if( c==' ' ) return 000000; + if( c=='0' ) return 075557; + if( c=='1' ) return 026227; + if( c=='2' ) return 061247; + if( c=='3' ) return 061216; + if( c=='4' ) return 055711; + if( c=='5' ) return 074616; + if( c=='6' ) return 034757; + if( c=='7' ) return 071222; + if( c=='8' ) return 075757; + if( c=='9' ) return 075716; + if( c=='A' ) return 025755; + if( c=='B' ) return 065656; + if( c=='C' ) return 034443; + if( c=='D' ) return 065556; + if( c=='E' ) return 074647; + if( c=='F' ) return 074644; + if( c=='G' ) return 034553; + if( c=='H' ) return 055755; + if( c=='I' ) return 072227; + if( c=='J' ) return 011152; + if( c=='K' ) return 055655; + if( c=='L' ) return 044447; + if( c=='M' ) return 057755; + if( c=='N' ) return 057775; + if( c=='O' ) return 025552; + if( c=='P' ) return 065644; + if( c=='Q' ) return 025573; + if( c=='R' ) return 065655; + if( c=='S' ) return 034216; + if( c=='T' ) return 072222; + if( c=='U' ) return 055557; + if( c=='V' ) return 055552; + if( c=='W' ) return 055775; + if( c=='X' ) return 055255; + if( c=='Y' ) return 055222; + if( c=='Z' ) return 071247; + if( c=='-' ) return 000700; + if( c=='.' ) return 000002; + if( c==':' ) return 002020; + if( c=='!' ) return 022202; + if( c=='?' ) return 061202; + return 061202; +}; + +// Pixel coverage for a round-ended segment, separate from traversal. +val _onLine = { i64 px, i64 py, i64 x0, i64 y0, i64 x1, i64 y1, i64 thickness -> + f64 dx = x1; dx -= x0; + f64 dy = y1; dy -= y0; + f64 vx = px; vx -= x0; + f64 vy = py; vy -= y0; + f64 length2 = dx*dx+dy*dy; + bool point = x0==x1 && y0==y1; + f64 t = point ? 0.0 : (vx*dx+vy*dy)/length2; + t = t<0 ? 0.0 : t>1 ? 1.0 : t; + f64 ex = vx-t*dx, ey = vy-t*dy; + f64 half = thickness/2.0; + return ex*ex+ey*ey<=half*half; +}; diff --git a/src/main/smp/sys/gfx.smp b/src/main/smp/sys/gfx.smp new file mode 100644 index 00000000..cde2cc0d --- /dev/null +++ b/src/main/smp/sys/gfx.smp @@ -0,0 +1,42 @@ +// Minimal pixel-window interface; drawing belongs in Simple. +// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB. +// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR. +int OK = 0; +int CLOSED = 1; +int ERROR = -1; +int LEFT = 1; +int ESCAPE = 256; + +{ i64 u32 u32 -> i32 } simple_gfx_open = "C"; +{ i64 u32 -> i32 } simple_gfx_present = "C"; +{ i32 -> i32 } simple_gfx_poll = "C"; +{ i32 } simple_gfx_mouse_x = "C"; +{ i32 } simple_gfx_mouse_y = "C"; +{ i32 -> i32 } simple_gfx_button = "C"; +{ i32 -> i32 } simple_gfx_key = "C"; +{ i32 } simple_gfx_close = "C"; +{ i64 -> i32 } simple_gfx_wait = "C"; + +// Failure prints a diagnostic to stderr and returns ERROR; callers decide +// whether to exit or fall back to text. No native library is loaded until here. +val open = { u8[~] title, u32 width, u32 height -> + i64 ptr = aryu8.str0(title); + simple_gfx_open(ptr,width,height); +}; +// Array length must be width*height. The native layer copies for OS repaints; +// it never keeps a pointer into the Simple heap after this call returns. +val present = { u32[~] pixels -> + i64 ptr = pixels; + simple_gfx_present(ptr,pixels#); +}; +// wait=true waits up to 17ms, waking early for input to avoid a busy loop. +val poll = { bool wait -> simple_gfx_poll(wait); }; +val mouseX = { -> simple_gfx_mouse_x(); }; +val mouseY = { -> simple_gfx_mouse_y(); }; +val button = { int button -> simple_gfx_button(button); }; +// Printable keys use uppercase ASCII; special keys use MiniFB's key codes. +val key = { int key -> simple_gfx_key(key); }; +val close = { -> simple_gfx_close(); }; +// Wait for input or at most timeout milliseconds; 0 polls, -1 waits indefinitely. +// This blocks the game thread, dispatches window events, and may wake early. +val wait = { i64 timeout -> simple_gfx_wait(timeout); }; diff --git a/src/main/smp/sys/text.smp b/src/main/smp/sys/text.smp index a414ff45..a28edf6d 100644 --- a/src/main/smp/sys/text.smp +++ b/src/main/smp/sys/text.smp @@ -96,3 +96,8 @@ val f64 = { f64 x -> end(floating(begin(),x, 0 ,0,-1)); }; val char = { i64 x -> end(integer (begin(),x,'c',0,-1)); }; val bool = { bool x -> end(integer (begin(),x,'b',0,-1)); }; val hex = { i64 x -> end(integer (begin(),x,'x',0,-1)); }; + +// Time selectors occupy spec bits 16..23; padding shares string formatting. +val time = { !aryu8 b, i64 value, i64 spec, i64 width, i64 precision -> + string(b,sys.time.format(value,(spec>>>16)&255),spec,width,-1); +}; diff --git a/src/main/smp/sys/time.smp b/src/main/smp/sys/time.smp new file mode 100644 index 00000000..cdcb5055 --- /dev/null +++ b/src/main/smp/sys/time.smp @@ -0,0 +1,191 @@ +// Instants: bare i64 UTC Unix-epoch milliseconds. Durations: i64 milliseconds. +// Monotonic readings are a separate clock domain, for deadlines/elapsed time. +val DateTime = DateTime; +val Timers = Timers; +i64 SECOND=1000, MINUTE=60000, HOUR=3600000, DAY=86400000; +i64 MIN=-62135596800000, MAX=253402300799999; // Calendar years 1..9999. +i64 LIMIT=9223372036854775807; +int ISO=0, CLOCK12=1, CLOCK24=2, ELAPSED=3, DATE=4, CLOCK24_SECONDS=5; +{ i64 } simple_time_now = "C"; +{ i64 } simple_time_mono = "C"; +{ i64 } simple_time_nano = "C"; +{ i64 i32 -> i32 } simple_time_sleep = "C"; +{ i64 i64 -> i32 } simple_time_local = "C"; +{ i64 i64 u32 -> i32 } simple_time_zone = "C"; +val now = { -> simple_time_now(); }; +val monotonic = { -> simple_time_mono(); }; +val nanoTime = { -> simple_time_nano(); }; +val sleep = { i64 ms -> simple_time_sleep(ms,0); }; +val sleepUntil = { i64 deadline -> simple_time_sleep(deadline,1); }; + +// Signed duration conversions. Overflow returns -1; reverse conversions truncate +// toward zero. An exact duration day is 24 hours, independent of DST. +val _scale = { i64 n, i64 unit -> + if( n>LIMIT/unit || n<(-LIMIT-1)/unit ) return -1; + return n*unit; +}; +val seconds = { i64 n -> _scale(n,SECOND); }; +val minutes = { i64 n -> _scale(n,MINUTE); }; +val hours = { i64 n -> _scale(n,HOUR); }; +val days = { i64 n -> _scale(n,DAY); }; +val toSeconds = { i64 ms -> ms/SECOND; }; +val toMinutes = { i64 ms -> ms/MINUTE; }; +val toHours = { i64 ms -> ms/HOUR; }; +val toDays = { i64 ms -> ms/DAY; }; +val fromNanos = { i64 ns -> ns/1000000; }; +val toNanos = { i64 ms -> _scale(ms,1000000); }; +// Clamp to zero once due, and saturate for widely separated i64 readings. +val remaining = { i64 deadline, i64 current -> + if( deadline<=current ) return 0; + if( current<0 && deadline>LIMIT+current ) return LIMIT; + return deadline-current; +}; +val _mod = { i64 n, i64 unit -> n-n/unit*unit; }; +val leap = { int year -> _mod(year,4)==0 && (_mod(year,100)!=0 || _mod(year,400)==0); }; +val monthDays = { int year, int month -> + if( month<1 || month>12 ) return 0; + return month==2 ? (leap(year) ? 29 : 28) : + month==4 || month==6 || month==9 || month==11 ? 30 : 31; +}; + +// Gregorian civil dates, expressed using 400-year eras; no per-year loops. +val _days = { i64 year, i64 month, i64 day -> + year -= month<=2 ? 1 : 0; + i64 era=year/400, y=year-era*400; + i64 mp=month+(month>2 ? -3 : 9); + return era*146097+y*365+y/4-y/100+(153*mp+2)/5+day-1-719468; +}; +val valid = { int year, int month, int day, int hour, int minute, int second, int millisecond -> + return year>=1 && year<=9999 && month>=1 && month<=12 && day>=1 && day<=monthDays(year,month) && + hour>=0 && hour<=23 && minute>=0 && minute<=59 && second>=0 && second<=59 && + millisecond>=0 && millisecond<=999; +}; +val makeUTC = { int year, int month, int day, int hour, int minute, int second, int millisecond -> + if( !valid(year,month,day,hour,minute,second,millisecond) ) return -1; + return _days(year,month,day)*DAY+hour*HOUR+minute*MINUTE+second*SECOND+millisecond; +}; +val utc = { i64 instant -> + val d=DateTime.empty(); + if( instant<MIN || instant>MAX ) return d; + i64 day=instant/DAY, rest=_mod(instant,DAY); + if( rest<0 ) { rest+=DAY; day--; } + i64 z=day+719468, era=z/146097, doe=z-era*146097; + i64 y=(doe-doe/1460+doe/36524-doe/146096)/365; + i64 doy=doe-(365*y+y/4-y/100), mp=(5*doy+2)/153; + d.day=doy-(153*mp+2)/5+1; + d.month=mp+(mp<10 ? 3 : -9); + d.year=y+era*400+(d.month<=2 ? 1 : 0); + d.hour=rest/HOUR; d.minute=_mod(rest/MINUTE,60); + d.second=_mod(rest/SECOND,60); d.millisecond=_mod(rest,SECOND); + d.weekday=_mod(day+4,7); + if( d.weekday<0 ) d.weekday+=7; + return d; +}; +val local = { i64 instant -> + val d=DateTime.empty(); + val f=new i64[10]; + i64 ptr=f; + if( simple_time_local(instant,ptr)!=0 ) return d; + d.year=f[0]; d.month=f[1]; d.day=f[2]; + d.hour=f[3]; d.minute=f[4]; d.second=f[5]; d.millisecond=f[6]; + d.weekday=f[7]; d.offsetSeconds=f[8]; d.daylight=f[9]!=0; + return d; +}; +val zoneName = { i64 instant -> + val bytes=new u8[128]; + i64 ptr=bytes; + int n=simple_time_zone(instant,ptr,bytes#); + if( n<0 ) return ""; + val b=new aryu8(); + for( int i=0; i<n; i++ ) b.add(bytes[i]); + return text.end(b); +}; + +val _digits = { !aryu8 b, i64 n, int width -> + text.integer(b,n,'d'|4096,width,-1); +}; +val _format = { i64 instant, int style, bool localZone -> + val d=localZone ? local(instant) : utc(instant); + if( d.year==0 || style<0 || style>5 || style==3 ) return "<invalid time>"; + val b=new aryu8(); + if( style==4 ) { + _digits(b,d.day,2).add('/'); _digits(b,d.month,2).add('/'); _digits(b,_mod(d.year,100),2); + } else { + if( style==0 ) { + _digits(b,d.year,4).add('-'); _digits(b,d.month,2).add('-'); _digits(b,d.day,2).add('T'); + } + int hour=d.hour; + if( style==1 ) { hour=_mod(hour,12); if( hour==0 ) hour=12; } + _digits(b,hour,style==1 ? 1 : 2).add(':'); _digits(b,d.minute,2); + if( style==0 || style==5 ) { b.add(':'); _digits(b,d.second,2); } + if( style==1 ) b.addAry(d.hour<12 ? "am" : "pm"); + if( style==0 ) { + b.add('.'); _digits(b,d.millisecond,3); + if( d.offsetSeconds==0 ) b.add('Z'); + else { + i64 offset=d.offsetSeconds; + b.add(offset<0 ? '-' : '+'); + if( offset<0 ) offset=-offset; + _digits(b,offset/3600,2).add(':'); _digits(b,_mod(offset/60,60),2); + if( _mod(offset,60)!=0 ) { b.add(':'); _digits(b,_mod(offset,60),2); } + } + } + } + return text.end(b); +}; + +val elapsed = { i64 ms -> + val b=new aryu8(); + // Nonpositive arithmetic handles the minimum i64 without negating it. + if( ms<0 ) b.add('-'); else ms=-ms; + _digits(b,-(ms/MINUTE),2).add(':'); + _digits(b,-_mod(ms/SECOND,60),2).add('.'); _digits(b,-_mod(ms,SECOND),3); + return text.end(b); +}; +val format = { i64 instant, int style -> + if( style==ELAPSED ) return elapsed(instant); + return _format(instant,style,false); +}; +val formatLocal = { i64 instant, int style -> + if( style==ELAPSED ) return elapsed(instant); + return _format(instant,style,true); +}; + +val _number = { u8[~] s, i64 at, int count -> + if( at<0 || count<1 || at+count>s# ) return -1; + int n=0; + for( int i=0; i<count; i++ ) { + int c=s[at+i]; + if( c<'0' || c>'9' ) return -1; + n=n*10+c-'0'; + } + return n; +}; +// Keep suffix validation separate from calendar construction. +val _parseZone_noInline = { u8[~] s, int at, i64 instant -> + if( at<0 || at>=s# ) return -1; + i64 offset=0; + if( s[at]=='Z' ) { if( at+1!=s# ) return -1; } + else { + int sign=s[at]=='+' ? 1 : s[at]=='-' ? -1 : 0; + if( !sign || (s#!=at+6 && s#!=at+9) || s[at+3]!=':' ) return -1; + int oh=_number(s,at+1,2), om=_number(s,at+4,2), os=0; + if( s#==at+9 ) { if( s[at+6]!=':' ) return -1; os=_number(s,at+7,2); } + if( oh<0 || oh>23 || om<0 || om>59 || os<0 || os>59 ) return -1; + offset=sign*(oh*HOUR+om*MINUTE+os*SECOND); + } + instant-=offset; + return instant<MIN || instant>MAX ? -1 : instant; +}; + +// Strict ISO absolute instants only. -1 is reserved for failure (and therefore +// cannot distinguish the valid instant one millisecond before Unix epoch). +val parse = { u8[~] s -> + if( s#<20 || s[4]!='-' || s[7]!='-' || s[10]!='T' || s[13]!=':' || s[16]!=':' ) return -1; + int y=_number(s,0,4), m=_number(s,5,2), d=_number(s,8,2); + int h=_number(s,11,2), min=_number(s,14,2), sec=_number(s,17,2); + bool fraction=s[19]=='.'; + int ms=fraction ? _number(s,20,3) : 0; + if( !valid(y,m,d,h,min,sec,ms) ) return -1; + return _parseZone_noInline(s,fraction ? 23 : 19,makeUTC(y,m,d,h,min,sec,ms)); +}; diff --git a/src/main/smp/sys/time/DateTime.smp b/src/main/smp/sys/time/DateTime.smp new file mode 100644 index 00000000..5d117467 --- /dev/null +++ b/src/main/smp/sys/time/DateTime.smp @@ -0,0 +1,11 @@ +// Calendar view of an instant. Month/day start at 1; weekday is Sunday=0. +// year==0 means out of the supported calendar range (years 1 through 9999). +struct DateTime { + int !year=0, !month=0, !day=0; + int !hour=0, !minute=0, !second=0, !millisecond=0; + int !weekday=0, !offsetSeconds=0; + bool !daylight=false; +}; + +// Factory in the defining unit, so callers need no forward instance type. +val empty = { -> new DateTime(); }; diff --git a/src/main/smp/sys/time/Timers.smp b/src/main/smp/sys/time/Timers.smp new file mode 100644 index 00000000..b98c96ef --- /dev/null +++ b/src/main/smp/sys/time/Timers.smp @@ -0,0 +1,55 @@ +// Cooperative timers. poll returns IDs; callbacks never run on another thread. +struct Timers { + i64[] !_ids=new i64[8], !_due=new i64[8], !_period=new i64[8]; + i64 !_next=1; + + val _add = { i64 current, i64 delay, i64 period -> + if( current<0 || delay<0 || current>time.LIMIT-delay || _next==time.LIMIT ) return -1; + int slot=0; + while( slot<_ids# && _ids[slot]!=0 ) slot++; + if( slot==_ids# ) { + if( _ids#>1073741823 ) return -1; + val ids=new i64[_ids#*2], due=new i64[_ids#*2], periods=new i64[_ids#*2]; + for( int i=0; i<_ids#; i++ ) { ids[i]=_ids[i]; due[i]=_due[i]; periods[i]=_period[i]; } + _ids=ids; _due=due; _period=periods; + // Retry against the newly published arrays after growing. + return _add(current,delay,period); + } + i64 id=_next++; + _ids[slot]=id; _due[slot]=current+delay; _period[slot]=period; + return id; + }; + // Explicit-clock versions make game logic and tests deterministic. + val afterAt = { i64 current, i64 delay -> _add(current,delay,0); }; + val everyAt = { i64 current, i64 period -> period<=0 ? -1 : _add(current,period,period); }; + val after = { i64 delay -> afterAt(time.monotonic(),delay); }; + val every = { i64 period -> everyAt(time.monotonic(),period); }; + val cancel = { i64 id -> + if( id<=0 ) return false; + for( int i=0; i<_ids#; i++ ) if( _ids[i]==id ) { _ids[i]=0; return true; } + return false; + }; + val nextDeadline = { -> + i64 next=-1; + for( int i=0; i<_ids#; i++ ) + if( _ids[i]!=0 && (next<0 || _due[i]<next) ) next=_due[i]; + return next; + }; + // Earliest deadline first; equal deadlines use registration order. + // Repeats skip missed ticks and retain their phase, avoiding bursts/drift. + val poll = { i64 current -> + int slot=-1; + for( int i=0; i<_ids#; i++ ) + if( _ids[i]!=0 && _due[i]<=current && (slot<0 || _due[i]<_due[slot] || + (_due[i]==_due[slot] && _ids[i]<_ids[slot])) ) slot=i; + if( slot<0 ) return -1; + i64 id=_ids[slot], period=_period[slot]; + if( period==0 ) _ids[slot]=0; + else { + i64 steps=(current-_due[slot])/period; + if( steps>=(time.LIMIT-_due[slot])/period ) _ids[slot]=0; + else _due[slot]+=(steps+1)*period; + } + return id; + }; +}; |
