--- a/hedgewars/uAIMisc.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uAIMisc.pas Mon Aug 26 15:44:03 2019 -0600
@@ -799,7 +799,7 @@
begin
dead:= true;
Targets.reset:= true;
- if dX < 0.035 then
+ if gdX < 0.035 then
begin
subrate:= RealRateExplosion(Me, round(pX), round(pY), 61, afErasesLand or afTrackFall);
if abs(subrate) > 2000 then inc(Rate,subrate div 1024)
--- a/hedgewars/uConsts.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uConsts.pas Mon Aug 26 15:44:03 2019 -0600
@@ -110,6 +110,8 @@
ifCritical = $00000002; // image is critical for gameplay (exit game if unable to load)
ifColorKey = $00000004; // image uses transparent pixels (color keying)
ifIgnoreCaps = $00000008; // ignore hardware capabilities when loading (i.e. image will not be drawn using OpenGL)
+ ifDigestAlpha = $00000010; // add alpha channel to the digest
+ ifDigestAll = $00000020; // add all channels to the digest
// texture priority (allows OpenGL to keep frequently used textures in video memory more easily)
tpLowest = 0.00;
--- a/hedgewars/uGears.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uGears.pas Mon Aug 26 15:44:03 2019 -0600
@@ -276,7 +276,7 @@
end;
if curHandledGear^.Active then
begin
- if curHandledGear^.RenderTimer then
+ if (not cOnlyStats) and curHandledGear^.RenderTimer then
begin
// Mine timer
if (curHandledGear^.Kind in [gtMine, gtSMine, gtAirMine]) then
--- a/hedgewars/uGearsHandlersMess.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uGearsHandlersMess.pas Mon Aug 26 15:44:03 2019 -0600
@@ -2422,7 +2422,7 @@
doStepCase(Gear)
else
// health texture (FlightTime = health when the last texture was generated)
- if (Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil) then
+ if (not cOnlyStats) and ((Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil)) then
begin
Gear^.FlightTime:= Gear^.Health;
FreeAndNilTexture(Gear^.Tex);
@@ -2500,7 +2500,7 @@
dec(Gear^.Health, Gear^.Damage);
Gear^.Damage := 0;
// health texture (FlightTime = health when the last texture was generated)
- if (Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil) then
+ if (not cOnlyStats) and ((Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil)) then
begin
Gear^.FlightTime:= Gear^.Health;
FreeAndNilTexture(Gear^.Tex);
@@ -2523,12 +2523,14 @@
i:= 1
else
i:= 0
+ else if cOnlyStats then
+ i:= 0
// Always show health (default)
else
i:= 1;
if i = 1 then
begin
- if (Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil) then
+ if (not cOnlyStats) and ((Gear^.Health <> Gear^.FlightTime) or (Gear^.Tex = nil)) then
begin
Gear^.FlightTime:= Gear^.Health;
FreeAndNilTexture(Gear^.Tex);
@@ -2537,7 +2539,7 @@
end
else
begin
- if (Gear^.FlightTime <> $ffffffff) or (Gear^.Tex = nil) then
+ if (not cOnlyStats) and ((Gear^.FlightTime <> $ffffffff) or (Gear^.Tex = nil)) then
begin
Gear^.FlightTime:= $ffffffff;
FreeAndNilTexture(Gear^.Tex);
@@ -4615,7 +4617,7 @@
i:= Gear^.Health div 20;
- if (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
+ if (not cOnlyStats) and (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
begin
Gear^.Damage:= i;
FreeAndNilTexture(Gear^.Tex);
@@ -5887,7 +5889,7 @@
else
begin
i:= Gear^.Health div 5;
- if (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
+ if (not cOnlyStats) and (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
begin
Gear^.Damage:= i;
FreeAndNilTexture(Gear^.Tex);
@@ -5978,7 +5980,7 @@
else
begin
i:= Gear^.Health div 10;
- if (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
+ if (not cOnlyStats) and (i <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
begin
Gear^.Damage:= i;
FreeAndNilTexture(Gear^.Tex);
@@ -6497,7 +6499,7 @@
t:LongInt;
begin
t:= Gear^.Health div 10;
- if (t <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
+ if (not cOnlyStats) and (t <> Gear^.Damage) and ((GameTicks and $3F) = 0) then
begin
Gear^.Damage:= t;
FreeAndNilTexture(Gear^.Tex);
--- a/hedgewars/uLandObjects.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uLandObjects.pas Mon Aug 26 15:44:03 2019 -0600
@@ -36,7 +36,7 @@
implementation
uses uStore, uConsts, uConsole, uRandom, uSound
, uTypes, uVariables, uDebug, uUtils
- , uPhysFSLayer, adler32, uRenderUtils;
+ , uPhysFSLayer, uRenderUtils;
const MaxRects = 512;
MAXOBJECTRECTS = 16;
@@ -346,51 +346,13 @@
CountNonZeroz:= lRes;
end;
-procedure ChecksumLandObjectImage(Image: PSDL_Surface; alphaOnly: boolean);
-var y, x: LongInt;
-var rowData: PByteArray;
-begin
- if Image = nil then exit;
-
- if alphaOnly then
- rowData := GetMem(Image^.w);
-
- if SDL_MustLock(Image) then
- SDL_LockSurface(Image);
-
- if checkFails(Image^.format^.BytesPerPixel = 4, 'Land object image should be 32bit', true) then
- begin
- if SDL_MustLock(Image) then
- SDL_UnlockSurface(Image);
- exit
- end;
-
- for y := 0 to Image^.h - 1 do
- if alphaOnly then
- begin
- for x := 0 to Image^.w - 1 do
- rowData^[x] := PByteArray(Image^.pixels)^[y * Image^.pitch + x * 4 + AByteIndex];
- syncedPixelDigest:= Adler32Update(syncedPixelDigest, rowData, Image^.w);
- end
- else
- syncedPixelDigest:= Adler32Update(syncedPixelDigest, @PByteArray(Image^.pixels)^[y*Image^.pitch], Image^.w*4);
-
- if SDL_MustLock(Image) then
- SDL_UnlockSurface(Image);
-
- if alphaOnly then
- FreeMem(rowData, Image^.w);
-end;
-
function AddGirder(gX: LongInt; var girSurf: PSDL_Surface): boolean;
var x1, x2, y, k, i, girderHeight: LongInt;
rr: TSDL_Rect;
bRes: boolean;
begin
if girSurf = nil then
- girSurf:= LoadDataImageAltPath(ptCurrTheme, ptGraphics, 'Girder', ifCritical or ifColorKey or ifIgnoreCaps);
-
-ChecksumLandObjectImage(girsurf, true);
+ girSurf:= LoadDataImageAltPath(ptCurrTheme, ptGraphics, 'Girder', ifCritical or ifColorKey or ifIgnoreCaps or ifDigestAlpha);
girderHeight:= girSurf^.h;
@@ -745,11 +707,10 @@
Delete(s, 1, i);
i:= Pos(',', s);
if i = 0 then i:= Succ(Length(S));
- Surf:= LoadDataImage(ptCurrTheme, Trim(Copy(s, 1, Pred(i))), ifColorKey or ifIgnoreCaps or ifCritical);
+ Surf:= LoadDataImage(ptCurrTheme, Trim(Copy(s, 1, Pred(i))), ifColorKey or ifIgnoreCaps or ifCritical or ifDigestAlpha );
Width:= Surf^.w;
Height:= Surf^.h;
Delete(s, 1, i);
- ChecksumLandObjectImage(Surf, true);
end;
end;
@@ -949,19 +910,22 @@
begin
i:= Pos(',', s);
Name:= Trim(Copy(s, 1, Pred(i)));
- Surf:= LoadDataImage(ptCurrTheme, Name, ifColorKey or ifIgnoreCaps or ifCritical);
+
+ Mask:= LoadDataImage(ptCurrTheme, Trim(Copy(s, 1, Pred(i)))+'_mask', ifColorKey or ifIgnoreCaps or ifDigestAll);
+ if Mask = nil then
+ Surf:= LoadDataImage(ptCurrTheme, Name, ifColorKey or ifIgnoreCaps or ifCritical or ifDigestAlpha)
+ else
+ Surf:= LoadDataImage(ptCurrTheme, Name, ifColorKey or ifIgnoreCaps or ifCritical);
+
Width:= Surf^.w;
Height:= Surf^.h;
- Mask:= LoadDataImage(ptCurrTheme, Trim(Copy(s, 1, Pred(i)))+'_mask', ifColorKey or ifIgnoreCaps);
+
Delete(s, 1, i);
i:= Pos(',', s);
Maxcnt:= StrToInt(Trim(Copy(s, 1, Pred(i))));
Delete(s, 1, i);
if (Maxcnt < 1) or (Maxcnt > MAXTHEMEOBJECTS) then
OutError('Broken theme. Object''s max. count should be between 1 and '+ inttostr(MAXTHEMEOBJECTS) +' (it was '+ inttostr(Maxcnt) +').', true);
- if Mask = nil then
- ChecksumLandObjectImage(Surf, true);
- ChecksumLandObjectImage(Mask, false);
inrectcnt := 0;
--- a/hedgewars/uRender.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uRender.pas Mon Aug 26 15:44:03 2019 -0600
@@ -2177,6 +2177,7 @@
procedure freeModule;
begin
+ if cOnlyStats then exit;
{$IFDEF GL2}
glDeleteProgram(shaderMain);
glDeleteProgram(shaderWater);
--- a/hedgewars/uStore.pas Mon Aug 26 15:40:15 2019 -0600
+++ b/hedgewars/uStore.pas Mon Aug 26 15:44:03 2019 -0600
@@ -385,8 +385,7 @@
var ii: TSprite;
ai: TAmmoType;
tmpsurf, tmpoverlay: PSDL_Surface;
- i, y, x, imflags: LongInt;
- rowData: PByteArray;
+ i, imflags: LongInt;
keyConfirm, keyQuit: shortstring;
begin
AddFileLog('StoreLoad()');
@@ -431,18 +430,10 @@
imflags := (imflags or ifCritical);
// load the image
- tmpsurf := LoadDataImageAltPath(Path, AltPath, FileName, imflags);
- if (tmpsurf <> nil) and checkSum then
- begin
- rowData := GetMem(tmpsurf^.w);
- for y := 0 to tmpsurf^.h-1 do
- begin
- for x := 0 to tmpsurf^.w - 1 do
- rowData^[x] := PByteArray(tmpsurf^.pixels)^[y * tmpsurf^.pitch + x * 4 + AByteIndex];
- syncedPixelDigest:= Adler32Update(syncedPixelDigest, rowData, tmpsurf^.w);
- end;
- FreeMem(rowData, tmpsurf^.w);
- end;
+ if checkSum then
+ tmpsurf := LoadDataImageAltPath(Path, AltPath, FileName, imflags or ifDigestAlpha)
+ else
+ tmpsurf := LoadDataImageAltPath(Path, AltPath, FileName, imflags);
end;
if tmpsurf <> nil then
@@ -646,10 +637,13 @@
function LoadImage(const filename: shortstring; imageFlags: LongInt): PSDL_Surface;
var tmpsurf: PSDL_Surface;
s: shortstring;
- logMsg: shortstring;
+ logMsg, digestMsg: shortstring;
rwops: PSDL_RWops;
+ y, x: LongInt;
+ rowData: PByteArray;
begin
LoadImage:= nil;
+ digestMsg:= '';
logMsg:= msgLoading + filename + '.png [flags: ' + inttostr(imageFlags) + ']';
s:= filename + '.png';
@@ -700,8 +694,37 @@
if (imageFlags and ifColorKey) <> 0 then
if checkFails(SDL_SetColorKey(tmpsurf, SDL_TRUE, 0) = 0, errmsgTransparentSet, true) then exit;
+ if ((imageFlags and (ifDigestAll or ifDigestAlpha)) <> 0)
+ and (tmpsurf^.format^.BytesPerPixel = 4)then
+ begin
+ if SDL_MustLock(tmpsurf) then
+ SDL_LockSurface(tmpsurf);
+
+ if (imageFlags and ifDigestAll) <> 0 then
+ begin
+ for y := 0 to tmpsurf^.h - 1 do
+ syncedPixelDigest:= Adler32Update(syncedPixelDigest, @PByteArray(tmpsurf^.pixels)^[y*tmpsurf^.pitch], tmpsurf^.w*4);
+ digestMsg := ' [CD: ' + inttostr(syncedPixelDigest) + ']'
+ end
+ else if (imageFlags and ifDigestAlpha) <> 0 then
+ begin
+ rowData := GetMem(tmpsurf^.w);
+ for y := 0 to tmpsurf^.h - 1 do
+ begin
+ for x := 0 to tmpsurf^.w - 1 do
+ rowData^[x] := PByteArray(tmpsurf^.pixels)^[y * tmpsurf^.pitch + x * 4 + AByteIndex];
+ syncedPixelDigest:= Adler32Update(syncedPixelDigest, rowData, tmpsurf^.w);
+ end;
+ FreeMem(rowData, tmpsurf^.w);
+ digestMsg := ' [AD: ' + inttostr(syncedPixelDigest) + ']'
+ end;
+
+ if SDL_MustLock(tmpsurf) then
+ SDL_UnlockSurface(tmpsurf);
+ end;
+
// log success
- WriteLnToConsole(logMsg + ' ' + msgOK + ' (' + inttostr(tmpsurf^.w) + 'x' + inttostr(tmpsurf^.h) + ')');
+ WriteLnToConsole(logMsg + ' ' + msgOK + ' (' + inttostr(tmpsurf^.w) + 'x' + inttostr(tmpsurf^.h) + ')' + digestMsg);
LoadImage:= tmpsurf //Result
end;
--- a/rust/hwphysics/src/data.rs Mon Aug 26 15:40:15 2019 -0600
+++ b/rust/hwphysics/src/data.rs Mon Aug 26 15:44:03 2019 -0600
@@ -3,19 +3,19 @@
any::TypeId,
mem::{size_of, MaybeUninit},
num::NonZeroU16,
- ptr::NonNull,
+ ptr::{copy_nonoverlapping, NonNull},
slice,
};
-pub trait TypeTuple: Sized {
+pub unsafe trait TypeTuple: Sized {
fn len() -> usize;
fn get_types(dest: &mut Vec<TypeId>);
- unsafe fn iter<F>(slices: &[NonNull<u8>], count: usize, f: F)
+ unsafe fn iter<F>(slices: &[NonNull<u8>], count: usize, mut f: F)
where
- F: Fn(Self);
+ F: FnMut(Self);
}
-impl<T: 'static> TypeTuple for (&T,) {
+unsafe impl<T: 'static> TypeTuple for (&T,) {
fn len() -> usize {
1
}
@@ -24,9 +24,9 @@
dest.push(TypeId::of::<T>());
}
- unsafe fn iter<F>(slices: &[NonNull<u8>], count: usize, f: F)
+ unsafe fn iter<F>(slices: &[NonNull<u8>], count: usize, mut f: F)
where
- F: Fn(Self),
+ F: FnMut(Self),
{
let slice1 = slice::from_raw_parts(slices[0].as_ptr() as *const T, count);
for i in 0..count {
@@ -41,7 +41,7 @@
max_elements: u16,
elements_count: u16,
data: Box<[u8; BLOCK_SIZE]>,
- blocks: [Option<NonNull<u8>>; 64],
+ component_blocks: [Option<NonNull<u8>>; 64],
}
impl Unpin for DataBlock {}
@@ -51,18 +51,18 @@
let total_size: u16 = element_sizes
.iter()
.enumerate()
- .filter(|(i, _)| mask & (164 << *i as u64) != 0)
+ .filter(|(i, _)| mask & (1 << *i as u64) != 0)
.map(|(_, size)| *size)
.sum();
let max_elements = (BLOCK_SIZE / total_size as usize) as u16;
let mut data: Box<[u8; BLOCK_SIZE]> =
- Box::new(unsafe { std::mem::MaybeUninit::uninit().assume_init() });
+ Box::new(unsafe { MaybeUninit::uninit().assume_init() });
let mut blocks = [None; 64];
let mut offset = 0;
for i in 0..64 {
- if mask & (164 << i) != 0 {
+ if mask & (1 << i) != 0 {
blocks[i] = Some(NonNull::new(data[offset..].as_mut_ptr()).unwrap());
offset += element_sizes[i] as usize * max_elements as usize;
}
@@ -71,7 +71,7 @@
elements_count: 0,
max_elements,
data,
- blocks,
+ component_blocks: blocks,
}
}
@@ -112,27 +112,70 @@
}
fn move_between_blocks(&mut self, from_block_index: u16, from_index: u16, to_block_index: u16) {
+ debug_assert!(from_block_index != to_block_index);
let source_mask = self.block_masks[from_block_index as usize];
let destination_mask = self.block_masks[to_block_index as usize];
debug_assert!(source_mask & destination_mask == source_mask);
let source = &self.blocks[from_block_index as usize];
let destination = &self.blocks[to_block_index as usize];
+ debug_assert!(from_index < source.elements_count);
+ debug_assert!(!destination.is_full());
+
for i in 0..64 {
- unimplemented!()
+ if source_mask & 1 << i != 0 {
+ unsafe {
+ copy_nonoverlapping(
+ source.component_blocks[i].unwrap().as_ptr(),
+ destination.component_blocks[i].unwrap().as_ptr(),
+ self.element_sizes[i] as usize,
+ );
+ }
+ }
}
+ self.blocks[from_block_index as usize].elements_count -= 1;
+ self.blocks[to_block_index as usize].elements_count += 1;
}
- fn add_to_block<T>(&mut self, block_index: u16, value: &T) {
- unimplemented!()
+ fn add_to_block<T: Clone>(&mut self, block_index: u16, value: &T) {
+ debug_assert!(self.block_masks[block_index as usize].count_ones() == 1);
+
+ let block = &mut self.blocks[block_index as usize];
+ debug_assert!(block.elements_count < block.max_elements);
+
+ unsafe {
+ let slice = slice::from_raw_parts_mut(
+ block.data.as_mut_ptr() as *mut T,
+ block.max_elements as usize,
+ );
+ *slice.get_unchecked_mut(block.elements_count as usize) = value.clone();
+ };
+ block.elements_count += 1;
}
fn remove_from_block(&mut self, block_index: u16, index: u16) {
- unimplemented!()
+ let block = &mut self.blocks[block_index as usize];
+ debug_assert!(index < block.elements_count);
+
+ for (i, size) in self.element_sizes.iter().cloned().enumerate() {
+ if index < block.elements_count - 1 {
+ if let Some(ptr) = block.component_blocks[i] {
+ unsafe {
+ copy_nonoverlapping(
+ ptr.as_ptr()
+ .add((size * (block.elements_count - 1)) as usize),
+ ptr.as_ptr().add((size * index) as usize),
+ size as usize,
+ );
+ }
+ }
+ }
+ }
+ block.elements_count -= 1;
}
#[inline]
- fn ensure_group(&mut self, mask: u64) -> u16 {
+ fn ensure_block(&mut self, mask: u64) -> u16 {
if let Some(index) = self
.block_masks
.iter()
@@ -142,13 +185,14 @@
index as u16
} else {
self.blocks.push(DataBlock::new(mask, &self.element_sizes));
+ self.block_masks.push(mask);
(self.blocks.len() - 1) as u16
}
}
pub fn add<T: Clone + 'static>(&mut self, gear_id: GearId, value: &T) {
if let Some(type_index) = self.get_type_index::<T>() {
- let type_bit = 1u64 << type_index as u64;
+ let type_bit = 1 << type_index as u64;
let entry = self.lookup[gear_id.get() as usize - 1];
if let Some(index) = entry.index {
@@ -156,11 +200,11 @@
let new_mask = mask | type_bit;
if new_mask != mask {
- let dest_block_index = self.ensure_group(new_mask);
+ let dest_block_index = self.ensure_block(new_mask);
self.move_between_blocks(entry.block_index, index.get() - 1, dest_block_index);
}
} else {
- let dest_block_index = self.ensure_group(type_bit);
+ let dest_block_index = self.ensure_block(type_bit);
self.add_to_block(dest_block_index, value);
}
} else {
@@ -172,15 +216,36 @@
if let Some(type_index) = self.get_type_index::<T>() {
let entry = self.lookup[gear_id.get() as usize - 1];
if let Some(index) = entry.index {
- self.remove_from_block(entry.block_index, index.get() - 1);
+ let destination_mask =
+ self.block_masks[entry.block_index as usize] & !(1 << type_index as u64);
+
+ if destination_mask == 0 {
+ self.remove_all(gear_id)
+ } else {
+ let destination_block_index = self.ensure_block(destination_mask);
+ self.move_between_blocks(
+ entry.block_index,
+ index.get() - 1,
+ destination_block_index,
+ );
+ }
}
+ } else {
+ panic!("Unregistered type")
+ }
+ }
+
+ pub fn remove_all(&mut self, gear_id: GearId) {
+ let entry = self.lookup[gear_id.get() as usize - 1];
+ if let Some(index) = entry.index {
+ self.remove_from_block(entry.block_index, index.get() - 1);
}
}
pub fn register<T: 'static>(&mut self) {
- assert!(!std::mem::needs_drop::<T>());
- assert!(self.types.len() <= 64);
- assert!(size_of::<T>() <= u16::max_value() as usize);
+ debug_assert!(!std::mem::needs_drop::<T>());
+ debug_assert!(self.types.len() <= 64);
+ debug_assert!(size_of::<T>() <= u16::max_value() as usize);
let id = TypeId::of::<T>();
if !self.types.contains(&id) {
@@ -190,26 +255,38 @@
}
fn create_selector(&self, types: &[TypeId]) -> u64 {
- let mut selector = 0u64;
+ let mut selector = 0;
for (i, typ) in self.types.iter().enumerate() {
if types.contains(&typ) {
- selector |= 1u64 << (i as u64)
+ selector |= 1 << (i as u64)
}
}
selector
}
- pub fn iter<T: TypeTuple + 'static, F: Fn(T) + Copy>(&self, f: F) {
+ pub fn iter<T: TypeTuple + 'static, F: FnMut(T)>(&self, mut f: F) {
let mut types = vec![];
T::get_types(&mut types);
- let types_count = types.len();
+ debug_assert!(types.iter().all(|t| self.types.contains(t)));
+ let types_count = types.len();
let selector = self.create_selector(&types);
+
+ let mut slices = Vec::with_capacity(64);
+
for (block_index, mask) in self.block_masks.iter().enumerate() {
if mask & selector == selector {
+ slices.clear();
let block = &self.blocks[block_index];
- for element_index in 0..block.max_elements {
- unimplemented!()
+
+ for (i, ptr_opt) in block.component_blocks.iter().cloned().enumerate() {
+ if let Some(ptr) = ptr_opt {
+ slices.push(ptr);
+ }
+ }
+
+ unsafe {
+ T::iter(&slices[..], block.elements_count as usize, |x| f(x));
}
}
}
@@ -218,16 +295,26 @@
#[cfg(test)]
mod test {
- use super::GearDataManager;
+ use super::{super::common::GearId, GearDataManager};
+ #[derive(Clone)]
struct Datum {
value: u32,
}
#[test]
- fn iteration() {
+ fn single_component_iteration() {
+ assert!(std::mem::size_of::<Datum>() > 0);
+
let mut manager = GearDataManager::new();
manager.register::<Datum>();
- manager.iter(|d: (&Datum,)| {});
+ for i in 1..=5 {
+ manager.add(GearId::new(i as u16).unwrap(), &Datum { value: i });
+ }
+
+ let mut sum = 0;
+ manager.iter(|(d,): (&Datum,)| sum += d.value);
+
+ assert_eq!(sum, 15);
}
}